Skip to content

Commit 4171939

Browse files
committed
test: add trim_headers functional test
(cherry picked from commit 630870f)
1 parent 46a3ee2 commit 4171939

File tree

2 files changed

+262
-0
lines changed

2 files changed

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

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
'rpc_getnewblockhex.py',
113113
'wallet_elements_regression_1172.py --legacy-wallet',
114114
'wallet_elements_regression_1259.py --legacy-wallet',
115+
'feature_trim_headers.py',
115116
# Longest test should go first, to favor running tests in parallel
116117
'wallet_hd.py --legacy-wallet',
117118
'wallet_hd.py --descriptors',

0 commit comments

Comments
 (0)