#!/usr/bin/env python2
# Copyright (c) 2016-2024 The Hush developers
# Copyright (c) 2018 The Zcash developers
# Distributed under the GPLv3 software license, see the accompanying
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html

#
# Test proper expiry for transactions >= version 3
#

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, \
    connect_nodes_bi, sync_blocks, start_nodes, \
    wait_and_assert_operationid_status

from decimal import Decimal

class MempoolTxExpiryTest(BitcoinTestFramework):

    def setup_nodes(self):
        return start_nodes(4, self.options.tmpdir, [["-nuparams=5ba81b19:205", "-txexpirydelta=4", "-debug=mempool"]] * 4)

    # Test before, at, and after expiry block
    # chain is at block height 199 when run_test executes
    def run_test(self):
        alice = self.nodes[0].getnewaddress()
        z_alice = self.nodes[0].z_getnewaddress()
        bob = self.nodes[2].getnewaddress()
        z_bob = self.nodes[2].z_getnewaddress()

        # When Overwinter not yet activated, no expiryheight in tx
        sapling_tx = self.nodes[0].sendtoaddress(bob, 0.01)
        rawtx = self.nodes[0].getrawtransaction(sapling_tx, 1)
        assert_equal(rawtx["overwintered"], False)
        assert("expiryheight" not in rawtx)

        self.nodes[0].generate(6)
        self.sync_all()

        print "Splitting network..."
        self.split_network()

        # When Overwinter is activated, test dependent txs
        firstTx = self.nodes[0].sendtoaddress(alice, 0.1)
        firstTxInfo = self.nodes[0].getrawtransaction(firstTx, 1)
        print "First tx expiry height:", firstTxInfo['expiryheight']
        # Mine first transaction
        self.nodes[0].generate(1)
        for outpoint in firstTxInfo['vout']:
            if outpoint['value'] == Decimal('0.10000000'):
                vout = outpoint
                break
        inputs = [{'txid': firstTx, 'vout': vout['n'], 'scriptPubKey': vout['scriptPubKey']['hex']}]
        outputs = {alice: 0.1}
        rawTx = self.nodes[0].createrawtransaction(inputs, outputs)
        rawTxSigned = self.nodes[0].signrawtransaction(rawTx)
        assert(rawTxSigned['complete'])
        secondTx = self.nodes[0].sendrawtransaction(rawTxSigned['hex'])
        secondTxInfo = self.nodes[0].getrawtransaction(secondTx, 1)
        print "Second tx expiry height:", secondTxInfo['expiryheight']
        # Mine second, dependent transaction
        self.nodes[0].generate(1)
        print "Mine 6 competing blocks on Node 2..."
        blocks = self.nodes[2].generate(6)
        print "Connect nodes to force a reorg"
        connect_nodes_bi(self.nodes,0,2)
        self.is_network_split = False
        print "Syncing blocks"
        sync_blocks(self.nodes)
        print "Ensure that both txs are dropped from mempool of node 0"
        print "Blockheight node 0:", self.nodes[0].getblockchaininfo()['blocks']
        print "Blockheight node 2:", self.nodes[2].getblockchaininfo()['blocks']
        assert_equal(set(self.nodes[0].getrawmempool()), set())
        assert_equal(set(self.nodes[2].getrawmempool()), set())

        ## Shield one of Alice's coinbase funds to her zaddr
        res = self.nodes[0].z_shieldcoinbase("*", z_alice, 0.0001, 1)
        wait_and_assert_operationid_status(self.nodes[0], res['opid'])
        self.nodes[0].generate(1)
        self.sync_all()

        # Get balance on node 0
        bal = self.nodes[0].z_gettotalbalance()
        print "Balance before zsend, after shielding 10: ", bal
        assert_equal(Decimal(bal["private"]), Decimal("9.9999"))

        print "Splitting network..."
        self.split_network()

        # Create transactions
        blockheight = self.nodes[0].getblockchaininfo()['blocks']
        zsendamount = Decimal('1.0') - Decimal('0.0001')
        recipients = []
        recipients.append({"address": z_bob, "amount": zsendamount})
        myopid = self.nodes[0].z_sendmany(z_alice, recipients)
        persist_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
        persist_transparent = self.nodes[0].sendtoaddress(bob, 0.01)
        # Verify transparent transaction is version 3 intended for Overwinter branch
        rawtx = self.nodes[0].getrawtransaction(persist_transparent, 1)
        assert_equal(rawtx["version"], 3)
        assert_equal(rawtx["overwintered"], True)
        assert_equal(rawtx["expiryheight"], blockheight + 5)
        print "Blockheight at persist_transparent & persist_shielded creation:", self.nodes[0].getblockchaininfo()['blocks']
        print "Expiryheight of persist_transparent:", rawtx['expiryheight']
        # Verify shielded transaction is version 3 intended for Overwinter branch
        rawtx = self.nodes[0].getrawtransaction(persist_shielded, 1)
        print "Expiryheight of persist_shielded", rawtx['expiryheight']
        assert_equal(rawtx["version"], 3)
        assert_equal(rawtx["overwintered"], True)
        assert_equal(rawtx["expiryheight"], blockheight + 5)

        print "\n Blockheight advances to less than expiry block height. After reorg, txs should persist in mempool"
        assert(persist_transparent in self.nodes[0].getrawmempool())
        assert(persist_shielded in self.nodes[0].getrawmempool())
        assert_equal(set(self.nodes[2].getrawmempool()), set())
        print "mempool node 0:", self.nodes[0].getrawmempool()
        print "mempool node 2:", self.nodes[2].getrawmempool()
        bal = self.nodes[0].z_gettotalbalance()
        print "Printing balance before persist_shielded & persist_transparent are initially mined from mempool", bal
        # Txs are mined on node 0; will later be rolled back
        self.nodes[0].generate(1)
        print "Node 0 generated 1 block"
        print "Node 0 height:", self.nodes[0].getblockchaininfo()['blocks']
        print "Node 2 height:", self.nodes[2].getblockchaininfo()['blocks']
        bal = self.nodes[0].z_gettotalbalance()
        print "Printing balance after persist_shielded & persist_transparent are mined:", bal
        assert_equal(set(self.nodes[0].getrawmempool()), set())

        print "Mine 2 competing blocks on Node 2..."
        blocks = self.nodes[2].generate(2)
        for block in blocks:
            blk = self.nodes[2].getblock(block)
            print "Height: {0}, Mined block txs: {1}".format(blk["height"], blk["tx"])
        print "Connect nodes to force a reorg"
        connect_nodes_bi(self.nodes,0,2)
        self.is_network_split = False

        print "Syncing blocks"
        sync_blocks(self.nodes)

        print "Ensure that txs are back in mempool of node 0"
        print "Blockheight node 0:", self.nodes[0].getblockchaininfo()['blocks']
        print "Blockheight node 2:", self.nodes[2].getblockchaininfo()['blocks']
        print "mempool node 0: ", self.nodes[0].getrawmempool()
        print "mempool node 2: ", self.nodes[2].getrawmempool()
        assert(persist_transparent in self.nodes[0].getrawmempool())
        assert(persist_shielded in self.nodes[0].getrawmempool())
        bal = self.nodes[0].z_gettotalbalance()
        # Mine txs to get them out of the way of mempool sync in split_network()
        print "Generating another block on node 0 to clear txs from mempool"
        self.nodes[0].generate(1)
        assert_equal(set(self.nodes[0].getrawmempool()), set())
        sync_blocks(self.nodes)

        print "Splitting network..."
        self.split_network()

        print "\n Blockheight advances to equal expiry block height. After reorg, txs should persist in mempool"
        myopid = self.nodes[0].z_sendmany(z_alice, recipients)
        persist_shielded_2 = wait_and_assert_operationid_status(self.nodes[0], myopid)
        persist_transparent_2 = self.nodes[0].sendtoaddress(bob, 0.01)
        rawtx_trans = self.nodes[0].getrawtransaction(persist_transparent_2, 1)
        rawtx_shield = self.nodes[0].getrawtransaction(persist_shielded_2, 1)
        print "Blockheight node 0 at persist_transparent_2 creation:", self.nodes[0].getblockchaininfo()['blocks']
        print "Blockheight node 2 at persist_transparent_2 creation:", self.nodes[2].getblockchaininfo()['blocks']
        print "Expiryheight of persist_transparent_2:", rawtx_trans['expiryheight']
        print "Expiryheight of persist_shielded_2:", rawtx_shield['expiryheight']
        blocks = self.nodes[2].generate(4)
        for block in blocks:
            blk = self.nodes[2].getblock(block)
            print "Height: {0}, Mined block txs: {1}".format(blk["height"], blk["tx"])
        print "Connect nodes to force a reorg"
        connect_nodes_bi(self.nodes, 0, 2)
        self.is_network_split = False
        sync_blocks(self.nodes)
        print "Ensure that persist_transparent_2 & persist_shielded_2 are in mempool at expiry block height"
        print "Blockheight node 0:", self.nodes[0].getblockchaininfo()['blocks']
        print "Blockheight node 2:", self.nodes[2].getblockchaininfo()['blocks']
        print "mempool node 0: ", self.nodes[0].getrawmempool()
        print "mempool node 2: ", self.nodes[2].getrawmempool()
        assert(persist_transparent_2 in self.nodes[0].getrawmempool())
        assert(persist_shielded_2 in self.nodes[0].getrawmempool())
        # Mine persist txs to get them out of the way of mempool sync in split_network()
        self.nodes[0].generate(1)
        assert_equal(set(self.nodes[0].getrawmempool()), set())
        sync_blocks(self.nodes)
        print "Balance after persist_shielded_2 is mined to remove from mempool: ", self.nodes[0].z_gettotalbalance()

        print "Splitting network..."
        self.split_network()

        print "\n Blockheight advances to greater than expiry block height. After reorg, txs should expire from mempool"
        print "Balance before expire_shielded is sent: ", self.nodes[0].z_gettotalbalance()
        myopid = self.nodes[0].z_sendmany(z_alice, recipients)
        expire_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
        expire_transparent = self.nodes[0].sendtoaddress(bob, 0.01)
        print "Blockheight node 0 at expire_transparent creation:", self.nodes[0].getblockchaininfo()['blocks']
        print "Blockheight node 2 at expire_shielded creation:", self.nodes[2].getblockchaininfo()['blocks']
        print "Expiryheight of expire_transparent:", self.nodes[0].getrawtransaction(expire_transparent, 1)['expiryheight']
        print "Expiryheight of expire_shielded:", self.nodes[0].getrawtransaction(expire_shielded, 1)['expiryheight']
        assert(expire_transparent in self.nodes[0].getrawmempool())
        assert(expire_shielded in self.nodes[0].getrawmempool())
        blocks = self.nodes[2].generate(6)
        for block in blocks:
            blk = self.nodes[2].getblock(block)
            print "Height: {0}, Mined block txs: {1}".format(blk["height"], blk["tx"])
        print "Connect nodes to force a reorg"
        connect_nodes_bi(self.nodes, 0, 2)
        self.is_network_split = False
        sync_blocks(self.nodes)
        print "Ensure that expire_transparent & expire_shielded are in mempool at expiry block height"
        print "mempool node 0: ", self.nodes[0].getrawmempool()
        print "mempool node 2: ", self.nodes[2].getrawmempool()
        assert_equal(set(self.nodes[0].getrawmempool()), set())
        print "Ensure balance of node 0 is correct"
        bal = self.nodes[0].z_gettotalbalance()
        print "Balance after expire_shielded has expired: ", bal
        assert_equal(Decimal(bal["private"]), Decimal("7.9999"))


if __name__ == '__main__':
    MempoolTxExpiryTest().main()
