#!/usr/bin/env python3
# Copyright (c) 2016-2024 The Hush developers
# Copyright (c) 2016 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


from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_true, bitcoind_processes, \
    connect_nodes_bi, initialize_chain_clean, p2p_port, start_node, start_nodes, \
    sync_blocks, wait_and_assert_operationid_status

from decimal import Decimal

class WalletNullifiersTest (BitcoinTestFramework):

    # The framework default setup_chain() calls initialize_chain(), which pre-builds a
    # 200-block chain in a *relative* "cache/" directory shared by every test process
    # running out of this tree. That directory is guarded only by the datadir lock, so two
    # qa/rpc-tests running at once collide on it and the second one hangs forever inside
    # "dragonx-cli -rpcwait" (observed here while a sibling test held cache/node0..3).
    # Build the identical pre-condition -- 4 nodes, two rounds of 25 blocks each, i.e. 25
    # mature + 25 immature coinbases per node -- directly in this test's private tmpdir.
    # This is a setup change only: no assertion below is relaxed, removed or reordered.
    def setup_chain(self):
        print("Initializing test directory "+self.options.tmpdir)
        initialize_chain_clean(self.options.tmpdir, 4)

    # Three networking facts about this daemon force extra flags here. None of them
    # change what the test exercises; without them the 4 nodes either never peer with
    # each other, or peer with the LIVE DragonX network instead.
    #
    # 1. start_node() hardcodes "-connect=0", which also soft-sets -listen=0, so nothing
    #    binds p2p_port(i) and connect_nodes_bi() can never form the regtest mesh --
    #    sync_blocks() then spins forever (observed: node0 at 25 blocks, nodes 1-3 stuck
    #    at 0, nothing listening on 11005-11008). -listen=1 -bind=127.0.0.1 restores the
    #    mesh and keeps it on loopback.
    # 2. hush_args() appends node1..node10.dragonx.is to -addnode unconditionally, -regtest
    #    included, and regtest reuses mainnet's network magic. A "regtest" node therefore
    #    joins the live network: node0 of an earlier run handshook 8 production peers
    #    ("receive version message: /DragonX:1.0.3/ ... blocks=3254266") and ingested their
    #    headers. -dns=0 stops those hostname -addnode entries from resolving; RPC addnode
    #    with a numeric 127.0.0.1:port is unaffected.
    # 3. hush_args() runs BEFORE the config file is read, so its GetArg("-port",0) never
    #    sees the "port=" line initialize_datadir() wrote and GetDefaultPort() stays at the
    #    mainnet p2p port. "-connect=0" is then parsed as the address 0.0.0.0:<mainnet
    #    port>, i.e. the production dragonxd listening on this box -- every node in runs 2
    #    and 3 picked up exactly one peer reporting blocks=3254269. Repeating -port on the
    #    command line points GetDefaultPort() at this node's own regtest port instead.
    #
    # -autoshield is on by default on DragonX and is not part of what this test measures:
    # a background thread sweeps each node's matured coinbase into a seed-derived zaddr
    # (8 "autoshield operation finished" ops per node while the chain is being mined). That
    # empties the very taddr this test spends from, and the resulting transactions do not
    # settle identically on every node ("ERROR: AcceptToMemoryPool: ContextualCheckTransaction
    # failed" on node1), so sync_mempools() never converges and the run wedges until the
    # timeout. Turn the background sweeper off; the test does its own shielding explicitly.
    def net_args(self, i):
        return ['-listen=1', '-bind=127.0.0.1', '-dns=0', '-autoshield=0',
                '-port=%d' % p2p_port(i)]

    def setup_nodes(self):
        return start_nodes(4, self.options.tmpdir,
                           extra_args=[['-experimentalfeatures', '-developerencryptwallet']
                                       + self.net_args(i) for i in range(4)])

    def setup_network(self, split = False):
        super().setup_network(split)
        # Same block layout initialize_chain() would have handed us.
        for _ in range(2):
            for peer in range(4):
                self.nodes[peer].generate(25)
                sync_blocks(self.nodes)
        self.sync_all()

    def run_test (self):
        # add zaddr to node 0
        myzaddr0 = self.nodes[0].z_getnewaddress()

        # send node 0 taddr to zaddr to get out of coinbase
        mytaddr = self.nodes[0].getnewaddress()
        recipients = []
        recipients.append({"address":myzaddr0, "amount":Decimal('10.0')-Decimal('0.0001')}) # utxo amount less fee

        wait_and_assert_operationid_status(self.nodes[0], self.nodes[0].z_sendmany(mytaddr, recipients), timeout=120)

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

        # add zaddr to node 2
        myzaddr = self.nodes[2].z_getnewaddress()

        # import node 2 zaddr into node 1
        myzkey = self.nodes[2].z_exportkey(myzaddr)
        self.nodes[1].z_importkey(myzkey)

        # encrypt node 1 wallet and wait to terminate
        self.nodes[1].encryptwallet("test")
        bitcoind_processes[1].wait()

        # restart node 1
        self.nodes[1] = start_node(1, self.options.tmpdir, self.net_args(1))
        connect_nodes_bi(self.nodes, 0, 1)
        connect_nodes_bi(self.nodes, 1, 2)
        self.sync_all()

        # send node 0 zaddr to note 2 zaddr
        recipients = []
        recipients.append({"address":myzaddr, "amount":7.0})

        wait_and_assert_operationid_status(self.nodes[0], self.nodes[0].z_sendmany(myzaddr0, recipients), timeout=120)

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

        # check zaddr balance
        zsendmanynotevalue = Decimal('7.0')
        assert_equal(self.nodes[2].z_getbalance(myzaddr), zsendmanynotevalue)
        assert_equal(self.nodes[1].z_getbalance(myzaddr), zsendmanynotevalue)

        # add zaddr to node 3
        myzaddr3 = self.nodes[3].z_getnewaddress()

        # send node 2 zaddr to note 3 zaddr
        recipients = []
        recipients.append({"address":myzaddr3, "amount":2.0})

        wait_and_assert_operationid_status(self.nodes[2], self.nodes[2].z_sendmany(myzaddr, recipients), timeout=120)

        self.sync_all()
        self.nodes[2].generate(1)
        self.sync_all()

        # check zaddr balance
        zsendmany2notevalue = Decimal('2.0')
        zsendmanyfee = Decimal('0.0001')
        zaddrremaining = zsendmanynotevalue - zsendmany2notevalue - zsendmanyfee
        assert_equal(self.nodes[3].z_getbalance(myzaddr3), zsendmany2notevalue)
        assert_equal(self.nodes[2].z_getbalance(myzaddr), zaddrremaining)

        # Parallel encrypted wallet can't cache nullifiers for received notes,
        # and therefore can't detect spends. So it sees a balance corresponding
        # to the sum of both notes it received (one as change).
        # TODO: Devise a way to avoid this issue (#1528)
        assert_equal(self.nodes[1].z_getbalance(myzaddr), zsendmanynotevalue + zaddrremaining)

        # send node 2 zaddr on node 1 to taddr
        # This requires that node 1 be unlocked, which triggers caching of
        # uncached nullifiers.
        self.nodes[1].walletpassphrase("test", 600)
        mytaddr1 = self.nodes[1].getnewaddress()
        recipients = []
        recipients.append({"address":mytaddr1, "amount":1.0})

        wait_and_assert_operationid_status(self.nodes[1], self.nodes[1].z_sendmany(myzaddr, recipients), timeout=120)

        self.sync_all()
        self.nodes[1].generate(1)
        self.sync_all()

        # check zaddr balance
        # Now that the encrypted wallet has been unlocked, the note nullifiers
        # have been cached and spent notes can be detected. Thus the two wallets
        # are in agreement once more.
        zsendmany3notevalue = Decimal('1.0')
        zaddrremaining2 = zaddrremaining - zsendmany3notevalue - zsendmanyfee
        assert_equal(self.nodes[1].z_getbalance(myzaddr), zaddrremaining2)
        assert_equal(self.nodes[2].z_getbalance(myzaddr), zaddrremaining2)

        # Test viewing keys

        node3mined = Decimal('250.0')
        assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance().items()}, {
            'transparent': node3mined,
            'private': zsendmany2notevalue,
            'total': node3mined + zsendmany2notevalue,
        })

        # add node 1 address and node 2 viewing key to node 3
        myzvkey = self.nodes[2].z_exportviewingkey(myzaddr)
        self.nodes[3].importaddress(mytaddr1)
        self.nodes[3].z_importviewingkey(myzvkey, 'whenkeyisnew', 1)

        # Check the address has been imported
        assert_equal(myzaddr in self.nodes[3].z_listaddresses(), False)
        assert_equal(myzaddr in self.nodes[3].z_listaddresses(True), True)

        # Node 3 should see the same received notes as node 2; however,
        # some of the notes were change for node 2 but not for node 3.
        # Aside from that the recieved notes should be the same. So,
        # group by txid and then check that all properties aside from
        # change are equal.
        node2Received = dict([r['txid'], r] for r in self.nodes[2].z_listreceivedbyaddress(myzaddr))
        node3Received = dict([r['txid'], r] for r in self.nodes[3].z_listreceivedbyaddress(myzaddr))
        assert_equal(len(node2Received), len(node2Received))
        for txid in node2Received:
            received2 = node2Received[txid]
            received3 = node3Received[txid]
            # the change field will be omitted for received3, but all other fields should be shared
            assert_true(len(received2) >= len(received3))
            for key in received2:
                # check all the properties except for change
                if key != 'change':
                    assert_equal(received2[key], received3[key])

        # Node 3's balances should be unchanged without explicitly requesting
        # to include watch-only balances
        assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance().items()}, {
            'transparent': node3mined,
            'private': zsendmany2notevalue,
            'total': node3mined + zsendmany2notevalue,
        })

        # Wallet can't cache nullifiers for notes received by addresses it only has a
        # viewing key for, and therefore can't detect spends. So it sees a balance
        # corresponding to the sum of all notes the address received.
        # TODO: Fix this during the Sapling upgrade (via #2277)
        assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance(1, True).items()}, {
            'transparent': node3mined + Decimal('1.0'),
            'private': zsendmany2notevalue + zsendmanynotevalue + zaddrremaining + zaddrremaining2,
            'total': node3mined + Decimal('1.0') + zsendmany2notevalue + zsendmanynotevalue + zaddrremaining + zaddrremaining2,
        })

        # Check individual balances reflect the above
        assert_equal(self.nodes[3].z_getbalance(mytaddr1), Decimal('1.0'))
        assert_equal(self.nodes[3].z_getbalance(myzaddr), zsendmanynotevalue + zaddrremaining + zaddrremaining2)

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