-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathfeature_interim_daa.py
More file actions
executable file
·444 lines (359 loc) · 18.7 KB
/
feature_interim_daa.py
File metadata and controls
executable file
·444 lines (359 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
#!/usr/bin/env python3
# Copyright (c) 2024 The FACT0RN developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test interim DAA hardfork activation, behavior, and reversion.
This test builds a single continuous chain to verify:
- Phase 1: BIP9 state machine (DEFINED -> STARTED -> LOCKED_IN -> ACTIVE)
- Phase 2: Active period (DAA bands, difficulty adjustments)
- Phase 3: Reversion (auto-revert after max_active_blocks)
"""
from test_framework.blocktools import (
NORMAL_GBT_REQUEST_PARAMS,
create_block,
)
from test_framework.messages import msg_block
from test_framework.p2p import P2PInterface
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
# Interim DAA parameters (from consensus/params.h)
INTERIM_DAA_PERIOD = 42
INTERIM_DAA_THRESHOLD = 40
INTERIM_DAA_MAX_ACTIVE = 1344
INTERIM_DAA_BIT = 25
# Version bits
VERSIONBITS_TOP_BITS = 0x20000000
INTERIM_DAA_VERSION = VERSIONBITS_TOP_BITS | (1 << INTERIM_DAA_BIT)
NO_SIGNAL_VERSION = VERSIONBITS_TOP_BITS
# Target block time in seconds (30 minutes)
TARGET_BLOCK_TIME = 30 * 60
class InterimDAATest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
# Prevent `[net] [net.cpp:1336] [InactivityCheck] socket receive timeout: 117243310s peer=0`
# Somehow node freezes with 9223372036854775807, so we use 2^62
self.extra_args = [[f"-peertimeout={2**62}"]]
def setup_network(self):
self.setup_nodes()
# Instance variables to track chain state across tests
self.activation_height = None
self.last_active_height = None
self.first_reverted_height = None
# =========================================================================
# Helper Methods
# =========================================================================
def send_blocks(
self,
peer,
node,
numblocks: int,
version: int,
time_per_block: int = TARGET_BLOCK_TIME,
) -> list[str]:
"""
Send numblocks to peer with specified version and timing.
Uses getblocktemplate to obtain the correct nBits value for each block,
Uses setmocktime to control the node's time perception for DAA testing.
Args:
peer: P2P connection
node: Node RPC interface
numblocks: Number of blocks to send
version: Block version (for signaling)
time_per_block: Seconds between blocks (for DAA testing)
Returns:
List of block hashes created
"""
tip_header = node.getblockheader(node.getbestblockhash())
block_time = tip_header["time"] + time_per_block
hashes = []
expected_height = node.getblockcount() + 1
for _ in range(numblocks):
# Set mocktime so getblocktemplate returns correct curtime and bits
node.setmocktime(block_time)
# Get block template with correct nBits from the node
tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)
tmpl_bits = tmpl["bits"]
tmpl_height = tmpl["height"]
# Create block using template
block = create_block(tmpl=tmpl, ntime=block_time)
block.nVersion = version
block.solve()
# Log for progress tracking
if expected_height % INTERIM_DAA_PERIOD == 0:
self.log.info(
f"DAA boundary block {expected_height}: nBits={block.nBits}"
)
peer.send_message(msg_block(block))
# Wait for the block to be connected to the chain, not just received.
# sync_with_ping() only waits for ping/pong, but the block connection
# might still be pending. We need to wait for the height to increase.
try:
self.wait_until(
lambda: node.getblockcount() >= expected_height, timeout=10
)
except AssertionError:
self.log.error(
f"Block {expected_height} not accepted. Template bits={tmpl_bits}, height={tmpl_height}"
)
raise
block_time += time_per_block
hashes.append(block.sha256)
expected_height += 1
# Clear mocktime after mining
node.setmocktime(0)
return hashes
def get_softfork_status(self, node, deployment: str = "interim_daa"):
"""Get the status of a softfork deployment."""
info = node.getblockchaininfo()
return info["softforks"].get(deployment, {})
def check_interim_daa_status(self, node, expected_status, expected_active):
"""Verify interim_daa deployment status."""
sf = self.get_softfork_status(node)
assert_equal(sf["type"], "bip9")
assert_equal(sf["bip9"]["status"], expected_status)
assert_equal(sf["active"], expected_active)
return sf
def get_next_block_bits(self, node):
"""Get nBits from a block."""
tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)
return tmpl["bits"]
# =========================================================================
# Phase 1: BIP9 State Machine Tests
# =========================================================================
def test_defined_state(self, node, peer):
"""Verify DEFINED -> STARTED transition at block 84."""
self.log.info("=== Phase 1a: Testing DEFINED -> STARTED transition ===")
# Verify initial state is DEFINED
self.check_interim_daa_status(node, "defined", False)
self.log.info("Initial state: DEFINED")
# Mine through DEFINED period
self.log.info(f"Mining {INTERIM_DAA_PERIOD} blocks to exit DEFINED state...")
self.send_blocks(peer, node, INTERIM_DAA_PERIOD, NO_SIGNAL_VERSION)
# Verify transition to STARTED
self.check_interim_daa_status(node, "started", False)
assert_equal(node.getblockcount(), INTERIM_DAA_PERIOD)
self.log.info(f"Transitioned to STARTED at height {node.getblockcount()}")
def test_threshold_failure(self, node, peer):
"""Verify 39/42 signals does NOT trigger LOCKED_IN (stays STARTED)."""
self.log.info("=== Phase 1b: Testing threshold failure (39/42) ===")
# Mine 39 signaling blocks (one below threshold)
below_threshold = INTERIM_DAA_THRESHOLD - 1
self.log.info(f"Mining {below_threshold} signaling blocks (below threshold)...")
self.send_blocks(peer, node, below_threshold, INTERIM_DAA_VERSION)
# Mine remaining non-signaling blocks to complete period
remaining = INTERIM_DAA_PERIOD - below_threshold
self.log.info(f"Mining {remaining} non-signaling blocks...")
self.send_blocks(peer, node, remaining, NO_SIGNAL_VERSION)
# Verify still in STARTED (threshold not met)
self.check_interim_daa_status(node, "started", False)
assert_equal(node.getblockcount(), INTERIM_DAA_PERIOD * 2)
self.log.info(
f"{below_threshold}/{INTERIM_DAA_PERIOD} signals: stays STARTED at height {node.getblockcount()}"
)
def test_threshold_success(self, node, peer):
"""Verify 40/42 signals triggers LOCKED_IN."""
self.log.info("=== Phase 1c: Testing threshold success (40/42) ===")
# Mine exactly threshold signaling blocks
self.log.info(f"Mining {INTERIM_DAA_THRESHOLD} signaling blocks...")
self.send_blocks(peer, node, INTERIM_DAA_THRESHOLD, INTERIM_DAA_VERSION)
# Mine remaining non-signaling blocks
remaining = INTERIM_DAA_PERIOD - INTERIM_DAA_THRESHOLD
self.log.info(f"Mining {remaining} non-signaling blocks...")
self.send_blocks(peer, node, remaining, NO_SIGNAL_VERSION)
# Verify LOCKED_IN
self.check_interim_daa_status(node, "locked_in", False)
locked_in_height = node.getblockcount()
assert_equal(locked_in_height, INTERIM_DAA_PERIOD * 3)
self.log.info(
f"{INTERIM_DAA_THRESHOLD}/{INTERIM_DAA_PERIOD} signals: LOCKED_IN at height {locked_in_height}"
)
def test_locked_in_to_active(self, node, peer):
"""Verify LOCKED_IN -> ACTIVE transition after one period.
This test also verifies that when interim DAA activates at a height that is
also a 42-block adjustment boundary, the difficulty adjustment uses
the interim DAA formula (delta = +6 for fast blocks), not normal DAA
(which would give delta = +4 for the same timing).
"""
self.log.info(
"=== Phase 1d: Testing LOCKED_IN -> ACTIVE transition behavior ==="
)
bits_before_activation = self.get_next_block_bits(node)
locked_in_height = node.getblockcount()
# Mine one more period to activate
self.log.info(f"Mining {INTERIM_DAA_PERIOD} blocks to activate...")
self.send_blocks(peer, node, INTERIM_DAA_PERIOD, NO_SIGNAL_VERSION, 1)
# Verify ACTIVE
sf = self.check_interim_daa_status(node, "active", True)
self.activation_height = sf["bip9"]["since"]
# Calculate and store reversion heights
self.last_active_height = self.activation_height + INTERIM_DAA_MAX_ACTIVE - 1
self.first_reverted_height = self.last_active_height + 1
# Verify activation height matches expectation
expected_activation = locked_in_height + INTERIM_DAA_PERIOD
assert_equal(self.activation_height, expected_activation)
assert_equal(node.getblockcount(), INTERIM_DAA_PERIOD * 4)
# Verify difficulty adjustment is correct
bits_after_activation = self.get_next_block_bits(node)
bits_delta = bits_after_activation - bits_before_activation
assert_equal(bits_delta, 6)
self.log.info(
f"ACTIVE at height {self.activation_height}, "
f"last_active={self.last_active_height}, first_reverted={self.first_reverted_height}, bits_delta={bits_delta}"
)
# =========================================================================
# Phase 2: Active Period Tests
# =========================================================================
def test_daa_all_time_bands(self, node, peer):
"""Test DAA behavior across all time proportion bands."""
self.log.info("=== Phase 2b: Testing all DAA time bands ===")
target_timespan = INTERIM_DAA_PERIOD * TARGET_BLOCK_TIME
# Test parameters: (description, time_per_block, expected_delta)
test_cases = [
# Normal case
("Normal (27-31min avg)", 30 * 60, 0),
# Small adjustments
("Slightly fast (20-27min avg)", 27 * 60 - 1, 2),
("Slightly slow (31-45min avg)", 31 * 60 + 1, -2),
# Moderate adjustments
("Moderately fast (15-20min avg)", 20 * 60 - 1, 4),
("Moderately slow (45-60min avg)", 45 * 60 + 1, -4),
# Large adjustments
("Very fast (<15min avg)", 15 * 60 - 1, 6),
("Very slow (>60min avg)", 60 * 60 + 1, -6),
]
for desc, time_per_block, expected_delta in test_cases:
self.log.info(f"--- {desc} ---")
bits_before = self.get_next_block_bits(node)
height_before = node.getblockcount()
self.log.info(f" nBits before: {bits_before}, height: {height_before}")
# Mine a full period with this timing
self.send_blocks(
peer, node, INTERIM_DAA_PERIOD, NO_SIGNAL_VERSION, time_per_block
)
bits_after = self.get_next_block_bits(node)
actual_delta = bits_after - bits_before
self.log.info(
f" nBits after: {bits_after}, delta: {actual_delta}, expected: {expected_delta}"
)
assert_equal(actual_delta, expected_delta)
self.log.info(f" Passed: {desc}")
# =========================================================================
# Phase 3: Reversion Tests
# =========================================================================
def test_reversion_and_normal_daa(self, node, peer):
"""Verify reversion and that normal DAA is restored after expiration.
This test:
1. Identifies the 672-block window containing the reversion point
2. Records nBits at the start of this window
3. Mines through with one absurdly long block to skew the time proportion
4. Verifies the delta matches normal DAA (-4 or -3), not interim DAA (-6)
The absurdly long block makes proportion > 2.0, placed at the second-to-last
interim DAA block since the last block's time is ignored by the DAA calculation.
"""
self.log.info("=== Phase 3: Testing reversion and normal DAA behavior ===")
current_height = node.getblockcount()
normal_interval = 672
normal_time = TARGET_BLOCK_TIME
absurd_time = (
normal_interval * TARGET_BLOCK_TIME * 3
) # 3x entire target timespan
# =====================================================================
# Step 1: Calculate the 672-block window containing reversion
# =====================================================================
window_end = self.first_reverted_height + (
normal_interval - (self.first_reverted_height % normal_interval)
)
if self.first_reverted_height % normal_interval == 0:
window_end = self.first_reverted_height
window_start = window_end - normal_interval
self.log.info(f"672-block window: {window_start} to {window_end}")
self.log.info(f"Reversion at: {self.first_reverted_height}")
if self.first_reverted_height == window_end:
raise AssertionError(
f"Reversion at height {self.first_reverted_height} coincides with "
f"672-block boundary. Cannot test post-reversion DAA behavior."
)
# =====================================================================
# Step 2: Mine to window start and record initial nBits
# =====================================================================
if current_height < window_start:
blocks_to_window = window_start - current_height
self.log.info(
f"Mining {blocks_to_window} blocks to window start at {window_start}..."
)
self.send_blocks(peer, node, blocks_to_window, NO_SIGNAL_VERSION)
bits_at_window_start = self.get_next_block_bits(node)
height_at_window_start = node.getblockcount()
assert_equal(height_at_window_start, window_start)
self.log.info(
f"At window start: height={height_at_window_start}, nBits={bits_at_window_start}"
)
# =====================================================================
# Step 3: Mine to one block before the absurd timing block
# The absurd block is placed at last_active_height - 1 (second-to-last
# interim DAA block) since the last block's time is ignored by DAA.
# =====================================================================
absurd_block_height = self.last_active_height - 1
blocks_before_absurd = (absurd_block_height - 1) - height_at_window_start
if blocks_before_absurd > 0:
self.log.info(f"Mining {blocks_before_absurd} blocks with normal timing...")
self.send_blocks(
peer, node, blocks_before_absurd, NO_SIGNAL_VERSION, normal_time
)
# =====================================================================
# Step 4: Mine one block with absurd timing to skew the time proportion
# =====================================================================
self.log.info(f"Mining block {absurd_block_height} with absurd timing...")
self.send_blocks(peer, node, 1, NO_SIGNAL_VERSION, absurd_time)
assert_equal(node.getblockcount(), absurd_block_height)
# =====================================================================
# Step 5: Mine remaining blocks to window end (crosses reversion)
# =====================================================================
blocks_to_end = window_end - node.getblockcount()
self.log.info(
f"Mining {blocks_to_end} blocks to window end (crosses reversion at {self.first_reverted_height})..."
)
self.send_blocks(peer, node, blocks_to_end, NO_SIGNAL_VERSION, normal_time)
assert_equal(node.getblockcount(), window_end)
# BIP9 still shows 'active' (permanent state), but time-limited check now returns false
sf = self.get_softfork_status(node)
assert_equal(sf["bip9"]["status"], "active")
# =====================================================================
# Step 6: Verify normal DAA formula was used
# =====================================================================
bits_at_window_end = self.get_next_block_bits(node)
total_delta = bits_at_window_end - bits_at_window_start
# Normal DAA: -4 (even nBits) or -3 (odd nBits) for proportion > 2.0
# Interim DAA would give -6 or -7 for the same proportion
expected_delta = -4 if (bits_at_window_start % 2 == 0) else -3
self.log.info(f"At window end: height={window_end}, nBits={bits_at_window_end}")
self.log.info(f"Total delta: {total_delta}, expected: {expected_delta}")
assert_equal(total_delta, expected_delta)
self.log.info("Reversion verified: normal DAA formula used")
# =========================================================================
# Main Test Runner
# =========================================================================
def run_test(self):
"""Main test runner - builds single continuous chain."""
node = self.nodes[0]
peer = node.add_p2p_connection(P2PInterface())
self.log.info("=" * 60)
self.log.info("Starting interim DAA test - single continuous chain")
self.log.info("=" * 60)
# Phase 1: BIP9 State Machine
self.test_defined_state(node, peer)
self.test_threshold_failure(node, peer)
self.test_threshold_success(node, peer)
self.test_locked_in_to_active(node, peer)
# Phase 2: Active Period
self.test_daa_all_time_bands(node, peer)
# Phase 3: Reversion and normal DAA verification
self.test_reversion_and_normal_daa(node, peer)
self.log.info("=" * 60)
self.log.info(
f"All interim DAA tests passed! Final height: {node.getblockcount()}"
)
self.log.info("=" * 60)
if __name__ == "__main__":
InterimDAATest().main()