def _create_policy_freeze_block(self, spendable_out, node): freeze_tx = self._create_tx(spendable_out, b'', CScript([OP_TRUE])) self.log.info( f"Mining block with transaction {freeze_tx.hash} whose output will be frozen later" ) self._mine_and_send_block(freeze_tx, node) self.log.info(f"Freezing TXO {freeze_tx.hash},0 on policy blacklist") result = node.rpc.addToPolicyBlacklist( {"funds": [{ "txOut": { "txId": freeze_tx.hash, "vout": 0 } }]}) assert_equal(result["notProcessed"], []) spend_frozen_tx = self._create_tx( PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE])) self.log.info( f"Mining block with transaction {spend_frozen_tx.hash} spending frozen TXO {freeze_tx.hash},0 and checking that is accepted" ) self._mine_and_send_block(spend_frozen_tx, node) # block is accepted as consensus freeze is not in effect assert_equal(node.rpc.getbestblockhash(), self.chain.tip.hash)
def _test_soft_consensus_freeze_clear_all(self, spendable_out, node): self.log.info("*** Performing soft consensus freeze on clear all") # perform initial clear so that other tests don't interfere with this one node.rpc.clearBlacklists({"removeAllEntries": True}) first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) # block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) first_frozen_block = self._mine_and_check_rejected( node, first_spend_frozen_tx) # clear all frozen entries result = node.rpc.clearBlacklists({"removeAllEntries": True}) assert_equal(result["numRemovedEntries"], 1) # block is expected to still be frozen even though we've changed the freeze # duration as once the frozen calculation is performed on a block it is # never changed self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) # all blocks are unfrozen - this proves that the old duration remained # in place self._mine_and_send_block(None, node)
def _test_default_freeze(self, spendable_out, node): self.log.info("*** Performing soft consensus freeze checks") first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) # block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) first_frozen_block = self._mine_and_check_rejected( node, first_spend_frozen_tx) # both blocks are still frozen self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) last_frozen_block = self._mine_and_send_block( None, node, False, node.rpc.getbestblockhash()) bestblockhash = node.rpc.getbestblockhash() # check that precious block does not have impact on soft freeze duration node.rpc.preciousblock(last_frozen_block.hash) # assert preciousblock did not change the tip assert (bestblockhash == node.rpc.getbestblockhash()) # all blocks are unfrozen self._mine_and_send_block(None, node)
def _test_soft_consensus_freeze_submitblock(self, spendable_out, node): self.log.info( "*** Performing soft consensus freeze with submitblock RPC") frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) last_valid_block_hash = node.rpc.getbestblockhash() # block should not become new tip as it contains transaction spending frozen TXO spend_frozen_tx = self._create_tx( PreviousSpendableOutput(frozen_tx, 0), b'', CScript([OP_TRUE])) frozen_block = self._mine_block(spend_frozen_tx) self.submit_block_and_check_tip(node, frozen_block, last_valid_block_hash) # block is still frozen self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block_hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block_hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block_hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block_hash) # all blocks are now unfrozen new_valid_block = self._mine_block(None) self.submit_block_and_check_tip(node, new_valid_block, new_valid_block.hash)
def _create_consensus_freeze_block(self, spendable_out, node): freeze_tx = self._create_tx(spendable_out, b'', CScript([OP_TRUE])) self.log.info( f"Mining block with transaction {freeze_tx.hash} whose output will be frozen later" ) self._mine_and_send_block(freeze_tx, node) self.log.info( f"Freezing TXO {freeze_tx.hash},0 on consensus blacklist") result = node.rpc.addToConsensusBlacklist({ "funds": [{ "txOut": { "txId": freeze_tx.hash, "vout": 0 }, "enforceAtHeight": [{ "start": 0 }], "policyExpiresWithConsensus": False }] }) assert_equal(result["notProcessed"], []) spend_frozen_tx = self._create_tx( PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE])) # block is rejected as consensus freeze is in effect rejected_block_hash = self._mine_and_check_rejected( spend_frozen_tx, node) return (freeze_tx.hash, rejected_block_hash)
def _test_unlimited_freeze(self, spendable_out, node): self.log.info( "*** Performing soft consensus freeze checks (unlimited)") node.restart_node() first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) last_valid_tip_hash = node.rpc.getbestblockhash() # this block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) self.log.info( f"Mining block with transaction {first_spend_frozen_tx.hash} spending TXO {first_spend_frozen_tx.vin[0].prevout.hash:064x},{first_spend_frozen_tx.vin[0].prevout.n}" ) first_frozen_block = self._mine_block(first_spend_frozen_tx) self.log.info(f"Mining descendants of block {first_frozen_block.hash}") subsequent_frozen_blocks = [] for i in range( 14 ): # since we cannot check unlimited number of blocks, number 14 was chosen arbitrarily and we assume it is close enough to infinity for the purpose of this test subsequent_frozen_blocks.append(self._mine_block(None)) # Send block headers first to check that they are also all correctly marked as frozen after the actual block is received. self.log.info( f"Sending headers for block {first_frozen_block.hash} and descendants" ) msg_hdrs = msg_headers() msg_hdrs.headers.append(first_frozen_block) msg_hdrs.headers.extend(subsequent_frozen_blocks) node.p2p.send_and_ping(msg_hdrs) # check that headers were received for tip in node.rpc.getchaintips(): if tip["status"] == "active" and tip["hash"] == last_valid_tip_hash: continue if tip["status"] == "headers-only" and tip[ "hash"] == subsequent_frozen_blocks[-1].hash: continue assert False, "Unexpected tip: " + str(tip) self.log.info( f"Sending block {first_frozen_block.hash} and checking that it is rejected" ) node.send_block(first_frozen_block, last_valid_tip_hash, True) assert (node.check_frozen_tx_log(first_frozen_block.hash)) assert (node.check_log( "Block was rejected because it included a transaction, which tried to spend a frozen transaction output.*" + first_frozen_block.hash)) self.log.info( f"Sending descendants of block {first_frozen_block.hash} and checking that they do not become tip" ) for b in subsequent_frozen_blocks: node.send_block(b, last_valid_tip_hash, False)
def _test_soft_consensus_freeze_on_refreeze(self, spendable_out, node): self.log.info( "*** Performing soft consensus freeze on refreeze checks") first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) tip_height = node.rpc.getblockcount() # block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) first_frozen_block = self._mine_and_check_rejected( node, first_spend_frozen_tx) first_frozen_block_height = tip_height + 1 freeze_for_two_blocks = first_frozen_block_height + 2 # limit the duration of freeze self.log.info( f"Freezing TXO {first_frozen_tx.hash} on consensus blacklist until height {freeze_for_two_blocks}" ) result = node.rpc.addToConsensusBlacklist({ "funds": [{ "txOut": { "txId": first_frozen_tx.hash, "vout": 0 }, "enforceAtHeight": [{ "start": 0, "stop": freeze_for_two_blocks }], "policyExpiresWithConsensus": False }] }) assert_equal(result["notProcessed"], []) # block is expected to still be frozen self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) # block is expected to still be frozen even though we've changed the freeze # duration as once the frozen calculation is performed on a block it is # never changed self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) # all blocks are unfrozen - this proves that the old duration remained # in place self._mine_and_send_block(None, node)
def get_chained_transactions(self, spend, num_of_transactions, money_to_spend=5000000000): txns = [] for _ in range(0, num_of_transactions): money_to_spend = money_to_spend - 1000 # one satoshi to fee tx = create_transaction(spend.tx, spend.n, b"", money_to_spend, self.locking_script) self.sign_tx(tx, spend.tx, spend.n) tx.rehash() txns.append(tx) spend = PreviousSpendableOutput(tx, 0) return txns
def _test_soft_consensus_freeze(self, spendable_out, node): self.log.info("*** Performing soft consensus freeze checks") first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out[0]) second_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out[1]) # block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) first_frozen_block = self._mine_and_check_rejected( node, first_spend_frozen_tx) # block is accepted but ignored since freeze is in place for previous block second_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(second_frozen_tx, 0), b'', CScript([OP_TRUE])) second_frozen_block = self._mine_and_send_block( second_spend_frozen_tx, node, False, node.rpc.getbestblockhash()) # both blocks are still frozen self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) # first block is unfrozen but since height restriction is not met due # to second block being frozen, we remain on the old tip self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) node.reject_check(second_frozen_block) # all blocks are unfrozen self._mine_and_send_block(None, node)
def get_chained_transactions(self, spend, num_of_transactions, *, money_to_spend=5000000000, bad_chain=False): ok = [] bad = [] orphan = [] bad_transaction = num_of_transactions // 2 if bad_chain else num_of_transactions for i in range(0, num_of_transactions): money_to_spend = money_to_spend - 1000 # one satoshi to fee tx = create_transaction(spend.tx, spend.n, b"", money_to_spend, self.locking_script) self.sign_tx(tx, spend.tx, spend.n, key = self.coinbase_key if i != bad_transaction else self.wrong_key) tx.rehash() txns = ok if i < bad_transaction else bad if i == bad_transaction else orphan txns.append(tx) spend = PreviousSpendableOutput(tx, 0) return ok, bad, orphan
def get_chained_txs(self, spend, num_of_txs, unlocking_script, locking_script, money_to_spend, vout_size): txns = [] for _ in range(0, num_of_txs): # Create a new transaction. tx = create_transaction(spend.tx, spend.n, unlocking_script, money_to_spend, locking_script) # Extend the number of outputs to the required vout_size size. tx.vout.extend(tx.vout * (vout_size-1)) # Sign txn. self.sign_tx(tx, spend.tx, spend.n) tx.rehash() txns.append(tx) # Use the first outpoint to spend in the second iteration. spend = PreviousSpendableOutput(tx, 0) return txns
def get_chained_transactions(self, spend, num_of_transactions): money_to_spend = 5000000000 txns = [] for _ in range(0, num_of_transactions): money_to_spend = money_to_spend - 1 # one satoshi to fee tx2 = create_transaction(spend.tx, spend.n, b"", money_to_spend, CScript([OP_TRUE])) txns.append(tx2) money_to_spend = money_to_spend - 1 tx3 = create_transaction(tx2, 0, b"", money_to_spend, scriptPubKey=CScript([OP_TRUE])) txns.append(tx3) spend = PreviousSpendableOutput(tx3, 0) return txns
def FreezeTXO0(tx): h = node.rpc.getblockcount() self.log.info(f"Current height: {h}") self.log.info(f"Freezing TXO {tx.hash},0 on consensus blacklist at heights [{h+1}, {h+3}), [{h+5}, {h+7})") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": h+1, "stop": h+3}, {"start": h+5, "stop": h+7}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) tx2=self._create_tx(PreviousSpendableOutput(tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Creating transaction {tx2.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n}") return tx2
def _test_minimal_freeze(self, spendable_out, node): self.log.info( "*** Performing soft consensus freeze checks (freeze for one block)" ) node.restart_node([ "-whitelist=127.0.0.1", "-minrelaytxfee=0", "-limitfreerelay=999999", "-softconsensusfreezeduration=1" ]) frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) # block is rejected as consensus freeze is in effect for parent transaction spend_frozen_tx = self._create_tx( PreviousSpendableOutput(frozen_tx, 0), b'', CScript([OP_TRUE])) self._mine_and_check_rejected(node, spend_frozen_tx) # this block will still be frozen self._mine_and_send_block(None, node, False, node.rpc.getbestblockhash()) # blocks are unfrozen self._mine_and_send_block(None, node)
def run_test(self): (node0, node1) = self.init_(2) out = self.chain.get_spendable_output() freeze_tx = self.create_tx_(out, b'', CScript([OP_TRUE])) self.log.info(f"Mining block with transaction {freeze_tx.hash} whose output will be frozen later") self.mine_and_send_block_(freeze_tx, node0) spend_frozen_tx = self.create_tx_(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Sending transaction {spend_frozen_tx.hash} spending TXO {freeze_tx.hash},0") node0.send_and_ping(msg_tx(spend_frozen_tx)) spend_frozen_tx2 = self.create_tx_(PreviousSpendableOutput(spend_frozen_tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Sending transaction {spend_frozen_tx2.hash} spending TXO {spend_frozen_tx.hash},0") node0.send_and_ping(msg_tx(spend_frozen_tx2)) sync_mempools(self.nodes) self.log.info("Checking that transactions were accepted on both nodes") for no in range(0, 2): mp = self.nodes[no].getrawmempool() assert_equal(len(mp), 2) assert(spend_frozen_tx.hash in mp and spend_frozen_tx2.hash in mp) template_txns = self.nodes[no].getblocktemplate()["transactions"] assert_equal(len(template_txns), 2) bt = [template_txns[0]['txid'], template_txns[1]['txid']] assert(spend_frozen_tx.hash in mp and spend_frozen_tx2.hash in bt) current_height = self.nodes[0].getblockcount() self.log.info(f"Current height: {current_height}") enforce_height = current_height + 2 self.log.info(f"Freezing TXO {freeze_tx.hash},0 on consensus blacklist at height {enforce_height} on both nodes") for no in range(0, 2): self.nodes[no].addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height}], "policyExpiresWithConsensus": False }] }); self.log.info("Checking that both transactions were removed from mempool and block template on both nodes") for no in range(0, 2): assert_equal(self.nodes[no].getrawmempool(), []) assert_equal(self.nodes[no].getblocktemplate()["transactions"], []) enforce_stop_height = enforce_height + 1 self.log.info(f"Unfreezing TXO {freeze_tx.hash},0 from consensus and policy blacklists at height {enforce_stop_height} on both nodes") for no in range(0, 2): self.nodes[no].addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height, "stop": enforce_stop_height}], "policyExpiresWithConsensus": True }] }); self.log.info(f"Generating blocks so that mempool reaches height {enforce_stop_height+1}") while self.nodes[0].getblockcount() < enforce_stop_height: self.nodes[0].generate(1) sync_blocks(self.nodes) spend_unfrozen_tx3 = self.create_tx_(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_NOP, OP_TRUE])) self.log.info(f"Sending transaction {spend_unfrozen_tx3.hash} spending now unfrozen TXO {freeze_tx.hash},0") node0.send_and_ping(msg_tx(spend_unfrozen_tx3)) spend_unfrozen_tx4 = self.create_tx_(PreviousSpendableOutput(spend_unfrozen_tx3, 0), b'', CScript([OP_NOP, OP_TRUE])) self.log.info(f"Sending transaction {spend_unfrozen_tx4.hash} spending TXO {spend_unfrozen_tx3.hash},0") node0.send_and_ping(msg_tx(spend_unfrozen_tx4)) sync_mempools(self.nodes) self.log.info("Checking that transactions were accepted on both nodes") for no in range(0, 2): mp = self.nodes[no].getrawmempool() assert_equal(len(mp), 2) assert(spend_unfrozen_tx3.hash in mp and spend_unfrozen_tx4.hash in mp) template_txns = self.nodes[no].getblocktemplate()["transactions"] assert_equal(len(template_txns), 2) bt = [template_txns[0]['txid'], template_txns[1]['txid']] assert(spend_unfrozen_tx3.hash in mp and spend_unfrozen_tx4.hash in bt) self.log.info("Invalidating chain tip on both nodes to force reorg back one block") for no in range(0, 2): self.nodes[no].invalidateblock( self.nodes[no].getbestblockhash() ) assert(self.nodes[no].getblockcount() == enforce_height) mempool_scan_check_log_string = "Removing any transactions that spend TXOs, which were previously not considered policy frozen" self.log.info("Checking that transactions are still in mempool on both nodes") for no in range(0, 2): mp = self.nodes[no].getrawmempool() assert_equal(len(mp), 2) assert(spend_unfrozen_tx3.hash in mp and spend_unfrozen_tx4.hash in mp) template_txns = self.nodes[no].getblocktemplate()["transactions"] assert_equal(len(template_txns), 2) bt = [template_txns[0]['txid'], template_txns[1]['txid']] assert(spend_unfrozen_tx3.hash in mp and spend_unfrozen_tx4.hash in bt) # bitcoind sould not unnecessarily scan whole mempool to find transactions that spend TXOs, which could become frozen again. assert( not self.check_log(self.nodes[no], mempool_scan_check_log_string) ) self.log.info("Invalidating chain tip on both nodes to force reorg back to height where TXO is still frozen") for no in range(0, 2): self.nodes[no].invalidateblock( self.nodes[no].getbestblockhash() ) assert(self.nodes[no].getblockcount() == enforce_height - 1) self.log.info("Checking that both transactions were removed from mempool and block template on both nodes") for no in range(0, 2): assert_equal(self.nodes[no].getrawmempool(), []) assert_equal(self.nodes[no].getblocktemplate()["transactions"], []) # bitcoind now should scan whole mempool. assert( self.check_log(self.nodes[no], mempool_scan_check_log_string) ) self.log.info("Unfreezing all frozen outputs on both nodes") for no in range(0, 2): self.nodes[no].invalidateblock( self.nodes[no].getbestblockhash() ) result = self.nodes[no].clearBlacklists({ "removeAllEntries" : True }) assert_equal(result["numRemovedEntries"], 1) self.log.info(f"Freezing TXO {freeze_tx.hash},0 on policy blacklist on node0 (but not on node1)") result = self.nodes[0].addToPolicyBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 } }] }); assert_equal(result["notProcessed"], []) spend_frozen_tx1a = self.create_tx_(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_NOP, OP_NOP, OP_TRUE])) self.log.info(f"Sending transaction {spend_frozen_tx1a.hash} spending not frozen TXO {freeze_tx.hash},0 to node1") node1.send_and_ping(msg_tx(spend_frozen_tx1a)) spend_frozen_tx2a = self.create_tx_(PreviousSpendableOutput(spend_frozen_tx1a, 0), b'', CScript([OP_NOP, OP_NOP, OP_TRUE])) self.log.info(f"Sending transaction {spend_frozen_tx2a.hash} spending TXO {spend_frozen_tx1a.hash},0 to node1") node1.send_and_ping(msg_tx(spend_frozen_tx2a)) self.log.info("Checking that transactions were accepted on node1") mp = self.nodes[1].getrawmempool() assert_equal(len(mp), 2) assert(spend_frozen_tx1a.hash in mp and spend_frozen_tx2a.hash in mp) time.sleep(6) # need to wait >5s for the block assembler to create new block template_txns = self.nodes[1].getblocktemplate()["transactions"] assert_equal(len(template_txns), 2) bt = [template_txns[0]['txid'], template_txns[1]['txid']] assert(spend_frozen_tx1a.hash in mp and spend_frozen_tx2a.hash in bt) self.log.info("Checking that transactions are not present in mempool on node0") assert_equal(self.nodes[0].getrawmempool(), []) self.log.info("Generate block that contains both transactions on node1") height_before_block_spending_policy_frozen_txo = self.nodes[1].getblockcount() hash_block_spending_policy_frozen_txo = self.nodes[1].generate(1)[0] sync_blocks(self.nodes) for no in range(0, 2): assert(self.nodes[no].getblockcount() == height_before_block_spending_policy_frozen_txo + 1) assert_equal(self.nodes[no].getrawmempool(), []) assert_equal(self.nodes[no].getblocktemplate()["transactions"], []) self.log.info("Invalidating chain tip on both nodes to force reorg back one block") for no in range(0, 2): self.nodes[no].invalidateblock( hash_block_spending_policy_frozen_txo ) assert(self.nodes[no].getblockcount() == height_before_block_spending_policy_frozen_txo) self.log.info("Checking that transactions are not present in mempool on node0") assert_equal(self.nodes[0].getrawmempool(), []) assert_equal(self.nodes[0].getblocktemplate()["transactions"], []) self.log.info("Checking that transactions were put back to mempool on node1") mp = self.nodes[1].getrawmempool() assert_equal(len(mp), 2) assert(spend_frozen_tx1a.hash in mp and spend_frozen_tx2a.hash in mp) template_txns = self.nodes[1].getblocktemplate()["transactions"] assert_equal(len(template_txns), 2) bt = [template_txns[0]['txid'], template_txns[1]['txid']] assert(spend_frozen_tx1a.hash in mp and spend_frozen_tx2a.hash in bt)
def _test_soft_consensus_freeze_invalidate_block(self, spendable_out, node): self.log.info( "*** Performing soft consensus freeze and invalidate block checks") first_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out[0]) second_frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out[1]) # block is rejected as consensus freeze is in effect for parent transaction first_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(first_frozen_tx, 0), b'', CScript([OP_TRUE])) first_frozen_block = self._mine_and_check_rejected( node, first_spend_frozen_tx) # block is accepted but ignored since freeze is in place for previous block second_spend_frozen_tx = self._create_tx( PreviousSpendableOutput(second_frozen_tx, 0), b'', CScript([OP_TRUE])) second_frozen_block = self._mine_and_send_block( second_spend_frozen_tx, node, False, node.rpc.getbestblockhash()) block_before_frozen_blocks_hash = node.rpc.getbestblockhash() # both blocks are still frozen self._mine_and_send_block(None, node, False, block_before_frozen_blocks_hash) self._mine_and_send_block(None, node, False, block_before_frozen_blocks_hash) self._mine_and_send_block(None, node, False, block_before_frozen_blocks_hash) # first block is unfrozen but since height restriction is not met due # to second block being frozen, we remain on the old tip self._mine_and_send_block(None, node, False, block_before_frozen_blocks_hash) node.reject_check(second_frozen_block) # save hash and time of the last soft frozen block for later last_soft_frozen_hash = self.chain.tip.hash last_soft_frozen_time = self.chain.tip.nTime # all blocks are unfrozen block = self._mine_and_send_block(None, node) node.rpc.invalidateblock(block.hash) assert (block_before_frozen_blocks_hash == node.rpc.getbestblockhash()) # check that reconsidering the block works as expected node.rpc.reconsiderblock(block.hash) assert (block.hash == node.rpc.getbestblockhash()) # check that verifychain works after node restart assert node.rpc.verifychain(4, 0) node.restart_node() assert node.rpc.verifychain(4, 0) # check that invalidateblock works after node restart node.restart_node() assert (block.hash == node.rpc.getbestblockhash()) node.rpc.invalidateblock(block.hash) assert (block_before_frozen_blocks_hash == node.rpc.getbestblockhash()) # create coinbase output that pays to much invalid_coinbase_tx = create_coinbase(height=node.rpc.getblockcount() + 1, outputValue=300) invalid_block = create_block(int(last_soft_frozen_hash, 16), invalid_coinbase_tx, last_soft_frozen_time + 1) invalid_block.solve() node.p2p.send_and_ping(msg_block(invalid_block)) assert (node.check_log( f"ConnectBlock {invalid_block.hash} failed \\(bad-cb-amount \\(code 16\\)\\)" )) # make sure tip is still the same assert (block_before_frozen_blocks_hash == node.rpc.getbestblockhash()) node.rpc.reconsiderblock(block.hash) assert (block.hash == node.rpc.getbestblockhash())
def _test_soft_consensus_freeze_competing_chains(self, spendable_txo, node): self.log.info( "*** Performing soft consensus freeze with competing chains") frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_txo) root_chain_tip = self.get_chain_tip() # mine 5 blocks on valid chain (one less than is needed for the frozen chain to become active) self.log.info("Mining blocks on valid chain") self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) last_valid_block = self._mine_and_send_block(None, node) valid_chain_tip = self.get_chain_tip() self.set_chain_tip(root_chain_tip) self.log.info("Mining blocks on frozen chain") # block should not become new tip as it contains transaction spending frozen TXO spend_frozen_tx = self._create_tx( PreviousSpendableOutput(frozen_tx, 0), b'', CScript([OP_TRUE])) frozen_block = self._mine_block(spend_frozen_tx) self.submit_block_and_check_tip(node, frozen_block, last_valid_block.hash) # next 4 blocks are also considered soft consensus frozen and must not become new tip self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) # this block is high enough for the frozen chain to become active and should become new tip new_frozen_tip = self._mine_and_send_block(None, node) frozen_chain_tip = self.get_chain_tip() self.log.info("Mining blocks on valid chain") # 2 new blocks on valid chain should trigger reorg back to valid chain self.set_chain_tip(valid_chain_tip) next_frozen_tip = self._mine_block(None) self.submit_block_and_check_tip(node, next_frozen_tip, new_frozen_tip.hash) new_valid_tip = self._mine_block(None) node.p2p.send_and_ping(msg_block(new_valid_tip)) assert_equal(new_valid_tip.hash, node.rpc.getbestblockhash()) assert ( node.check_frozen_tx_log(next_frozen_tip.hash) ) # NOTE: Reject is expected because transaction spending frozen TXO is added back to mempool and its validation must fail when checked against new tip. self.log.info("Mining blocks on frozen chain") # 2 new blocks on frozen chain should trigger reorg back to frozen chain self.set_chain_tip(frozen_chain_tip) self.submit_block_and_check_tip(node, self._mine_block(None), new_valid_tip.hash) self._mine_and_send_block(None, node)
def _test_soft_consensus_freeze_invalid_frozen_block( self, spendable_txos, node): self.log.info( "*** Performing soft consensus freeze with invalid frozen block") frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_txos[0]) root_chain_tip = self.get_chain_tip() # mine 5 blocks on valid chain (one less than is needed for the frozen chain to become active) self.log.info("Mining blocks on valid chain") self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) self._mine_and_send_block(None, node) last_valid_block = self._mine_and_send_block(None, node) self.set_chain_tip(root_chain_tip) self.log.info("Mining blocks on frozen chain") # block should not become new tip as it is not high enough # it should also be considered soft consensus frozen because it contains transaction spending frozen TXO # and is invalid because coinbase pays too much spend_frozen_tx = self._create_tx( PreviousSpendableOutput(frozen_tx, 0), b'', CScript([OP_TRUE])) frozen_block = self._mine_block(spend_frozen_tx) frozen_block.vtx[0].vout[ 0].nValue = 300 * COIN # coinbase that pays too much frozen_block.vtx[0].rehash() self.chain.update_block(self.block_count - 1, []) self.submit_block_and_check_tip(node, frozen_block, last_valid_block.hash) # next 4 blocks would also be considered soft consensus frozen and must not become new tip self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) # invalid block has not yet been validated frozen_block_block_checked_log_string = f"ConnectBlock {frozen_block.hash} failed \\(bad-cb-amount \\(code 16\\)\\)" assert (not node.check_log(frozen_block_block_checked_log_string)) # this block is high enough for the frozen chain to become active but # it should not, because the block is invalid new_frozen_tip = self._mine_and_send_block(None, node, False, last_valid_block.hash) # invalid block has now been validated assert (node.check_log(frozen_block_block_checked_log_string)) # same thing again but with frozen block that is also invalid because it contains invalid transaction self.set_chain_tip(root_chain_tip) frozen_block = self._mine_block(spend_frozen_tx) valid_tx = self._create_tx(spendable_txos[1], b'', CScript([OP_TRUE, OP_DROP] * 15)) frozen_block.vtx.extend([valid_tx]) invalid_tx = self._create_tx(PreviousSpendableOutput(valid_tx, 0), CScript([OP_FALSE]), CScript([OP_TRUE])) frozen_block.vtx.extend([invalid_tx]) self.chain.update_block(self.block_count - 1, []) self.submit_block_and_check_tip(node, frozen_block, last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) self.submit_block_and_check_tip(node, self._mine_block(None), last_valid_block.hash) frozen_block_block_checked_log_string = f"ConnectBlock {frozen_block.hash} failed \\(blk-bad-inputs, parallel script check failed \\(code 16\\)\\)" assert (not node.check_log(frozen_block_block_checked_log_string)) self._mine_and_send_block(None, node, False, last_valid_block.hash) assert (node.check_log(frozen_block_block_checked_log_string))
def run_test(self): node_cb = self._init() node = Send_node(self.options.tmpdir, self.log, 0, node_cb, self.nodes[0]) self.log.info( "*** Testing soft consensus freeze during node startup/IBD") spendable_out = self.chain.get_spendable_output() frozen_tx = self._create_tx_mine_block_and_freeze_tx( node, spendable_out) last_valid_tip_hash = node.rpc.getbestblockhash() last_valid_tip_height = node.rpc.getblockcount() # this block must not become tip because it contains a transaction trying to spend consensus frozen output spend_frozen_tx = self._create_tx( PreviousSpendableOutput(frozen_tx, 0), b'', CScript([OP_TRUE])) self._mine_and_check_rejected(node, spend_frozen_tx) # child blocks are still considered frozen self._mine_and_send_block(None, node, False, last_valid_tip_hash) self._mine_and_send_block(None, node, False, last_valid_tip_hash) self._mine_and_send_block(None, node, False, last_valid_tip_hash) node.restart_node() # must remain at the same tip as before assert_equal(last_valid_tip_hash, node.rpc.getbestblockhash()) self.log.info("Starting second node") self.start_node(1) self.log.info( f"Freezing TXO {frozen_tx.hash},0 on consensus blacklist on second node" ) result = self.nodes[1].addToConsensusBlacklist({ "funds": [{ "txOut": { "txId": frozen_tx.hash, "vout": 0 }, "enforceAtHeight": [{ "start": 0 }], "policyExpiresWithConsensus": False }] }) assert_equal(result["notProcessed"], []) self.log.info("Connecting first and second node") connect_nodes(self.nodes, 1, 0) self.log.info( f"Waiting for block height {last_valid_tip_height} via rpc on second node" ) self.nodes[1].waitforblockheight(last_valid_tip_height) self.log.info( "Checking that tip on second node stays on the last valid block") time.sleep(2) assert_equal(last_valid_tip_hash, self.nodes[1].getbestblockhash()) self.log.info("Disconnecting first and second node") disconnect_nodes(self.nodes[1], 0) # mine another block that should still be frozen self._mine_and_send_block(None, node, False, last_valid_tip_hash) self.log.info("Connecting first and second node") connect_nodes(self.nodes, 1, 0) self.log.info( "Checking that tip on second node stays on the last valid block") time.sleep(2) assert_equal(last_valid_tip_hash, self.nodes[1].getbestblockhash()) # all blocks are unfrozen new_valid_tip = self._mine_and_send_block(None, node) self.nodes[1].waitforblockheight(last_valid_tip_height + 6) assert_equal(new_valid_tip.hash, self.nodes[0].getbestblockhash()) assert_equal(new_valid_tip.hash, self.nodes[1].getbestblockhash())
def _test_policy_freeze(self, spendable_out, node): self.log.info("*** Performing policy freeze checks") freeze_tx = self._create_tx(spendable_out, b'', CScript([OP_TRUE])) self.log.info(f"Mining block with transaction {freeze_tx.hash} whose output will be frozen later") self._mine_and_send_block(freeze_tx, node) self.log.info(f"Freezing TXO {freeze_tx.hash},0 on policy blacklist") result = node.rpc.addToPolicyBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 } }] }); assert_equal(result["notProcessed"], []) spend_frozen_tx = self._create_tx(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Sending transaction spending frozen TXO {freeze_tx.hash},0 and checking that it is rejected") # must not be accepted as parent transaction is frozen node.send_tx(spend_frozen_tx, True) assert_equal(node.rpc.getrawmempool(), []) assert(node.check_frozen_tx_log(spend_frozen_tx.hash)); assert(node.check_log("Transaction was rejected because it tried to spend a frozen transaction output.*"+spend_frozen_tx.hash)); self.log.info(f"Mining block with transaction {spend_frozen_tx.hash} spending frozen TXO {freeze_tx.hash},0 and checking that is accepted") self._mine_and_send_block(spend_frozen_tx, node) # block is still accepted as consensus freeze is not in effect assert_equal(node.rpc.getbestblockhash(), self.chain.tip.hash) spend_frozen_tx2 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Sending transaction {spend_frozen_tx2.hash} spending TXO {spend_frozen_tx.hash},0 that is not yet frozen") node.send_tx(spend_frozen_tx2) assert_equal(node.rpc.getrawmempool(), [spend_frozen_tx2.hash]) self.log.info(f"Freezing TXO {spend_frozen_tx.hash},0 on policy blacklist") result = node.rpc.addToPolicyBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx.hash, "vout" : 0 } }] }); assert_equal(result["notProcessed"], []) self.log.info(f"Checking that transaction {spend_frozen_tx2.hash} is removed from mempool") assert_equal(node.rpc.getrawmempool(), []) assert(node.check_frozen_tx_log(spend_frozen_tx2.hash)); assert(node.check_log("Transaction was rejected because it tried to spend a frozen transaction output.*"+spend_frozen_tx2.hash)); self.log.info(f"Unfreezing TXO {spend_frozen_tx.hash},0 from policy blacklist") result = node.rpc.removeFromPolicyBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx.hash, "vout" : 0 } }] }); assert_equal(result["notProcessed"], []) self.log.info(f"Sending transaction {spend_frozen_tx2.hash} again and checking that it is accepted") node.send_tx(spend_frozen_tx2) assert_equal(node.rpc.getrawmempool(), [spend_frozen_tx2.hash]) self.log.info(f"Checking that transaction {spend_frozen_tx2.hash} is removed from mempool if TXO is re-frozen") result = node.rpc.addToPolicyBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx.hash, "vout" : 0 } }] }); assert_equal(result["notProcessed"], []) assert_equal(node.rpc.getrawmempool(), [])
def _test_consensus_freeze(self, spendable_out, node): self.log.info("*** Performing consensus freeze checks") # Helper to send tx and check it is rejected because of frozen inputs def SendTxAndCheckRejected(tx): self.log.info(f"Sending transaction {tx.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n} and checking that it is rejected") node.send_tx(tx, True) assert_equal(node.rpc.getrawmempool(), []) assert(node.check_frozen_tx_log(tx.hash)); assert(node.check_log("Transaction was rejected because it tried to spend a frozen transaction output.*"+tx.hash)); # Helper to send tx and check it is accepted def SendTxAndCheckAccepted(tx): self.log.info(f"Sending transaction {tx.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n} and checking that it is accepted") node.send_tx(tx) assert_equal(node.rpc.getrawmempool(), [tx.hash]) # Helper to mine block with tx and check it is rejected because of frozen inputs def MineAndCheckRejected(tx): self.log.info(f"Mining block with transaction {tx.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n} and checking that it is rejected") old_tip = self.chain.tip self._mine_and_send_block(tx, node, True) assert_equal(node.rpc.getbestblockhash(), old_tip.hash) assert(node.check_frozen_tx_log(self.chain.tip.hash)); assert(node.check_log("Block was rejected because it included a transaction, which tried to spend a frozen transaction output.*"+self.chain.tip.hash)); self._remove_last_block() # Helper to mine block with tx and check it is accepted def MineAndCheckAccepted(tx): self.log.info(f"Mining block with transaction {tx.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n} and checking that is accepted") self._mine_and_send_block(tx, node) assert_equal(node.rpc.getbestblockhash(), self.chain.tip.hash) def MineEmptyBlock(): self.log.info(f"Mining block with no transactions to increase height") self._mine_and_send_block(None, node) assert_equal(node.rpc.getbestblockhash(), self.chain.tip.hash) freeze_tx = self._create_tx(spendable_out, b'', CScript([OP_TRUE])) self.log.info(f"Mining block with transaction {freeze_tx.hash} whose output will be frozen later") self._mine_and_send_block(freeze_tx, node) self.log.info(f"Freezing TXO {freeze_tx.hash},0 on consensus blacklist") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": 0}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) # must not be accepted as parent transaction is frozen spend_frozen_tx = self._create_tx(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE])) SendTxAndCheckRejected(spend_frozen_tx) # block is rejected as consensus freeze is in effect MineAndCheckRejected(spend_frozen_tx) current_height = node.rpc.getblockcount() self.log.info(f"Current height: {current_height}") enforce_height = current_height + 2 self.log.info(f"Freezing TXO {freeze_tx.hash},0 on consensus blacklist at height {enforce_height}") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : freeze_tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) # must not be accepted even if consensus blacklist is not yet enforced spend_frozen_tx2 = self._create_tx(PreviousSpendableOutput(freeze_tx, 0), b'', CScript([OP_TRUE, OP_NOP])) SendTxAndCheckRejected(spend_frozen_tx2) # block is accepted as consensus freeze is not yet enforced at this height MineAndCheckAccepted(spend_frozen_tx2) self.log.info(f"Freezing TXO {spend_frozen_tx2.hash},0 on consensus blacklist at height {enforce_height}") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx2.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) # block is rejected as consensus freeze is enforced at this height spend_frozen_tx3 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx2, 0), b'', CScript([OP_TRUE])) MineAndCheckRejected(spend_frozen_tx3) self.log.info(f"Unfreezing TXO {spend_frozen_tx2.hash},0 from consensus blacklist at height {enforce_height+2}") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx2.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height, "stop": enforce_height+2}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) MineEmptyBlock() # block is rejected as consensus freeze is still enforced at this height spend_frozen_tx3_1 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx2, 0), b'', CScript([OP_TRUE, OP_NOP])) MineAndCheckRejected(spend_frozen_tx3_1) MineEmptyBlock() # must not be accepted because policy blacklist enforcement does not expire with consensus spend_frozen_tx3_2 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx2, 0), b'', CScript([OP_TRUE, OP_NOP, OP_NOP])) SendTxAndCheckRejected(spend_frozen_tx3_2) self.log.info(f"Unfreezing TXO {spend_frozen_tx2.hash},0 from consensus and policy blacklist at height {enforce_height+2}") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : spend_frozen_tx2.hash, "vout" : 0 }, "enforceAtHeight": [{"start": enforce_height, "stop": enforce_height+2}], "policyExpiresWithConsensus": True }] }); assert_equal(result["notProcessed"], []) # must be accepted because policy blacklist enforcement expires with consensus at this height spend_frozen_tx3_3 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx2, 0), b'', CScript([OP_TRUE, OP_NOP, OP_NOP, OP_NOP])) SendTxAndCheckAccepted(spend_frozen_tx3_3) # block is accepted as consensus freeze is not enforced anymore at this height spend_frozen_tx3_4 = self._create_tx(PreviousSpendableOutput(spend_frozen_tx2, 0), b'', CScript([OP_TRUE, OP_NOP, OP_NOP, OP_NOP, OP_NOP])) MineAndCheckAccepted(spend_frozen_tx3_4) self.log.info("*** Performing consensus freeze checks with several block height enforcement intervals") # Helper to freeze output 0 of given tx on heights [h+1,h+3), [h+5,h+7), where h is current block height # and return a tx that spends that output. def FreezeTXO0(tx): h = node.rpc.getblockcount() self.log.info(f"Current height: {h}") self.log.info(f"Freezing TXO {tx.hash},0 on consensus blacklist at heights [{h+1}, {h+3}), [{h+5}, {h+7})") result=node.rpc.addToConsensusBlacklist({ "funds": [ { "txOut" : { "txId" : tx.hash, "vout" : 0 }, "enforceAtHeight": [{"start": h+1, "stop": h+3}, {"start": h+5, "stop": h+7}], "policyExpiresWithConsensus": False }] }); assert_equal(result["notProcessed"], []) tx2=self._create_tx(PreviousSpendableOutput(tx, 0), b'', CScript([OP_TRUE])) self.log.info(f"Creating transaction {tx2.hash} spending TXO {tx.vin[0].prevout.hash:064x},{tx.vin[0].prevout.n}") return tx2 tx = spend_frozen_tx3_4 # Check first interval tx = FreezeTXO0(tx) MineAndCheckRejected(tx) # block is rejected in first interval MineEmptyBlock() MineAndCheckRejected(tx) MineEmptyBlock() SendTxAndCheckRejected(tx) # tx is rejected because policy freeze also applies in gaps between enforcement intervals MineAndCheckAccepted(tx) # block is accepted as consensus freeze is not enforced in a gap between enforcement intervals # Same as above, but check the second block in a gap between enforcement intervals tx=FreezeTXO0(tx) MineAndCheckRejected(tx) MineEmptyBlock() MineAndCheckRejected(tx) MineEmptyBlock() MineEmptyBlock() SendTxAndCheckRejected(tx) MineAndCheckAccepted(tx) # Check second interval tx=FreezeTXO0(tx) MineAndCheckRejected(tx) MineEmptyBlock() MineAndCheckRejected(tx) MineEmptyBlock() MineEmptyBlock() MineEmptyBlock() MineAndCheckRejected(tx) # block is rejected in second interval MineEmptyBlock() MineAndCheckRejected(tx) MineEmptyBlock() SendTxAndCheckRejected(tx) # tx is rejected because policy freeze also applies after enforcement intervals if policyExpiresWithConsensus=false MineAndCheckAccepted(tx) # block is accepted after the last interval