class FrozenTXOTransactionMining(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.chain = ChainManager() self.extra_args = [["-disablesafemode=1", "-minrelaytxfee=0", "-limitfreerelay=999999", "-blockmintxfee=0", "-blockassembler=legacy"], ["-disablesafemode=1", "-minrelaytxfee=0", "-limitfreerelay=999999", "-blockmintxfee=0", "-blockassembler=journaling"]] self.block_count = 0 def init_(self, nodes_count): nodes = [] for no in range(0, nodes_count): # Create a P2P connections node = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(no), self.nodes[no], node) node.add_connection(connection) nodes.append(node) NetworkThread().start() for no in range(0, nodes_count): # wait_for_verack ensures that the P2P connection is fully up. nodes[no].wait_for_verack() self.init_chain_(nodes[0], nodes_count) return nodes def init_chain_(self, leading_node, nodes_count): self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() leading_node.send_message(msg_block(block)) for i in range(100): block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() leading_node.send_message(msg_block(block)) self.log.info("Waiting for block height 101 via rpc") for no in range(0, nodes_count): self.nodes[no].waitforblockheight(101) def mine_and_send_block_(self, tx, node): block = self.chain.next_block(self.block_count) self.chain.update_block(self.block_count, [tx]) node.send_and_ping(msg_block(block)) self.block_count += 1 self.log.debug(f"attempted mining block: {block.hash}") assert_equal(block.hash, self.nodes[0].getbestblockhash()) def create_tx_(self, tx_out, unlock, lock): unlock_script = b'' if callable(unlock) else unlock tx = create_transaction(tx_out.tx, tx_out.n, unlock_script, 1, lock) if callable(unlock): tx.vin[0].scriptSig = unlock(tx, tx_out.tx) tx.calc_sha256() return tx def check_log(self, node, line_text): for line in open(glob.glob(node.datadir + "/regtest/bitcoind.log")[0]): if re.search(line_text, line) is not None: self.log.debug("Found line in bitcoind.log: %s", line.strip()) return True return False 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)
class PBVWithSigOps(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-whitelist=127.0.0.1"]] self.coinbase_key = CECKey() self.coinbase_key.set_secretbytes(b"horsebattery") self.coinbase_pubkey = self.coinbase_key.get_pubkey() self.chain = ChainManager() def sign_expensive_tx(self, tx, spend_tx, n, sigChecks): sighash = SignatureHashForkId( spend_tx.vout[n].scriptPubKey, tx, 0, SIGHASH_ALL | SIGHASH_FORKID, spend_tx.vout[n].nValue) tx.vin[0].scriptSig = CScript( [self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])), self.coinbase_pubkey] * sigChecks + [self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])), self.coinbase_pubkey]) def get_hard_transactions(self, spend, money_to_spend, num_of_transactions, num_of_sig_checks, expensive_script): 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(expensive_script)) sign_tx(tx2, spend.tx, spend.n, self.coinbase_key) tx2.rehash() txns.append(tx2) money_to_spend = money_to_spend - 1 tx3 = create_transaction(tx2, 0, b"", money_to_spend, scriptPubKey=CScript([OP_TRUE])) self.sign_expensive_tx(tx3, tx2, 0, num_of_sig_checks) tx3.rehash() txns.append(tx3) spend = PreviousSpendableOutput(tx3, 0) return txns def run_test(self): block_count = 0 # Create a P2P connection node0 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0) node0.add_connection(connection) network_thread = NetworkThread() network_thread.start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) _, out, block_count = prepare_init_chain(self.chain, 101, 100, block_0=False, start_block=0, node=node0) self.log.info("waiting for block height 101 via rpc") self.nodes[0].waitforblockheight(101) block1_num = block_count - 1 # num of sig operations in one transaction num_of_sig_checks = 70 expensive_scriptPubKey = [OP_DUP, OP_HASH160, hash160(self.coinbase_pubkey), OP_EQUALVERIFY, OP_CHECKSIG, OP_DROP] * num_of_sig_checks + [OP_DUP, OP_HASH160, hash160( self.coinbase_pubkey), OP_EQUALVERIFY, OP_CHECKSIG] money_to_spend = 5000000000 spend = out[0] block2_hard = self.chain.next_block(block_count) # creates 4000 hard transaction and 4000 transaction to spend them. It will be 8k transactions in total add_txns = self.get_hard_transactions(spend, money_to_spend=money_to_spend, num_of_transactions=4000, num_of_sig_checks=num_of_sig_checks, expensive_script=expensive_scriptPubKey) self.chain.update_block(block_count, add_txns) block_count += 1 self.log.info(f"block2_hard hash: {block2_hard.hash}") self.chain.set_tip(block1_num) block3_easier = self.chain.next_block(block_count) add_txns = self.get_hard_transactions(spend, money_to_spend=money_to_spend, num_of_transactions=1000, num_of_sig_checks=num_of_sig_checks, expensive_script=expensive_scriptPubKey) self.chain.update_block(block_count, add_txns) self.log.info(f"block3_easier hash: {block3_easier.hash}") node0.send_message(msg_block(block2_hard)) node0.send_message(msg_block(block3_easier)) def wait_for_log(): text_activation = f"Block {block2_hard.hash} was not activated as best" text_block2 = "Verify 8000 txins" text_block3 = "Verify 2000 txins" results = 0 for line in open(glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if text_activation in line: results += 1 elif text_block2 in line: results += 1 elif text_block3 in line: results += 1 return True if results == 3 else False # wait that everything is written to the log # try accounting for slower machines by having a large timeout wait_until(wait_for_log, timeout=120) text_activation = f"Block {block2_hard.hash} was not activated as best" text_block2 = "Verify 8000 txins" text_block3 = "Verify 2000 txins" for line in open(glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if text_activation in line: self.log.info(f"block2_hard was not activated as block3_easy won the validation race") elif text_block2 in line: line = line.split() self.log.info(f"block2_hard took {line[len(line) - 1]} to verify") elif text_block3 in line: line = line.split() self.log.info(f"block3_easy took {line[len(line)-1]} to verify") assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash()) node0.connection.close()
class PBVWithSigOps(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-whitelist=127.0.0.1"]] self.coinbase_key = CECKey() self.coinbase_key.set_secretbytes(b"horsebattery") self.coinbase_pubkey = self.coinbase_key.get_pubkey() self.chain = ChainManager() def sign_tx(self, tx, spend_tx, n): scriptPubKey = bytearray(spend_tx.vout[n].scriptPubKey) if (scriptPubKey[0] == OP_TRUE): # an anyone-can-spend tx.vin[0].scriptSig = CScript() return sighash = SignatureHashForkId(spend_tx.vout[n].scriptPubKey, tx, 0, SIGHASH_ALL | SIGHASH_FORKID, spend_tx.vout[n].nValue) tx.vin[0].scriptSig = CScript([ self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])) ]) def sign_expensive_tx(self, tx, spend_tx, n, sigChecks): sighash = SignatureHashForkId(spend_tx.vout[n].scriptPubKey, tx, 0, SIGHASH_ALL | SIGHASH_FORKID, spend_tx.vout[n].nValue) tx.vin[0].scriptSig = CScript([ self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])), self.coinbase_pubkey ] * sigChecks + [ self.coinbase_key.sign(sighash) + bytes(bytearray([SIGHASH_ALL | SIGHASH_FORKID])), self.coinbase_pubkey ]) def next_block(self, number, spend=None, additional_coinbase_value=0, script=CScript([OP_TRUE])): if self.chain.tip == None: base_block_hash = self.chain._genesis_hash block_time = int(time.time()) + 1 else: base_block_hash = self.chain.tip.sha256 block_time = self.chain.tip.nTime + 1 # First create the coinbase height = self.chain.block_heights[base_block_hash] + 1 coinbase = create_coinbase(height, self.coinbase_pubkey) coinbase.vout[0].nValue += additional_coinbase_value coinbase.rehash() if spend == None: block = create_block(base_block_hash, coinbase, block_time) else: # All but one satoshi for each txn to fees for s in spend: coinbase.vout[0].nValue += s.tx.vout[s.n].nValue - 1 coinbase.rehash() block = create_block(base_block_hash, coinbase, block_time) # Add as many txns as required for s in spend: # Spend 1 satoshi tx = create_transaction(s.tx, s.n, b"", 1, script) self.sign_tx(tx, s.tx, s.n) self.chain.add_transactions_to_block(block, [tx]) block.hashMerkleRoot = block.calc_merkle_root() # Do PoW, which is very inexpensive on regnet block.solve() self.chain.tip = block self.chain.block_heights[block.sha256] = height assert number not in self.chain.blocks self.chain.blocks[number] = block return block def get_hard_transactions(self, spend, money_to_spend, num_of_transactions, num_of_sig_checks, expensive_script): 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(expensive_script)) self.sign_tx(tx2, spend.tx, spend.n) tx2.rehash() txns.append(tx2) money_to_spend = money_to_spend - 1 tx3 = create_transaction(tx2, 0, b"", money_to_spend, scriptPubKey=CScript([OP_TRUE])) self.sign_expensive_tx(tx3, tx2, 0, num_of_sig_checks) tx3.rehash() txns.append(tx3) spend = PreviousSpendableOutput(tx3, 0) return txns def run_test(self): block_count = 0 # Create a P2P connection node0 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0) node0.add_connection(connection) network_thread = NetworkThread() network_thread.start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) block = self.chain.next_block(block_count) block_count += 1 self.chain.save_spendable_output() node0.send_message(msg_block(block)) for i in range(100): block = self.next_block(block_count) block_count += 1 self.chain.save_spendable_output() node0.send_message(msg_block(block)) out = [] for i in range(100): out.append(self.chain.get_spendable_output()) self.log.info("waiting for block height 101 via rpc") self.nodes[0].waitforblockheight(101) block1_num = block_count - 1 # num of sig operations in one transaction num_of_sig_checks = 70 expensive_scriptPubKey = [ OP_DUP, OP_HASH160, hash160(self.coinbase_pubkey), OP_EQUALVERIFY, OP_CHECKSIG, OP_DROP ] * num_of_sig_checks + [ OP_DUP, OP_HASH160, hash160(self.coinbase_pubkey), OP_EQUALVERIFY, OP_CHECKSIG ] money_to_spend = 5000000000 spend = out[0] block2_hard = self.next_block(block_count) # creates 4000 hard transaction and 4000 transaction to spend them. It will be 8k transactions in total add_txns = self.get_hard_transactions( spend, money_to_spend=money_to_spend, num_of_transactions=4000, num_of_sig_checks=num_of_sig_checks, expensive_script=expensive_scriptPubKey) self.chain.update_block(block_count, add_txns) block_count += 1 self.log.info(f"block2_hard hash: {block2_hard.hash}") self.chain.set_tip(block1_num) block3_easier = self.next_block(block_count) add_txns = self.get_hard_transactions( spend, money_to_spend=money_to_spend, num_of_transactions=1000, num_of_sig_checks=num_of_sig_checks, expensive_script=expensive_scriptPubKey) self.chain.update_block(block_count, add_txns) self.log.info(f"block3_easier hash: {block3_easier.hash}") node0.send_message(msg_block(block2_hard)) node0.send_message(msg_block(block3_easier)) def wait_for_log(): text_activation = f"Block {block2_hard.hash} was not activated as best" text_block2 = "Verify 8000 txins" text_block3 = "Verify 2000 txins" results = 0 for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if text_activation in line: results += 1 elif text_block2 in line: results += 1 elif text_block3 in line: results += 1 return True if results == 3 else False # wait that everything is written to the log # try accounting for slower machines by having a large timeout wait_until(wait_for_log, timeout=120) text_activation = f"Block {block2_hard.hash} was not activated as best" text_block2 = "Verify 8000 txins" text_block3 = "Verify 2000 txins" for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if text_activation in line: self.log.info( f"block2_hard was not activated as block3_easy won the validation race" ) elif text_block2 in line: line = line.split() self.log.info( f"block2_hard took {line[len(line) - 1]} to verify") elif text_block3 in line: line = line.split() self.log.info( f"block3_easy took {line[len(line)-1]} to verify") assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash()) node0.connection.close()
class PBVReorg(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = ChainManager() self.extra_args = [["-whitelist=127.0.0.1", "-relaypriority=0"]] def run_test(self): block_count = 0 # Create a P2P connections node0 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0) node0.add_connection(connection) node1 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node1) node1.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) block = self.chain.next_block(block_count) block_count += 1 self.chain.save_spendable_output() node0.send_message(msg_block(block)) num_blocks = 150 for i in range(num_blocks): block = self.chain.next_block(block_count) block_count += 1 self.chain.save_spendable_output() node0.send_message(msg_block(block)) out = [] for i in range(num_blocks): out.append(self.chain.get_spendable_output()) self.log.info("waiting for block height 151 via rpc") self.nodes[0].waitforblockheight(num_blocks + 1) tip_block_num = block_count - 1 # left branch block2 = self.chain.next_block(block_count, spend=out[0:9], extra_txns=8) block_count += 1 node0.send_message(msg_block(block2)) self.log.info(f"block2 hash: {block2.hash}") self.nodes[0].waitforblockheight(num_blocks + 2) # send blocks 3,4 for parallel validation on left branch self.chain.set_tip(tip_block_num) block3 = self.chain.next_block(block_count, spend=out[10:19], extra_txns=10) block_count += 1 block4 = self.chain.next_block(block_count, spend=out[20:29], extra_txns=8) block_count += 1 # send two "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"block3 hash: {block3.hash}") self.log.info(f"block4 hash: {block4.hash}") self.nodes[0].waitaftervalidatingblock(block4.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block4.hash}, self.nodes[0], self.log) node1.send_message(msg_block(block3)) node1.send_message(msg_block(block4)) # make sure we started validating blocks wait_for_validating_blocks({block4.hash}, self.nodes[0], self.log) # right branch self.chain.set_tip(tip_block_num) block5 = self.chain.next_block(block_count) # Add some txns from block2 & block3 to block5, just to check that they get # filtered from the mempool and not re-added block5_duplicated_txns = block3.vtx[1:3] + block2.vtx[1:3] self.chain.update_block(block_count, block5_duplicated_txns) block_count += 1 node0.send_message(msg_block(block5)) self.log.info(f"block5 hash: {block5.hash}") # and two blocks to extend second branch to cause reorg # - they must be sent from the same node as otherwise they will be # rejected with "prev block not found" as we don't wait for the first # block to arrive so there is a race condition which block is seen # first when using multiple connections block6 = self.chain.next_block(block_count) node0.send_message(msg_block(block6)) self.log.info(f"block6 hash: {block6.hash}") block_count += 1 block7 = self.chain.next_block(block_count) node0.send_message(msg_block(block7)) self.log.info(f"block7 hash: {block7.hash}") block_count += 1 self.nodes[0].waitforblockheight(num_blocks + 4) assert_equal(block7.hash, self.nodes[0].getbestblockhash()) self.log.info( "releasing wait status on parallel blocks to finish their validation" ) self.nodes[0].waitaftervalidatingblock(block4.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # block that arrived last on competing chain should be active assert_equal(block7.hash, self.nodes[0].getbestblockhash()) # make sure that transactions from block2 and 3 (except coinbase, and those also # in block 5) are in mempool not_expected_in_mempool = set() for txn in block5_duplicated_txns: not_expected_in_mempool.add(txn.hash) expected_in_mempool = set() for txn in block2.vtx[1:] + block3.vtx[1:]: expected_in_mempool.add(txn.hash) expected_in_mempool = expected_in_mempool.difference( not_expected_in_mempool) mempool = self.nodes[0].getrawmempool() assert_equal(expected_in_mempool, set(mempool))
class FrozenTXOTransactionFreeze(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = ChainManager() self.extra_args = [["-whitelist=127.0.0.1", "-minrelaytxfee=0", "-limitfreerelay=999999"]] self.block_count = 0 def _init(self): node_no = 0 # Create a P2P connections node = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[node_no], node) node.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node.wait_for_verack() self.chain.set_genesis_hash(int(self.nodes[node_no].getbestblockhash(), 16)) block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() node.send_message(msg_block(block)) for i in range(100): block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() node.send_message(msg_block(block)) self.log.info("Waiting for block height 101 via rpc") self.nodes[node_no].waitforblockheight(101) return node def _create_tx(self, tx_out, unlock, lock): unlock_script = b'' if callable(unlock) else unlock tx = create_transaction(tx_out.tx, tx_out.n, unlock_script, 1, lock) if callable(unlock): tx.vin[0].scriptSig = unlock(tx, tx_out.tx) tx.calc_sha256() return tx def _mine_and_send_block(self, tx, node, expect_reject = False): block = self.chain.next_block(self.block_count) self.chain.update_block(self.block_count, [tx] if tx else []) self.log.debug(f"attempting mining block: {block.hash}") node.send_block(block, expect_reject) self.block_count += 1 def _remove_last_block(self): # remove last block from chain manager del self.chain.block_heights[self.chain.blocks[self.block_count-1].sha256] del self.chain.blocks[self.block_count-1] self.block_count -= 1 self.chain.set_tip(self.block_count-1) 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 def run_test(self): node = self._init() out_policy_freeze_txo_p2p = self.chain.get_spendable_output() out_consensus_freeze_txo_p2p = self.chain.get_spendable_output() out_policy_freeze_txo_rpc = self.chain.get_spendable_output() out_consensus_freeze_txo_rpc = self.chain.get_spendable_output() # p2p send test p2p_send_node = P2P_send_node(self.options.tmpdir, self.log, 0, node, self.nodes[0]) self._test_policy_freeze(out_policy_freeze_txo_p2p, p2p_send_node) self._test_consensus_freeze(out_consensus_freeze_txo_p2p, p2p_send_node) # rpc send test rpc_send_node = RPC_send_node(self.options.tmpdir, self.log, 0, node, self.nodes[0]) self._test_policy_freeze(out_policy_freeze_txo_rpc, rpc_send_node) self._test_consensus_freeze(out_consensus_freeze_txo_rpc, rpc_send_node)
class FrozenTXOReindex(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = ChainManager() self.block_count = 0 def _init(self): node_no = 0 # Create a P2P connections node = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[node_no], node) node.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node.wait_for_verack() self.chain.set_genesis_hash( int(self.nodes[node_no].getbestblockhash(), 16)) block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() node.send_message(msg_block(block)) for i in range(100): block = self.chain.next_block(self.block_count) self.block_count += 1 self.chain.save_spendable_output() node.send_message(msg_block(block)) self.log.info("Waiting for block height 101 via rpc") self.nodes[node_no].waitforblockheight(101) return node def _create_tx(self, tx_out, unlock, lock): unlock_script = b'' if callable(unlock) else unlock return create_transaction(tx_out.tx, tx_out.n, unlock_script, 1, lock) def _mine_and_send_block(self, tx, node, expect_reject=False): block = self.chain.next_block(self.block_count) self.chain.update_block(self.block_count, [tx] if tx else []) self.log.debug(f"attempting mining block: {block.hash}") node.send_block(block, expect_reject) self.block_count += 1 return block.hash def _remove_last_block(self): # remove last block from chain manager del self.chain.block_heights[self.chain.blocks[self.block_count - 1].sha256] del self.chain.blocks[self.block_count - 1] self.block_count -= 1 self.chain.set_tip(self.block_count - 1) def _mine_and_check_rejected(self, tx, node): 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 rejected_block_hash = 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)) # remove rejected block from test node - the only remaining copy after this point is on remote node disk self._remove_last_block() return rejected_block_hash 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 _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 run_test(self): node = self._init() out_policy_freeze_txo = self.chain.get_spendable_output() out_consensus_freeze_txo = self.chain.get_spendable_output() send_node = Send_node(self.options.tmpdir, self.log, 0, node, self.nodes[0]) self._create_policy_freeze_block(out_policy_freeze_txo, send_node) [freeze_tx_hash, rejected_block_hash ] = self._create_consensus_freeze_block(out_consensus_freeze_txo, send_node) node_chain_info = send_node.rpc.getblockchaininfo() old_tip_hash = node_chain_info['bestblockhash'] old_tip_height = node_chain_info['blocks'] assert (rejected_block_hash != old_tip_hash) # Make sure that we get to the same height: # best block with transactions policy frozen - should get to this point # best block with transactions consensus frozen - should not get to this block self.stop_node(0) self.start_node(0, extra_args=["-reindex=1"]) send_node.rpc.waitforblockheight(old_tip_height) assert_equal(send_node.rpc.getbestblockhash(), old_tip_hash) # Unfreeze and reconsider block to show that the block was still stored on disk result = self.nodes[0].clearBlacklists({"removeAllEntries": True}) assert_equal(result["numRemovedEntries"], 2) self.stop_node(0) self.start_node(0, extra_args=["-reindex=1"]) send_node.rpc.waitforblockheight(old_tip_height + 1) self.log.info(send_node.rpc.getblockchaininfo()) assert_equal(send_node.rpc.getbestblockhash(), rejected_block_hash)
class FrozenTXOSoftConsensusFreeze(SoftConsensusFreezeBase): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.chain = ChainManager() self.extra_args = [[ "-whitelist=127.0.0.1", "-minrelaytxfee=0", "-limitfreerelay=999999", "-softconsensusfreezeduration=4", "-disablesafemode=1" ]] self.block_count = 0 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 _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 _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_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_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 _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 = self._init() send_node = Send_node(self.options.tmpdir, self.log, 0, node, self.nodes[0]) spendable_out_1 = [ self.chain.get_spendable_output(), self.chain.get_spendable_output() ] spendable_out_2 = self.chain.get_spendable_output() spendable_out_3 = self.chain.get_spendable_output() spendable_out_4 = [ self.chain.get_spendable_output(), self.chain.get_spendable_output() ] spendable_out_5 = self.chain.get_spendable_output() spendable_out_6 = self.chain.get_spendable_output() spendable_out_7 = [ self.chain.get_spendable_output(), self.chain.get_spendable_output() ] self._test_soft_consensus_freeze(spendable_out_1, send_node) self._test_soft_consensus_freeze_on_refreeze(spendable_out_2, send_node) self._test_soft_consensus_freeze_clear_all(spendable_out_3, send_node) self._test_soft_consensus_freeze_invalidate_block( spendable_out_4, send_node) self._test_soft_consensus_freeze_submitblock(spendable_out_5, send_node) self._test_soft_consensus_freeze_competing_chains( spendable_out_6, send_node) self._test_soft_consensus_freeze_invalid_frozen_block( spendable_out_7, send_node)
class BSVCheckTTORViolation(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-whitelist=127.0.0.1"]] self.chain = ChainManager() # generating transactions in order so first transaction's output will be input for second transaction 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 run_test(self): block_count = 0 # Create a P2P connections node0 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0) node0.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) getDataMessages = [] def on_getdata(conn, message): getDataMessages.append(message) node0.on_getdata = on_getdata # ***** 1. ***** # starting_blocks are needed to provide spendable outputs starting_blocks = MIN_TTOR_VALIDATION_DISTANCE + 1 for i in range(starting_blocks): block = self.chain.next_block(block_count) block_count += 1 self.chain.save_spendable_output() node0.send_message(msg_block(block)) out = [] for i in range(starting_blocks): out.append(self.chain.get_spendable_output()) self.nodes[0].waitforblockheight(starting_blocks) tip_block_index = block_count - 1 self.log.info("Block tip height: %d " % block_count) # ***** 2. ***** # branch with blocks that do not violate TTOR valid_ttor_branch_height = MIN_TTOR_VALIDATION_DISTANCE + 1 for i in range(0, valid_ttor_branch_height): block = self.chain.next_block(block_count, spend=out[i], extra_txns=8) block_count += 1 node0.send_message(msg_block(block)) chaintip_valid_branch = block self.nodes[0].waitforblockheight(starting_blocks + valid_ttor_branch_height) self.log.info("Node's active chain height: %d " % (starting_blocks + valid_ttor_branch_height)) # ***** 3. ***** # branch with invalid transaction order that will try to cause a reorg self.chain.set_tip(tip_block_index) blocks_invalid_ttor = [] headers_message = msg_headers() headers_message.headers = [] invalid_ttor_branch_height = MIN_TTOR_VALIDATION_DISTANCE + 1 for i in range(0, invalid_ttor_branch_height): spend = out[i] block = self.chain.next_block(block_count) add_txns = self.get_chained_transactions(spend, num_of_transactions=10) # change order of transaction that output uses transaction that comes later (makes block violate TTOR) temp1 = add_txns[1] temp2 = add_txns[2] add_txns[1] = temp2 add_txns[2] = temp1 self.chain.update_block(block_count, add_txns) blocks_invalid_ttor.append(block) block_count += 1 if (i == 0): first_block = block # wait with sending header for the last block if (i != MIN_TTOR_VALIDATION_DISTANCE): headers_message.headers.append(CBlockHeader(block)) self.log.info("Sending %d headers..." % MIN_TTOR_VALIDATION_DISTANCE) node0.send_message(headers_message) # Wait to make sure we do not receive GETDATA messages yet. time.sleep(1) # Check that getData is not received until this chain is long at least as the active chain. assert_equal(len(getDataMessages), 0) self.log.info("Sending 1 more header...") # Send HEADERS message for the last block. headers_message.headers = [CBlockHeader(block)] node0.send_message(headers_message) node0.wait_for_getdata() self.log.info("Received GETDATA.") assert_equal(len(getDataMessages), 1) # Send the first block on invalid chain. Chain should be invalidated. node0.send_message(msg_block(first_block)) def wait_to_invalidate_fork(): chaintips = self.nodes[0].getchaintips() if len(chaintips) > 1: chaintips_status = [ chaintips[0]["status"], chaintips[1]["status"] ] if "active" in chaintips_status and "invalid" in chaintips_status: active_chain_tip_hash = chaintips[0]["hash"] if chaintips[ 0]["status"] == "active" else chaintips[1]["hash"] invalid_fork_tip_hash = chaintips[0]["hash"] if chaintips[ 0]["status"] == "invalid" else chaintips[1]["hash"] assert (active_chain_tip_hash != invalid_fork_tip_hash) for block in blocks_invalid_ttor: if block.hash == invalid_fork_tip_hash: return True return False else: return False else: return False wait_until(wait_to_invalidate_fork) # chaintip of valid branch should be active assert_equal(self.nodes[0].getbestblockhash(), chaintip_valid_branch.hash) # check log file that reorg didnt happen disconnect_block_log = False for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if f"Disconnect block" in line: disconnect_block_log = True self.log.info("Found line: %s", line.strip()) break # we should not find information about disconnecting blocks assert_equal(disconnect_block_log, False) # check log file that contains information about TTOR violation ttor_violation_log = False for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if f"violates TTOR order" in line: ttor_violation_log = True self.log.info("Found line: %s", line.strip()) break # we should find information about TTOR being violated assert_equal(ttor_violation_log, True)