Skip to content

Commit

Permalink
[Script] Introduce new OP_CHECKCOLDSTAKEVERIFY opcode
Browse files Browse the repository at this point in the history
without free outputs
  • Loading branch information
random-zebra committed May 11, 2021
1 parent b194386 commit df11631
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 53 deletions.
75 changes: 42 additions & 33 deletions src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -960,30 +960,16 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
}
break;

case OP_CHECKCOLDSTAKEVERIFY:
{
return checker.CheckColdStake(false, script, stack, flags, serror);
}
break;

case OP_CHECKCOLDSTAKEVERIFY_LOF:
{
if (g_IsV6Active) {
// the stack can contain only <sig> <pk> <pkh> at this point
if ((int)stack.size() != 3) {
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
}
// check pubkey/signature encoding
valtype& vchSig = stacktop(-3);
valtype& vchPubKey = stacktop(-2);
if (!CheckSignatureEncoding(vchSig, flags, serror) ||
!CheckPubKeyEncoding(vchPubKey, flags, serror)) {
// serror is set
return false;
}
// check hash size
valtype& vchPubKeyHash = stacktop(-1);
if ((int)vchPubKeyHash.size() != 20) {
return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);
}
}
if(!checker.CheckColdStake(script)) {
return set_error(serror, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY);
}
// Allow last output script "free"
return checker.CheckColdStake(true, script, stack, flags, serror);
}
break;

Expand Down Expand Up @@ -1357,36 +1343,59 @@ bool TransactionSignatureChecker::CheckLockTime(const CScriptNum& nLockTime) con
return true;
}

bool TransactionSignatureChecker::CheckColdStake(const CScript& prevoutScript) const
bool TransactionSignatureChecker::CheckColdStake(bool fAllowLastOutputFree, const CScript& prevoutScript, std::vector<valtype>& stack, unsigned int flags, ScriptError* serror) const
{
if (g_IsV6Active) {
// the stack can contain only <sig> <pk> <pkh> at this point
if ((int)stack.size() != 3) {
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
}
// check pubkey/signature encoding
valtype& vchSig = stacktop(-3);
valtype& vchPubKey = stacktop(-2);
if (!CheckSignatureEncoding(vchSig, flags, serror) ||
!CheckPubKeyEncoding(vchPubKey, flags, serror)) {
// serror is set
return false;
}
// check hash size
valtype& vchPubKeyHash = stacktop(-1);
if ((int)vchPubKeyHash.size() != 20) {
return set_error(serror, SCRIPT_ERR_SCRIPT_SIZE);
}
}

// check it is used in a valid cold stake transaction.
// Transaction must be a coinstake tx
if (!txTo->IsCoinStake()) {
return false;
return set_error(serror, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY);
}
// There must be one single input
if (txTo->vin.size() != 1) {
return false;
return set_error(serror, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY);
}
// Since this is a coinstake, it has at least 2 outputs
const unsigned int outs = txTo->vout.size();
assert(outs >= 2);
// All outputs, except the first, and (for cold stakes with outs >=3) the last one,
// must have the same pubKeyScript, and it must match the script we are spending.
// If the coinstake has at least 3 outputs, the last one is left free, to be used for
// budget/masternode payments, and is checked in CheckColdstakeFreeOutput().
// All outputs must have the same pubKeyScript, and it must match the script we are spending.
// If the coinstake has at least 3 outputs, the last one can be left free, to be used for
// budget/masternode payments (before v6.0 enforcement), and is checked in CheckColdstakeFreeOutput().
// Here we verify only that input amount goes to the non-free outputs.
CAmount outValue{0};
for (unsigned int i = 1; i < outs; i++) {
if (txTo->vout[i].scriptPubKey != prevoutScript) {
// Only the last one can be different (and only when outs >=3)
if (i != outs-1 || outs < 3) {
return false;
// Only the last one can be different (and only when outs >=3 and fAllowLastOutputFree=true)
if (!fAllowLastOutputFree || i != outs-1 || outs < 3) {
return set_error(serror, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY);
}
} else {
outValue += txTo->vout[i].nValue;
}
}
return outValue > amount;
if (outValue < amount) {
return set_error(serror, SCRIPT_ERR_CHECKCOLDSTAKEVERIFY);
}
return true;
}


Expand Down
4 changes: 2 additions & 2 deletions src/script/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class BaseSignatureChecker
return false;
}

virtual bool CheckColdStake(const CScript& script) const
virtual bool CheckColdStake(bool fAllowLastOutputFree, const CScript& prevoutScript, std::vector<valtype>& stack, unsigned int flags, ScriptError* error) const
{
return false;
}
Expand All @@ -132,7 +132,7 @@ class TransactionSignatureChecker : public BaseSignatureChecker

bool CheckSig(const std::vector<unsigned char>& scriptSig, const std::vector<unsigned char>& vchPubKey, const CScript& scriptCode, SigVersion sigversion) const override ;
bool CheckLockTime(const CScriptNum& nLockTime) const override;
bool CheckColdStake(const CScript& prevoutScript) const override;
bool CheckColdStake(bool fAllowLastOutputFree, const CScript& prevoutScript, std::vector<valtype>& stack, unsigned int flags, ScriptError* serror) const override;
};

class MutableTransactionSignatureChecker : public TransactionSignatureChecker
Expand Down
9 changes: 8 additions & 1 deletion src/script/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const char* GetOpName(opcodetype opcode)

// cold staking
case OP_CHECKCOLDSTAKEVERIFY_LOF : return "OP_CHECKCOLDSTAKEVERIFY_LOF";
case OP_CHECKCOLDSTAKEVERIFY : return "OP_CHECKCOLDSTAKEVERIFY";

case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE";

Expand Down Expand Up @@ -229,14 +230,15 @@ bool CScript::IsPayToScriptHash() const
// can be removed once v6 enforcement is activated.
std::atomic<bool> g_IsV6Active{false};

// P2CS script: either with or without last output free
bool CScript::IsPayToColdStaking() const
{
return (this->size() == 51 &&
(!g_IsV6Active || (*this)[0] == OP_DUP) &&
(!g_IsV6Active || (*this)[1] == OP_HASH160) &&
(*this)[2] == OP_ROT &&
(!g_IsV6Active || (*this)[3] == OP_IF) &&
(*this)[4] == OP_CHECKCOLDSTAKEVERIFY_LOF &&
((*this)[4] == OP_CHECKCOLDSTAKEVERIFY || (*this)[4] == OP_CHECKCOLDSTAKEVERIFY_LOF) &&
(*this)[5] == 0x14 &&
(!g_IsV6Active || (*this)[26] == OP_ELSE) &&
(*this)[27] == 0x14 &&
Expand All @@ -245,6 +247,11 @@ bool CScript::IsPayToColdStaking() const
(*this)[50] == OP_CHECKSIG);
}

bool CScript::IsPayToColdStakingLOF() const
{
return IsPayToColdStaking() && (*this)[4] == OP_CHECKCOLDSTAKEVERIFY_LOF;
}

bool CScript::StartsWithOpcode(const opcodetype opcode) const
{
return (!this->empty() && (*this)[0] == opcode);
Expand Down
2 changes: 2 additions & 0 deletions src/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ enum opcodetype

// cold staking
OP_CHECKCOLDSTAKEVERIFY_LOF = 0xd1, // last output free for masternode/budget payments
OP_CHECKCOLDSTAKEVERIFY = 0xd2,

OP_INVALIDOPCODE = 0xff,
};
Expand Down Expand Up @@ -632,6 +633,7 @@ class CScript : public CScriptBase
bool IsPayToPublicKeyHash() const;
bool IsPayToScriptHash() const;
bool IsPayToColdStaking() const;
bool IsPayToColdStakingLOF() const;
bool StartsWithOpcode(const opcodetype opcode) const;
bool IsZerocoinMint() const;
bool IsZerocoinSpend() const;
Expand Down
10 changes: 10 additions & 0 deletions src/script/standard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@ CScript GetScriptForRawPubKey(const CPubKey& pubKey)
}

CScript GetScriptForStakeDelegation(const CKeyID& stakingKey, const CKeyID& spendingKey)
{
CScript script;
script << OP_DUP << OP_HASH160 << OP_ROT <<
OP_IF << OP_CHECKCOLDSTAKEVERIFY << ToByteVector(stakingKey) <<
OP_ELSE << ToByteVector(spendingKey) << OP_ENDIF <<
OP_EQUALVERIFY << OP_CHECKSIG;
return script;
}

CScript GetScriptForStakeDelegationLOF(const CKeyID& stakingKey, const CKeyID& spendingKey)
{
CScript script;
script << OP_DUP << OP_HASH160 << OP_ROT <<
Expand Down
1 change: 1 addition & 0 deletions src/script/standard.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ CScript GetScriptForDestination(const CTxDestination& dest);
CScript GetScriptForRawPubKey(const CPubKey& pubKey);
CScript GetScriptForMultisig(int nRequired, const std::vector<CPubKey>& keys);
CScript GetScriptForStakeDelegation(const CKeyID& stakingKey, const CKeyID& spendingKey);
CScript GetScriptForStakeDelegationLOF(const CKeyID& stakingKey, const CKeyID& spendingKey);
CScript GetScriptForOpReturn(const uint256& message);

#endif // BITCOIN_SCRIPT_STANDARD_H
18 changes: 9 additions & 9 deletions src/test/script_P2CS_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ BOOST_AUTO_TEST_CASE(extract_cold_staking_destination_keys)
CheckValidKeyId(destVector[1], ownerId);
}

static CScript GetNewP2CS(CKey& stakerKey, CKey& ownerKey)
static CScript GetNewP2CS(CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
{
stakerKey = DecodeSecret("91yo52JPHDVUG3jXWLKGyzEdjn1a9nbnurLdmQEf2UzbgzkTc2c");
ownerKey = DecodeSecret("92KgNFNfmVVJRQuzssETc7NhwufGuHsLvPQxW9Nwmxs7PB4ByWB");
return GetScriptForStakeDelegation(stakerKey.GetPubKey().GetID(),
ownerKey.GetPubKey().GetID());
return fLastOutFree ? GetScriptForStakeDelegationLOF(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID())
: GetScriptForStakeDelegation(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID());
}

static CScript GetDummyP2CS(const CKeyID& dummyKeyID)
Expand All @@ -78,9 +78,9 @@ static CScript GetDummyP2PKH(const CKeyID& dummyKeyID)
static const CAmount amtIn = 200 * COIN;
static const unsigned int flags = STANDARD_SCRIPT_VERIFY_FLAGS;

static CMutableTransaction CreateNewColdStakeTx(CScript& scriptP2CS, CKey& stakerKey, CKey& ownerKey)
static CMutableTransaction CreateNewColdStakeTx(CScript& scriptP2CS, CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
{
scriptP2CS = GetNewP2CS(stakerKey, ownerKey);
scriptP2CS = GetNewP2CS(stakerKey, ownerKey, fLastOutFree);

// Create prev transaction:
CMutableTransaction txFrom;
Expand Down Expand Up @@ -122,14 +122,14 @@ static bool CheckP2CSScript(const CScript& scriptSig, const CScript& scriptPubKe
return VerifyScript(scriptSig, scriptPubKey, flags, MutableTransactionSignatureChecker(&tx, 0, amtIn), tx.GetRequiredSigVersion(), &err);
}

BOOST_AUTO_TEST_CASE(coldstake_script)
BOOST_AUTO_TEST_CASE(coldstake_lof_script)
{
SelectParams(CBaseChainParams::REGTEST);
CScript scriptP2CS;
CKey stakerKey, ownerKey;

// create unsigned coinstake transaction
CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey);
CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey, true);

// sign the input with the staker key
SignColdStake(good_tx, 0, scriptP2CS, stakerKey, true);
Expand Down Expand Up @@ -160,8 +160,8 @@ BOOST_AUTO_TEST_CASE(coldstake_script)
BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));

// Transfer more coins to the masternode
tx.vout[2].nValue -= 2 * COIN;
tx.vout[3].nValue += 2 * COIN;
tx.vout[2].nValue -= 3 * COIN;
tx.vout[3].nValue += 3 * COIN;
SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
Expand Down
20 changes: 14 additions & 6 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2705,19 +2705,27 @@ bool FindUndoPos(CValidationState& state, int nFile, CDiskBlockPos& pos, unsigne

bool CheckColdStakeFreeOutput(const CTransaction& tx, const int nHeight)
{
if (!tx.HasP2CSOutputs())
assert(tx.IsCoinStake());
// This check applies only to coinstakes spending a P2CS_LOF script.
// The script-check ensures that all but the first and the last output
// (if the coinstake has more than 3 outputs) have the same scriptPubKey.
// If the second script is not a P2CS_LOF, then either this is a "regular"
// P2PKH stake, or it fails the script verification.
if (!tx.vout[1].scriptPubKey.IsPayToColdStakingLOF()) {
return true;

}
// If the last output is different, then it can be either a masternode
// or a budget proposal payment
const unsigned int outs = tx.vout.size();
const CTxOut& lastOut = tx.vout[outs-1];
if (outs >=3 && lastOut.scriptPubKey != tx.vout[outs-2].scriptPubKey) {
if (Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_V6_0)) {
// after v6.0, masternode and budgets are paid in the coinbase. No more free outputs allowed.
return false;
}
if (lastOut.nValue == GetMasternodePayment())
return true;

// This could be a budget block.
if (Params().IsRegTestNet())
return false;

// if mnsync is incomplete, we cannot verify if this is a budget block.
// so we check that the staker is not transferring value to the free output
if (!masternodeSync.IsSynced()) {
Expand Down
4 changes: 2 additions & 2 deletions test/functional/mining_pos_coldStaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

# filter utxos based on first 5 bytes of scriptPubKey
def getDelegatedUtxos(utxos):
return [x for x in utxos if x["scriptPubKey"][:10] == '76a97b63d1']
return [x for x in utxos if x["scriptPubKey"][:10] == '76a97b63d1' or x["scriptPubKey"][:10] == '76a97b63d2']


class PIVX_ColdStakingTest(PivxTestFramework):
Expand Down Expand Up @@ -340,7 +340,7 @@ def run_test(self):
# Try to submit the block
ret = self.nodes[1].submitblock(bytes_to_hex_str(new_block.serialize()))
self.log.info("Block %s submitted." % new_block.hash)
assert_equal(ret, "bad-p2cs-outs")
assert ret in ["bad-p2cs-outs", "rejected"]

# Verify that nodes[0] rejects it
self.sync_blocks()
Expand Down

0 comments on commit df11631

Please sign in to comment.