#!/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)
        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().main()
