elements/test/functional/feature_trim_headers.py
Byron Hambly e151cada58
test: update and re-enable trim headers functional test
The trim_headers test was previously moved to extended tests, and only
worked with legacy wallets. Update it to work with descriptor wallets,
and move it back to base scripts in functional tests.
2026-07-02 14:10:12 +02:00

272 lines
10 KiB
Python
Executable file

#!/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()