Skip to content

Commit 792361c

Browse files
ajtownsluke-jr
andcommitted
tests: add functional test for bip8 activation
Co-authored-by: Luke Dashjr <luke-jr+git@utopios.org>
1 parent 42a717d commit 792361c

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

test/functional/feature_bip8b.py

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2021 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test bip8 activation
6+
"""
7+
8+
import random
9+
import time
10+
11+
from test_framework.test_framework import BitcoinTestFramework
12+
from test_framework.util import assert_equal, assert_greater_than_or_equal
13+
from test_framework.blocktools import create_block, create_coinbase
14+
from test_framework.p2p import P2PDataStore, p2p_lock
15+
from test_framework.messages import CBlockHeader, msg_headers
16+
17+
SEED = random.randrange(2**128)
18+
19+
VB_SIGNAL = 0x20000000 | (0x01 << 28)
20+
VB_NOSIGNAL = 0x20000000
21+
22+
MAX_HEADERS = 2000
23+
24+
BASE_TIME = int(time.time()) - 12*60*60
25+
26+
class P2PBlockCheck(P2PDataStore):
27+
def __init__(self, node, blocks):
28+
super().__init__()
29+
self.node = node
30+
with p2p_lock:
31+
for block in blocks:
32+
self.block_store[block.sha256] = block
33+
self.last_block_hash = block.sha256
34+
35+
def refresh_headers(self):
36+
headers = []
37+
with p2p_lock:
38+
for _, block in self.block_store.items():
39+
headers.append(CBlockHeader(block))
40+
while headers:
41+
self.send_message(msg_headers(headers[:MAX_HEADERS]))
42+
headers = headers[MAX_HEADERS:]
43+
44+
def send_blocks(self, blocks, *, reject_reason=None, timeout=20):
45+
tiphash = blocks[-1].hash
46+
47+
with p2p_lock:
48+
for block in blocks:
49+
self.block_store[block.sha256] = block
50+
self.last_block_hash = block.sha256
51+
52+
reject_reason = [reject_reason] if reject_reason else []
53+
success = reject_reason == []
54+
55+
with self.node.assert_debug_log(expected_msgs=reject_reason, timeout=timeout):
56+
self.send_message(msg_headers([CBlockHeader(block) for block in blocks]))
57+
self.sync_with_ping(timeout)
58+
if success:
59+
self.wait_until(lambda: self.node.getbestblockhash() == tiphash, timeout=timeout)
60+
61+
if not success:
62+
assert self.node.getbestblockhash() != tiphash
63+
64+
class BIP8Test(BitcoinTestFramework):
65+
def set_test_params(self):
66+
self.setup_clean_chain = True
67+
self.num_nodes = 4
68+
69+
def setup_network(self):
70+
# don't want to connect the bitcoinds to each other
71+
self.setup_nodes()
72+
73+
def generate_blocks(self, versioniter, prevblock, tipheight):
74+
test_blocks = []
75+
76+
for version in versioniter:
77+
blocktime = BASE_TIME + (tipheight * 6) # 6 seconds between blocks
78+
block = create_block(prevblock, create_coinbase(tipheight), blocktime)
79+
block.nVersion = version
80+
block.rehash()
81+
block.solve()
82+
test_blocks.append(block)
83+
prevblock = block.sha256
84+
tipheight = tipheight + 1
85+
86+
return test_blocks
87+
88+
def apply_blocks_and_check(self, versions, base_height, state, height=None):
89+
start = base_height + 288
90+
stop = base_height + 720
91+
92+
if height is None:
93+
bci = self.nodes[0].getblockchaininfo()
94+
prevblock = int("0x" + bci["bestblockhash"], 0)
95+
tipheight = bci["blocks"] + 1
96+
else:
97+
prevblock = int("0x" + self.nodes[0].getblockhash(height), 0)
98+
tipheight = height + 1
99+
100+
blocks = self.generate_blocks(versions, prevblock, tipheight)
101+
102+
self.helper[0].send_blocks(blocks)
103+
self.helper[1].send_blocks(blocks)
104+
self.helper[2].send_blocks(blocks)
105+
if state[0] == "stopped":
106+
ok_cnt = state[1]-tipheight
107+
if ok_cnt > 0:
108+
self.helper[3].send_blocks(blocks[:ok_cnt])
109+
if ok_cnt >= 0:
110+
self.helper[3].send_blocks(blocks[ok_cnt:], reject_reason="bad-vbit-unset-testdummy")
111+
else:
112+
self.helper[3].send_blocks(blocks)
113+
114+
heights, status = self.get_softfork_status()
115+
116+
# compare results
117+
assert_equal(heights[0], heights[1], heights[2])
118+
assert_greater_than_or_equal(heights[0], heights[3])
119+
if state[0] != "stopped":
120+
assert_equal(heights[0], heights[3])
121+
122+
if state[0] != "stopped":
123+
assert_equal(status[2]["bip8"].get("statistics", None), status[3]["bip8"].get("statistics", None))
124+
assert_equal(status[2]["active"], status[3]["active"])
125+
126+
# never active
127+
assert_equal(status[0], None)
128+
129+
# always active
130+
assert_equal(status[1], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': -1, 'timeoutheight': 0, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': 0}, 'height': 0, 'active': True})
131+
132+
# lockinontimeout=false
133+
if "statistics" in status[2]["bip8"]:
134+
status[2]["bip8"]["statistics"] = None
135+
136+
if state[0] == "defined":
137+
assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'active': False})
138+
elif state[0] == "started" or state[0] == "must_signal":
139+
since = state[1] if state[0] == "started" else (state[1] - 288)
140+
assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': since, 'statistics': None}, 'active': False})
141+
elif state[0] == "locked_in":
142+
assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1], 'statistics': None}, 'active': False})
143+
elif state[0] == "active":
144+
assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'height': state[1], 'active': True})
145+
elif state[0] == "stopped":
146+
failat = state[1] - (state[1] % 144) + 144
147+
assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'failed', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': failat}, 'active': False})
148+
else:
149+
assert False, ("bad state %r" % (state))
150+
151+
# lockinontimeout=true
152+
if "statistics" in status[3]["bip8"]:
153+
assert_equal(status[3]["bip8"]["statistics"]["possible"], True)
154+
status[3]["bip8"]["statistics"] = None
155+
156+
if state[0] == "defined":
157+
assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'active': False})
158+
elif state[0] == "started":
159+
assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False})
160+
elif state[0] == "must_signal" or state[0] == "stopped":
161+
since = state[1] - state[1] % 144
162+
assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'must_signal', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': since, 'statistics': None}, 'active': False})
163+
elif state[0] == "locked_in":
164+
assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False})
165+
elif state[0] == "active":
166+
assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'height': state[1], 'active': True})
167+
else:
168+
assert False, ("bad state %r" % (state))
169+
170+
return blocks
171+
172+
173+
def get_softfork_status(self):
174+
info = [node.getblockchaininfo() for node in self.nodes]
175+
return zip( *[(i["blocks"], i['softforks'].get('testdummy', None))
176+
for i in info] )
177+
178+
def setup_vbparams(self):
179+
base_height = self.nodes[0].getblockcount()
180+
if base_height:
181+
base_height += 1
182+
assert base_height % 144 == 0
183+
start = base_height + 288
184+
stop = base_height + 720
185+
186+
for node in self.nodes:
187+
node.disconnect_p2ps()
188+
self.helper = []
189+
self.stop_nodes()
190+
191+
self.start_nodes(extra_args = [
192+
# never active
193+
["-vbparams=testdummy:@-2:@-2:0"],
194+
# always active
195+
["-vbparams=testdummy:@-1:@0:1"],
196+
# abandon softfork if not locked in by timeout
197+
["-vbparams=testdummy:@%s:@%s:0" % (start, stop)],
198+
# reject blocks if not locked in by timeout
199+
["-vbparams=testdummy:@%s:@%s:1" % (start, stop)],
200+
])
201+
for n in range(1, 4):
202+
self.connect_nodes(0, n)
203+
self.sync_all()
204+
for n in range(1, 4):
205+
self.disconnect_nodes(0, n)
206+
self.helper = [n.add_p2p_connection(P2PBlockCheck(n, self.all_blocks)) for n in self.nodes]
207+
return base_height
208+
209+
def do_test(self, signalling, expstate, expheight):
210+
base_height = self.setup_vbparams()
211+
212+
# track the expected state/height for rejecting-node
213+
state = "defined", 0
214+
215+
period = 0
216+
for cnt in signalling:
217+
nblocks = 144
218+
if base_height == 0 and period == 0:
219+
nblocks -= 1
220+
cnt = max(0, min(cnt, nblocks))
221+
bits = [VB_SIGNAL]*cnt + [VB_NOSIGNAL]*(nblocks - cnt)
222+
223+
random.shuffle(bits)
224+
225+
# what will the state be after these blocks are mined for a lockinontimeout=true node?
226+
period += 1
227+
if state[0] == "defined" and period == 2:
228+
state = "started", base_height + (period*144)
229+
elif state[0] == "started":
230+
if cnt >= 108:
231+
state = "locked_in", base_height + (period*144)
232+
elif period == 4:
233+
state = "must_signal", base_height + (period*144)
234+
elif state[0] == "must_signal":
235+
if cnt >= 108:
236+
state = "locked_in", base_height + (period*144)
237+
else:
238+
oknosig = 144-108
239+
howmany = None
240+
for i, x in enumerate(bits):
241+
if x != VB_SIGNAL:
242+
if oknosig == 0:
243+
howmany = i
244+
break
245+
oknosig -= 1
246+
state = "stopped", base_height + (period*144) - 144 + howmany
247+
elif state[0] == "locked_in":
248+
state = "active", base_height + (period*144)
249+
250+
self.all_blocks.extend(self.apply_blocks_and_check(bits, base_height, state))
251+
252+
assert_equal(state[0], expstate)
253+
assert_greater_than_or_equal(state[1] - base_height, expheight)
254+
if expstate != "stopped":
255+
assert_equal(state[1] - base_height, expheight)
256+
else:
257+
last_possible = expheight - expheight%144 + 143
258+
assert_greater_than_or_equal(last_possible, state[1] - base_height)
259+
self.log.info("Completed test %s %s" % (base_height, state))
260+
261+
def run_test(self):
262+
self.all_blocks = []
263+
264+
random.seed(SEED)
265+
N = random.randrange(0,108)
266+
Y = random.randrange(108,145)
267+
self.log.info("seed %s, signal vals %s/%s" % (SEED, Y, N))
268+
269+
self.do_test([N,N,N,N,107,144], "stopped", 612)
270+
self.do_test([144,144,Y,0,0,0], "active", 576)
271+
self.do_test([Y,Y,0,0,0,Y], "stopped", 612)
272+
self.do_test([N,N,N,N,N,144], "stopped", 612)
273+
self.do_test([0,0,0,Y,0,0], "active", 720)
274+
self.do_test([0,144,N,N,Y,0], "active", 864)
275+
276+
if __name__ == '__main__':
277+
BIP8Test().main()

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
'wallet_hd.py --descriptors',
9191
'wallet_backup.py',
9292
'wallet_backup.py --descriptors',
93+
'feature_bip8b.py',
9394
# vv Tests less than 5m vv
9495
'mining_getblocktemplate_longpoll.py',
9596
'feature_maxuploadtarget.py',

0 commit comments

Comments
 (0)