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"]] 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) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.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.chain.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) tip_block_num = block_count-1 # left branch block2 = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block2_num = block_count block_count += 1 node0.send_message(msg_block(block2)) self.log.info(f"block2 hash: {block2.hash}") self.nodes[0].waitforblockheight(102) # send blocks 3,4 for parallel validation on left branch block3 = self.chain.next_block(block_count, spend=out[1], extra_txns=10) block_count += 1 self.chain.set_tip(block2_num) block4 = self.chain.next_block(block_count, spend=out[1], 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.nodes[0].waitaftervalidatingblock(block3.hash, "add") 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({block3.hash, block4.hash}, self.nodes[0], self.log) node0.send_message(msg_block(block3)) node2.send_message(msg_block(block4)) # make sure we started validating blocks wait_for_validating_blocks({block3.hash, block4.hash}, self.nodes[0], self.log) # right branch self.chain.set_tip(tip_block_num) block5 = self.chain.next_block(block_count, spend=out[0], extra_txns=10) block_count += 1 node1.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" block6 = self.chain.next_block(block_count) node1.send_message(msg_block(block6)) self.log.info(f"block6 hash: {block6.hash}") block_count += 1 block7 = self.chain.next_block(block_count) node1.send_message(msg_block(block7)) self.log.info(f"block7 hash: {block7.hash}") block_count += 1 self.nodes[0].waitforblockheight(104) 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(block3.hash, "remove") 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())
class PBVWaitAfterValidation(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"]] 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) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.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.chain.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) tip_block_num = block_count - 1 # adding extra transactions to get different block hashes block2_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3_easier = self.chain.next_block(block_count, spend=out[0], extra_txns=2) block_count += 1 self.chain.set_tip(tip_block_num) block4_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=10) block_count += 1 # send two "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block2 hash: {block2_hard.hash}") self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "add") self.log.info(f"hard block4 hash: {block4_hard.hash}") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) node0.send_message(msg_block(block2_hard)) node1.send_message(msg_block(block4_hard)) # make sure we started validating blocks wait_for_validating_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) self.log.info(f"easier block3 hash: {block3_easier.hash}") node2.send_message(msg_block(block3_easier)) self.nodes[0].waitforblockheight(102) assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash()) # now we can remove waiting status from blocks and finish their validation self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "remove") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # easier block should still be on tip assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash())
class PBVSubmitBlock(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"]] def run_test(self): block_count = 0 # Create a P2P connections node0 = NodeConnCB() connection0 = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node0) node0.add_connection(connection0) node1 = NodeConnCB() connection1 = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node1) node1.add_connection(connection1) # *** Prepare node connection for early announcements testing node2 = NodeConnCB() node2.add_connection( NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2)) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() # *** Activate early announcement functionality for this connection # After this point the early announcements are not received yet - # we still need to set latest announced block (CNode::pindexBestKnownBlock) # which is set for e.g. by calling best headers message with locator # set to non-null node2.wait_for_verack() node2.send_message(msg_sendcmpct(announce=True)) self.chain.set_genesis_hash(int(self.nodes[0].getbestblockhash(), 16)) _, outs, block_count = prepare_init_chain(self.chain, 101, 1, block_0=False, start_block=0, node=node0) out = outs[0] self.log.info("waiting for block height 101 via rpc") self.nodes[0].waitforblockheight(101) tip_block_num = block_count - 1 # adding extra transactions to get different block hashes block2_hard = self.chain.next_block(block_count, spend=out, extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3_easier = self.chain.next_block(block_count, spend=out, extra_txns=2) block_count += 1 self.chain.set_tip(tip_block_num) block4_hard = self.chain.next_block(block_count, spend=out, extra_txns=10) block_count += 1 # send three "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block2 hash: {block2_hard.hash}") self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "add") self.log.info(f"hard block4 hash: {block4_hard.hash}") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) # *** Complete early announcement setup by sending getheaders message # with a non-null locator (pointing to the last block that we know # of on python side - we claim that we know of all the blocks that # bitcoind node knows of) # # We also set on_cmpctblock handler as early announced blocks are # announced via compact block messages instead of inv messages node2.send_and_ping( msg_getheaders( locator_have=[int(self.nodes[0].getbestblockhash(), 16)])) receivedAnnouncement = False waiting_for_announcement_block_hash = block2_hard.sha256 def on_cmpctblock(conn, message): nonlocal receivedAnnouncement message.header_and_shortids.header.calc_sha256() if message.header_and_shortids.header.sha256 == waiting_for_announcement_block_hash: receivedAnnouncement = True node2.on_cmpctblock = on_cmpctblock # send one block via p2p and one via rpc node0.send_message(msg_block(block2_hard)) # *** make sure that we receive announcement of the block before it has # been validated wait_until(lambda: receivedAnnouncement) # making rpc call submitblock in a separate thread because waitaftervalidation is blocking # the return of submitblock submitblock_thread = threading.Thread(target=self.nodes[0].submitblock, args=(ToHex(block4_hard), )) submitblock_thread.start() # because self.nodes[0] rpc is blocked we use another rpc client rpc_client = get_rpc_proxy(rpc_url( get_datadir_path(self.options.tmpdir, 0), 0), 0, coveragedir=self.options.coveragedir) wait_for_validating_blocks({block2_hard.hash, block4_hard.hash}, rpc_client, self.log) # *** prepare to intercept block3_easier announcement - it will not be # announced before validation is complete as early announcement is # limited to announcing one block per height (siblings are ignored) # but after validation is complete we should still get the announcing # compact block message receivedAnnouncement = False waiting_for_announcement_block_hash = block3_easier.sha256 self.log.info(f"easy block3 hash: {block3_easier.hash}") node1.send_message(msg_block(block3_easier)) # *** Make sure that we receive compact block announcement of the block # after the validation is complete even though it was not the first # block that was received by bitcoind node. # # Also make sure that we receive inv announcement of the block after # the validation is complete by the nodes that are not using early # announcement functionality. wait_until(lambda: receivedAnnouncement) node0.wait_for_inv([CInv(CInv.BLOCK, block3_easier.sha256)]) # node 1 was the sender but receives inv for block non the less # (with early announcement that's not the case - sender does not receive the announcement) node1.wait_for_inv([CInv(CInv.BLOCK, block3_easier.sha256)]) rpc_client.waitforblockheight(102) assert_equal(block3_easier.hash, rpc_client.getbestblockhash()) # now we can remove waiting status from blocks and finish their validation rpc_client.waitaftervalidatingblock(block2_hard.hash, "remove") rpc_client.waitaftervalidatingblock(block4_hard.hash, "remove") submitblock_thread.join() # wait till validation of block or blocks finishes node0.sync_with_ping() # easier block should still be on tip assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash())
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 PBVFirstValidActive(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"]] 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) node1 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node1) node1.add_connection(connection) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.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) tip_block_num = block_count - 1 block2_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3_easier = self.chain.next_block(block_count, spend=out[0], extra_txns=2) easier_block_num = block_count block_count += 1 self.chain.set_tip(tip_block_num) block4_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=10) block_count += 1 # make child block of easier block self.chain.set_tip(easier_block_num) block5 = self.chain.next_block(block_count) block5_num = block_count block_count += 1 # send two "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block2 hash: {block2_hard.hash}") self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "add") self.log.info(f"hard block4 hash: {block4_hard.hash}") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) # send blocks via different p2p connection node0.send_message(msg_block(block2_hard)) node1.send_message(msg_block(block4_hard)) # make sure we started validating blocks wait_for_validating_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) # send easier block through different p2p connection too node2.send_message(msg_block(block3_easier)) self.log.info(f"easier block hash: {block3_easier.hash}") self.nodes[0].waitforblockheight(102) assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash()) # child block of block3_easier self.log.info(f"child block hash: {block5.hash}") self.nodes[0].waitaftervalidatingblock(block5.hash, "add") # make sure child block is in waiting list and then send it wait_for_not_validating_blocks({block5.hash}, self.nodes[0], self.log) node2.send_message(msg_block(block5)) # make sure we started validating child block wait_for_validating_blocks({block5.hash}, self.nodes[0], self.log) # finish validation on block2_hard self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "remove") wait_for_not_validating_blocks({block2_hard.hash}, self.nodes[0], self.log) # finish validation on child block self.nodes[0].waitaftervalidatingblock(block5.hash, "remove") wait_for_not_validating_blocks({block5.hash}, self.nodes[0], self.log) # block5 should be active at this point assert_equal(block5.hash, self.nodes[0].getbestblockhash()) # finish validation on block4_hard self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "remove") wait_for_not_validating_blocks({block4_hard.hash}, self.nodes[0], self.log) # block5 should still be active at this point assert_equal(block5.hash, self.nodes[0].getbestblockhash()) # Make three siblings and send them via same p2p connection. block6_hard = self.chain.next_block(block_count, spend=out[1], extra_txns=8) block_count += 1 self.chain.set_tip(block5_num) block7_easier = self.chain.next_block(block_count, spend=out[1], extra_txns=2) block_count += 1 self.chain.set_tip(block5_num) block8_hard = self.chain.next_block(block_count, spend=out[1], extra_txns=10) block_count += 1 self.log.info(f"hard block6 hash: {block6_hard.hash}") self.nodes[0].waitaftervalidatingblock(block6_hard.hash, "add") self.log.info(f"hard block8 hash: {block8_hard.hash}") self.nodes[0].waitaftervalidatingblock(block8_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block6_hard.hash, block8_hard.hash}, self.nodes[0], self.log) # sending blocks via same p2p connection node0.send_message(msg_block(block6_hard)) node0.send_message(msg_block(block8_hard)) # make sure we started validating blocks wait_for_validating_blocks({block6_hard.hash, block8_hard.hash}, self.nodes[0], self.log) # send easier block through same p2p connection too node0.send_message(msg_block(block7_easier)) self.nodes[0].waitforblockheight(104) assert_equal(block7_easier.hash, self.nodes[0].getbestblockhash()) # now we can remove waiting status from blocks and finish their validation self.nodes[0].waitaftervalidatingblock(block6_hard.hash, "remove") self.nodes[0].waitaftervalidatingblock(block8_hard.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # easier block should still be on tip assert_equal(block7_easier.hash, self.nodes[0].getbestblockhash())
class PBVPreciousBlock(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"]] 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) node1 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node1) node1.add_connection(connection) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.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) tip_block_num = block_count - 1 block2_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3_easier = self.chain.next_block(block_count, spend=out[0], extra_txns=2) block_count += 1 self.chain.set_tip(tip_block_num) block4_hard = self.chain.next_block(block_count, spend=out[0], extra_txns=10) block_count += 1 # send two "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block2 hash: {block2_hard.hash}") self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "add") self.log.info(f"hard block4 hash: {block4_hard.hash}") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) node0.send_message(msg_block(block2_hard)) node1.send_message(msg_block(block4_hard)) # make sure we started validating blocks wait_for_validating_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) self.log.info(f"easier hash: {block3_easier.hash}") node2.send_message(msg_block(block3_easier)) self.nodes[0].waitforblockheight(102) assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash()) # now we can remove waiting status from blocks and finish their validation self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "remove") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # now we want our precious block to be one of the harder blocks (block4_hard) self.nodes[0].preciousblock(block4_hard.hash) assert_equal(block4_hard.hash, self.nodes[0].getbestblockhash())
class PBVInvalidate(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"]] def run_test(self): block_count = 0 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) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.wait_for_verack() self.log.info("Sending blocks to get spendable output") 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.chain.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) tip_block_num = block_count - 1 block2 = self.chain.next_block(block_count, spend=out[0], extra_txns=1) block2_count = block_count block_count += 1 self.log.info(f"blockA hash: {block2.hash}") node0.send_message(msg_block(block2)) self.nodes[0].waitforblockheight(102) block3_hard = self.chain.next_block(block_count, spend=out[1], extra_txns=8) block_count += 1 self.chain.set_tip(block2_count) block4_easier = self.chain.next_block(block_count, spend=out[1], extra_txns=2) block_count += 1 self.chain.set_tip(block2_count) block5_hard = self.chain.next_block(block_count, spend=out[1], extra_txns=10) block_count += 1 # send two "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block3 hash: {block3_hard.hash}") self.nodes[0].waitaftervalidatingblock(block3_hard.hash, "add") self.log.info(f"hard block5 hash: {block5_hard.hash}") self.nodes[0].waitaftervalidatingblock(block5_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block3_hard.hash, block5_hard.hash}, self.nodes[0], self.log) self.log.info( "Sending blocks 3,4,5 on branch 2 for parallel validation") node0.send_message(msg_block(block3_hard)) node2.send_message(msg_block(block5_hard)) # make sure we started validating blocks wait_for_validating_blocks({block3_hard.hash, block5_hard.hash}, self.nodes[0], self.log) self.log.info(f"easier hash: {block4_easier.hash}") node1.send_message(msg_block(block4_easier)) self.nodes[0].waitforblockheight(103) # Because 4 is easy to validate it will be validated first and set as active tip assert_equal(block4_easier.hash, self.nodes[0].getbestblockhash()) # now we can remove waiting status from blocks and finish their validation self.nodes[0].waitaftervalidatingblock(block3_hard.hash, "remove") self.nodes[0].waitaftervalidatingblock(block5_hard.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # easier block should still be on tip assert_equal(block4_easier.hash, self.nodes[0].getbestblockhash()) self.log.info("Sending blocks 6,7,8 on competing chain to cause reorg") self.chain.set_tip(tip_block_num) block6 = self.chain.next_block(block_count, spend=out[0], extra_txns=2) block_count += 1 self.log.info(f"block6 hash: {block6.hash}") node0.send_message(msg_block(block6)) block7 = self.chain.next_block(block_count) block_count += 1 self.log.info(f"block7: {block7.hash}") node0.send_message(msg_block(block7)) # send one to cause reorg this should be active block8 = self.chain.next_block(block_count) block_count += 1 self.log.info(f"block8: {block8.hash}") node0.send_message(msg_block(block8)) self.nodes[0].waitforblockheight(104) assert_equal(block8.hash, self.nodes[0].getbestblockhash()) self.log.info( "Invalidating block7 on competing chain to reorg to first branch again" ) self.log.info(f"invalidating hash {block7.hash}") self.nodes[0].invalidateblock(block7.hash) #after invalidating, active block should be the one first validated on first branch assert_equal(block4_easier.hash, self.nodes[0].getbestblockhash())
class PBVTerminate(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", "-maxparallelblocks=3", "-maxparallelblocksperpeer=3" ]] 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) node1 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node1) node1.add_connection(connection) node2 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node2) node2.add_connection(connection) node3 = NodeConnCB() connection = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node3) node3.add_connection(connection) NetworkThread().start() # wait_for_verack ensures that the P2P connection is fully up. node0.wait_for_verack() node1.wait_for_verack() node2.wait_for_verack() node3.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.chain.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) tip_block_num = block_count - 1 block2 = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3 = self.chain.next_block(block_count, spend=out[0], extra_txns=10) block_count += 1 self.chain.set_tip(tip_block_num) block4 = self.chain.next_block(block_count, spend=out[0], extra_txns=12) block_count += 1 self.chain.set_tip(tip_block_num) block5 = self.chain.next_block(block_count, spend=out[0], extra_txns=14) block5_num = block_count block_count += 1 block6 = self.chain.next_block(block_count, spend=out[1], extra_txns=8) block_count += 1 self.chain.set_tip(block5_num) block7 = self.chain.next_block(block_count, spend=out[1], extra_txns=10) self.log.info(f"block2 hash: {block2.hash}") self.nodes[0].waitaftervalidatingblock(block2.hash, "add") self.log.info(f"block3 hash: {block3.hash}") self.nodes[0].waitaftervalidatingblock(block3.hash, "add") 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({block2.hash, block3.hash, block4.hash}, self.nodes[0], self.log) node0.send_message(msg_block(block2)) # make sure we started validating block2 first as we expect this one to # be terminated later on in the test before its validation is complete # (algorithm for premature termination selects based on block height and # and validation duration - those that are in validation with smaller # height and longer are terminated first) wait_for_validating_blocks({block2.hash}, self.nodes[0], self.log) node1.send_message(msg_block(block3)) node2.send_message(msg_block(block4)) # make sure we started validating blocks wait_for_validating_blocks({block2.hash, block3.hash, block4.hash}, self.nodes[0], self.log) node3.send_message(msg_block(block5)) self.log.info(f"block5 hash: {block5.hash}") # check log file for logging about which block validation was terminated termination_log_found = False for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if f"Block {block2.hash} will not be considered by the current tip activation as the maximum parallel block" in line: termination_log_found = True self.log.info("Found line: %s", line.strip()) break self.log.info(f"block6 hash: {block6.hash}") self.nodes[0].waitaftervalidatingblock(block6.hash, "add") self.log.info(f"block7 hash: {block7.hash}") self.nodes[0].waitaftervalidatingblock(block7.hash, "add") wait_for_waiting_blocks({block6.hash, block7.hash}, self.nodes[0], self.log) node3.send_message(msg_block(block6)) wait_for_validating_blocks({block6.hash}, self.nodes[0], self.log) node3.send_message(msg_block(block7)) wait_for_validating_blocks({block7.hash}, self.nodes[0], self.log) self.nodes[0].waitaftervalidatingblock(block2.hash, "remove") # block2 should be canceled. wait_for_not_validating_blocks({block2.hash}, self.nodes[0], self.log) self.log.info("removing wait status from block7") self.nodes[0].waitaftervalidatingblock(block7.hash, "remove") # finish block7 validation wait_for_not_validating_blocks({block7.hash}, self.nodes[0], self.log) # remove wait status from block to finish its validations so the test exits properly self.nodes[0].waitaftervalidatingblock(block3.hash, "remove") self.nodes[0].waitaftervalidatingblock(block4.hash, "remove") self.nodes[0].waitaftervalidatingblock(block6.hash, "remove") # wait till validation of block or blocks finishes node0.sync_with_ping() # block7 should be active in the end assert_equal(block7.hash, self.nodes[0].getbestblockhash()) # check log file for logging about which block validation was terminated termination_log_found = False for line in open( glob.glob(self.options.tmpdir + "/node0" + "/regtest/bitcoind.log")[0]): if f"Block {block2.hash} validation was terminated before completion." in line: termination_log_found = True self.log.info("Found line: %s", line.strip()) break assert_equal(termination_log_found, True)
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 PBVSubmitMiningSolution(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"]] 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)) _, outs, block_count = prepare_init_chain(self.chain, 101, 1, block_0=False, start_block=0, node=node0) out = outs[0] self.log.info("waiting for block height 101 via rpc") self.nodes[0].waitforblockheight(101) tip_block_num = block_count - 1 # adding extra transactions to get different block hashes block2_hard = self.chain.next_block(block_count, spend=out, extra_txns=8) block_count += 1 self.chain.set_tip(tip_block_num) block3_easier = self.chain.next_block(block_count, spend=out, extra_txns=2) block_count += 1 mining_candidate = self.nodes[0].getminingcandidate() block4_hard = self.chain.next_block(block_count) block4_hard.hashPrevBlock = int(mining_candidate["prevhash"], 16) block4_hard.nTime = mining_candidate["time"] block4_hard.nVersion = mining_candidate["version"] block4_hard.solve() mining_solution = {"id": mining_candidate["id"], "nonce": block4_hard.nNonce, "coinbase": ToHex(block4_hard.vtx[0]), "time": mining_candidate["time"], "version": mining_candidate["version"]} # send three "hard" blocks, with waitaftervalidatingblock we artificially # extend validation time. self.log.info(f"hard block2 hash: {block2_hard.hash}") self.nodes[0].waitaftervalidatingblock(block2_hard.hash, "add") self.log.info(f"hard block4 hash: {block4_hard.hash}") self.nodes[0].waitaftervalidatingblock(block4_hard.hash, "add") # make sure block hashes are in waiting list wait_for_waiting_blocks({block2_hard.hash, block4_hard.hash}, self.nodes[0], self.log) # send one block via p2p and one via rpc node0.send_message(msg_block(block2_hard)) # making rpc call submitminingsolution in a separate thread because waitaftervalidation is blocking # the return of submitminingsolution submitminingsolution_thread = threading.Thread(target=self.nodes[0].submitminingsolution, args=(mining_solution,)) submitminingsolution_thread.start() # because self.nodes[0] rpc is blocked we use another rpc client rpc_client = get_rpc_proxy(rpc_url(get_datadir_path(self.options.tmpdir, 0), 0), 0, coveragedir=self.options.coveragedir) wait_for_validating_blocks({block2_hard.hash, block4_hard.hash}, rpc_client, self.log) self.log.info(f"easy block3 hash: {block3_easier.hash}") node1.send_message(msg_block(block3_easier)) rpc_client.waitforblockheight(102) assert_equal(block3_easier.hash, rpc_client.getbestblockhash()) # now we can remove waiting status from blocks and finish their validation rpc_client.waitaftervalidatingblock(block2_hard.hash, "remove") rpc_client.waitaftervalidatingblock(block4_hard.hash, "remove") submitminingsolution_thread.join() # wait till validation of block or blocks finishes node0.sync_with_ping() # easier block should still be on tip assert_equal(block3_easier.hash, self.nodes[0].getbestblockhash())
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 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)
class PBVReorgShutdown(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"]] 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)) for i in range(100): 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(100): out.append(self.chain.get_spendable_output()) self.log.info("waiting for block height 101 via rpc") self.nodes[0].waitforblockheight(101) tip_block_num = block_count - 1 # left branch block2 = self.chain.next_block(block_count, spend=out[0], extra_txns=8) block2_num = block_count block_count += 1 node0.send_message(msg_block(block2)) self.log.info(f"block2 hash: {block2.hash}") self.nodes[0].waitforblockheight(102) # send blocks 3,4 to force a reorg self.chain.set_tip(tip_block_num) block3 = self.chain.next_block(block_count, spend=out[1], extra_txns=10) block_count += 1 block4 = self.chain.next_block(block_count, spend=out[1], extra_txns=8) block_count += 1 self.log.info(f"block3 hash: {block3.hash}") self.nodes[0].waitaftervalidatingblock(block3.hash, "add") # make sure block hash is in waiting list wait_for_waiting_blocks({block3.hash}, self.nodes[0], self.log) node0.send_message(msg_block(block3)) node0.send_message(msg_block(block4)) # make sure we started validating blocks wait_for_validating_blocks({block3.hash}, self.nodes[0], self.log) self.stop_node(0)