#!/usr/bin/env python3 import codecs from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework import ( key, ) from test_framework.messages import ( CBlock, from_hex, ser_uint256, ) from test_framework.script import ( OP_NOP, OP_RETURN, CScript, SIGHASH_ALL, ) # The signblockscript is a Bitcoin Script k-of-n multisig script. def make_signblockscript(num_nodes, required_signers, keys): assert num_nodes >= required_signers script = "{}".format(50 + required_signers) for i in range(num_nodes): k = keys[i] script += "21" script += codecs.encode(k.get_pubkey().get_bytes(), 'hex_codec').decode("utf-8") script += "{}".format(50 + num_nodes) # num keys script += "ae" # OP_CHECKMULTISIG return script class TrimHeadersTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): self.add_wallet_options(parser) # Dynamically generate N keys to be used for block signing. def init_keys(self, num_keys): self.keys = [] for i in range(num_keys): k = key.ECKey() k.generate() self.keys.append(k) def set_test_params(self): self.num_nodes = 3 self.num_keys = 1 self.required_signers = 1 self.setup_clean_chain = True self.init_keys(self.num_keys) signblockscript = make_signblockscript(self.num_keys, self.required_signers, self.keys) self.witnessScript = signblockscript # post-dynafed this becomes witnessScript args = [ "-signblockscript={}".format(signblockscript), "-con_max_block_sig_size={}".format(self.required_signers * 74 + self.num_nodes * 33), "-anyonecanspendaremine=1", "-evbparams=dynafed:0:::", "-con_dyna_deploy_signal=1", ] self.trim_args = args + ["-trim_headers=1"] self.prune_args = self.trim_args + ["-prune=1"] self.extra_args = [ args, self.trim_args, self.prune_args, ] def setup_network(self): self.setup_nodes() self.connect_nodes(0, 1) self.connect_nodes(0, 2) def check_height(self, expected_height, all=False, verbose=True): if verbose: self.log.info(f"Check height {expected_height}") if all: for n in self.nodes: assert_equal(n.getblockcount(), expected_height) else: assert_equal(self.nodes[0].getblockcount(), expected_height) # The signblock RPC only works on legacy (BDB) wallets. # Sign the block header hash directly with the key instead, # then use combineblocksigs (a non-wallet RPC). def sign_block(self, key, block): block.rehash() msg = ser_uint256(block.sha256) sig = key.sign_ecdsa(msg) if not block.m_dynafed_params.is_null(): sig += bytes([SIGHASH_ALL]) return [{ "pubkey": key.get_pubkey().get_bytes().hex(), "sig": sig.hex(), }] def mine_block(self, make_transactions): # alternate mining between the signing nodes mineridx = self.nodes[0].getblockcount() % self.required_signers # assuming in sync mineridx_next = (self.nodes[0].getblockcount() + 1) % self.required_signers miner = self.nodes[mineridx] miner_next = self.nodes[mineridx_next] # If dynafed is enabled, this means signblockscript has been WSH-wrapped blockchain_info = self.nodes[0].getblockchaininfo() deployment_info = self.nodes[0].getdeploymentinfo() dynafed_active = deployment_info['deployments']['dynafed']['bip9']['status'] == "active" if dynafed_active: wsh_wrap = self.nodes[0].decodescript(self.witnessScript)['segwit']['hex'] assert_equal(wsh_wrap, blockchain_info['current_signblock_hex']) # Make a few transactions to make non-empty blocks for compact transmission if make_transactions: for i in range(10): miner.sendtoaddress(miner_next.getnewaddress(), 1, "", "", True) # miner makes a block block = miner.getnewblockhex() block_struct = from_hex(CBlock(), block) # make another block with the commitment field filled out dummy_block = miner.getnewblockhex(commit_data="deadbeef") dummy_struct = from_hex(CBlock(), dummy_block) assert_equal(len(dummy_struct.vtx[0].vout), len(block_struct.vtx[0].vout) + 1) # OP_RETURN deadbeef assert_equal(CScript(dummy_struct.vtx[0].vout[0].scriptPubKey).hex(), '6a04deadbeef') # All nodes get compact blocks, first node may get complete # block in 0.5 RTT even with transactions thanks to p2p connection # with non-signing node being miner for i in range(self.num_keys): sketch = miner.getcompactsketch(block) compact_response = self.nodes[i].consumecompactsketch(sketch) if "block_tx_req" in compact_response: block_txn = self.nodes[i].consumegetblocktxn(block, compact_response["block_tx_req"]) final_block = self.nodes[i].finalizecompactblock(sketch, block_txn, compact_response["found_transactions"]) else: # If there's only coinbase, it should succeed immediately final_block = compact_response["blockhex"] # Block should be complete, sans signatures self.nodes[i].testproposedblock(final_block) # collect num_keys signatures from signers, reduce to required_signers sigs during combine sigs = [] for i in range(self.num_keys): result = miner.combineblocksigs(block, sigs, self.witnessScript) sigs = sigs + self.sign_block(self.keys[i], block_struct) assert_equal(result["complete"], i >= self.required_signers) # submitting should have no effect pre-threshhold if i < self.required_signers: miner.submitblock(result["hex"]) result = miner.combineblocksigs(block, sigs, self.witnessScript) assert_equal(result["complete"], True) self.nodes[0].submitblock(result["hex"]) def mine_blocks(self, num_blocks, transactions): for _ in range(num_blocks): self.mine_block(transactions) def mine_large_blocks(self, n): big_script = CScript([OP_RETURN] + [OP_NOP] * 950000) node = self.nodes[0] for _ in range(n): hex = node.getnewblockhex() block = from_hex(CBlock(), hex) tx = block.vtx[0] tx.vout[0].scriptPubKey = big_script tx.rehash() block.vtx[0] = tx block.hashMerkleRoot = block.calc_merkle_root() block.solve() h = block.serialize().hex() sigs = self.sign_block(self.keys[0], block) result = node.combineblocksigs(h, sigs, self.witnessScript) assert_equal(result["complete"], True) node.submitblock(result["hex"]) def run_test(self): expected_height = 0 self.check_height(expected_height, all=True) self.log.info("Mining and signing 101 blocks to unlock funds") expected_height += 101 self.mine_blocks(101, False) self.sync_all() self.check_height(expected_height, all=True) # check the new field in getblockchaininfo assert not self.nodes[0].getblockchaininfo()["trim_headers"] assert self.nodes[1].getblockchaininfo()["trim_headers"] assert self.nodes[2].getblockchaininfo()["trim_headers"] self.log.info("Shut down trimmed nodes") self.stop_node(1) self.stop_node(2) self.log.info("Mining and signing non-empty blocks") expected_height += 10 self.mine_blocks(10, True) self.check_height(expected_height) # signblock rpc field stuff tip = self.nodes[0].getblockhash(self.nodes[0].getblockcount()) header = self.nodes[0].getblockheader(tip) block = self.nodes[0].getblock(tip) assert 'signblock_witness_asm' in header assert 'signblock_witness_hex' in header assert 'signblock_witness_asm' in block assert 'signblock_witness_hex' in block assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['dynafed']['bip9']['status'], "defined") # activate dynafed blocks_til_dynafed = 431 - self.nodes[0].getblockcount() self.log.info("Activating dynafed") self.mine_blocks(blocks_til_dynafed, False) expected_height += blocks_til_dynafed self.check_height(expected_height) assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['dynafed']['bip9']['status'], "locked_in") num = 3000 self.log.info(f"Mine {num} dynamic federation blocks without txns") self.mine_blocks(num, False) expected_height += num self.check_height(expected_height) num = 10 self.log.info(f"Mine {num} dynamic federation blocks with txns") self.mine_blocks(num, True) expected_height += num self.check_height(expected_height) num = 777 self.log.info(f"Mine {num} large blocks") expected_height += num self.mine_large_blocks(num) self.log.info("Restart the trimmed nodes") self.start_node(1, extra_args=self.trim_args) self.start_node(2, extra_args=self.prune_args) self.connect_nodes(0, 1) self.connect_nodes(0, 2) self.sync_all(timeout1=360) self.check_height(expected_height, all=True) self.log.info("Prune the pruned node") self.nodes[2].pruneblockchain(4000) hash = self.nodes[0].getblockhash(expected_height) block = self.nodes[0].getblock(hash) for i in range(1, self.num_nodes): assert_equal(hash, self.nodes[i].getblockhash(expected_height)) assert_equal(block, self.nodes[i].getblock(hash)) self.log.info(f"All nodes at height {expected_height} with block hash {hash}") if __name__ == '__main__': TrimHeadersTest(__file__).main()