#!/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:, 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 ()