|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import codecs |
| 4 | +from decimal import Decimal |
| 5 | +from time import sleep |
| 6 | +from test_framework.blocktools import create_coinbase |
| 7 | + |
| 8 | +from test_framework.test_framework import BitcoinTestFramework |
| 9 | +from test_framework.util import (assert_raises_rpc_error, assert_equal) |
| 10 | +from test_framework import ( |
| 11 | + address, |
| 12 | + key, |
| 13 | +) |
| 14 | +from test_framework.messages import ( |
| 15 | + COIN, |
| 16 | + CBlock, |
| 17 | + from_hex, |
| 18 | +) |
| 19 | +from test_framework.script import ( |
| 20 | + OP_NOP, |
| 21 | + OP_RETURN, |
| 22 | + CScript |
| 23 | +) |
| 24 | + |
| 25 | +# Generate wallet import format from private key. |
| 26 | +def wif(pk): |
| 27 | + # Base58Check version for regtest WIF keys is 0xef = 239 |
| 28 | + pk_compressed = pk + bytes([0x1]) |
| 29 | + return address.byte_to_base58(pk_compressed, 239) |
| 30 | + |
| 31 | +# The signblockscript is a Bitcoin Script k-of-n multisig script. |
| 32 | +def make_signblockscript(num_nodes, required_signers, keys): |
| 33 | + assert num_nodes >= required_signers |
| 34 | + script = "{}".format(50 + required_signers) |
| 35 | + for i in range(num_nodes): |
| 36 | + k = keys[i] |
| 37 | + script += "21" |
| 38 | + script += codecs.encode(k.get_pubkey().get_bytes(), 'hex_codec').decode("utf-8") |
| 39 | + script += "{}".format(50 + num_nodes) # num keys |
| 40 | + script += "ae" # OP_CHECKMULTISIG |
| 41 | + return script |
| 42 | + |
| 43 | +class TrimHeadersTest(BitcoinTestFramework): |
| 44 | + def skip_test_if_missing_module(self): |
| 45 | + self.skip_if_no_wallet() |
| 46 | + |
| 47 | + # Dynamically generate N keys to be used for block signing. |
| 48 | + def init_keys(self, num_keys): |
| 49 | + self.keys = [] |
| 50 | + self.wifs = [] |
| 51 | + for i in range(num_keys): |
| 52 | + k = key.ECKey() |
| 53 | + k.generate() |
| 54 | + w = wif(k.get_bytes()) |
| 55 | + self.keys.append(k) |
| 56 | + self.wifs.append(w) |
| 57 | + |
| 58 | + def set_test_params(self): |
| 59 | + self.num_nodes = 3 |
| 60 | + self.num_keys = 1 |
| 61 | + self.required_signers = 1 |
| 62 | + self.setup_clean_chain = True |
| 63 | + self.init_keys(self.num_keys) |
| 64 | + signblockscript = make_signblockscript(self.num_keys, self.required_signers, self.keys) |
| 65 | + self.witnessScript = signblockscript # post-dynafed this becomes witnessScript |
| 66 | + args = [ |
| 67 | + "-signblockscript={}".format(signblockscript), |
| 68 | + "-con_max_block_sig_size={}".format(self.required_signers * 74 + self.num_nodes * 33), |
| 69 | + "-anyonecanspendaremine=1", |
| 70 | + "-evbparams=dynafed:0:::", |
| 71 | + "-con_dyna_deploy_signal=1", |
| 72 | + ] |
| 73 | + self.trim_args = args + ["-trim_headers=1"] |
| 74 | + self.prune_args = self.trim_args + ["-prune=1"] |
| 75 | + self.extra_args = [ |
| 76 | + args, |
| 77 | + self.trim_args, |
| 78 | + self.prune_args, |
| 79 | + ] |
| 80 | + |
| 81 | + def setup_network(self): |
| 82 | + self.setup_nodes() |
| 83 | + self.connect_nodes(0, 1) |
| 84 | + self.connect_nodes(0, 2) |
| 85 | + |
| 86 | + def check_height(self, expected_height, all=False, verbose=True): |
| 87 | + if verbose: |
| 88 | + self.log.info(f"Check height {expected_height}") |
| 89 | + if all: |
| 90 | + for n in self.nodes: |
| 91 | + assert_equal(n.getblockcount(), expected_height) |
| 92 | + else: |
| 93 | + assert_equal(self.nodes[0].getblockcount(), expected_height) |
| 94 | + |
| 95 | + def mine_block(self, make_transactions): |
| 96 | + # alternate mining between the signing nodes |
| 97 | + mineridx = self.nodes[0].getblockcount() % self.required_signers # assuming in sync |
| 98 | + mineridx_next = (self.nodes[0].getblockcount() + 1) % self.required_signers |
| 99 | + miner = self.nodes[mineridx] |
| 100 | + miner_next = self.nodes[mineridx_next] |
| 101 | + |
| 102 | + # If dynafed is enabled, this means signblockscript has been WSH-wrapped |
| 103 | + blockchain_info = self.nodes[0].getblockchaininfo() |
| 104 | + deployment_info = self.nodes[0].getdeploymentinfo() |
| 105 | + dynafed_active = deployment_info['deployments']['dynafed']['bip9']['status'] == "active" |
| 106 | + if dynafed_active: |
| 107 | + wsh_wrap = self.nodes[0].decodescript(self.witnessScript)['segwit']['hex'] |
| 108 | + assert_equal(wsh_wrap, blockchain_info['current_signblock_hex']) |
| 109 | + |
| 110 | + # Make a few transactions to make non-empty blocks for compact transmission |
| 111 | + if make_transactions: |
| 112 | + for i in range(10): |
| 113 | + miner.sendtoaddress(miner_next.getnewaddress(), 1, "", "", True) |
| 114 | + # miner makes a block |
| 115 | + block = miner.getnewblockhex() |
| 116 | + block_struct = from_hex(CBlock(), block) |
| 117 | + |
| 118 | + # make another block with the commitment field filled out |
| 119 | + dummy_block = miner.getnewblockhex(commit_data="deadbeef") |
| 120 | + dummy_struct = from_hex(CBlock(), dummy_block) |
| 121 | + assert_equal(len(dummy_struct.vtx[0].vout), len(block_struct.vtx[0].vout) + 1) |
| 122 | + # OP_RETURN deadbeef |
| 123 | + assert_equal(CScript(dummy_struct.vtx[0].vout[0].scriptPubKey).hex(), '6a04deadbeef') |
| 124 | + |
| 125 | + # All nodes get compact blocks, first node may get complete |
| 126 | + # block in 0.5 RTT even with transactions thanks to p2p connection |
| 127 | + # with non-signing node being miner |
| 128 | + for i in range(self.num_keys): |
| 129 | + sketch = miner.getcompactsketch(block) |
| 130 | + compact_response = self.nodes[i].consumecompactsketch(sketch) |
| 131 | + if "block_tx_req" in compact_response: |
| 132 | + block_txn = self.nodes[i].consumegetblocktxn(block, compact_response["block_tx_req"]) |
| 133 | + final_block = self.nodes[i].finalizecompactblock(sketch, block_txn, compact_response["found_transactions"]) |
| 134 | + else: |
| 135 | + # If there's only coinbase, it should succeed immediately |
| 136 | + final_block = compact_response["blockhex"] |
| 137 | + # Block should be complete, sans signatures |
| 138 | + self.nodes[i].testproposedblock(final_block) |
| 139 | + |
| 140 | + # collect num_keys signatures from signers, reduce to required_signers sigs during combine |
| 141 | + sigs = [] |
| 142 | + for i in range(self.num_keys): |
| 143 | + result = miner.combineblocksigs(block, sigs, self.witnessScript) |
| 144 | + sigs = sigs + self.nodes[i].signblock(block, self.witnessScript) |
| 145 | + assert_equal(result["complete"], i >= self.required_signers) |
| 146 | + # submitting should have no effect pre-threshhold |
| 147 | + if i < self.required_signers: |
| 148 | + miner.submitblock(result["hex"]) |
| 149 | + |
| 150 | + result = miner.combineblocksigs(block, sigs, self.witnessScript) |
| 151 | + assert_equal(result["complete"], True) |
| 152 | + |
| 153 | + self.nodes[0].submitblock(result["hex"]) |
| 154 | + |
| 155 | + def mine_blocks(self, num_blocks, transactions): |
| 156 | + for _ in range(num_blocks): |
| 157 | + self.mine_block(transactions) |
| 158 | + |
| 159 | + def mine_large_blocks(self, n): |
| 160 | + big_script = CScript([OP_RETURN] + [OP_NOP] * 950000) |
| 161 | + node = self.nodes[0] |
| 162 | + |
| 163 | + for _ in range(n): |
| 164 | + hex = node.getnewblockhex() |
| 165 | + block = from_hex(CBlock(), hex) |
| 166 | + tx = block.vtx[0] |
| 167 | + tx.vout[0].scriptPubKey = big_script |
| 168 | + tx.rehash() |
| 169 | + block.vtx[0] = tx |
| 170 | + block.hashMerkleRoot = block.calc_merkle_root() |
| 171 | + block.solve() |
| 172 | + h = block.serialize().hex() |
| 173 | + |
| 174 | + sigs = node.signblock(h, self.witnessScript) |
| 175 | + |
| 176 | + result = node.combineblocksigs(h, sigs, self.witnessScript) |
| 177 | + assert_equal(result["complete"], True) |
| 178 | + |
| 179 | + node.submitblock(result["hex"]) |
| 180 | + |
| 181 | + |
| 182 | + def run_test(self): |
| 183 | + for i in range(self.num_keys): |
| 184 | + self.nodes[i].importprivkey(self.wifs[i]) |
| 185 | + |
| 186 | + expected_height = 0 |
| 187 | + self.check_height(expected_height, all=True) |
| 188 | + |
| 189 | + self.log.info("Mining and signing 101 blocks to unlock funds") |
| 190 | + expected_height += 101 |
| 191 | + self.mine_blocks(101, False) |
| 192 | + self.sync_all() |
| 193 | + self.check_height(expected_height, all=True) |
| 194 | + |
| 195 | + self.log.info("Shut down trimmed nodes") |
| 196 | + self.stop_node(1) |
| 197 | + self.stop_node(2) |
| 198 | + |
| 199 | + self.log.info("Mining and signing non-empty blocks") |
| 200 | + expected_height += 10 |
| 201 | + self.mine_blocks(10, True) |
| 202 | + self.check_height(expected_height) |
| 203 | + |
| 204 | + # signblock rpc field stuff |
| 205 | + tip = self.nodes[0].getblockhash(self.nodes[0].getblockcount()) |
| 206 | + header = self.nodes[0].getblockheader(tip) |
| 207 | + block = self.nodes[0].getblock(tip) |
| 208 | + info = self.nodes[0].getblockchaininfo() |
| 209 | + |
| 210 | + assert 'signblock_witness_asm' in header |
| 211 | + assert 'signblock_witness_hex' in header |
| 212 | + assert 'signblock_witness_asm' in block |
| 213 | + assert 'signblock_witness_hex' in block |
| 214 | + |
| 215 | + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['dynafed']['bip9']['status'], "defined") |
| 216 | + |
| 217 | + # activate dynafed |
| 218 | + blocks_til_dynafed = 431 - self.nodes[0].getblockcount() |
| 219 | + self.log.info("Activating dynafed") |
| 220 | + self.mine_blocks(blocks_til_dynafed, False) |
| 221 | + expected_height += blocks_til_dynafed |
| 222 | + self.check_height(expected_height) |
| 223 | + |
| 224 | + assert_equal(self.nodes[0].getdeploymentinfo()['deployments']['dynafed']['bip9']['status'], "locked_in") |
| 225 | + |
| 226 | + num = 3000 |
| 227 | + self.log.info(f"Mine {num} dynamic federation blocks without txns") |
| 228 | + self.mine_blocks(num, False) |
| 229 | + expected_height += num |
| 230 | + self.check_height(expected_height) |
| 231 | + |
| 232 | + num = 10 |
| 233 | + self.log.info(f"Mine {num} dynamic federation blocks with txns") |
| 234 | + self.mine_blocks(num, True) |
| 235 | + expected_height += num |
| 236 | + self.check_height(expected_height) |
| 237 | + |
| 238 | + num = 777 |
| 239 | + self.log.info(f"Mine {num} large blocks") |
| 240 | + expected_height += num |
| 241 | + self.mine_large_blocks(num) |
| 242 | + |
| 243 | + info = self.nodes[0].getblockchaininfo() |
| 244 | + |
| 245 | + self.log.info("Restart the trimmed nodes") |
| 246 | + self.start_node(1, extra_args=self.trim_args) |
| 247 | + self.start_node(2, extra_args=self.prune_args) |
| 248 | + self.connect_nodes(0, 1) |
| 249 | + self.connect_nodes(0, 2) |
| 250 | + |
| 251 | + self.sync_all() |
| 252 | + self.check_height(expected_height, all=True) |
| 253 | + |
| 254 | + self.log.info("Prune the pruned node") |
| 255 | + self.nodes[2].pruneblockchain(4000) |
| 256 | + |
| 257 | + info = self.nodes[0].getblockchaininfo() |
| 258 | + hash = self.nodes[0].getblockhash(expected_height) |
| 259 | + block = self.nodes[0].getblock(hash) |
| 260 | + for i in range(1, self.num_nodes): |
| 261 | + assert_equal(hash, self.nodes[i].getblockhash(expected_height)) |
| 262 | + assert_equal(block, self.nodes[i].getblock(hash)) |
| 263 | + |
| 264 | + self.log.info(f"All nodes at height {expected_height} with block hash {hash}") |
| 265 | + |
| 266 | + |
| 267 | +if __name__ == '__main__': |
| 268 | + TrimHeadersTest().main() |
0 commit comments