diff --git a/src/test/zerocoin_transactions_tests.cpp b/src/test/zerocoin_transactions_tests.cpp index 979ded2182c31..c99a999573544 100644 --- a/src/test/zerocoin_transactions_tests.cpp +++ b/src/test/zerocoin_transactions_tests.cpp @@ -12,13 +12,14 @@ #include "wallet/walletdb.h" #include "txdb.h" #include "zpiv/zpivmodule.h" +#include "test/test_pivx.h" #include #include using namespace libzerocoin; -BOOST_AUTO_TEST_SUITE(zerocoin_transactions_tests) +BOOST_FIXTURE_TEST_SUITE(zerocoin_transactions_tests, TestingSetup) static CWallet cWallet("unlocked.dat"); diff --git a/test/functional/fake_stake/base_test.py b/test/functional/fake_stake/base_test.py index b597de18479c7..88da198e60253 100644 --- a/test/functional/fake_stake/base_test.py +++ b/test/functional/fake_stake/base_test.py @@ -48,7 +48,9 @@ def init_test(self): :param: :return: ''' - self.log.info("\n\n*** Starting %s ***\n------------------------\n%s\n", self.__class__.__name__, self.description) + title = "*** Starting %s ***" % self.__class__.__name__ + underline = "-" * len(title) + self.log.info("\n\n%s\n%s\n%s\n", title, underline, self.description) # Global Test parameters (override in run_test) self.DEFAULT_FEE = 0.1 # Spam blocks to send in current test @@ -92,8 +94,6 @@ def create_spam_block(self, hashPrevBlock, stakingPrevOuts, height, fStakeDouble :return block: (CBlock) generated block ''' - self.log.info("Creating Spam Block") - # If not given inputs to create spam txes, use a copy of the staking inputs if len(spendingPrevOuts) == 0: spendingPrevOuts = dict(stakingPrevOuts) @@ -117,8 +117,6 @@ def create_spam_block(self, hashPrevBlock, stakingPrevOuts, height, fStakeDouble if not block.solve_stake(stakingPrevOuts): raise Exception("Not able to solve for any prev_outpoint") - self.log.info("Stake found. Signing block...") - # Sign coinstake TX and add it to the block signed_stake_tx = self.sign_stake_tx(block, stakingPrevOuts[block.prevoutStake][0], fZPoS) block.vtx.append(signed_stake_tx) @@ -130,10 +128,10 @@ def create_spam_block(self, hashPrevBlock, stakingPrevOuts, height, fStakeDouble # remove a random prevout from the list # (to randomize block creation if the same height is picked two times) - del spendingPrevOuts[choice(list(spendingPrevOuts))] + if len(spendingPrevOuts) > 0: + del spendingPrevOuts[choice(list(spendingPrevOuts))] # Create spam for the block. Sign the spendingPrevouts - self.log.info("Creating spam TXes...") for outPoint in spendingPrevOuts: value_out = int(spendingPrevOuts[outPoint][0] - self.DEFAULT_FEE * COIN) tx = create_transaction(outPoint, b"", value_out, nTime, scriptPubKey=CScript([self.block_sig_key.get_pubkey(), OP_CHECKSIG])) diff --git a/test/functional/zerocoin_publicSpend_reorg.py b/test/functional/zerocoin_publicSpend_reorg.py new file mode 100644 index 0000000000000..29d509fbde086 --- /dev/null +++ b/test/functional/zerocoin_publicSpend_reorg.py @@ -0,0 +1,208 @@ +# !/usr/bin/env python3 +# Copyright (c) 2019 The PIVX Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +''' +Covers the scenario of two valid PoS blocks with same height +and same coinstake input. +''' + +from copy import deepcopy +from io import BytesIO +import time +from test_framework.messages import CTransaction, CBlock +from test_framework.util import bytes_to_hex_str, hex_str_to_bytes, assert_equal +from fake_stake.base_test import PIVX_FakeStakeTest + + +class ZerocoinPublicSpendReorg(PIVX_FakeStakeTest): + + def run_test(self): + self.description = "Covers the reorg with a zc public spend in vtx" + self.init_test() + DENOM_TO_USE = 10 # zc denomination + INITAL_MINED_BLOCKS = 321 # First mined blocks (rewards collected to mint) + MORE_MINED_BLOCKS = 105 # More blocks mined before spending zerocoins + + # 1) Starting mining blocks + self.log.info("Mining %d blocks.." % INITAL_MINED_BLOCKS) + self.node.generate(INITAL_MINED_BLOCKS) + + # 2) Mint 2 zerocoins + self.node.mintzerocoin(DENOM_TO_USE) + self.node.generate(1) + self.node.mintzerocoin(DENOM_TO_USE) + self.node.generate(1) + + # 3) Mine additional blocks and collect the mints + self.log.info("Mining %d blocks.." % MORE_MINED_BLOCKS) + self.node.generate(MORE_MINED_BLOCKS) + self.log.info("Collecting mints...") + mints = self.node.listmintedzerocoins(True, False) + assert len(mints) == 2, "mints list has len %d (!= 2)" % len(mints) + + # 4) Get unspent coins and chain tip + self.unspent = self.node.listunspent() + block_count = self.node.getblockcount() + pastBlockHash = self.node.getblockhash(block_count) + self.log.info("Block count: %d - Current best: %s..." % (self.node.getblockcount(), self.node.getbestblockhash()[:5])) + pastBlock = CBlock() + pastBlock.deserialize(BytesIO(hex_str_to_bytes(self.node.getblock(pastBlockHash, False)))) + checkpoint = pastBlock.nAccumulatorCheckpoint + + # 5) get the raw zerocoin spend txes + self.log.info("Getting the raw zerocoin public spends...") + public_spend_A = self.node.createrawzerocoinpublicspend(mints[0].get("serial hash")) + tx_A = CTransaction() + tx_A.deserialize(BytesIO(hex_str_to_bytes(public_spend_A))) + tx_A.rehash() + public_spend_B = self.node.createrawzerocoinpublicspend(mints[1].get("serial hash")) + tx_B = CTransaction() + tx_B.deserialize(BytesIO(hex_str_to_bytes(public_spend_B))) + tx_B.rehash() + # Spending same coins to different recipients to get different txids + my_addy = "yAVWM5urwaTyhiuFQHP2aP47rdZsLUG5PH" + public_spend_A2 = self.node.createrawzerocoinpublicspend(mints[0].get("serial hash"), my_addy) + tx_A2 = CTransaction() + tx_A2.deserialize(BytesIO(hex_str_to_bytes(public_spend_A2))) + tx_A2.rehash() + public_spend_B2 = self.node.createrawzerocoinpublicspend(mints[1].get("serial hash"), my_addy) + tx_B2 = CTransaction() + tx_B2.deserialize(BytesIO(hex_str_to_bytes(public_spend_B2))) + tx_B2.rehash() + self.log.info("tx_A id: %s" % str(tx_A.hash)) + self.log.info("tx_B id: %s" % str(tx_B.hash)) + self.log.info("tx_A2 id: %s" % str(tx_A2.hash)) + self.log.info("tx_B2 id: %s" % str(tx_B2.hash)) + + + self.test_nodes[0].handle_connect() + + # 6) create block_A --> main chain + self.log.info("") + self.log.info("*** block_A ***") + self.log.info("Creating block_A [%d] with public spend tx_A in it." % (block_count + 1)) + block_A = self.new_block(block_count, pastBlock, checkpoint, tx_A) + self.log.info("Hash of block_A: %s..." % block_A.hash[:5]) + self.log.info("sending block_A...") + var = self.node.submitblock(bytes_to_hex_str(block_A.serialize())) + if var is not None: + self.log.info("result: %s" % str(var)) + raise Exception("block_A not accepted") + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count+1) + assert_equal(self.node.getbestblockhash(), block_A.hash) + self.log.info(" >> block_A connected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_A [%d]\n" % (block_count, block_count+1)) + + # 7) create block_B --> forked chain + self.log.info("*** block_B ***") + self.log.info("Creating block_B [%d] with public spend tx_B in it." % (block_count + 1)) + block_B = self.new_block(block_count, pastBlock, checkpoint, tx_B) + self.log.info("Hash of block_B: %s..." % block_B.hash[:5]) + self.log.info("sending block_B...") + var = self.node.submitblock(bytes_to_hex_str(block_B.serialize())) + self.log.info("result of block_B submission: %s" % str(var)) + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count+1) + assert_equal(self.node.getbestblockhash(), block_A.hash) + # block_B is not added. Chain remains the same + self.log.info(" >> block_B not connected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_A [%d]\n" % (block_count, block_count+1)) + + # 8) Create new block block_C on the forked chain (block_B) + block_count += 1 + self.log.info("*** block_C ***") + self.log.info("Creating block_C [%d] on top of block_B triggering the reorg" % (block_count + 1)) + block_C = self.new_block(block_count, block_B, checkpoint) + self.log.info("Hash of block_C: %s..." % block_C.hash[:5]) + self.log.info("sending block_C...") + var = self.node.submitblock(bytes_to_hex_str(block_C.serialize())) + if var is not None: + self.log.info("result: %s" % str(var)) + raise Exception("block_C not accepted") + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count+1) + assert_equal(self.node.getbestblockhash(), block_C.hash) + self.log.info(" >> block_A disconnected / block_B and block_C connected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_B [%d] --> block_C [%d]\n" % ( + block_count - 1, block_count, block_count+1 + )) + + # 7) Now create block_D which tries to spend same coin of tx_B again on the (new) main chain + # (this block will be rejected) + block_count += 1 + self.log.info("*** block_D ***") + self.log.info("Creating block_D [%d] trying to double spend the coin of tx_B" % (block_count + 1)) + block_D = self.new_block(block_count, block_C, checkpoint, tx_B2) + self.log.info("Hash of block_D: %s..." % block_D.hash[:5]) + self.log.info("sending block_D...") + var = self.node.submitblock(bytes_to_hex_str(block_D.serialize())) + self.log.info("result of block_D submission: %s" % str(var)) + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count) + assert_equal(self.node.getbestblockhash(), block_C.hash) + # block_D is not added. Chain remains the same + self.log.info(" >> block_D rejected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_B [%d] --> block_C [%d]\n" % ( + block_count - 2, block_count - 1, block_count + )) + + # 8) Now create block_E which spends tx_A again on main chain + # (this block will be accepted and connected since tx_A was spent on block_A now disconnected) + self.log.info("*** block_E ***") + self.log.info("Creating block_E [%d] trying spend tx_A on main chain" % (block_count + 1)) + block_E = self.new_block(block_count, block_C, checkpoint, tx_A) + self.log.info("Hash of block_E: %s..." % block_E.hash[:5]) + self.log.info("sending block_E...") + var = self.node.submitblock(bytes_to_hex_str(block_E.serialize())) + if var is not None: + self.log.info("result: %s" % str(var)) + raise Exception("block_E not accepted") + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count+1) + assert_equal(self.node.getbestblockhash(), block_E.hash) + self.log.info(" >> block_E connected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_B [%d] --> block_C [%d] --> block_E [%d]\n" % ( + block_count - 2, block_count - 1, block_count, block_count+1 + )) + + # 9) Now create block_F which tries to double spend the coin in tx_A + # # (this block will be rejected) + block_count += 1 + self.log.info("*** block_F ***") + self.log.info("Creating block_F [%d] trying to double spend the coin in tx_A" % (block_count + 1)) + block_F = self.new_block(block_count, block_E, checkpoint, tx_A2) + self.log.info("Hash of block_F: %s..." % block_F.hash[:5]) + self.log.info("sending block_F...") + var = self.node.submitblock(bytes_to_hex_str(block_F.serialize())) + self.log.info("result of block_F submission: %s" % str(var)) + time.sleep(2) + assert_equal(self.node.getblockcount(), block_count) + assert_equal(self.node.getbestblockhash(), block_E.hash) + self.log.info(" >> block_F rejected <<") + self.log.info("Current chain: ... --> block_0 [%d] --> block_B [%d] --> block_C [%d] --> block_E [%d]\n" % ( + block_count - 3, block_count - 2, block_count - 1, block_count + )) + self.log.info("All good.") + + + + def new_block(self, block_count, prev_block, checkpoint, zcspend = None): + if prev_block.hash is None: + prev_block.rehash() + staking_utxo_list = [self.unspent.pop()] + pastBlockHash = prev_block.hash + stakingPrevOuts = self.get_prevouts(staking_utxo_list, block_count) + block = self.create_spam_block(pastBlockHash, stakingPrevOuts, block_count + 1) + if zcspend is not None: + block.vtx.append(zcspend) + block.hashMerkleRoot = block.calc_merkle_root() + block.nAccumulatorCheckpoint = checkpoint + block.rehash() + block.sign_block(self.block_sig_key) + return block + +if __name__ == '__main__': + ZerocoinPublicSpendReorg().main()