Skip to content

Commit 262dcf2

Browse files
committed
test: add trim_headers functional test
1 parent cfc10a5 commit 262dcf2

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python3
2+
3+
import codecs
4+
from decimal import Decimal
5+
from time import sleep
6+
7+
from test_framework.test_framework import BitcoinTestFramework
8+
from test_framework.util import (assert_raises_rpc_error, assert_equal)
9+
from test_framework import (
10+
address,
11+
key,
12+
)
13+
from test_framework.messages import (
14+
COIN,
15+
CBlock,
16+
from_hex,
17+
)
18+
from test_framework.script import (
19+
CScript
20+
)
21+
22+
# Generate wallet import format from private key.
23+
def wif(pk):
24+
# Base58Check version for regtest WIF keys is 0xef = 239
25+
pk_compressed = pk + bytes([0x1])
26+
return address.byte_to_base58(pk_compressed, 239)
27+
28+
# The signblockscript is a Bitcoin Script k-of-n multisig script.
29+
def make_signblockscript(num_nodes, required_signers, keys):
30+
assert num_nodes >= required_signers
31+
script = "{}".format(50 + required_signers)
32+
for i in range(num_nodes):
33+
k = keys[i]
34+
script += "21"
35+
script += codecs.encode(k.get_pubkey().get_bytes(), 'hex_codec').decode("utf-8")
36+
script += "{}".format(50 + num_nodes) # num keys
37+
script += "ae" # OP_CHECKMULTISIG
38+
return script
39+
40+
class TrimHeadersTest(BitcoinTestFramework):
41+
def skip_test_if_missing_module(self):
42+
self.skip_if_no_wallet()
43+
44+
# Dynamically generate N keys to be used for block signing.
45+
def init_keys(self, num_keys):
46+
self.keys = []
47+
self.wifs = []
48+
for i in range(num_keys):
49+
k = key.ECKey()
50+
k.generate()
51+
w = wif(k.get_bytes())
52+
self.keys.append(k)
53+
self.wifs.append(w)
54+
55+
def set_test_params(self):
56+
# 1 block creator/signer, 1 tx receiver, and the rest trimmed headers
57+
self.num_nodes = 5
58+
self.num_keys = 2
59+
self.required_signers = 2
60+
self.setup_clean_chain = True
61+
self.init_keys(self.num_keys)
62+
signblockscript = make_signblockscript(self.num_keys, self.required_signers, self.keys)
63+
self.witnessScript = signblockscript # post-dynafed this becomes witnessScript
64+
args = [
65+
"-signblockscript={}".format(signblockscript),
66+
"-con_max_block_sig_size={}".format(self.required_signers * 74 + self.num_nodes * 33),
67+
"-anyonecanspendaremine=1",
68+
"-evbparams=dynafed:0:::",
69+
"-con_dyna_deploy_signal=1",
70+
]
71+
self.trim_args = args + [ "-trim_headers=1" ]
72+
self.extra_args = [
73+
args,
74+
args,
75+
self.trim_args,
76+
self.trim_args,
77+
self.trim_args,
78+
]
79+
80+
def setup_network(self):
81+
self.setup_nodes()
82+
self.connect_nodes(0, 1)
83+
for i in range(2, self.num_nodes):
84+
self.connect_nodes(0, i)
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+
assert_equal(self.nodes[1].getblockcount(), expected_height)
95+
96+
def mine_block(self, make_transactions):
97+
# alternate mining between the signing nodes
98+
mineridx = self.nodes[0].getblockcount() % self.required_signers # assuming in sync
99+
mineridx_next = (self.nodes[0].getblockcount() + 1) % self.required_signers
100+
miner = self.nodes[mineridx]
101+
miner_next = self.nodes[mineridx_next]
102+
103+
# If dynafed is enabled, this means signblockscript has been WSH-wrapped
104+
blockchain_info = self.nodes[0].getblockchaininfo()
105+
dynafed_active = blockchain_info['softforks']['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 i in range(num_blocks):
157+
self.mine_block(transactions)
158+
159+
def run_test(self):
160+
for i in range(self.num_keys):
161+
self.nodes[i].importprivkey(self.wifs[i])
162+
163+
expected_height = 0
164+
self.check_height(expected_height, all=True)
165+
166+
self.log.info("Mining and signing 101 blocks to unlock funds")
167+
expected_height += 101
168+
self.mine_blocks(101, False)
169+
self.sync_all()
170+
self.check_height(expected_height, all=True)
171+
172+
self.log.info("Shut down trimmed nodes")
173+
for i in range(2, self.num_nodes):
174+
self.stop_node(i)
175+
176+
self.log.info("Mining and signing non-empty blocks")
177+
expected_height += 10
178+
self.mine_blocks(10, True)
179+
self.check_height(expected_height)
180+
181+
# signblock rpc field stuff
182+
tip = self.nodes[0].getblockhash(self.nodes[0].getblockcount())
183+
header = self.nodes[0].getblockheader(tip)
184+
block = self.nodes[0].getblock(tip)
185+
info = self.nodes[0].getblockchaininfo()
186+
187+
assert 'signblock_witness_asm' in header
188+
assert 'signblock_witness_hex' in header
189+
assert 'signblock_witness_asm' in block
190+
assert 'signblock_witness_hex' in block
191+
192+
assert_equal(info['softforks']['dynafed']['bip9']['status'], "defined")
193+
194+
# activate dynafed
195+
blocks_til_dynafed = 431 - self.nodes[0].getblockcount()
196+
self.log.info("Activating dynafed")
197+
self.mine_blocks(blocks_til_dynafed, False)
198+
expected_height += blocks_til_dynafed
199+
self.check_height(expected_height)
200+
201+
assert_equal(self.nodes[0].getblockchaininfo()['softforks']['dynafed']['bip9']['status'], "active")
202+
203+
self.log.info("Mine some dynamic federation blocks without txns")
204+
self.mine_blocks(10, False)
205+
expected_height += 10
206+
self.check_height(expected_height)
207+
208+
self.log.info("Mine some dynamic federation blocks with txns")
209+
self.mine_blocks(10, True)
210+
expected_height += 10
211+
self.check_height(expected_height)
212+
213+
num = 1100 - expected_height
214+
self.log.info(f"Mine {num} blocks without txns")
215+
self.mine_blocks(num, False)
216+
expected_height += num
217+
self.check_height(expected_height)
218+
219+
num = 2100 - expected_height
220+
self.log.info(f"Mine {num} blocks without txns")
221+
self.mine_blocks(num, False)
222+
expected_height += num
223+
224+
self.log.info("Start the trimmed nodes")
225+
for i in range(2, self.num_nodes):
226+
self.start_node(i, extra_args=self.trim_args)
227+
self.connect_nodes(0, i)
228+
self.sync_all()
229+
230+
self.check_height(expected_height, all=True)
231+
232+
info = self.nodes[0].getblockchaininfo()
233+
hash = self.nodes[0].getblockhash(expected_height)
234+
block = self.nodes[0].getblock(hash)
235+
for i in range(2, self.num_nodes):
236+
assert_equal(info, self.nodes[i].getblockchaininfo())
237+
assert_equal(hash, self.nodes[i].getblockhash(expected_height))
238+
assert_equal(block, self.nodes[i].getblock(hash))
239+
240+
self.log.info(f"All nodes at height {expected_height} with block hash {hash}")
241+
242+
243+
if __name__ == '__main__':
244+
TrimHeadersTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
'feature_progress.py',
111111
'rpc_getnewblockhex.py',
112112
'wallet_elements_regression_1172.py',
113+
'feature_trim_headers.py',
113114
# Longest test should go first, to favor running tests in parallel
114115
'wallet_hd.py --legacy-wallet',
115116
'wallet_hd.py --descriptors',

0 commit comments

Comments
 (0)