#!/usr/bin/env python3
# Copyright (c) 2016 The Bitcoin Core developers
# Distributed under the MIT/X11 software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from time import sleep
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error, Decimal, assert_greater_than

'''
    This test focuses on enforcement of PAK, not on transitioning lists
    or RPC return values for PAK enforcement settings which is covered
    in feature_dynafed.py

    TODO: Test non-activated dynafed means no block enforcement of PAK
'''

class PAKTest (BitcoinTestFramework):

    def set_test_params(self):
        self.num_nodes = 3
        self.setup_clean_chain = True
        self.extra_args = [["-enforce_pak=1", "-evbparams=dynafed:-1:::", "-initialfreecoins=210000000000000", "-anyonecanspendaremine=1", "-parent_bech32_hrp=lol", "-pubkeyprefix=112", "-scriptprefix=197", "-con_connect_genesis_outputs=1"] for i in range(self.num_nodes)]
        # First node doesn't enforce PAK, a "HF" of the other two nodes
        # -acceptnonstdtxn=1 is required for first node as IsStandard will block larger pegout scripts if -enforce_pak=0
        self.extra_args[0] = ["-acceptnonstdtxn=1"] + self.extra_args[0][1:]

    def add_options(self, parser):
        self.add_wallet_options(parser)

    def skip_test_if_missing_module(self):
        self.skip_if_no_wallet()

    def run_test(self):

        # Transitioning PAK lists and checking lists in RPC tested in feature_dynafed

        self.log.info("Test wallet PAK")

        # We will re-use the same xpub, but each wallet will create its own online pak
        # so the lists will be incompatible, even if all else was synced
        xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
        xpub_desc = "pkh("+xpub+"/0/*)" # Transform this into a descriptor
        init_results = []
        info_results = []
        for i in range(self.num_nodes):
            if i == 0:
                assert_raises_rpc_error(-8, "PAK enforcement is not enabled on this network.", self.nodes[i].initpegoutwallet, xpub)
                init_results += [None]
                info_results += [None]
                continue

            init_results += [ self.nodes[i].initpegoutwallet(xpub) ]
            info_results += [ self.nodes[i].getwalletpakinfo() ]
            assert_equal(init_results[i]["address_lookahead"], info_results[i]["address_lookahead"])
            assert_equal(init_results[i]["pakentry"], info_results[i]["pakentry"])
            assert_equal(init_results[i]["liquid_pak"], info_results[i]["liquid_pak"])
            assert_equal(init_results[i]["liquid_pak_address"], info_results[i]["liquid_pak_address"])
            assert_equal(info_results[i]["bitcoin_descriptor"], xpub_desc)
            assert_equal(info_results[i]["bip32_counter"], "0")
            validata = self.nodes[i].validateaddress(init_results[i]["address_lookahead"][0])
            assert not validata["isvalid"]
            assert validata["isvalid_parent"]
            assert not validata["parent_address_info"]["isscript"]
            assert not validata["parent_address_info"]["iswitness"]

        # Use custom derivation counter values, check if stored correctly,
        # address lookahead looks correct and that new liquid_pak was chosen
        assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, -1)

        assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[1].initpegoutwallet, xpub, 1000000001)

        # Make sure we can also prepend the key origin to the xpub.
        self.nodes[1].initpegoutwallet("pkh([deadbeef/44h/0h/0h]"+xpub+"/0/*)")
        new_init = self.nodes[1].initpegoutwallet(xpub, 2)
        assert_equal(self.nodes[1].getwalletpakinfo()["bip32_counter"], "2")
        assert_equal(new_init["address_lookahead"][0], init_results[1]["address_lookahead"][2])
        assert new_init["liquid_pak"] != init_results[1]["liquid_pak"]

        self.log.info("initpegoutwallet with null argument")
        null_pak_init = self.nodes[2].initpegoutwallet(xpub, 5, None)
        assert_equal(self.nodes[2].getwalletpakinfo()["bip32_counter"], "5")
        assert null_pak_init["liquid_pak"]

        # Restart and connect peers to check wallet persistence
        self.stop_nodes()
        self.start_nodes()
        self.connect_nodes(0,1)
        self.connect_nodes(1,2)

        # Check PAK settings persistence in wallet across restart
        restarted_info = self.nodes[1].getwalletpakinfo()
        assert_equal(restarted_info["bitcoin_descriptor"], xpub_desc)
        assert_equal(restarted_info["liquid_pak"], new_init["liquid_pak"])
        assert_equal(restarted_info["bip32_counter"], "2")

        # Compile list of extension space entries for pak enforcement
        extension_space_proposal = []
        for entry in init_results:
            if entry is not None:
                pakentry = entry["pakentry"]
                extension_space_proposal += [pakentry[4:4+66]+pakentry[4+66+1:]]

        self.log.info("Test mempool enforcement of PAK peg-outs")

        # Transition to a pak list that only node 1 can peg-out to
        WSH_OP_TRUE = self.nodes[0].decodescript("51")["segwit"]["hex"]
        for _ in range(9):
            block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":[extension_space_proposal[0]]})
            assert_equal(self.nodes[1].submitblock(block), None)

        self.sync_all()
        assert_equal(self.nodes[0].getblockchaininfo()["extension_space"], [extension_space_proposal[0]])

        # node 1 has wrong pak entry in wallet
        assert_raises_rpc_error(-4, "Given online key is not in Pegout Authorization Key List", self.nodes[1].sendtomainchain, "", 1)

        # put back init_info version that's in pak list
        self.nodes[1].initpegoutwallet(xpub, 0, init_results[1]["liquid_pak"])

        # Node 1 will now make a PAK peg-out, accepted in all mempools and blocks
        pegout_info = self.nodes[1].sendtomainchain("", 1)
        raw_node1_pegout = self.nodes[1].gettransaction(pegout_info["txid"])["hex"]
        self.sync_all() # mempool sync
        self.generatetoaddress(self.nodes[1], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
        assert_greater_than(self.nodes[1].gettransaction(pegout_info["txid"])["confirmations"], 0)

        # Re-org keep node 1 peg-out unconfirmed and transition to "full list"
        # then check peg-out fails

        # Invalidate back to block 1, then make 9 new blocks to hit transition
        # If you roll back to genesis block p2p code gets flakey
        num_block_rollback = self.nodes[1].getblockcount()-2
        fork_hash = self.nodes[1].getblockhash(2)
        for i in range(self.num_nodes):
           self.nodes[i].invalidateblock(fork_hash)
        self.sync_blocks()

        for _ in range(num_block_rollback):
            block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":extension_space_proposal})
            self.nodes[1].submitblock(block)

        self.sync_blocks()

        # node 0 puts the peg-out back in its mempool, can't sync all
        self.nodes[0].sendrawtransaction(raw_node1_pegout)
        self.sync_mempools(self.nodes[1:])

        # rejected in mempool
        assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_node1_pegout)
        assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_node1_pegout)
        # node 0 tries to make bad block
        wrong_pak_prop = self.nodes[0].getnewblockhex()
        # rejected in blocks
        assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, wrong_pak_prop, True)
        assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, wrong_pak_prop, True)

        self.log.info("Test various RPC arguments")

        # Fail to peg-out too-small value
        assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.00100000 BTC", self.nodes[1].sendtomainchain, "", Decimal('0.0009'))

        # Use wrong network's extended pubkey
        mainnetxpub = "xpub6AATBi58516uxLogbuaG3jkom7x1qyDoZzMN2AePBuQnMFKUV9xC2BW9vXsFJ9rELsvbeGQcFWhtbyM4qDeijM22u3AaSiSYEvuMZkJqtLn"
        assert_raises_rpc_error(-8, "'%s' is not a valid descriptor function" % mainnetxpub, self.nodes[1].initpegoutwallet, mainnetxpub)

        # Test fixed online pubkey
        init_info = self.nodes[1].initpegoutwallet(xpub)
        init_info2 = self.nodes[1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
        assert_equal(init_info, init_info2)
        init_info3 = self.nodes[1].initpegoutwallet(xpub)
        assert init_info != init_info3

        # Test Descriptor PAK Support

        # Non-supported descriptors
        assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")

        assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")

        # Peg out with each new type, check that destination script matches
        wpkh_desc = "wpkh("+xpub+"/0/*)"
        # add a valid checksum
        wpkh_desc = self.nodes[1].getdescriptorinfo(wpkh_desc)["descriptor"]
        wpkh_info = self.nodes[1].initpegoutwallet(wpkh_desc)
        wpkh_pak_info = self.nodes[1].getwalletpakinfo()

        # Transition to wpkh entry list
        wpkh_pak_entry = wpkh_info["pakentry"]
        wpkh_pak_prop = [wpkh_pak_entry[4:4+66]+wpkh_pak_entry[4+66+1:]]
        for _ in range(10):
            block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":wpkh_pak_prop})
            self.nodes[1].submitblock(block)
        self.sync_blocks()

        # Get some block subsidy and send off
        self.generatetoaddress(self.nodes[1], 101, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
        wpkh_stmc = self.nodes[1].sendtomainchain("", 1)
        wpkh_txid = wpkh_stmc['txid']

        self.log.info("sendtomainchain with null argument")
        verbose_stmc = self.nodes[1].sendtomainchain("", 1, None, True)
        assert isinstance(verbose_stmc, dict)
        assert 'txid' in verbose_stmc
        assert 'fee_reason' in verbose_stmc

        # Also check some basic return fields of sendtomainchain with pak
        assert_equal(wpkh_stmc["bitcoin_address"], wpkh_info["address_lookahead"][0])
        validata = self.nodes[1].validateaddress(wpkh_stmc["bitcoin_address"])
        assert not validata["isvalid"]
        assert validata["isvalid_parent"]
        assert not validata["parent_address_info"]["isscript"]
        assert validata["parent_address_info"]["iswitness"]
        assert_equal(wpkh_pak_info["bip32_counter"], wpkh_stmc["bip32_counter"])
        assert_equal(wpkh_pak_info["bitcoin_descriptor"], wpkh_stmc["bitcoin_descriptor"])

        sh_wpkh_desc = "sh(wpkh("+xpub+"/0/1/*))"
        sh_wpkh_info = self.nodes[1].initpegoutwallet(sh_wpkh_desc)

        validata = self.nodes[1].validateaddress(sh_wpkh_info["address_lookahead"][0])
        assert not validata["isvalid"]
        assert validata["isvalid_parent"]
        assert validata["parent_address_info"]["isscript"]
        assert not validata["parent_address_info"]["iswitness"]

        # Transition to sh_wpkh entry list
        sh_wpkh_pak_entry = sh_wpkh_info["pakentry"]
        sh_wpkh_pak_prop = [sh_wpkh_pak_entry[4:4+66]+sh_wpkh_pak_entry[4+66+1:]]
        for _ in range(10):
            block = self.nodes[1].getnewblockhex(0, {"signblockscript":WSH_OP_TRUE, "max_block_witness":3, "fedpegscript":"51", "extension_space":sh_wpkh_pak_prop})
            self.nodes[1].submitblock(block)
        self.sync_blocks()

        self.generatetoaddress(self.nodes[1], 1, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
        sh_wpkh_txid = self.nodes[1].sendtomainchain("", 1)['txid']

        # Make sure peg-outs look correct
        wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(wpkh_txid)['hex'])
        sh_wpkh_raw = self.nodes[1].decoderawtransaction(self.nodes[1].gettransaction(sh_wpkh_txid)['hex'])

        peg_out_found = False
        for output in wpkh_raw["vout"]:
            if "pegout_address" in output["scriptPubKey"]:
                if output["scriptPubKey"]["pegout_address"] == wpkh_info["address_lookahead"][0]:
                    peg_out_found = True
                    break
                else:
                    raise Exception("Found unexpected peg-out output")
        assert peg_out_found

        peg_out_found = False
        for output in sh_wpkh_raw["vout"]:
            if "pegout_address" in output["scriptPubKey"]:
                if output["scriptPubKey"]["pegout_address"] == sh_wpkh_info["address_lookahead"][0]:
                    peg_out_found = True
                    break
                else:
                    raise Exception("Found unexpected peg-out output")
        assert peg_out_found

        # Make sure they all confirm
        self.generatetoaddress(self.nodes[1], 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op)
        for tx_id in [wpkh_txid, sh_wpkh_txid]:
            assert_greater_than(self.nodes[1].gettransaction(tx_id)["confirmations"], 0)

        self.sync_blocks()

        self.log.info("Test that pak-less pegouts are rejected")

        # Last test of a pak-less peg-out failing to get into mempool/block
        # Note it leaves a transaction in node 0's mempool, so sync_all cannot
        # work after unless it's somehow booted.

        # node 0 will now create a pegout, will fail to enter mempool of node 1 or 2
        # since it's pak-less
        pegout_address = "n3NkSZqoPMCQN5FENxUBw4qVATbytH6FDK"
        nopak_pegout_txid = self.nodes[0].sendtomainchain(pegout_address, 1)
        pegout_tx = self.nodes[0].gettransaction(nopak_pegout_txid,True,True)
        assert_equal(pegout_tx["decoded"]["vout"][[d["value"] for d in pegout_tx["decoded"]["vout"]].index(1)]["scriptPubKey"]["pegout_address"],pegout_address)
        raw_pakless_pegout = self.nodes[0].gettransaction(nopak_pegout_txid)["hex"]
        sleep(1) # hacky fix for flaky timing where getrawmempool doesn't have nopak_pegout_txid yet
        assert nopak_pegout_txid in self.nodes[0].getrawmempool()

        assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[1].sendrawtransaction, raw_pakless_pegout)
        assert_raises_rpc_error(-26, "invalid-pegout-proof", self.nodes[2].sendrawtransaction, raw_pakless_pegout)

        # node 0 makes a block that includes the pakless pegout, rejected by others
        # by consensus
        bad_prop = self.nodes[0].getnewblockhex()
        assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[1].testproposedblock, bad_prop, True)
        assert_raises_rpc_error(-25, "bad-pak-tx", self.nodes[2].testproposedblock, bad_prop, True)

        # Test that subtracting fee from output works
        self.generatetoaddress(self.nodes[1], 101, self.nodes[1].getnewaddress(), sync_fun=self.no_op)
        self.nodes[1].sendtomainchain("", self.nodes[1].getbalance()["bitcoin"], True)
        assert_equal(self.nodes[1].getbalance()["bitcoin"], 0)

        # TODO: create rawsendtomainchain to do transaction surgery for testing

if __name__ == '__main__':
    PAKTest(__file__).main()
