-
Notifications
You must be signed in to change notification settings - Fork 388
/
Copy pathfeature_dynafed.py
executable file
·583 lines (490 loc) · 31.5 KB
/
feature_dynafed.py
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
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
#!/usr/bin/env python3
# Copyright (c) 2019 The Elements Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test dynamic federations state machine logic
NOTE: This test is not testing the behavior not related to transitions themselves.
That is for other tests such as feature_pak, feature_fedpeg, feature_blocksign
1) Test "legacy" params are still in play before versionbits activation
2) Test transition to dynafed preserves expected chainparams
3) Test a full epoch with no votes
4) Test full epoch with just under 4/5 votes, with competing random proposals
5) Test full epoch with just at 4/5 votes, with competing random proposals
6) Test full epoch with 5/5 votes
7) Test that peg-outs(PAK) and peg-ins are ejected from mempool block before transition
and rejected when re-submitted if there is a parameter mismatch
8) Test that reorging a transition results in transitions being undone,
previously ejected transactions are allowed back into the mempool when appropriate
"""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_raises_rpc_error, assert_equal
# Hardcoded PAK that's in chainparams to make sure PAK is enforced even when dynafed is not
initial_online = "02fcba7ecf41bc7e1be4ee122d9d22e3333671eb0a3a87b5cdf099d59874e1940f"
# Random key to make new pak
initial_offline = "03808355deeb0555203b53df7ef8f36edaf66ab0207ca1b11968a7ac421554e621"
initial_extension = [initial_online+initial_online]
new_extension = [initial_offline+initial_online]
initial_cpe_root = "3700bdb2975ff8e0dadaaba2b33857b0ca2610c950a92b1db725025e3647a8e1"
ERR_MP_INVALID_PEGOUT = "invalid-pegout-proof"
ERR_MP_INVALID_PEGIN = "pegin-no-witness"
def validate_no_vote_op_true(node, block, first_dynafed_active_block):
block_info = node.getblock(block)
dynamic_parameters = block_info["dynamic_parameters"]
block_height = block_info["height"]
assert "current" in dynamic_parameters
assert "proposed" in dynamic_parameters
# signblockscript is now the P2WSH-ification of OP_TRUE
WSH_OP_TRUE = node.decodescript("51")["segwit"]["hex"]
assert_equal(dynamic_parameters["current"]["signblockscript"], WSH_OP_TRUE)
if block_height % 10 == 0 or first_dynafed_active_block:
assert_equal(dynamic_parameters["current"]["type"], "full")
assert_equal(dynamic_parameters["current"]["root"], initial_cpe_root)
assert_equal(dynamic_parameters["current"]["fedpegscript"], "51")
assert_equal(dynamic_parameters["current"]["extension_space"], initial_extension)
else:
assert_equal(dynamic_parameters["current"]["type"], "compact")
assert_equal(dynamic_parameters["current"]["root"], initial_cpe_root)
assert not "fedpegscript" in dynamic_parameters["proposed"]
assert not "extension_space" in dynamic_parameters["proposed"]
assert_equal(dynamic_parameters["current"]["max_block_witness"], 74)
# nothing was proposed, null fields make impossible to be valid blockheader
# due to script rules requiring bool true on stack
assert_equal(dynamic_parameters["proposed"]["type"], "null")
assert not "root" in dynamic_parameters["proposed"]
assert not "signblockscript" in dynamic_parameters["proposed"]
assert not "max_block_witness" in dynamic_parameters["proposed"]
assert not "fedpegscript" in dynamic_parameters["proposed"]
assert not "extension_space" in dynamic_parameters["proposed"]
class DynaFedTest(BitcoinTestFramework):
def go_to_epoch_end(self, node):
epoch_info = node.getblockchaininfo()
blocks_to_mine = epoch_info["epoch_length"] - epoch_info["epoch_age"] - 1
self.generatetoaddress(node, blocks_to_mine, node.getnewaddress(), sync_fun=self.no_op)
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2
# We want to test activation of dynafed
self.extra_args = [[
"-evbparams=dynafed:1000:::",
"-enforce_pak=1",
"-con_parent_chain_signblockscript=51",
"-peginconfirmationdepth=1",
"-parentscriptprefix=75",
"-parent_bech32_hrp=ert",
"-con_dyna_deploy_signal=1",
] for i in range(self.num_nodes)]
# second node will not mine transactions
self.extra_args[1].append("-blocksonly=1")
# Make sure nothing breaks if peers have a different activation.
self.extra_args[1][0] = "-evbparams=dynafed:937:::"
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def test_legacy_params(self):
self.log.info("Testing legacy parameters...")
for i in range(self.num_nodes):
assert_equal(self.nodes[i].getblockcount(), 0)
# Check deployment exists and is not active
dyna_activate = self.nodes[i].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]
assert_equal(dyna_activate["status"], "defined")
# fedpegscript is OP_TRUE
legacy_sc_info = self.nodes[i].getsidechaininfo()
assert_equal(legacy_sc_info["fedpegscript"], "51")
# No history yet, only one "live" fedpegscript
assert_equal(legacy_sc_info["current_fedpegscripts"], ["51"])
# blocksigner is OP_TRUE, extension space is hardcoded one in chainparams
signblock_info = self.nodes[i].getblockchaininfo()
assert_equal(signblock_info["current_signblock_hex"], "51")
assert_equal(signblock_info["max_block_witness"], 74)
assert_equal(signblock_info["extension_space"], initial_extension)
pak_info = self.nodes[i].getpakinfo()
assert_equal(pak_info["block_paklist"]["reject"], False)
assert_equal(pak_info["block_paklist"]["online"], [initial_online])
assert_equal(pak_info["block_paklist"]["offline"], [initial_online])
# can not put proposed params into blockheader pre-dynafed
assert_raises_rpc_error(-8, "Dynamic federations is not active on this network. Proposed parameters are not needed.", self.nodes[i].getnewblockhex, 0, {})
# TODO Reject serialized dynamic federations blocks before activation
def test_dynafed_activation(self):
self.log.info("Testing dynafed versionbits activation...")
# Signaling window is in height, not time, so first block that will signal is
# at height 1008 which is evenly divisible by 144(regtest bip9 window size)
# Giving funds to node 1 to avoid a transaction size blowup when sweeping later
blocks = self.generatetoaddress(self.nodes[0], 1007, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]["status"], "defined")
blocks += self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]["status"], "started")
blocks += self.generatetoaddress(self.nodes[0], 144, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]["status"], "locked_in")
# Move chain forward to activation, any new blocks will be enforced
# bitcoin PR #23508 changed bip9 status to the current block instead of the next block
blocks += self.generatetoaddress(self.nodes[0], 143, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
self.sync_blocks(timeout=240)
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]["status"], "locked_in")
# Check the root hash
assert_equal(self.nodes[0].getblockchaininfo()["current_params_root"], initial_cpe_root)
# Existing blocks should have null dynafed fields
for block in blocks:
assert "dynamic_parameters" not in self.nodes[0].getblock(block)
# Next block is first dynamic federation block
block = self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())[0]
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["dynafed"]["bip9"]["status"], "active")
# We publish full block on BIP9 transition
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block, True)
def test_illegal_proposals(self):
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
# fedpegscript proposals starting with OP_DEPTH(0x74) are illegal when witness v0
assert_raises_rpc_error(-1, "invalid-dyna-fed, Proposed fedpegscript starts with OP_DEPTH, which is illegal", self.nodes[0].getnewblockhex, 0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":100, "fedpegscript":"74", "extension_space":[]})
# but it's ok to have the opcode elsewhere
self.nodes[0].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":100, "fedpegscript":"0074", "extension_space":[]})
# signblockscript proposals must be native segwit scriptpubkeys
assert_raises_rpc_error(-1, "invalid-dyna-fed, proposed signblockscript must be native segwit scriptPubkey", self.nodes[0].getnewblockhex, 0, {"signblockscript":"51", "max_block_witness":100, "fedpegscript":"51", "extension_space":[]})
assert_raises_rpc_error(-1, "invalid-dyna-fed, proposed signblockscript must be native segwit scriptPubkey", self.nodes[0].getnewblockhex, 0, {"signblockscript":"00"+WSH_OP_TRUE, "max_block_witness":100, "fedpegscript":"51", "extension_space":[]})
# Since we're enforcing PAK, extension space entries *must* be 66 bytes
# each 33 of which are serialized compressed pubkeys
assert_raises_rpc_error(-1, "invalid-dyna-fed, Extension space is not list of valid PAK entries", self.nodes[0].getnewblockhex, 0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":100, "fedpegscript":"51", "extension_space":["00"]})
assert_raises_rpc_error(-1, "invalid-dyna-fed, Extension space is not list of valid PAK entries", self.nodes[0].getnewblockhex, 0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":100, "fedpegscript":"51", "extension_space":["", initial_extension[0]]})
def test_no_vote(self):
self.log.info("Testing no-vote epoch...")
self.go_to_epoch_end(self.nodes[0])
# Mine epoch_length blocks with no proposals
blocks = self.generatetoaddress(self.nodes[0], 10, self.nodes[0].getnewaddress())
for i in range(self.num_nodes):
for block in blocks:
validate_no_vote_op_true(self.nodes[i], block, False)
# Now transition using vanilla getnewblockhex, nothing changed
block = self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())[0]
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block, False)
def test_under_vote(self):
self.log.info("Testing failed voting epoch...")
self.go_to_epoch_end(self.nodes[0])
# Mine 7 blocks with agreeing proposals for single-sig, falls short of 4/5 of 10
new_signblock = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress("", "bech32"))["scriptPubKey"]
cur_height = self.nodes[0].getblockcount()
for _ in range(7):
prop_block = self.nodes[0].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":100, "fedpegscript":"52", "extension_space":new_extension})
self.nodes[0].submitblock(prop_block)
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+7)
# Now mine 3 blank blocks
self.generatetoaddress(self.nodes[0], 3, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
# No transition will take place, generatetoaddress still works for new epoch
block = self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())[0]
for i in range(self.num_nodes):
validate_no_vote_op_true(self.nodes[i], block, False)
def test_four_fifth_vote(self):
self.log.info("Testing just-successful transition epoch...")
self.go_to_epoch_end(self.nodes[0])
# Mine 8 blocks with agreeing proposals for single-sig, triggering transition
new_signblock = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress("", "bech32"))["scriptPubKey"]
cur_height = self.nodes[0].getblockcount()
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
for _ in range(8):
# Check that things don't change until the 10th block is submitted
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(chain_info["max_block_witness"], 74)
assert_equal(chain_info["extension_space"], initial_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["51", "51"])
prop_block = self.nodes[0].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":107, "fedpegscript":"52", "extension_space":new_extension})
self.nodes[0].submitblock(prop_block)
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+8)
# Now mine 1 blank block
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())
# Old parameters still enforced for next block...
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(chain_info["max_block_witness"], 74)
assert_equal(chain_info["extension_space"], initial_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["51", "51"])
# Last blank block
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())
# We have now transitioned, next block must have signature
unsigned_block = self.nodes[0].getnewblockhex()
assert_equal(self.nodes[0].submitblock(unsigned_block), "block-proof-invalid")
assert_equal(self.nodes[0].getblockcount(), cur_height+10)
# New params now enforced
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], new_signblock)
assert_equal(chain_info["max_block_witness"], 107) # 72+33+2
assert_equal(chain_info["extension_space"], new_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["52", "51"])
def test_all_vote(self):
self.log.info("Testing unanimous transition epoch...")
# We have now transitioned to single-sig blocks from node 0
# Let's transition node 1's key with all votes to it
cur_height = self.nodes[0].getblockcount()
new_signblock = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress("", "bech32"))["scriptPubKey"]
for _ in range(10):
# Check that things don't change until the 10th block is submitted
for i in range(self.num_nodes):
chain_info = self.nodes[i].getblockchaininfo()
fedpeg_info = self.nodes[i].getsidechaininfo()
assert chain_info["current_signblock_hex"] != new_signblock
assert_equal(chain_info["max_block_witness"], 107)
assert_equal(chain_info["extension_space"], new_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["52", "51"])
block = self.nodes[1].getnewblockhex(0, {"signblockscript":new_signblock, "max_block_witness":108, "fedpegscript":"53", "extension_space":new_extension})
sig = self.nodes[0].signblock(block, "")
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[1].signblock, block, "")
comb_result = self.nodes[0].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[1].submitblock(comb_result["hex"])
self.sync_all()
self.sync_all()
assert_equal(self.nodes[0].getblockcount(), cur_height+10)
chain_info = self.nodes[0].getblockchaininfo()
fedpeg_info = self.nodes[0].getsidechaininfo()
assert_equal(chain_info["current_signblock_hex"], new_signblock)
assert_equal(chain_info["max_block_witness"], 108)
assert_equal(chain_info["extension_space"], new_extension)
assert_equal(fedpeg_info["current_fedpegscripts"], ["53", "52"])
# Now node 1 is the signer
block = self.nodes[0].getnewblockhex()
sig = self.nodes[1].signblock(block, "")
assert_raises_rpc_error(-25, "Could not sign the block.", self.nodes[0].signblock, block, "")
comb_result = self.nodes[1].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[0].submitblock(comb_result["hex"])
assert_equal(self.nodes[0].getblockcount(), cur_height+11)
def test_transition_mempool_eject(self):
self.log.info("Testing mempool (r)ejection policy on transitions...")
# node 1 is still signer, let's transition to something we can PAK peg-out to
# and OP_TRUE fedpegscript, and set signblockscript back to OP_TRUE
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
init_details = self.nodes[0].initpegoutwallet(xpub)
pak_entry = init_details["pakentry"]
# stitch the extension space together using the relevant keys
extension_space = [pak_entry[4:4+66]+pak_entry[4+66+1:]]
pak_prop = {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space}
epoch_info = self.nodes[0].getblockchaininfo()
blocks_to_end = epoch_info["epoch_length"] - epoch_info["epoch_age"] - 1
for _ in range(blocks_to_end):
block = self.nodes[1].getnewblockhex(0, pak_prop)
sig = self.nodes[1].signblock(block, "")
comb_result = self.nodes[1].combineblocksigs(block, sig, "")
assert comb_result["complete"]
self.nodes[1].submitblock(comb_result["hex"])
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(self.nodes[1].getsidechaininfo()["current_fedpegscripts"], ["51", "53"])
# Transactions
# Peg-in prep:
# hack: since we're not validating peg-ins in parent chain, just make
# both the funding and claim tx on same chain (printing money)
fund_info = self.nodes[0].getpeginaddress()
peg_id = self.nodes[0].sendtoaddress(fund_info["mainchain_address"], 1)
peg_tx = self.nodes[0].gettransaction(peg_id)["hex"]
self.nodes[0].testmempoolaccept([peg_tx])
# only one confirm needed in this setup, we do 10 to sync with epoch_length
self.generatetoaddress(self.nodes[0], 10, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
proof = self.nodes[0].gettxoutproof([peg_id])
raw_tx = self.nodes[0].gettransaction(peg_id)["hex"]
# Now, peg-in and PAK peg-out in node 0 mempool
# We need this transaction to get into the mempool, then transition
# to new fedpegscript, then wait another epoch, to get dumped.
claim_id = self.nodes[0].claimpegin(raw_tx, proof, fund_info["claim_script"])
# saving for re-submission later
raw_claim = self.nodes[0].gettransaction(claim_id)["hex"]
# This transaction will be dumped as soon as transition activates
pegout_id = self.nodes[0].sendtomainchain("", 1)["txid"]
# Chain payment, this should get "recursively" kicked on transition
pegout_child_id = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), self.nodes[0].getbalance()['bitcoin'], "", "", True)
raw_pegout = self.nodes[0].gettransaction(pegout_id)["hex"]
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# node 1 is blocksonly, no mempool so it won't mine node 0's transactions
assert_equal(self.nodes[1].getrawmempool(), [])
# Now generate an epoch of blocks on node 1 to show that non-transitions don't dump
# PAK or peg-in transactions from mempool
self.generatetoaddress(self.nodes[1], 10, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
self.sync_blocks()
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# Transactions are still in mempool
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# Now have node 1 transition to exact same pak and fedpegscript
for _ in range(10):
block = self.nodes[1].getnewblockhex(0, pak_prop)
assert_equal(self.nodes[1].submitblock(block), None)
self.sync_blocks()
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# After the 10th block, nothing gets the boot
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
# Now have node 1 transition to new pak and fedpegscript
pak_prop["fedpegscript"] = "52"
pak_prop["extension_space"] = initial_extension
for _ in range(10):
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id in raw_pool
assert pegout_child_id in raw_pool
block = self.nodes[1].getnewblockhex(0, pak_prop)
assert_equal(self.nodes[1].submitblock(block), None)
self.sync_blocks()
assert_equal(self.nodes[0].getblockchaininfo()["epoch_age"], 9)
# After 10 blocks, PAK and child is booted, peg-in still lingers for 1 more epoch
raw_pool = self.nodes[0].getrawmempool()
assert claim_id in raw_pool
assert pegout_id not in raw_pool
assert pegout_child_id not in raw_pool
# Re-submission fails
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
for _ in range(10):
assert claim_id in self.nodes[0].getrawmempool()
self.nodes[1].submitblock(self.nodes[1].getnewblockhex())
self.sync_blocks()
# After 10 blocks(no proposal), peg-in is finally dumped
assert claim_id not in self.nodes[0].getrawmempool()
# Both claim and peg-out rejected from submission as well
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
assert_raises_rpc_error(-26, "pegin-no-witness, Peg-in tx is invalid.", self.nodes[0].sendrawtransaction, raw_claim)
# Now we test reorg behavior
best_blockhash = self.nodes[0].getbestblockhash()
# Invalidate tip, peg-in should be allowed back into mempool but not pegout
self.nodes[0].invalidateblock(best_blockhash)
self.nodes[0].sendrawtransaction(raw_claim)
assert claim_id in self.nodes[0].getrawmempool()
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
# Reconsider best block, should be booted and invalid again
self.nodes[0].reconsiderblock(best_blockhash)
assert claim_id not in self.nodes[0].getrawmempool()
# Go back 20 blocks to let peg-out back in
old_blockhash = self.nodes[0].getblockhash(self.nodes[0].getblockcount()-20)
self.nodes[0].invalidateblock(old_blockhash)
self.nodes[0].sendrawtransaction(raw_claim)
self.nodes[0].sendrawtransaction(raw_pegout)
assert claim_id in self.nodes[0].getrawmempool()
assert pegout_id in self.nodes[0].getrawmempool()
# Again go back to tip, both booted and not let back in
self.nodes[0].reconsiderblock(best_blockhash)
assert claim_id not in self.nodes[0].getrawmempool()
assert pegout_id not in self.nodes[0].getrawmempool()
assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[0].sendrawtransaction, raw_pegout)
assert_raises_rpc_error(-26, "pegin-no-witness, Peg-in tx is invalid.", self.nodes[0].sendrawtransaction, raw_claim)
def assert_accepted(self, tx):
ret = self.nodes[0].testmempoolaccept([tx])[0]
assert ret["allowed"], ret["reject-reason"]
def test_valid_epochs(self):
self.log.info("Testing pegins and pegouts stay valid for some epochs")
# previous test leaves us at age 9
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress())
assert_equal(self.nodes[1].getblockchaininfo()["epoch_age"], 0)
# signblockscript is OP_TRUE, let's transition to something we can PAK peg-out to
# and OP_TRUE fedpegscript, and set signblockscript back to OP_TRUE
WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
init_details = self.nodes[1].initpegoutwallet(xpub)
pak_entry = init_details["pakentry"]
# stitch the extension space together using the relevant keys
extension_space = [pak_entry[4:4+66]+pak_entry[4+66+1:]]
pak_prop = {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space}
# generate blocks with the new proposal
for _ in range(9):
self.nodes[1].submitblock(self.nodes[1].getnewblockhex(0, pak_prop))
self.sync_all()
assert_equal(self.nodes[1].getblockchaininfo()["epoch_age"], 9)
assert_equal(self.nodes[1].getblockchaininfo()["current_signblock_hex"], WSH_OP_TRUE)
assert_equal(self.nodes[1].getsidechaininfo()["current_fedpegscripts"], ["51", "52"])
# Transactions
# pegout prep is easy, just pegout
pegout_tx = self.nodes[1].gettransaction(self.nodes[1].sendtomainchain("", 1)["txid"])["hex"]
self.assert_accepted(pegout_tx)
# Peg-in prep:
# hack: since we're not validating peg-ins in parent chain, just make
# both the funding and claim tx on same chain (printing money)
fund_info = self.nodes[0].getpeginaddress()
peg_id = self.nodes[0].sendtoaddress(fund_info["mainchain_address"], 1)
peg_tx = self.nodes[0].gettransaction(peg_id)["hex"]
# we need the confirmation of the peg tx, so we can't easily assert
# that the pegin tx would be accepted at this very point
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
proof = self.nodes[0].gettxoutproof([peg_id])
pegin_tx = self.nodes[0].createrawpegin(peg_tx, proof, fund_info["claim_script"])["hex"]
pegin_tx = self.nodes[0].signrawtransactionwithwallet(pegin_tx)["hex"]
# both should be allowed after that block
assert_equal(self.nodes[0].getsidechaininfo()["current_fedpegscripts"], ["51", "52"])
self.assert_accepted(pegin_tx)
self.assert_accepted(pegout_tx)
# let's generate 20 blocks to pass through 2 new epochs without there being a transition
for _ in range(20):
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
self.assert_accepted(pegin_tx)
self.assert_accepted(pegout_tx)
assert_equal(self.nodes[0].getsidechaininfo()["current_fedpegscripts"], ["51", "51"])
self.sync_blocks()
# Now have node 1 transition to new pak and fedpegscript
pak_prop["fedpegscript"] = "52"
pak_prop["extension_space"] = initial_extension
for _ in range(9):
self.assert_accepted(pegin_tx)
self.assert_accepted(pegout_tx)
self.nodes[1].submitblock(self.nodes[1].getnewblockhex(0, pak_prop))
self.sync_blocks()
# so right before the next epoch, the new params are active and
# the pegout is already invalid while the pegin is still valid
self.assert_accepted(pegin_tx)
assert_equal(self.nodes[0].testmempoolaccept([pegout_tx])[0]["reject-reason"], ERR_MP_INVALID_PEGOUT)
# lets go back one block and make sure we can mine the pegout tx
old_tip = self.nodes[0].getbestblockhash()
self.nodes[0].invalidateblock(old_tip)
pegout_txid = self.nodes[0].sendrawtransaction(pegout_tx)
self.nodes[0].submitblock(self.nodes[0].getnewblockhex(0, pak_prop))
tip = self.nodes[0].getbestblockhash()
assert_equal(self.nodes[0].getrawtransaction(pegout_txid, True, tip)["confirmations"], 1)
# undo it again so that we can make sure it is no longer allowed after this point
self.nodes[0].invalidateblock(tip)
self.nodes[0].reconsiderblock(old_tip)
# and after the 10th block of course that is still the case
self.nodes[1].submitblock(self.nodes[1].getnewblockhex(0, pak_prop))
self.sync_blocks()
self.assert_accepted(pegin_tx)
assert_equal(self.nodes[0].testmempoolaccept([pegout_tx])[0]["reject-reason"], ERR_MP_INVALID_PEGOUT)
# We're in the next epoch, in this one the pegin tx should be accepted up until the last block
assert_equal(self.nodes[1].getsidechaininfo()["current_fedpegscripts"], ["52", "51"])
for _ in range(9):
self.assert_accepted(pegin_tx)
assert_equal(self.nodes[0].testmempoolaccept([pegout_tx])[0]["reject-reason"], ERR_MP_INVALID_PEGOUT)
self.generatetoaddress(self.nodes[1], 1, self.nodes[1].getnewaddress())
# so on the last block both should not be allowed
assert_equal(self.nodes[1].getsidechaininfo()["current_fedpegscripts"], ["52", "52"])
assert_equal(self.nodes[0].testmempoolaccept([pegout_tx])[0]["reject-reason"], ERR_MP_INVALID_PEGOUT)
assert_equal(self.nodes[0].testmempoolaccept([pegin_tx])[0]["reject-reason"], ERR_MP_INVALID_PEGIN)
# then undo the last block and make sure we could have mined the pegin in the very last block
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
# first use the pegin tx created earlier
pegin_txid = self.nodes[0].sendrawtransaction(pegin_tx)
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
assert_equal(self.nodes[0].gettransaction(pegin_txid)["confirmations"], 1)
# make sure that using claimpegin directly also works
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
pegin_txid = self.nodes[0].claimpegin(peg_tx, proof, fund_info["claim_script"])
self.generatetoaddress(self.nodes[0], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
assert_equal(self.nodes[0].gettransaction(pegin_txid)["confirmations"], 1)
def run_test(self):
self.test_legacy_params()
self.test_dynafed_activation()
self.test_illegal_proposals()
self.test_no_vote()
self.test_under_vote()
self.test_four_fifth_vote()
self.test_all_vote()
self.test_transition_mempool_eject()
self.test_valid_epochs()
if __name__ == '__main__':
DynaFedTest().main()