elements/test/functional/feature_dynafed.py

587 lines
32 KiB
Python
Executable file

#!/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 add_options(self, parser):
self.add_wallet_options(parser)
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)
self.sync_blocks()
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(__file__).main()