#!/usr/bin/env python2 # 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.authproxy import JSONRPCException from test_framework.mininode import COIN from test_framework.util import assert_equal, initialize_chain_clean, \ start_nodes, connect_nodes_bi, wait_and_assert_operationid_status import sys import timeit from decimal import Decimal def check_value_pool(node, name, total): value_pools = node.getblockchaininfo()['valuePools'] found = False for pool in value_pools: if pool['id'] == name: found = True assert_equal(pool['monitored'], True) assert_equal(pool['chainValue'], total) assert_equal(pool['chainValueZat'], total * COIN) assert(found) class WalletProtectCoinbaseTest (BitcoinTestFramework): def setup_chain(self): print("Initializing test directory "+self.options.tmpdir) initialize_chain_clean(self.options.tmpdir, 4) # Start nodes with -regtestprotectcoinbase to set fCoinbaseMustBeProtected to true. def setup_network(self, split=False): self.nodes = start_nodes(4, self.options.tmpdir, extra_args=[['-regtestprotectcoinbase', '-debug=zrpcunsafe']] * 4 ) connect_nodes_bi(self.nodes,0,1) connect_nodes_bi(self.nodes,1,2) connect_nodes_bi(self.nodes,0,2) connect_nodes_bi(self.nodes,0,3) self.is_network_split=False self.sync_all() def run_test (self): print "Mining blocks..." self.nodes[0].generate(4) walletinfo = self.nodes[0].getwalletinfo() assert_equal(walletinfo['immature_balance'], 40) assert_equal(walletinfo['balance'], 0) self.sync_all() self.nodes[1].generate(101) self.sync_all() assert_equal(self.nodes[0].getbalance(), 40) assert_equal(self.nodes[1].getbalance(), 10) assert_equal(self.nodes[2].getbalance(), 0) assert_equal(self.nodes[3].getbalance(), 0) check_value_pool(self.nodes[0], 'sprout', 0) check_value_pool(self.nodes[1], 'sprout', 0) check_value_pool(self.nodes[2], 'sprout', 0) check_value_pool(self.nodes[3], 'sprout', 0) # Send will fail because we are enforcing the consensus rule that # coinbase utxos can only be sent to a zaddr. errorString = "" try: self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1) except JSONRPCException,e: errorString = e.error['message'] assert_equal("Coinbase funds can only be sent to a zaddr" in errorString, True) # Prepare to send taddr->zaddr mytaddr = self.nodes[0].getnewaddress() myzaddr = self.nodes[0].z_getnewaddress() # Node 3 will test that watch only address utxos are not selected self.nodes[3].importaddress(mytaddr) recipients= [{"address":myzaddr, "amount": Decimal('1')}] myopid = self.nodes[3].z_sendmany(mytaddr, recipients) wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "no UTXOs found for taddr from address", 10) # This send will fail because our wallet does not allow any change when protecting a coinbase utxo, # as it's currently not possible to specify a change address in z_sendmany. recipients = [] recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')}) myopid = self.nodes[0].z_sendmany(mytaddr, recipients) error_result = wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "wallet does not allow any change", 10) # Test that the returned status object contains a params field with the operation's input parameters assert_equal(error_result["method"], "z_sendmany") params = error_result["params"] assert_equal(params["fee"], Decimal('0.0001')) # default assert_equal(params["minconf"], Decimal('1')) # default assert_equal(params["fromaddress"], mytaddr) assert_equal(params["amounts"][0]["address"], myzaddr) assert_equal(params["amounts"][0]["amount"], Decimal('1.23456789')) # Add viewing key for myzaddr to Node 3 myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr) self.nodes[3].z_importviewingkey(myviewingkey, "no") # This send will succeed. We send two coinbase utxos totalling 20.0 less a fee of 0.00010000, with no change. shieldvalue = Decimal('20.0') - Decimal('0.0001') recipients = [] recipients.append({"address":myzaddr, "amount": shieldvalue}) myopid = self.nodes[0].z_sendmany(mytaddr, recipients) mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() # Verify that z_listunspent can return a note that has zero confirmations results = self.nodes[0].z_listunspent() assert(len(results) == 0) results = self.nodes[0].z_listunspent(0) # set minconf to zero assert(len(results) == 1) assert_equal(results[0]["address"], myzaddr) assert_equal(results[0]["amount"], shieldvalue) assert_equal(results[0]["confirmations"], 0) # Mine the tx self.nodes[1].generate(1) self.sync_all() # Verify that z_listunspent returns one note which has been confirmed results = self.nodes[0].z_listunspent() assert(len(results) == 1) assert_equal(results[0]["address"], myzaddr) assert_equal(results[0]["amount"], shieldvalue) assert_equal(results[0]["confirmations"], 1) assert_equal(results[0]["spendable"], True) # Verify that z_listunspent returns note for watchonly address on node 3. results = self.nodes[3].z_listunspent(1, 999, True) assert(len(results) == 1) assert_equal(results[0]["address"], myzaddr) assert_equal(results[0]["amount"], shieldvalue) assert_equal(results[0]["confirmations"], 1) assert_equal(results[0]["spendable"], False) # Verify that z_listunspent returns error when address spending key from node 0 is not available in wallet of node 1. try: results = self.nodes[1].z_listunspent(1, 999, False, [myzaddr]) except JSONRPCException as e: errorString = e.error['message'] assert_equal("Invalid parameter, spending key for address does not belong to wallet" in errorString, True) # Verify that debug=zrpcunsafe logs params, and that full txid is associated with opid logpath = self.options.tmpdir+"/node0/regtest/debug.log" logcounter = 0 with open(logpath, "r") as myfile: logdata = myfile.readlines() for logline in logdata: if myopid + ": z_sendmany initialized" in logline and mytaddr in logline and myzaddr in logline: assert_equal(logcounter, 0) # verify order of log messages logcounter = logcounter + 1 if myopid + ": z_sendmany finished" in logline and mytxid in logline: assert_equal(logcounter, 1) logcounter = logcounter + 1 assert_equal(logcounter, 2) # check balances (the z_sendmany consumes 3 coinbase utxos) resp = self.nodes[0].z_gettotalbalance() assert_equal(Decimal(resp["transparent"]), Decimal('20.0')) assert_equal(Decimal(resp["private"]), Decimal('19.9999')) assert_equal(Decimal(resp["total"]), Decimal('39.9999')) # The Sprout value pool should reflect the send sproutvalue = shieldvalue check_value_pool(self.nodes[0], 'sprout', sproutvalue) # A custom fee of 0 is okay. Here the node will send the note value back to itself. recipients = [] recipients.append({"address":myzaddr, "amount": Decimal('19.9999')}) myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('0.0')) mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() self.nodes[1].generate(1) self.sync_all() resp = self.nodes[0].z_gettotalbalance() assert_equal(Decimal(resp["transparent"]), Decimal('20.0')) assert_equal(Decimal(resp["private"]), Decimal('19.9999')) assert_equal(Decimal(resp["total"]), Decimal('39.9999')) # The Sprout value pool should be unchanged check_value_pool(self.nodes[0], 'sprout', sproutvalue) # convert note to transparent funds unshieldvalue = Decimal('10.0') recipients = [] recipients.append({"address":mytaddr, "amount": unshieldvalue}) myopid = self.nodes[0].z_sendmany(myzaddr, recipients) mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid) assert(mytxid is not None) self.sync_all() # check that priority of the tx sending from a zaddr is not 0 mempool = self.nodes[0].getrawmempool(True) assert(Decimal(mempool[mytxid]['startingpriority']) >= Decimal('1000000000000')) self.nodes[1].generate(1) self.sync_all() # check balances sproutvalue -= unshieldvalue + Decimal('0.0001') resp = self.nodes[0].z_gettotalbalance() assert_equal(Decimal(resp["transparent"]), Decimal('30.0')) assert_equal(Decimal(resp["private"]), Decimal('9.9998')) assert_equal(Decimal(resp["total"]), Decimal('39.9998')) check_value_pool(self.nodes[0], 'sprout', sproutvalue) # z_sendmany will return an error if there is transparent change output considered dust. # UTXO selection in z_sendmany sorts in ascending order, so smallest utxos are consumed first. # At this point in time, unspent notes all have a value of 10.0 and standard z_sendmany fee is 0.0001. recipients = [] amount = Decimal('10.0') - Decimal('0.00010000') - Decimal('0.00000001') # this leaves change at 1 zatoshi less than dust threshold recipients.append({"address":self.nodes[0].getnewaddress(), "amount":amount }) myopid = self.nodes[0].z_sendmany(mytaddr, recipients) wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054)") # Send will fail because send amount is too big, even when including coinbase utxos errorString = "" try: self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 99999) except JSONRPCException,e: errorString = e.error['message'] assert_equal("Insufficient funds" in errorString, True) # z_sendmany will fail because of insufficient funds recipients = [] recipients.append({"address":self.nodes[1].getnewaddress(), "amount":Decimal('10000.0')}) myopid = self.nodes[0].z_sendmany(mytaddr, recipients) wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 10000.0001") myopid = self.nodes[0].z_sendmany(myzaddr, recipients) wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient shielded funds, have 9.9998, need 10000.0001") # Send will fail because of insufficient funds unless sender uses coinbase utxos try: self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 21) except JSONRPCException,e: errorString = e.error['message'] assert_equal("Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr" in errorString, True) # Verify that mempools accept tx with joinsplits which have at least the default z_sendmany fee. # If this test passes, it confirms that issue #1851 has been resolved, where sending from # a zaddr to 1385 taddr recipients fails because the default fee was considered too low # given the tx size, resulting in mempool rejection. errorString = '' recipients = [] num_t_recipients = 2500 amount_per_recipient = Decimal('0.00000546') # dust threshold # Note that regtest chainparams does not require standard tx, so setting the amount to be # less than the dust threshold, e.g. 0.00000001 will not result in mempool rejection. start_time = timeit.default_timer() for i in xrange(0,num_t_recipients): newtaddr = self.nodes[2].getnewaddress() recipients.append({"address":newtaddr, "amount":amount_per_recipient}) elapsed = timeit.default_timer() - start_time print("...invoked getnewaddress() {} times in {} seconds".format(num_t_recipients, elapsed)) # Issue #2263 Workaround START # HTTP connection to node 0 may fall into a state, during the few minutes it takes to process # loop above to create new addresses, that when z_sendmany is called with a large amount of # rpc data in recipients, the connection fails with a 'broken pipe' error. Making a RPC call # to node 0 before calling z_sendmany appears to fix this issue, perhaps putting the HTTP # connection into a good state to handle a large amount of data in recipients. self.nodes[0].getinfo() # Issue #2263 Workaround END myopid = self.nodes[0].z_sendmany(myzaddr, recipients) try: wait_and_assert_operationid_status(self.nodes[0], myopid) except JSONRPCException as e: print("JSONRPC error: "+e.error['message']) assert(False) except Exception as e: print("Unexpected exception caught during testing: "+str(sys.exc_info()[0])) assert(False) self.sync_all() self.nodes[1].generate(1) self.sync_all() # check balance node2balance = amount_per_recipient * num_t_recipients sproutvalue -= node2balance + Decimal('0.0001') assert_equal(self.nodes[2].getbalance(), node2balance) check_value_pool(self.nodes[0], 'sprout', sproutvalue) # Send will fail because fee is negative try: self.nodes[0].z_sendmany(myzaddr, recipients, 1, -1) except JSONRPCException,e: errorString = e.error['message'] assert_equal("Amount out of range" in errorString, True) # Send will fail because fee is larger than MAX_MONEY try: self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('21000000.00000001')) except JSONRPCException,e: errorString = e.error['message'] assert_equal("Amount out of range" in errorString, True) # Send will fail because fee is larger than sum of outputs try: self.nodes[0].z_sendmany(myzaddr, recipients, 1, (amount_per_recipient * num_t_recipients) + Decimal('0.00000001')) except JSONRPCException,e: errorString = e.error['message'] assert_equal("is greater than the sum of outputs" in errorString, True) # Send will succeed because the balance of non-coinbase utxos is 10.0 try: self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 9) except JSONRPCException: assert(False) self.sync_all() self.nodes[1].generate(1) self.sync_all() # check balance node2balance = node2balance + 9 assert_equal(self.nodes[2].getbalance(), node2balance) # Check that chained joinsplits in a single tx are created successfully. recipients = [] num_recipients = 3 amount_per_recipient = Decimal('0.002') minconf = 1 send_amount = num_recipients * amount_per_recipient custom_fee = Decimal('0.00012345') zbalance = self.nodes[0].z_getbalance(myzaddr) for i in xrange(0,num_recipients): newzaddr = self.nodes[2].z_getnewaddress() recipients.append({"address":newzaddr, "amount":amount_per_recipient}) myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, custom_fee) wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() self.nodes[1].generate(1) self.sync_all() # check balances and unspent notes resp = self.nodes[2].z_gettotalbalance() assert_equal(Decimal(resp["private"]), send_amount) notes = self.nodes[2].z_listunspent() sum_of_notes = sum([note["amount"] for note in notes]) assert_equal(Decimal(resp["private"]), sum_of_notes) resp = self.nodes[0].z_getbalance(myzaddr) assert_equal(Decimal(resp), zbalance - custom_fee - send_amount) sproutvalue -= custom_fee check_value_pool(self.nodes[0], 'sprout', sproutvalue) notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr]) sum_of_notes = sum([note["amount"] for note in notes]) assert_equal(Decimal(resp), sum_of_notes) if __name__ == '__main__': WalletProtectCoinbaseTest().main()