Skip to content

Commit

Permalink
Merge pull request #679 from qtumproject/djaen/fix-duplicate-stake
Browse files Browse the repository at this point in the history
Fix for duplicate stakes
  • Loading branch information
qtum-neil authored May 9, 2019
2 parents 8a91804 + e94f5f4 commit 63e544d
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 4 deletions.
16 changes: 12 additions & 4 deletions src/pos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,26 @@ bool CheckKernel(CBlockIndex* pindexPrev, unsigned int nBits, uint32_t nTimeBloc
//not found in cache (shouldn't happen during staking, only during verification which does not use cache)
Coin coinPrev;
if(!view.GetCoin(prevout, coinPrev)){
return false;
if(pindexPrev->GetBlockHash() != chainActive.Tip()->pprev->GetBlockHash()) {
return error("CheckKernel(): Could not find coin and did not fork at tip");
}

if(!GetSpentCoinFromTip(prevout, &coinPrev)) {
return error("CheckKernel(): Could not find coin and it was not at the tip");
}

LogPrintf("CheckKernel(): Uses spent stake from tip\n");
}

if(pindexPrev->nHeight + 1 - coinPrev.nHeight < COINBASE_MATURITY){
return false;
return error("CheckKernel(): Coin not matured");
}
CBlockIndex* blockFrom = pindexPrev->GetAncestor(coinPrev.nHeight);
if(!blockFrom) {
return false;
return error("CheckKernel(): Could not find block");
}
if(coinPrev.IsSpent()){
return false;
return error("CheckKernel(): Coin is spent");
}

return CheckStakeKernelHash(pindexPrev, nBits, blockFrom->nTime, coinPrev.out.nValue, prevout,
Expand Down
37 changes: 37 additions & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,43 @@ static int64_t nTimeTotal = 0;
static int64_t nBlocksTotal = 0;

/////////////////////////////////////////////////////////////////////// qtum
bool GetSpentCoinFromTip(COutPoint prevout, Coin* coin) {
CBlockIndex* tip = chainActive.Tip();
std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>();
CBlock& block = *pblock;
if (!ReadBlockFromDisk(block, tip, Params().GetConsensus())) {
return error("GetSpentCoinFromTip(): Could not read block from disk");
}

for(size_t j = 1; j < block.vtx.size(); ++j) {
CTransactionRef& tx = block.vtx[j];
for(size_t k = 0; k < tx->vin.size(); ++k) {
const COutPoint& tmpprevout = tx->vin[k].prevout;
if(tmpprevout == prevout) {
CBlockUndo undo;
if(!UndoReadFromDisk(undo, tip)) {
return error("GetSpentCoinFromTip(): Could not read undo block from disk");
}

if(undo.vtxundo.size() != block.vtx.size() - 1) {
return error("GetSpentCoinFromTip(): undo tx size not equal to block tx size");
}

CTxUndo &txundo = undo.vtxundo[j-1]; // no vtxundo for coinbase

if(txundo.vprevout.size() != tx->vin.size()) {
return error("GetSpentCoinFromTip(): undo tx vin size not equal to block tx vin size");
}

*coin = txundo.vprevout[k];
return true;
}

}
}
return false;
}

bool CheckSenderScript(const CCoinsViewCache& view, const CTransaction& tx){
CScript script = view.AccessCoin(tx.vin[0].prevout).out.scriptPubKey;
if(!script.IsPayToPubkeyHash() && !script.IsPayToPubkey()){
Expand Down
2 changes: 2 additions & 0 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ inline bool IsBlockPruned(const CBlockIndex* pblockindex)
bool CheckReward(const CBlock& block, CValidationState& state, int nHeight, const Consensus::Params& consensusParams, CAmount nFees, CAmount gasRefunds, CAmount nActualStakeReward, const std::vector<CTxOut>& vouts);

//////////////////////////////////////////////////////// qtum
bool GetSpentCoinFromTip(COutPoint prevout, Coin* coin);

std::vector<ResultExecute> CallContract(const dev::Address& addrContract, std::vector<unsigned char> opcode, const dev::Address& sender = dev::Address(), uint64_t gasLimit=0);

bool CheckSenderScript(const CCoinsViewCache& view, const CTransaction& tx);
Expand Down
153 changes: 153 additions & 0 deletions test/functional/qtum_duplicate_stake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env python3

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import *
from test_framework.script import *
from test_framework.mininode import *
from test_framework.messages import *
from test_framework.qtum import *
import time


class QtumDuplicateStakeTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2

def start_p2p_connection(self):
self.p2p_node = self.node.add_p2p_connection(P2PInterface())
self.p2p_alt_node = self.nodes[1].add_p2p_connection(P2PInterface())

def verify_duplicate_stakes_are_accepted_test(self):
tip = self.node.getblock(self.node.getbestblockhash())
t = (tip['time']+0x10) & 0xfffffff0
# Create one "normal" block
block, block_sig_key = create_unsigned_pos_block(self.node, self.staking_prevouts, nTime=t)
block.sign_block(block_sig_key)
block.rehash()

# Create a slightly different block using the same staking utxo (only difference is the nonce)
alt_block = CBlock(block)
alt_block.vtx = block.vtx[:]
alt_block.nNone = 1
alt_block.rehash()
alt_block.sign_block(block_sig_key)
alt_block.rehash()

# Send <block> to node
self.p2p_node.send_message(msg_block(block))
# Send <alt_block> to alt_node
self.p2p_alt_node.send_message(msg_block(alt_block))
time.sleep(2)

assert_equal(self.node.getbestblockhash(), block.hash)
assert_equal(self.alt_node.getbestblockhash(), alt_block.hash)

# Build a longer chain on alt_node
self.alt_node.generate(1)
self.sync_all()

def verify_spent_stake_is_accepted_in_fork_test(self):
tip = self.node.getblock(self.node.getbestblockhash())
t = (tip['time']+0x10) & 0xfffffff0
# Create one "normal" block
block, block_sig_key = create_unsigned_pos_block(self.node, self.staking_prevouts, nTime=t)
block.sign_block(block_sig_key)
block.rehash()

# Create a different block that spends the prevoutStake from <block>
alt_block, alt_block_sig_key = create_unsigned_pos_block(self.alt_node, self.alt_staking_prevouts, nTime=t)
tx = CTransaction()
tx.vin = [CTxIn(block.prevoutStake)]
tx.vout = [CTxOut(int(COIN), scriptPubKey=CScript([OP_TRUE]))]
tx = rpc_sign_transaction(self.node, tx)
alt_block.vtx.append(tx)
alt_block.hashMerkleRoot = alt_block.calc_merkle_root()
alt_block.rehash()
alt_block.sign_block(alt_block_sig_key)
alt_block.rehash()

# Send <alt_block> to alt_node
self.p2p_alt_node.send_message(msg_block(alt_block))
# Send <block> to node
self.p2p_node.send_message(msg_block(block))
time.sleep(2)

assert_equal(self.node.getbestblockhash(), block.hash)
assert_equal(self.alt_node.getbestblockhash(), alt_block.hash)
# Build a longer chain on alt_node
self.alt_node.generate(1)
self.sync_all()

def verify_spent_stake_in_old_block_is_rejected_test(self):
tip = self.node.getblock(self.node.getbestblockhash())
t = (tip['time']+0x10) & 0xfffffff0
# Create one "normal" block
block, block_sig_key = create_unsigned_pos_block(self.node, self.staking_prevouts, nTime=t)
block.sign_block(block_sig_key)
block.rehash()

# Create a different block that spends the prevoutStake from <block>
alt_block, alt_block_sig_key = create_unsigned_pos_block(self.alt_node, self.alt_staking_prevouts, nTime=t)
tx = CTransaction()
tx.vin = [CTxIn(block.prevoutStake)]
tx.vout = [CTxOut(int(COIN), scriptPubKey=CScript([OP_TRUE]))]
tx = rpc_sign_transaction(self.node, tx)
alt_block.vtx.append(tx)
alt_block.hashMerkleRoot = alt_block.calc_merkle_root()
alt_block.rehash()
alt_block.sign_block(alt_block_sig_key)
alt_block.rehash()

# Send <alt_block> to alt_node
self.p2p_alt_node.send_message(msg_block(alt_block))
time.sleep(2)
self.alt_node.generate(1)
time.sleep(2)

# Send <block> to node
self.p2p_node.send_message(msg_block(block))
time.sleep(2)
assert_raises_rpc_error(-5, "Block not found", self.node.getblockheader, block.hash)

time.sleep(2)
self.sync_all()

def run_test(self):
self.node = self.nodes[0]
self.alt_node = self.nodes[1]
self.node.setmocktime(int(time.time() - 100*24*60*60))
self.alt_node.setmocktime(int(time.time() - 100*24*60*60))
self.alt_node.generate(50)
self.sync_all()
self.node.generate(550)
self.sync_all()
self.staking_prevouts = collect_prevouts(self.node)
self.alt_staking_prevouts = collect_prevouts(self.alt_node)
self.node.setmocktime(0)
self.alt_node.setmocktime(0)
self.start_p2p_connection()

time.sleep(0x10)

self.verify_duplicate_stakes_are_accepted_test()
assert_equal(self.node.getblockcount(), self.alt_node.getblockcount())
assert_equal(self.node.getbestblockhash(), self.alt_node.getbestblockhash())

time.sleep(0x10)

self.staking_prevouts = collect_prevouts(self.node)
self.alt_staking_prevouts = collect_prevouts(self.alt_node)
self.verify_spent_stake_is_accepted_in_fork_test()
assert_equal(self.node.getblockcount(), self.alt_node.getblockcount())
assert_equal(self.node.getbestblockhash(), self.alt_node.getbestblockhash())

time.sleep(0x10)

self.staking_prevouts = collect_prevouts(self.node)
self.alt_staking_prevouts = collect_prevouts(self.alt_node)
self.verify_spent_stake_in_old_block_is_rejected_test()


if __name__ == '__main__':
QtumDuplicateStakeTest().main()

0 comments on commit 63e544d

Please sign in to comment.