mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
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.
272 lines
10 KiB
Python
Executable file
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()
|