diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 0db8b16b0..1d39e86ae 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -62,7 +62,6 @@ BASE_SCRIPTS= [ 'reorg_limit.py', 'mempool_limit.py', 'p2p-fullblocktest.py', - 'paymentdisclosure.py', # vv Tests less than 30s vv 'wallet_1941.py', 'wallet_addresses.py', diff --git a/qa/rpc-tests/finalsaplingroot.py b/qa/rpc-tests/finalsaplingroot.py index ae8a4c5f2..e95b6c68a 100755 --- a/qa/rpc-tests/finalsaplingroot.py +++ b/qa/rpc-tests/finalsaplingroot.py @@ -131,10 +131,8 @@ class FinalSaplingRootTest(BitcoinTestFramework): # Mine a block with a Sprout shielded tx and verify the final Sapling root does not change zaddr1 = self.nodes[1].z_getnewaddress('sprout') - recipients = [] - recipients.append({"address": zaddr1, "amount": Decimal('10')}) - myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0) - wait_and_assert_operationid_status(self.nodes[0], myopid) + result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr1, 0, 1) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) self.sync_all() self.nodes[0].generate(1) diff --git a/qa/rpc-tests/mergetoaddress_mixednotes.py b/qa/rpc-tests/mergetoaddress_mixednotes.py index d0a67544d..fd772bfd1 100755 --- a/qa/rpc-tests/mergetoaddress_mixednotes.py +++ b/qa/rpc-tests/mergetoaddress_mixednotes.py @@ -29,8 +29,8 @@ class MergeToAddressMixedNotes(BitcoinTestFramework): saplingAddr = self.nodes[0].z_getnewaddress('sapling') t_addr = self.nodes[1].getnewaddress() - opid = self.nodes[0].z_sendmany(coinbase_addr, [{"address": sproutAddr, "amount": Decimal('10')}], 1, 0) - wait_and_assert_operationid_status(self.nodes[0], opid) + result = self.nodes[0].z_shieldcoinbase(coinbase_addr, sproutAddr, 0, 1) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) self.nodes[0].generate(1) self.sync_all() assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('10')) diff --git a/qa/rpc-tests/paymentdisclosure.py b/qa/rpc-tests/paymentdisclosure.py deleted file mode 100755 index db9add407..000000000 --- a/qa/rpc-tests/paymentdisclosure.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2017 The Zcash developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or https://www.opensource.org/licenses/mit-license.php . - -from test_framework.test_framework import BitcoinTestFramework -from test_framework.authproxy import JSONRPCException -from test_framework.util import assert_equal, \ - start_node, connect_nodes_bi, wait_and_assert_operationid_status, \ - get_coinbase_address, DEFAULT_FEE - -from decimal import Decimal - -class PaymentDisclosureTest (BitcoinTestFramework): - - def __init__(self): - super().__init__() - self.num_nodes = 3 - self.setup_clean_chain = True - - def setup_network(self, split=False): - args = ['-debug=zrpcunsafe,paymentdisclosure', '-experimentalfeatures', '-paymentdisclosure', '-txindex=1'] - self.nodes = [] - self.nodes.append(start_node(0, self.options.tmpdir, args)) - self.nodes.append(start_node(1, self.options.tmpdir, args)) - # node 2 does not enable payment disclosure - args2 = ['-debug=zrpcunsafe', '-experimentalfeatures', '-txindex=1'] - self.nodes.append(start_node(2, self.options.tmpdir, args2)) - connect_nodes_bi(self.nodes,0,1) - connect_nodes_bi(self.nodes,1,2) - connect_nodes_bi(self.nodes,0,2) - self.is_network_split=False - self.sync_all() - - def run_test (self): - print("Mining blocks...") - - self.nodes[0].generate(4) - self.sync_all() - walletinfo = self.nodes[0].getwalletinfo() - assert_equal(walletinfo['immature_balance'], 40) - assert_equal(walletinfo['balance'], 0) - self.sync_all() - self.nodes[2].generate(3) - 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(), 30) - - mytaddr = get_coinbase_address(self.nodes[0]) - myzaddr = self.nodes[0].z_getnewaddress('sprout') - - # Check that Node 2 has payment disclosure disabled. - try: - self.nodes[2].z_getpaymentdisclosure("invalidtxid", 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("payment disclosure is disabled" in errorString) - - # Check that Node 0 returns an error for an unknown txid - try: - self.nodes[0].z_getpaymentdisclosure("invalidtxid", 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("No information available about transaction" in errorString) - - # Shield coinbase utxos from node 0 of value 40, default fee - recipients = [{"address": myzaddr, "amount": Decimal('40.0') - DEFAULT_FEE}] - myopid = self.nodes[0].z_sendmany(mytaddr, recipients) - txid = wait_and_assert_operationid_status(self.nodes[0], myopid) - - # Check the tx has joinsplits - assert( len(self.nodes[0].getrawtransaction("" + txid, 1)["vjoinsplit"]) > 0 ) - - # Sync mempools - self.sync_all() - - # Confirm that you can't create a payment disclosure for an unconfirmed tx - try: - self.nodes[0].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Transaction has not been confirmed yet" in errorString) - - try: - self.nodes[1].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Transaction has not been confirmed yet" in errorString) - - # Mine tx - self.nodes[0].generate(1) - self.sync_all() - - # Confirm that Node 1 cannot create a payment disclosure for a transaction which does not impact its wallet - try: - self.nodes[1].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Transaction does not belong to the wallet" in errorString) - - # Check that an invalid joinsplit index is rejected - try: - self.nodes[0].z_getpaymentdisclosure(txid, 1, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Invalid js_index" in errorString) - - try: - self.nodes[0].z_getpaymentdisclosure(txid, -1, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Invalid js_index" in errorString) - - # Check that an invalid output index is rejected - try: - self.nodes[0].z_getpaymentdisclosure(txid, 0, 2) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Invalid output_index" in errorString) - - try: - self.nodes[0].z_getpaymentdisclosure(txid, 0, -1) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Invalid output_index" in errorString) - - # Ask Node 0 to create and validate a payment disclosure for output 0 - message = "Here is proof of my payment!" - pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, message) - result = self.nodes[0].z_validatepaymentdisclosure(pd) - assert(result["valid"]) - output_value_sum = Decimal(result["value"]) - - # Ask Node 1 to confirm the payment disclosure is valid - result = self.nodes[1].z_validatepaymentdisclosure(pd) - assert(result["valid"]) - assert_equal(result["message"], message) - assert_equal(result["value"], output_value_sum) - - # Confirm that payment disclosure begins with prefix zpd: - assert(pd.startswith("zpd:")) - - # Confirm that payment disclosure without prefix zpd: fails validation - try: - self.nodes[1].z_validatepaymentdisclosure(pd[4:]) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("payment disclosure prefix not found" in errorString) - - # Check that total value of output index 0 and index 1 should equal shielding amount of 40 less standard fee. - pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 1) - result = self.nodes[0].z_validatepaymentdisclosure(pd) - output_value_sum += Decimal(result["value"]) - assert_equal(output_value_sum, Decimal('40.0') - DEFAULT_FEE) - - # Create a z->z transaction, sending shielded funds from node 0 to node 1 - node1zaddr = self.nodes[1].z_getnewaddress('sprout') - recipients = [{"address":node1zaddr, "amount":Decimal('1')}] - myopid = self.nodes[0].z_sendmany(myzaddr, recipients) - txid = wait_and_assert_operationid_status(self.nodes[0], myopid) - self.sync_all() - self.nodes[0].generate(1) - self.sync_all() - - # Confirm that Node 0 can create a valid payment disclosure - pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, "a message of your choice") - result = self.nodes[0].z_validatepaymentdisclosure(pd) - assert(result["valid"]) - - # Confirm that Node 1, even as recipient of shielded funds, cannot create a payment disclosure - # as the transaction was created by Node 0 and Node 1's payment disclosure database does not - # contain the necessary data to do so, where the data would only have been available on Node 0 - # when executing z_shieldcoinbase. - try: - self.nodes[1].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Could not find payment disclosure info for the given joinsplit output" in errorString) - - # Payment disclosures cannot be created for transparent transactions. - txid = self.nodes[2].sendtoaddress(mytaddr, 1.0) - self.sync_all() - - # No matter the type of transaction, if it has not been confirmed, it is ignored. - try: - self.nodes[0].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Transaction has not been confirmed yet" in errorString) - - self.nodes[0].generate(1) - self.sync_all() - - # Confirm that a payment disclosure can only be generated for a shielded transaction. - try: - self.nodes[0].z_getpaymentdisclosure(txid, 0, 0) - assert(False) - except JSONRPCException as e: - errorString = e.error['message'] - assert("Transaction is not a shielded transaction" in errorString) - -if __name__ == '__main__': - PaymentDisclosureTest().main() diff --git a/qa/rpc-tests/remove_sprout_shielding.py b/qa/rpc-tests/remove_sprout_shielding.py index 9a84d19f2..e3364f7c2 100755 --- a/qa/rpc-tests/remove_sprout_shielding.py +++ b/qa/rpc-tests/remove_sprout_shielding.py @@ -59,14 +59,6 @@ class RemoveSproutShieldingTest (BitcoinTestFramework): self.nodes[0].generate(1) self.sync_all() - # Create taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass - sendmany_tx_0 = self.nodes[0].z_sendmany(taddr_0, [{"address": self.nodes[1].z_getnewaddress('sprout'), "amount": 1}]) - wait_and_assert_operationid_status(self.nodes[0], sendmany_tx_0) - print("taddr -> Sprout z_sendmany tx accepted before Canopy on node 0") - - self.nodes[0].generate(1) - self.sync_all() - # Create mergetoaddress taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass merge_tx_0 = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], self.nodes[1].z_getnewaddress('sprout')) wait_and_assert_operationid_status(self.nodes[0], merge_tx_0['opid']) @@ -75,7 +67,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework): # Mine to one block before Canopy activation on node 0; adding value # to the Sprout pool will fail now since the transaction must be # included in the next (or later) block, after Canopy has activated. - self.nodes[0].generate(4) + self.nodes[0].generate(5) self.sync_all() # Shield coinbase to Sprout on node 0. Should fail @@ -91,7 +83,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework): sprout_addr = self.nodes[1].z_getnewaddress('sprout') assert_raises_message( JSONRPCException, - "Sprout shielding is not supported after Canopy", + "Sending funds into the Sprout pool is not supported by z_sendmany", self.nodes[0].z_sendmany, taddr_0, [{"address": sprout_addr, "amount": 1}]) print("taddr -> Sprout z_sendmany tx rejected at Canopy activation on node 0") diff --git a/qa/rpc-tests/sprout_sapling_migration.py b/qa/rpc-tests/sprout_sapling_migration.py index 78bcf7352..680e4a040 100755 --- a/qa/rpc-tests/sprout_sapling_migration.py +++ b/qa/rpc-tests/sprout_sapling_migration.py @@ -158,8 +158,8 @@ class SproutSaplingMigration(BitcoinTestFramework): def send_to_sprout_zaddr(self, tAddr, sproutAddr): # Send some ZEC to a Sprout address - opid = self.nodes[0].z_sendmany(tAddr, [{"address": sproutAddr, "amount": Decimal('10')}], 1, 0) - wait_and_assert_operationid_status(self.nodes[0], opid) + result = self.nodes[0].z_shieldcoinbase(tAddr, sproutAddr, 0, 1) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) self.nodes[0].generate(1) self.sync_all() diff --git a/qa/rpc-tests/turnstile.py b/qa/rpc-tests/turnstile.py index 4b02b9619..9102d118c 100755 --- a/qa/rpc-tests/turnstile.py +++ b/qa/rpc-tests/turnstile.py @@ -29,6 +29,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + fail, get_coinbase_address, start_node, start_nodes, sync_blocks, sync_mempools, @@ -88,9 +89,14 @@ class TurnstileTest (BitcoinTestFramework): # Node 0 shields some funds dest_addr = self.nodes[0].z_getnewaddress(POOL_NAME.lower()) taddr0 = get_coinbase_address(self.nodes[0]) - recipients = [] - recipients.append({"address": dest_addr, "amount": Decimal('10')}) - myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0) + if (POOL_NAME == "SPROUT"): + myopid = self.nodes[0].z_shieldcoinbase(taddr0, dest_addr, 0, 1)['opid'] + elif (POOL_NAME == "SAPLING"): + recipients = [] + recipients.append({"address": dest_addr, "amount": Decimal('10')}) + myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0) + else: + fail("Unrecognized pool name: " + POOL_NAME) wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() self.nodes[0].generate(1) diff --git a/qa/rpc-tests/wallet_changeaddresses.py b/qa/rpc-tests/wallet_changeaddresses.py index 4e3dbf17e..409e2b94e 100755 --- a/qa/rpc-tests/wallet_changeaddresses.py +++ b/qa/rpc-tests/wallet_changeaddresses.py @@ -81,15 +81,11 @@ class WalletChangeAddressesTest(BitcoinTestFramework): taddr = self.nodes[0].getnewaddress() saplingAddr = self.nodes[0].z_getnewaddress('sapling') - sproutAddr = self.nodes[0].z_getnewaddress('sprout') print() print('Checking z_sendmany(taddr->Sapling)') check_change_taddr_reuse(saplingAddr) print() - print('Checking z_sendmany(taddr->Sprout)') - check_change_taddr_reuse(sproutAddr) - print() print('Checking z_sendmany(taddr->taddr)') check_change_taddr_reuse(taddr) diff --git a/qa/rpc-tests/wallet_listnotes.py b/qa/rpc-tests/wallet_listnotes.py index bdfb6ccad..e0e501382 100755 --- a/qa/rpc-tests/wallet_listnotes.py +++ b/qa/rpc-tests/wallet_listnotes.py @@ -6,7 +6,6 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, - get_coinbase_address, wait_and_assert_operationid_status, DEFAULT_FEE ) @@ -20,7 +19,6 @@ class WalletListNotes(BitcoinTestFramework): # Current height = 200 assert_equal(200, self.nodes[0].getblockcount()) sproutzaddr = self.nodes[0].z_getnewaddress('sprout') - saplingzaddr = self.nodes[0].z_getnewaddress('sapling') # we've got lots of coinbase (taddr) but no shielded funds yet assert_equal(0, Decimal(self.nodes[0].z_gettotalbalance()['private'])) @@ -30,11 +28,10 @@ class WalletListNotes(BitcoinTestFramework): self.sync_all() assert_equal(201, self.nodes[0].getblockcount()) - # Shield coinbase funds (must be a multiple of 10, no change allowed) - receive_amount_10 = Decimal('10.0') - DEFAULT_FEE - recipients = [{"address":sproutzaddr, "amount":receive_amount_10}] - myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients) - txid_1 = wait_and_assert_operationid_status(self.nodes[0], myopid) + # Shield one coinbase output + receive_amount_1 = Decimal('10.0') - DEFAULT_FEE + result = self.nodes[0].z_shieldcoinbase('*', sproutzaddr, DEFAULT_FEE, 1) + txid_1 = wait_and_assert_operationid_status(self.nodes[0], result['opid']) self.sync_all() # No funds (with (default) one or more confirmations) in sproutzaddr yet @@ -51,7 +48,7 @@ class WalletListNotes(BitcoinTestFramework): assert_equal(txid_1, unspent_cb[0]['txid']) assert_equal(True, unspent_cb[0]['spendable']) assert_equal(sproutzaddr, unspent_cb[0]['address']) - assert_equal(receive_amount_10, unspent_cb[0]['amount']) + assert_equal(receive_amount_1, unspent_cb[0]['amount']) # list unspent, filtering by address, should produce same result unspent_cb_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr]) @@ -64,12 +61,12 @@ class WalletListNotes(BitcoinTestFramework): # Current height = 202 assert_equal(202, self.nodes[0].getblockcount()) - # Send 1.0 minus default fee from sproutzaddr to a new zaddr - sproutzaddr2 = self.nodes[0].z_getnewaddress('sprout') - receive_amount_1 = Decimal('1.0') - DEFAULT_FEE - change_amount_9 = receive_amount_10 - Decimal('1.0') - assert_equal('sprout', self.nodes[0].z_validateaddress(sproutzaddr2)['type']) - recipients = [{"address": sproutzaddr2, "amount":receive_amount_1}] + # Send 1.0 minus default fee from sproutzaddr to a new Sapling zaddr + saplingzaddr = self.nodes[0].z_getnewaddress('sapling') + receive_amount_2 = Decimal('1.0') + change_amount_2 = receive_amount_1 - receive_amount_2 - DEFAULT_FEE + assert_equal('sapling', self.nodes[0].z_validateaddress(saplingzaddr)['type']) + recipients = [{"address": saplingzaddr, "amount":receive_amount_2}] myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients) txid_2 = wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() @@ -82,16 +79,16 @@ class WalletListNotes(BitcoinTestFramework): assert_equal(False, unspent_tx[0]['change']) assert_equal(txid_2, unspent_tx[0]['txid']) assert_equal(True, unspent_tx[0]['spendable']) - assert_equal(sproutzaddr2, unspent_tx[0]['address']) - assert_equal(receive_amount_1, unspent_tx[0]['amount']) + assert_equal(saplingzaddr, unspent_tx[0]['address']) + assert_equal(receive_amount_2, unspent_tx[0]['amount']) assert_equal(True, unspent_tx[1]['change']) assert_equal(txid_2, unspent_tx[1]['txid']) assert_equal(True, unspent_tx[1]['spendable']) assert_equal(sproutzaddr, unspent_tx[1]['address']) - assert_equal(change_amount_9, unspent_tx[1]['amount']) + assert_equal(change_amount_2, unspent_tx[1]['amount']) - unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr2]) + unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr]) assert_equal(1, len(unspent_tx_filter)) assert_equal(unspent_tx[0], unspent_tx_filter[0]) @@ -99,15 +96,15 @@ class WalletListNotes(BitcoinTestFramework): assert_equal(1, len(unspent_tx_filter)) assert_equal(unspent_tx[1], unspent_tx_filter[0]) - # No funds in saplingzaddr yet - assert_equal(0, len(self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr]))) + self.nodes[0].generate(1) + self.sync_all() - # Send 2.0 minus default fee to our sapling zaddr - # (sending from a sprout zaddr to a sapling zaddr is disallowed, - # so send from coin base) - receive_amount_2 = Decimal('2.0') - DEFAULT_FEE - recipients = [{"address": saplingzaddr, "amount":receive_amount_2}] - myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients) + # Send 2.0 minus default fee to a new sapling zaddr + saplingzaddr2 = self.nodes[0].z_getnewaddress('sapling') + receive_amount_3 = Decimal('2.0') + change_amount_3 = change_amount_2 - receive_amount_3 - DEFAULT_FEE + recipients = [{"address": saplingzaddr2, "amount":receive_amount_3}] + myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients) txid_3 = wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() unspent_tx = self.nodes[0].z_listunspent(0) @@ -119,31 +116,31 @@ class WalletListNotes(BitcoinTestFramework): assert_equal(False, unspent_tx[0]['change']) assert_equal(txid_2, unspent_tx[0]['txid']) assert_equal(True, unspent_tx[0]['spendable']) - assert_equal(sproutzaddr2, unspent_tx[0]['address']) - assert_equal(receive_amount_1, unspent_tx[0]['amount']) + assert_equal(saplingzaddr, unspent_tx[0]['address']) + assert_equal(receive_amount_2, unspent_tx[0]['amount']) assert_equal(False, unspent_tx[1]['change']) assert_equal(txid_3, unspent_tx[1]['txid']) assert_equal(True, unspent_tx[1]['spendable']) - assert_equal(saplingzaddr, unspent_tx[1]['address']) - assert_equal(receive_amount_2, unspent_tx[1]['amount']) + assert_equal(saplingzaddr2, unspent_tx[1]['address']) + assert_equal(receive_amount_3, unspent_tx[1]['amount']) assert_equal(True, unspent_tx[2]['change']) - assert_equal(txid_2, unspent_tx[2]['txid']) + assert_equal(txid_3, unspent_tx[2]['txid']) assert_equal(True, unspent_tx[2]['spendable']) assert_equal(sproutzaddr, unspent_tx[2]['address']) - assert_equal(change_amount_9, unspent_tx[2]['amount']) + assert_equal(change_amount_3, unspent_tx[2]['amount']) unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr]) assert_equal(1, len(unspent_tx_filter)) - assert_equal(unspent_tx[1], unspent_tx_filter[0]) + assert_equal(unspent_tx[0], unspent_tx_filter[0]) # test that pre- and post-sapling can be filtered in a single call unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr, saplingzaddr]) assert_equal(2, len(unspent_tx_filter)) unspent_tx_filter = sorted(unspent_tx_filter, key=lambda k: k['amount']) - assert_equal(unspent_tx[1], unspent_tx_filter[0]) + assert_equal(unspent_tx[0], unspent_tx_filter[0]) assert_equal(unspent_tx[2], unspent_tx_filter[1]) # so far, this node has no watchonly addresses, so results are the same diff --git a/qa/rpc-tests/wallet_listreceived.py b/qa/rpc-tests/wallet_listreceived.py index d63053bc3..18d82ba15 100755 --- a/qa/rpc-tests/wallet_listreceived.py +++ b/qa/rpc-tests/wallet_listreceived.py @@ -226,7 +226,7 @@ class ListReceivedTest (BitcoinTestFramework): assert_equal(3, c[release], "Count of unconfirmed notes should be 3(2 in zaddr1 + 1 in zaddr2)") def run_test(self): - self.run_test_release('sprout', 200) + #self.run_test_release('sprout', 200) self.run_test_release('sapling', 214) if __name__ == '__main__': diff --git a/qa/rpc-tests/wallet_overwintertx.py b/qa/rpc-tests/wallet_overwintertx.py index fd2e3e069..5ccdd57b4 100755 --- a/qa/rpc-tests/wallet_overwintertx.py +++ b/qa/rpc-tests/wallet_overwintertx.py @@ -72,10 +72,8 @@ class WalletOverwinterTxTest (BitcoinTestFramework): # Node 0 shields to Node 2, a coinbase utxo of value 10.0 less default fee zsendamount = Decimal('10.0') - DEFAULT_FEE - recipients = [] - recipients.append({"address":zaddr2, "amount": zsendamount}) - myopid = self.nodes[0].z_sendmany(taddr0, recipients) - txid_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid) + result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr2, DEFAULT_FEE, 1) + txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid']) # Skip over the three blocks prior to activation; no transactions can be mined # in them due to the nearly-expiring restrictions. @@ -143,10 +141,8 @@ class WalletOverwinterTxTest (BitcoinTestFramework): # Node 0 shields to Node 3, a coinbase utxo of value 10.0 less default fee zsendamount = Decimal('10.0') - DEFAULT_FEE - recipients = [] - recipients.append({"address":zaddr3, "amount": zsendamount}) - myopid = self.nodes[0].z_sendmany(taddr0, recipients) - txid_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid) + result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr3, DEFAULT_FEE, 1) + txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid']) # Mine the first Blossom block self.sync_all() diff --git a/qa/rpc-tests/wallet_sapling.py b/qa/rpc-tests/wallet_sapling.py index ba9f17a91..19f88739d 100755 --- a/qa/rpc-tests/wallet_sapling.py +++ b/qa/rpc-tests/wallet_sapling.py @@ -169,7 +169,7 @@ class WalletSaplingTest(BitcoinTestFramework): ) raise AssertionError("Should have thrown an exception") except JSONRPCException as e: - assert_equal("Cannot send to both Sprout and Sapling addresses using z_sendmany", e.error['message']) + assert_equal("Sending funds into the Sprout pool is not supported by z_sendmany", e.error['message']) if __name__ == '__main__': WalletSaplingTest().main() diff --git a/qa/rpc-tests/wallet_sendmany_any_taddr.py b/qa/rpc-tests/wallet_sendmany_any_taddr.py index d88b09e2f..ef80d1404 100755 --- a/qa/rpc-tests/wallet_sendmany_any_taddr.py +++ b/qa/rpc-tests/wallet_sendmany_any_taddr.py @@ -26,7 +26,7 @@ class WalletSendManyAnyTaddr(BitcoinTestFramework): self.nodes[3], self.nodes[3].z_sendmany('ANY_TADDR', [{'address': recipient, 'amount': 100}]), 'failed', - 'Could not find any non-coinbase UTXOs to spend. Coinbase UTXOs can only be sent to a single zaddr recipient from a single taddr.', + 'Insufficient funds: have 0.00, need 100.00001; if you are attempting to shield transparent coinbase funds, ensure that you have specified only a single recipient address.', ) # Prepare some non-coinbase UTXOs diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index 6b67b659a..d16c0c72e 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -126,12 +126,12 @@ TransactionBuilderResult::TransactionBuilderResult(const CTransaction& tx) : may TransactionBuilderResult::TransactionBuilderResult(const std::string& error) : maybeError(error) {} -bool TransactionBuilderResult::IsTx() { return maybeTx != std::nullopt; } +bool TransactionBuilderResult::IsTx() { return maybeTx.has_value(); } -bool TransactionBuilderResult::IsError() { return maybeError != std::nullopt; } +bool TransactionBuilderResult::IsError() { return maybeError.has_value(); } CTransaction TransactionBuilderResult::GetTxOrThrow() { - if (maybeTx) { + if (maybeTx.has_value()) { return maybeTx.value(); } else { throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build transaction: " + GetError()); @@ -139,7 +139,7 @@ CTransaction TransactionBuilderResult::GetTxOrThrow() { } std::string TransactionBuilderResult::GetError() { - if (maybeError) { + if (maybeError.has_value()) { return maybeError.value(); } else { // This can only happen if isTx() is true in which case we should not call getError() @@ -216,7 +216,7 @@ void TransactionBuilder::AddSaplingOutput( libzcash::Zip212Enabled zip_212_enabled = libzcash::Zip212Enabled::BeforeZip212; // We use nHeight = chainActive.Height() + 1 since the output will be included in the next block - if (Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_CANOPY)) { + if (consensusParams.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_CANOPY)) { zip_212_enabled = libzcash::Zip212Enabled::AfterZip212; } diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index d67f2b1fa..0ce35a0f9 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -36,70 +36,33 @@ #include #include #include +#include #include #include using namespace libzcash; -int find_output(UniValue obj, int n) { - UniValue outputMapValue = find_value(obj, "outputmap"); - if (!outputMapValue.isArray()) { - throw JSONRPCError(RPC_WALLET_ERROR, "Missing outputmap for JoinSplit operation"); - } - - UniValue outputMap = outputMapValue.get_array(); - assert(outputMap.size() == ZC_NUM_JS_OUTPUTS); - for (size_t i = 0; i < outputMap.size(); i++) { - if (outputMap[i].get_int() == n) { - return i; - } - } - - throw std::logic_error("n is not present in outputmap"); -} - AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( - std::optional builder, - CMutableTransaction contextualTx, - std::string fromAddress, - std::vector tOutputs, - std::vector zOutputs, + TransactionBuilder builder, + PaymentSource paymentSource, + std::vector recipients, int minDepth, CAmount fee, UniValue contextInfo) : - tx_(contextualTx), fromaddress_(fromAddress), t_outputs_(tOutputs), z_outputs_(zOutputs), mindepth_(minDepth), fee_(fee), contextinfo_(contextInfo) + builder_(builder), paymentSource_(paymentSource), recipients_(recipients), mindepth_(minDepth), fee_(fee), contextinfo_(contextInfo) { assert(fee_ >= 0); + assert(mindepth_ >= 0); + assert(!recipients_.empty()); - if (minDepth < 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Minconf cannot be negative"); - } - - if (fromAddress.size() == 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "From address parameter missing"); - } - - if (tOutputs.size() == 0 && zOutputs.size() == 0) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients"); - } - - isUsingBuilder_ = false; - if (builder) { - isUsingBuilder_ = true; - builder_ = builder.value(); - } - - KeyIO keyIO(Params()); - - useanyutxo_ = fromAddress == "ANY_TADDR"; - if (useanyutxo_) { - isfromtaddr_ = true; - } else { - auto address = keyIO.DecodePaymentAddress(fromAddress); - if (address.has_value()) { + std::visit(match { + [&](const FromAnyTaddr& any) { + isfromtaddr_ = true; + }, + [&](const PaymentAddress& addr) { // We don't need to lock on the wallet as spending key related methods are thread-safe - if (!std::visit(HaveSpendingKeyForPaymentAddress(pwalletMain), address.value())) { + if (!std::visit(HaveSpendingKeyForPaymentAddress(pwalletMain), addr)) { throw JSONRPCError( RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address, no spending key found for address"); @@ -107,19 +70,15 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( std::visit(match { [&](const CKeyID& keyId) { - fromtaddr_ = keyId; isfromtaddr_ = true; }, [&](const CScriptID& scriptId) { - fromtaddr_ = scriptId; isfromtaddr_ = true; }, [&](const libzcash::SproutPaymentAddress& addr) { - frompaymentaddress_ = addr; isfromzaddr_ = true; }, [&](const libzcash::SaplingPaymentAddress& addr) { - frompaymentaddress_ = addr; isfromzaddr_ = true; }, [&](const libzcash::UnifiedAddress& addr) { @@ -127,11 +86,9 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( RPC_INVALID_ADDRESS_OR_KEY, "Unified addresses are not yet supported by z_sendmany"); } - }, address.value()); - } else { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address"); + }, addr); } - } + }, paymentSource); if (isfromzaddr_ && minDepth==0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Minconf cannot be zero when sending from zaddr"); @@ -143,9 +100,6 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( } else { LogPrint("zrpc", "%s: z_sendmany initialized\n", getId()); } - - // Enable payment disclosure if requested - paymentDisclosureMode = fExperimentalPaymentDisclosure; } AsyncRPCOperation_sendmany::~AsyncRPCOperation_sendmany() { @@ -164,8 +118,9 @@ void AsyncRPCOperation_sendmany::main() { GenerateBitcoins(false, 0, Params()); #endif + std::optional txid; try { - success = main_impl(); + txid = main_impl(); } catch (const UniValue& objError) { int code = find_value(objError, "code").get_int(); std::string message = find_value(objError, "message").get_str(); @@ -191,34 +146,19 @@ void AsyncRPCOperation_sendmany::main() { stop_execution_clock(); - if (success) { + if (txid.has_value()) { set_state(OperationStatus::SUCCESS); } else { set_state(OperationStatus::FAILED); } std::string s = strprintf("%s: z_sendmany finished (status=%s", getId(), getStateAsString()); - if (success) { - s += strprintf(", txid=%s)\n", tx_.GetHash().ToString()); + if (txid.has_value()) { + s += strprintf(", txid=%s)\n", txid.value().ToString()); } else { s += strprintf(", error=%s)\n", getErrorMessage()); } LogPrintf("%s",s); - - // !!! Payment disclosure START - if (success && paymentDisclosureMode && paymentDisclosureData_.size()>0) { - uint256 txidhash = tx_.GetHash(); - std::shared_ptr db = PaymentDisclosureDB::sharedInstance(); - for (PaymentDisclosureKeyInfo p : paymentDisclosureData_) { - p.first.hash = txidhash; - if (!db->Put(p.first, p.second)) { - LogPrint("paymentdisclosure", "%s: Payment Disclosure: Error writing entry to database for key %s\n", getId(), p.first.ToString()); - } else { - LogPrint("paymentdisclosure", "%s: Payment Disclosure: Successfully added entry to database for key %s\n", getId(), p.first.ToString()); - } - } - } - // !!! Payment disclosure END } struct TxValues { @@ -227,755 +167,429 @@ struct TxValues { CAmount t_outputs_total{0}; CAmount z_outputs_total{0}; CAmount targetAmount{0}; - bool selectedUTXOCoinbase{false}; }; +bool IsFromAnyTaddr(const PaymentSource& paymentSource) { + return std::visit(match { + [&](const FromAnyTaddr& fromAny) { + return true; + }, + [&](const PaymentAddress& addr) { + return false; + } + }, paymentSource); +} + +// Construct and send the transaction, returning the resulting txid. +// Errors in transaction construction will throw. +// // Notes: // 1. #1159 Currently there is no limit set on the number of joinsplits, so size of tx could be invalid. // 2. #1360 Note selection is not optimal // 3. #1277 Spendable notes are not locked, so an operation running in parallel could also try to use them -bool AsyncRPCOperation_sendmany::main_impl() { - +uint256 AsyncRPCOperation_sendmany::main_impl() { + // TODO UA: these flags will become meaningless. assert(isfromtaddr_ != isfromzaddr_); - bool isSingleZaddrOutput = (t_outputs_.size()==0 && z_outputs_.size()==1); - bool isMultipleZaddrOutput = (t_outputs_.size()==0 && z_outputs_.size()>=1); - bool isPureTaddrOnlyTx = (isfromtaddr_ && z_outputs_.size() == 0); - CAmount minersFee = fee_; TxValues txValues; // First calculate the target - for (SendManyRecipient & t : t_outputs_) { - txValues.t_outputs_total += t.amount; - } - - for (SendManyRecipient & t : z_outputs_) { - txValues.z_outputs_total += t.amount; + int shieldedRecipients = 0; + int transparentRecipients = 0; + for (const SendManyRecipient& recipient : recipients_) { + std::visit(match { + [&](const CKeyID& addr) { + transparentRecipients += 1; + txValues.t_outputs_total += recipient.amount; + }, + [&](const CScriptID& addr) { + transparentRecipients += 1; + txValues.t_outputs_total += recipient.amount; + }, + [&](const libzcash::SproutPaymentAddress& addr) { + // unreachable; currently disallowed by checks at construction + throw JSONRPCError(RPC_INVALID_PARAMETER, "Sending to Sprout is disabled."); + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + txValues.z_outputs_total += recipient.amount; + shieldedRecipients += 1; + }, + [&](const libzcash::UnifiedAddress& ua) { + // unreachable; currently disallowed by checks at construction + throw JSONRPCError(RPC_INVALID_PARAMETER, "Sending to unified addresses is disabled."); + } + }, recipient.address); } CAmount sendAmount = txValues.z_outputs_total + txValues.t_outputs_total; - txValues.targetAmount = sendAmount + minersFee; + txValues.targetAmount = sendAmount + fee_; - // When spending coinbase utxos, you can only specify a single zaddr as the change must go somewhere - // and if there are multiple zaddrs, we don't know where to send it. - if (isfromtaddr_) { - // Only select coinbase if we are spending from a single t-address to a single z-address. - if (!useanyutxo_ && isSingleZaddrOutput) { - bool b = find_utxos(true, txValues); - if (!b) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient transparent funds, no UTXOs found for taddr from address."); - } - } else { - bool b = find_utxos(false, txValues); - if (!b) { - if (isMultipleZaddrOutput) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any non-coinbase UTXOs to spend. Coinbase UTXOs can only be sent to a single zaddr recipient from a single taddr."); - } else { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any non-coinbase UTXOs to spend."); - } - } - } + builder_.SetFee(fee_); + + // Only select coinbase if we are spending from at most a single t-address. + bool allowTransparentCoinbase = + !IsFromAnyTaddr(paymentSource_) && // allow coinbase inputs from at most a single t-addr + transparentRecipients == 0; // cannot send transparent coinbase to transparent recipients + + // Find spendable inputs, and select a minimal set of them that + // can supply the required target amount. + auto spendable = FindSpendableInputs(allowTransparentCoinbase); + if (!spendable.LimitToAmount(txValues.targetAmount)) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds: have %s, need %s", + FormatMoney(spendable.Total()), FormatMoney(txValues.targetAmount)) + + (allowTransparentCoinbase ? "" : + "; if you are attempting to shield transparent coinbase funds, " + "ensure that you have specified only a single recipient address.") + ); } - if (isfromzaddr_ && !find_unspent_notes()) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds, no unspent notes found for zaddr from address."); - } + spendable.LogInputs(getId()); // At least one of z_sprout_inputs_ and z_sapling_inputs_ must be empty by design - assert(z_sprout_inputs_.empty() || z_sapling_inputs_.empty()); + // + // TODO: This restriction is true by construction as we have no mechanism + // for filtering for notes that will select both Sprout and Sapling notes + // simultaneously, but even if we did it would likely be safe to remove + // this limitation. + assert(spendable.sproutNoteEntries.empty() || spendable.saplingNoteEntries.empty()); - for (SendManyInputJSOP & t : z_sprout_inputs_) { - txValues.z_inputs_total += t.amount; + for (const auto& t : spendable.utxos) { + txValues.t_inputs_total += t.Value(); } - for (auto t : z_sapling_inputs_) { + for (const auto& t : spendable.sproutNoteEntries) { + txValues.z_inputs_total += t.note.value(); + } + for (const auto& t : spendable.saplingNoteEntries) { txValues.z_inputs_total += t.note.value(); } + // TODO UA: these restrictions should be removed. assert(!isfromtaddr_ || txValues.z_inputs_total == 0); assert(!isfromzaddr_ || txValues.t_inputs_total == 0); + if (isfromtaddr_ && (txValues.t_inputs_total < txValues.targetAmount)) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient transparent funds, have %s, need %s", + FormatMoney(txValues.t_inputs_total), FormatMoney(txValues.targetAmount))); + } if (isfromzaddr_ && (txValues.z_inputs_total < txValues.targetAmount)) { throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strprintf("Insufficient shielded funds, have %s, need %s", FormatMoney(txValues.z_inputs_total), FormatMoney(txValues.targetAmount))); } + // When spending transparent coinbase outputs, all inputs must be fully + // consumed, and they may only be sent to shielded recipients. + if (spendable.HasTransparentCoinbase()) { + if (txValues.t_inputs_total != txValues.targetAmount) { + throw JSONRPCError( + RPC_WALLET_ERROR, + strprintf( + "When shielding coinbase funds, the wallet does not allow any change. " + "The proposed transaction would result in %s in change.", + FormatMoney(txValues.t_inputs_total - txValues.targetAmount) + )); + } + if (txValues.t_outputs_total != 0) { + throw JSONRPCError( + RPC_WALLET_ERROR, + "Coinbase funds may only be sent to shielded recipients."); + } + } + if (isfromtaddr_) { LogPrint("zrpc", "%s: spending %s to send %s with fee %s\n", - getId(), FormatMoney(txValues.targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); + getId(), FormatMoney(txValues.targetAmount), FormatMoney(sendAmount), FormatMoney(fee_)); } else { LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n", - getId(), FormatMoney(txValues.targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); + getId(), FormatMoney(txValues.targetAmount), FormatMoney(sendAmount), FormatMoney(fee_)); } LogPrint("zrpc", "%s: transparent input: %s (to choose from)\n", getId(), FormatMoney(txValues.t_inputs_total)); LogPrint("zrpcunsafe", "%s: private input: %s (to choose from)\n", getId(), FormatMoney(txValues.z_inputs_total)); LogPrint("zrpc", "%s: transparent output: %s\n", getId(), FormatMoney(txValues.t_outputs_total)); LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(txValues.z_outputs_total)); - LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(minersFee)); + LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(fee_)); - KeyIO keyIO(Params()); + CReserveKey keyChange(pwalletMain); + uint256 ovk; - /** - * SCENARIO #0 - * - * Sprout not involved, so we just use the TransactionBuilder and we're done. - * We added the transparent inputs to the builder earlier. - */ - if (isUsingBuilder_) { - builder_.SetFee(minersFee); + auto getDefaultOVK = [&]() { + HDSeed seed = pwalletMain->GetHDSeedForRPC(); + return ovkForShieldingFromTaddr(seed); + }; - // Get various necessary keys - SaplingExpandedSpendingKey expsk; - uint256 ovk; - auto saplingKey = std::visit(GetSaplingKeyForPaymentAddress(pwalletMain), frompaymentaddress_); - if (saplingKey.has_value()) { - expsk = saplingKey.value().expsk; - ovk = expsk.full_viewing_key().ovk; - } else { - // Sending from a t-address, which we don't have an ovk for. Instead, - // generate a common one from the HD seed. This ensures the data is - // recoverable, while keeping it logically separate from the ZIP 32 - // Sapling key hierarchy, which the user might not be using. - HDSeed seed = pwalletMain->GetHDSeedForRPC(); - ovk = ovkForShieldingFromTaddr(seed); + auto setTransparentChangeRecipient = [&]() { + LOCK2(cs_main, pwalletMain->cs_wallet); + + EnsureWalletIsUnlocked(); + CPubKey vchPubKey; + bool ret = keyChange.GetReservedKey(vchPubKey); + if (!ret) { + // should never fail, as we just unlocked + throw JSONRPCError( + RPC_WALLET_KEYPOOL_RAN_OUT, + "Could not generate a taddr to use as a change address"); } - // Set change address if we are using transparent funds - // TODO: Should we just use fromtaddr_ as the change address? - CReserveKey keyChange(pwalletMain); - if (isfromtaddr_) { - LOCK2(cs_main, pwalletMain->cs_wallet); + CTxDestination changeAddr = vchPubKey.GetID(); + builder_.SendChangeTo(changeAddr); + }; - EnsureWalletIsUnlocked(); - CPubKey vchPubKey; - bool ret = keyChange.GetReservedKey(vchPubKey); - if (!ret) { - // should never fail, as we just unlocked - throw JSONRPCError( - RPC_WALLET_KEYPOOL_RAN_OUT, - "Could not generate a taddr to use as a change address"); - } + // FIXME: it would be better to use the most recent shielded pool change + // address for the wallet's default unified address account, and the + // associated OVK + std::visit(match { + [&](const FromAnyTaddr& fromAny) { + ovk = getDefaultOVK(); + setTransparentChangeRecipient(); + }, + [&](const PaymentAddress& addr) { + std::visit(match { + [&](const libzcash::SproutPaymentAddress& addr) { + ovk = getDefaultOVK(); + builder_.SendChangeTo(addr); + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + libzcash::SaplingExtendedSpendingKey saplingKey; + assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, saplingKey)); - CTxDestination changeAddr = vchPubKey.GetID(); - builder_.SendChangeTo(changeAddr); + ovk = saplingKey.expsk.full_viewing_key().ovk; + builder_.SendChangeTo(addr, ovk); + }, + [&](const auto& other) { + ovk = getDefaultOVK(); + setTransparentChangeRecipient(); + } + }, addr); } + }, paymentSource_); - // Select Sapling notes - std::vector ops; - std::vector notes; - CAmount sum = 0; - for (auto t : z_sapling_inputs_) { - ops.push_back(t.op); - notes.push_back(t.note); - sum += t.note.value(); - if (sum >= txValues.targetAmount) { - break; - } - } + // Track the total of notes that we've added to the builder + CAmount sum = 0; - // Fetch Sapling anchor and witnesses - uint256 anchor; - std::vector> witnesses; - { - LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->GetSaplingNoteWitnesses(ops, witnesses, anchor); - } + // Create Sapling outpoints + std::vector ops; + std::vector saplingNotes; + std::vector saplingKeys; - // Add Sapling spends - for (size_t i = 0; i < notes.size(); i++) { - if (!witnesses[i]) { - throw JSONRPCError(RPC_WALLET_ERROR, "Missing witness for Sapling note"); - } - builder_.AddSaplingSpend(expsk, notes[i], anchor, witnesses[i].value()); - } + for (const auto& t : spendable.saplingNoteEntries) { + ops.push_back(t.op); + saplingNotes.push_back(t.note); - // Add Sapling outputs - for (auto r : z_outputs_) { - auto address = r.address; - auto value = r.amount; - auto hexMemo = r.memo; + libzcash::SaplingExtendedSpendingKey saplingKey; + assert(pwalletMain->GetSaplingExtendedSpendingKey(t.address, saplingKey)); + saplingKeys.push_back(saplingKey); - auto addr = keyIO.DecodePaymentAddress(address); - assert(addr.has_value() && std::get_if(&addr.value()) != nullptr); - auto to = std::get(addr.value()); - - auto memo = get_memo_from_hex_string(hexMemo); - - builder_.AddSaplingOutput(ovk, to, value, memo); - } - - // Add transparent outputs - for (auto r : t_outputs_) { - auto outputAddress = r.address; - auto amount = r.amount; - - auto address = keyIO.DecodeDestination(outputAddress); - builder_.AddTransparentOutput(address, amount); - } - - // Build the transaction - tx_ = builder_.Build().GetTxOrThrow(); - - UniValue sendResult = SendTransaction(tx_, keyChange, testmode); - set_result(sendResult); - - return true; - } - /** - * END SCENARIO #0 - */ - - - // Grab the current consensus branch ID - { - LOCK(cs_main); - consensusBranchId_ = CurrentEpochBranchId(chainActive.Height() + 1, Params().GetConsensus()); - } - - /** - * SCENARIO #1 - * - * taddr -> taddrs - * - * There are no zaddrs or joinsplits involved. - */ - if (isPureTaddrOnlyTx) { - add_taddr_outputs_to_tx(); - - CAmount funds = txValues.t_inputs_total; - CAmount fundsSpent = txValues.t_outputs_total + minersFee; - CAmount change = funds - fundsSpent; - - CReserveKey keyChange(pwalletMain); - if (change > 0) { - add_taddr_change_output_to_tx(keyChange, change); - - LogPrint("zrpc", "%s: transparent change in transaction output (amount=%s)\n", - getId(), - FormatMoney(change) - ); - } - - UniValue obj(UniValue::VOBJ); - obj.pushKV("rawtxn", EncodeHexTx(tx_)); - auto txAndResult = SignSendRawTransaction(obj, keyChange, testmode); - tx_ = txAndResult.first; - set_result(txAndResult.second); - return true; - } - /** - * END SCENARIO #1 - */ - - - // Prepare raw transaction to handle JoinSplits - CMutableTransaction mtx(tx_); - ed25519_generate_keypair(&joinSplitPrivKey_, &joinSplitPubKey_); - mtx.joinSplitPubKey = joinSplitPubKey_; - tx_ = CTransaction(mtx); - - // Copy zinputs and zoutputs to more flexible containers - std::deque zInputsDeque; // zInputsDeque stores minimum numbers of notes for target amount - CAmount tmp = 0; - for (auto o : z_sprout_inputs_) { - zInputsDeque.push_back(o); - tmp += o.amount; - if (tmp >= txValues.targetAmount) { + sum += t.note.value(); + if (sum >= txValues.targetAmount) { break; } } - std::deque zOutputsDeque; - for (auto o : z_outputs_) { - zOutputsDeque.push_back(o); + + // Fetch Sapling anchor and witnesses + uint256 anchor; + std::vector> witnesses; + { + LOCK2(cs_main, pwalletMain->cs_wallet); + pwalletMain->GetSaplingNoteWitnesses(ops, witnesses, anchor); } + // Add Sapling spends + for (size_t i = 0; i < saplingNotes.size(); i++) { + if (!witnesses[i]) { + throw JSONRPCError(RPC_WALLET_ERROR, "Missing witness for Sapling note"); + } + + builder_.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value()); + } + + // Add Sapling and transparent outputs + for (const auto& r : recipients_) { + std::visit(match { + [&](const CKeyID& keyId) { + builder_.AddTransparentOutput(keyId, r.amount); + }, + [&](const CScriptID& scriptId) { + builder_.AddTransparentOutput(scriptId, r.amount); + }, + [&](const libzcash::SproutPaymentAddress& addr) { + //unreachable + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Sending funds to Sprout is disabled."); + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + auto value = r.amount; + auto memo = get_memo_from_hex_string(r.memo.has_value() ? r.memo.value() : ""); + + builder_.AddSaplingOutput(ovk, addr, value, memo); + }, + [&](const libzcash::UnifiedAddress& addr) { + //unreachable + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Unified addresses are not yet supported by z_sendmany"); + } + }, r.address); + } + + // Add transparent utxos + for (const auto& out : spendable.utxos) { + const CTxOut& txOut = out.tx->vout[out.i]; + builder_.AddTransparentInput(COutPoint(out.tx->GetHash(), out.i), txOut.scriptPubKey, txOut.nValue); + + sum += txOut.nValue; + if (sum >= txValues.targetAmount) { + break; + } + } + + // Find Sprout witnesses // When spending notes, take a snapshot of note witnesses and anchors as the treestate will // change upon arrival of new blocks which contain joinsplit transactions. This is likely // to happen as creating a chained joinsplit transaction can take longer than the block interval. - if (z_sprout_inputs_.size() > 0) { + // So, we need to take locks on cs_main and pwalletMain->cs_wallet so that the witnesses aren't + // updated. + std::vector> vSproutWitnesses; + { LOCK2(cs_main, pwalletMain->cs_wallet); - for (auto t : z_sprout_inputs_) { - JSOutPoint jso = t.point; - std::vector vOutPoints = { jso }; - uint256 inputAnchor; - std::vector> vInputWitnesses; - pwalletMain->GetSproutNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor); - jsopWitnessAnchorMap[ jso.ToString() ] = WitnessAnchorData{ vInputWitnesses[0], inputAnchor }; - } - } - - - /** - * SCENARIO #2 - * - * taddr -> taddrs - * -> zaddrs - * - * Note: Consensus rule states that coinbase utxos can only be sent to a zaddr. - * Local wallet rule does not allow any change when sending coinbase utxos - * since there is currently no way to specify a change address and we don't - * want users accidentally sending excess funds to a recipient. - */ - if (isfromtaddr_) { - add_taddr_outputs_to_tx(); - - CAmount funds = txValues.t_inputs_total; - CAmount fundsSpent = txValues.t_outputs_total + minersFee + txValues.z_outputs_total; - CAmount change = funds - fundsSpent; - - CReserveKey keyChange(pwalletMain); - if (change > 0) { - if (txValues.selectedUTXOCoinbase) { - assert(isSingleZaddrOutput); - throw JSONRPCError(RPC_WALLET_ERROR, strprintf( - "Change %s not allowed. When shielding coinbase funds, the wallet does not " - "allow any change as there is currently no way to specify a change address " - "in z_sendmany.", FormatMoney(change))); - } else { - add_taddr_change_output_to_tx(keyChange, change); - LogPrint("zrpc", "%s: transparent change in transaction output (amount=%s)\n", - getId(), - FormatMoney(change) - ); - } - } - - // Create joinsplits, where each output represents a zaddr recipient. - UniValue obj(UniValue::VOBJ); - while (zOutputsDeque.size() > 0) { - AsyncJoinSplitInfo info; - info.vpub_old = 0; - info.vpub_new = 0; - int n = 0; - while (n++ 0) { - SendManyRecipient smr = zOutputsDeque.front(); - std::string address = smr.address; - CAmount value = smr.amount; - std::string hexMemo = smr.memo; - zOutputsDeque.pop_front(); - - std::optional pa = keyIO.DecodePaymentAddress(address); - if (pa.has_value()) { - JSOutput jso = JSOutput(std::get(pa.value()), value); - if (hexMemo.size() > 0) { - jso.memo = get_memo_from_hex_string(hexMemo); - } - info.vjsout.push_back(jso); - } - - // Funds are removed from the value pool and enter the private pool - info.vpub_old += value; - } - obj = perform_joinsplit(info); - } - - auto txAndResult = SignSendRawTransaction(obj, keyChange, testmode); - tx_ = txAndResult.first; - set_result(txAndResult.second); - return true; - } - /** - * END SCENARIO #2 - */ - - - - /** - * SCENARIO #3 - * - * zaddr -> taddrs - * -> zaddrs - * - * Send to zaddrs by chaining JoinSplits together and immediately consuming any change - * Send to taddrs by creating dummy z outputs and accumulating value in a change note - * which is used to set vpub_new in the last chained joinsplit. - */ - UniValue obj(UniValue::VOBJ); - CAmount jsChange = 0; // this is updated after each joinsplit - int changeOutputIndex = -1; // this is updated after each joinsplit if jsChange > 0 - bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit - CAmount vpubNewTarget = minersFee; - if (txValues.t_outputs_total > 0) { - add_taddr_outputs_to_tx(); - vpubNewTarget += txValues.t_outputs_total; - } - - // Keep track of treestate within this transaction - // The SaltedTxidHasher is fine to use here; it salts the map keys automatically - // with randomness generated on construction. - boost::unordered_map intermediates; - std::vector previousCommitments; - - while (!vpubNewProcessed) { - AsyncJoinSplitInfo info; - info.vpub_old = 0; - info.vpub_new = 0; - - CAmount jsInputValue = 0; - uint256 jsAnchor; - std::vector> witnesses; - - JSDescription prevJoinSplit; - - // Keep track of previous JoinSplit and its commitments - if (tx_.vJoinSplit.size() > 0) { - prevJoinSplit = tx_.vJoinSplit.back(); - } - - // If there is no change, the chain has terminated so we can reset the tracked treestate. - if (jsChange==0 && tx_.vJoinSplit.size() > 0) { - intermediates.clear(); - previousCommitments.clear(); - } - - // - // Consume change as the first input of the JoinSplit. - // - if (jsChange > 0) { - LOCK2(cs_main, pwalletMain->cs_wallet); - - // Update tree state with previous joinsplit - SproutMerkleTree tree; - auto it = intermediates.find(prevJoinSplit.anchor); - if (it != intermediates.end()) { - tree = it->second; - } else if (!pcoinsTip->GetSproutAnchorAt(prevJoinSplit.anchor, tree)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor"); - } - - assert(changeOutputIndex != -1); - std::optional changeWitness; - int n = 0; - for (const uint256& commitment : prevJoinSplit.commitments) { - tree.append(commitment); - previousCommitments.push_back(commitment); - if (!changeWitness && changeOutputIndex == n++) { - changeWitness = tree.witness(); - } else if (changeWitness) { - changeWitness.value().append(commitment); - } - } - if (changeWitness) { - witnesses.push_back(changeWitness); - } - jsAnchor = tree.root(); - intermediates.insert(std::make_pair(tree.root(), tree)); // chained js are interstitial (found in between block boundaries) - - // Decrypt the change note's ciphertext to retrieve some data we need - // FIXME: make sure this .value() call is safe - auto sk = std::visit(GetSproutKeyForPaymentAddress(pwalletMain), frompaymentaddress_).value(); - ZCNoteDecryption decryptor(sk.receiving_key()); - auto hSig = ZCJoinSplit::h_sig( - prevJoinSplit.randomSeed, - prevJoinSplit.nullifiers, - tx_.joinSplitPubKey); - try { - SproutNotePlaintext plaintext = SproutNotePlaintext::decrypt( - decryptor, - prevJoinSplit.ciphertexts[changeOutputIndex], - prevJoinSplit.ephemeralKey, - hSig, - (unsigned char) changeOutputIndex); - - SproutNote note = plaintext.note(std::get(frompaymentaddress_)); - info.notes.push_back(note); - - jsInputValue += plaintext.value(); - - LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n", - getId(), - FormatMoney(plaintext.value()) - ); - - } catch (const std::exception& e) { - throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what())); - } - } - - - // - // Consume spendable non-change notes - // - std::vector vInputNotes; std::vector vOutPoints; - std::vector> vInputWitnesses; + for (const auto& t : spendable.sproutNoteEntries) { + vOutPoints.push_back(t.jsop); + } + + // inputAnchor is not needed by builder_.AddSproutInput as it is for Sapling. uint256 inputAnchor; - int numInputsNeeded = (jsChange>0) ? 1 : 0; - while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) { - SendManyInputJSOP t = zInputsDeque.front(); - JSOutPoint jso = t.point; - SproutNote note = t.note; - CAmount noteFunds = t.amount; - zInputsDeque.pop_front(); + pwalletMain->GetSproutNoteWitnesses(vOutPoints, vSproutWitnesses, inputAnchor); + } - WitnessAnchorData wad = jsopWitnessAnchorMap[ jso.ToString() ]; - vInputWitnesses.push_back(wad.witness); - if (inputAnchor.IsNull()) { - inputAnchor = wad.anchor; - } else if (inputAnchor != wad.anchor) { - throw JSONRPCError(RPC_WALLET_ERROR, "Selected input notes do not share the same anchor"); - } + // Add Sprout spends + for (int i = 0; i < spendable.sproutNoteEntries.size(); i++) { + const auto& t = spendable.sproutNoteEntries[i]; + libzcash::SproutSpendingKey sk; + assert(pwalletMain->GetSproutSpendingKey(t.address, sk)); - vOutPoints.push_back(jso); - vInputNotes.push_back(note); + builder_.AddSproutInput(sk, t.note, vSproutWitnesses[i].value()); - jsInputValue += noteFunds; - - int wtxHeight = -1; - int wtxDepth = -1; - { - LOCK2(cs_main, pwalletMain->cs_wallet); - const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash]; - // Zero-confirmation notes belong to transactions which have not yet been mined - if (mapBlockIndex.find(wtx.hashBlock) == mapBlockIndex.end()) { - throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); - } - wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight; - wtxDepth = wtx.GetDepthInMainChain(); - } - LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, height=%d, confirmations=%d)\n", - getId(), - jso.hash.ToString().substr(0, 10), - jso.js, - int(jso.n), // uint8_t - FormatMoney(noteFunds), - wtxHeight, - wtxDepth - ); - } - - // Add history of previous commitments to witness - if (vInputNotes.size() > 0) { - - if (vInputWitnesses.size()==0) { - throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment"); - } - - for (auto & optionalWitness : vInputWitnesses) { - if (!optionalWitness) { - throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null"); - } - SproutWitness w = *optionalWitness; // could use .value(); - if (jsChange > 0) { - for (const uint256& commitment : previousCommitments) { - w.append(commitment); - } - if (jsAnchor != w.root()) { - throw JSONRPCError(RPC_WALLET_ERROR, "Witness for spendable note does not have same anchor as change input"); - } - } - witnesses.push_back(w); - } - - // The jsAnchor is null if this JoinSplit is at the start of a new chain - if (jsAnchor.IsNull()) { - jsAnchor = inputAnchor; - } - - // Add spendable notes as inputs - std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes)); - } - - // Find recipient to transfer funds to - std::string address, hexMemo; - CAmount value = 0; - if (zOutputsDeque.size() > 0) { - SendManyRecipient smr = zOutputsDeque.front(); - address = smr.address; - value = smr.amount; - hexMemo = smr.memo; - zOutputsDeque.pop_front(); - } - - // Reset change - jsChange = 0; - CAmount outAmount = value; - - // Set vpub_new in the last joinsplit (when there are no more notes to spend or zaddr outputs to satisfy) - if (zOutputsDeque.size() == 0 && zInputsDeque.size() == 0) { - assert(!vpubNewProcessed); - if (jsInputValue < vpubNewTarget) { - throw JSONRPCError(RPC_WALLET_ERROR, - strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr outputs %s)", - FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(txValues.t_outputs_total))); - } - outAmount += vpubNewTarget; - info.vpub_new += vpubNewTarget; // funds flowing back to public pool - vpubNewProcessed = true; - jsChange = jsInputValue - outAmount; - assert(jsChange >= 0); - } - else { - // This is not the last joinsplit, so compute change and any amount still due to the recipient - if (jsInputValue > outAmount) { - jsChange = jsInputValue - outAmount; - } else if (outAmount > jsInputValue) { - // Any amount due is owed to the recipient. Let the miners fee get paid first. - CAmount due = outAmount - jsInputValue; - SendManyRecipient r(address, due, hexMemo); - zOutputsDeque.push_front(r); - - // reduce the amount being sent right now to the value of all inputs - value = jsInputValue; - } - } - - // create output for recipient - if (address.empty()) { - assert(value==0); - info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new - } else { - std::optional pa = keyIO.DecodePaymentAddress(address); - if (pa.has_value()) { - // If we are here, we know we have no Sapling outputs. - JSOutput jso = JSOutput(std::get(pa.value()), value); - if (hexMemo.size() > 0) { - jso.memo = get_memo_from_hex_string(hexMemo); - } - info.vjsout.push_back(jso); - } - } - - // create output for any change - if (jsChange>0) { - info.vjsout.push_back(JSOutput(std::get(frompaymentaddress_), jsChange)); - - LogPrint("zrpcunsafe", "%s: generating note for change (amount=%s)\n", - getId(), - FormatMoney(jsChange) - ); - } - - obj = perform_joinsplit(info, witnesses, jsAnchor); - - if (jsChange > 0) { - changeOutputIndex = find_output(obj, 1); + sum += t.note.value(); + if (sum >= txValues.targetAmount) { + break; } } - // Sanity check in case changes to code block above exits loop by invoking 'break' - assert(zInputsDeque.size() == 0); - assert(zOutputsDeque.size() == 0); - assert(vpubNewProcessed); + // Build the transaction + auto buildResult = builder_.Build(); + auto tx = buildResult.GetTxOrThrow(); - auto txAndResult = SignSendRawTransaction(obj, std::nullopt, testmode); - tx_ = txAndResult.first; - set_result(txAndResult.second); - return true; + UniValue sendResult = SendTransaction(tx, keyChange, testmode); + set_result(sendResult); + + return tx.GetHash(); } +SpendableInputs AsyncRPCOperation_sendmany::FindSpendableInputs(bool allowTransparentCoinbase) { + SpendableInputs unspent; -bool AsyncRPCOperation_sendmany::find_utxos(bool fAcceptCoinbase, TxValues& txValues) { - std::set destinations; - if (!useanyutxo_) { - destinations.insert(fromtaddr_); + auto filters = std::visit(match { + [&](const PaymentAddress& addr) { + return std::visit(match { + [&](const CKeyID& keyId) { + std::optional> t_filter = std::set({keyId}); + return std::make_pair(t_filter, AddrSet::Empty()); + }, + [&](const CScriptID& scriptId) { + std::optional> t_filter = std::set({scriptId}); + return std::make_pair(t_filter, AddrSet::Empty()); + }, + [&](const auto& other) { + std::optional> t_filter = std::nullopt; + return std::make_pair(t_filter, AddrSet::ForPaymentAddresses({addr})); + } + }, addr); + }, + [&](const FromAnyTaddr& taddr) { + std::optional> t_filter = std::set({}); + return std::make_pair(t_filter, AddrSet::Empty()); + } + }, paymentSource_); + + if (filters.first.has_value()) { + pwalletMain->AvailableCoins( + unspent.utxos, + false, // fOnlyConfirmed + nullptr, // coinControl + true, // fIncludeZeroValue + allowTransparentCoinbase, // fIncludeCoinBase + true, // fOnlySpendable + mindepth_, // nMinDepth + &filters.first.value()); // onlyFilterByDests } - pwalletMain->AvailableCoins( - t_inputs_, - false, // fOnlyConfirmed - nullptr, // coinControl - true, // fIncludeZeroValue - fAcceptCoinbase, // fIncludeCoinBase - true, // fOnlySpendable - mindepth_, // nMinDepth - &destinations); // onlyFilterByDests - if (t_inputs_.empty()) return false; - // sort in ascending order, so smaller utxos appear first - std::sort(t_inputs_.begin(), t_inputs_.end(), [](const COutput& i, const COutput& j) -> bool { - return i.Value() < j.Value(); - }); + pwalletMain->GetFilteredNotes( + unspent.sproutNoteEntries, + unspent.saplingNoteEntries, + filters.second, + mindepth_); - // Load transparent inputs - load_inputs(txValues); - - return t_inputs_.size() > 0; + return unspent; } -bool AsyncRPCOperation_sendmany::load_inputs(TxValues& txValues) { - // If from address is a taddr, select UTXOs to spend - CAmount selectedUTXOAmount = 0; - // Get dust threshold - CKey secret; - secret.MakeNewKey(true); - CScript scriptPubKey = GetScriptForDestination(secret.GetPubKey().GetID()); - CTxOut out(CAmount(1), scriptPubKey); - CAmount dustThreshold = out.GetDustThreshold(minRelayTxFee); - CAmount dustChange = -1; +bool SpendableInputs::LimitToAmount(CAmount amount) { + // select Sprout notes for spending first + std::sort(sproutNoteEntries.begin(), sproutNoteEntries.end(), + [](SproutNoteEntry i, SproutNoteEntry j) -> bool { + return i.note.value() > j.note.value(); + }); + auto sproutIt = sproutNoteEntries.begin(); + while (sproutIt != sproutNoteEntries.end() && amount > 0) { + amount -= sproutIt->note.value(); + ++sproutIt; + } + sproutNoteEntries.erase(sproutIt, sproutNoteEntries.end()); - std::vector selectedTInputs; - for (const COutput& out : t_inputs_) { + // next select transparent utxos + std::sort(utxos.begin(), utxos.end(), + [](COutput i, COutput j) -> bool { + return i.Value() > j.Value(); + }); + auto utxoIt = utxos.begin(); + while (utxoIt != utxos.end() && amount > 0) { + amount -= utxoIt->Value(); + ++utxoIt; + } + utxos.erase(utxoIt, utxos.end()); + + // finally select Sapling outputs + std::sort(saplingNoteEntries.begin(), saplingNoteEntries.end(), + [](SaplingNoteEntry i, SaplingNoteEntry j) -> bool { + return i.note.value() > j.note.value(); + }); + auto saplingIt = saplingNoteEntries.begin(); + while (saplingIt != saplingNoteEntries.end() && amount > 0) { + amount -= saplingIt->note.value(); + ++saplingIt; + } + saplingNoteEntries.erase(saplingIt, saplingNoteEntries.end()); + + return (amount <= 0); +} + +bool SpendableInputs::HasTransparentCoinbase() const { + for (const auto& out : utxos) { if (out.fIsCoinbase) { - txValues.selectedUTXOCoinbase = true; - } - selectedUTXOAmount += out.Value(); - selectedTInputs.emplace_back(out); - if (selectedUTXOAmount >= txValues.targetAmount) { - // Select another utxo if there is change less than the dust threshold. - dustChange = selectedUTXOAmount - txValues.targetAmount; - if (dustChange == 0 || dustChange >= dustThreshold) { - break; - } + return true; } } - t_inputs_ = selectedTInputs; - txValues.t_inputs_total = selectedUTXOAmount; - - if (txValues.t_inputs_total < txValues.targetAmount) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, - strprintf("Insufficient transparent funds, have %s, need %s", - FormatMoney(txValues.t_inputs_total), FormatMoney(txValues.targetAmount))); - } - - // If there is transparent change, is it valid or is it dust? - if (dustChange < dustThreshold && dustChange != 0) { - throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, - strprintf("Insufficient transparent funds, have %s, need %s more to avoid creating invalid change output %s (dust threshold is %s)", - FormatMoney(txValues.t_inputs_total), FormatMoney(dustThreshold - dustChange), FormatMoney(dustChange), FormatMoney(dustThreshold))); - } - - // update the transaction with these inputs - if (isUsingBuilder_) { - for (const auto& out : t_inputs_) { - const CTxOut& txOut = out.tx->vout[out.i]; - builder_.AddTransparentInput(COutPoint(out.tx->GetHash(), out.i), txOut.scriptPubKey, txOut.nValue); - } - } else { - CMutableTransaction rawTx(tx_); - for (const auto& out : t_inputs_) { - rawTx.vin.push_back(CTxIn(COutPoint(out.tx->GetHash(), out.i))); - } - tx_ = CTransaction(rawTx); - } - return true; + return false; } -bool AsyncRPCOperation_sendmany::find_unspent_notes() { - std::vector sproutEntries; - std::vector saplingEntries; - // TODO: move this to the caller - auto zaddr = KeyIO(Params()).DecodePaymentAddress(fromaddress_); - std::optional noteFilter = std::nullopt; - if (zaddr.has_value()) { - noteFilter = AddrSet::ForPaymentAddresses(std::vector({zaddr.value()})); - } - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, mindepth_); - - // If using the TransactionBuilder, we only want Sapling notes. - // If not using it, we only want Sprout notes. - // TODO: Refactor `GetFilteredNotes()` so we only fetch what we need. - if (isUsingBuilder_) { - sproutEntries.clear(); - } else { - saplingEntries.clear(); - } - - for (SproutNoteEntry & entry : sproutEntries) { - z_sprout_inputs_.push_back(SendManyInputJSOP(entry.jsop, entry.note, CAmount(entry.note.value()))); +void SpendableInputs::LogInputs(const AsyncRPCOperationId& id) const { + for (const auto& entry : sproutNoteEntries) { std::string data(entry.memo.begin(), entry.memo.end()); - LogPrint("zrpcunsafe", "%s: found unspent Sprout note (txid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, memo=%s)\n", - getId(), + LogPrint("zrpcunsafe", "%s: found unspent Sprout note (opid=%s, vJoinSplit=%d, jsoutindex=%d, amount=%s, memo=%s)\n", + id, entry.jsop.hash.ToString().substr(0, 10), entry.jsop.js, int(entry.jsop.n), // uint8_t @@ -984,257 +598,15 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() { ); } - for (auto entry : saplingEntries) { - z_sapling_inputs_.push_back(entry); + for (const auto& entry : saplingNoteEntries) { std::string data(entry.memo.begin(), entry.memo.end()); - LogPrint("zrpcunsafe", "%s: found unspent Sapling note (txid=%s, vShieldedSpend=%d, amount=%s, memo=%s)\n", - getId(), + LogPrint("zrpcunsafe", "%s: found unspent Sapling note (opid=%s, vShieldedSpend=%d, amount=%s, memo=%s)\n", + id, entry.op.hash.ToString().substr(0, 10), entry.op.n, FormatMoney(entry.note.value()), HexStr(data).substr(0, 10)); } - - if (z_sprout_inputs_.empty() && z_sapling_inputs_.empty()) { - return false; - } - - // sort in descending order, so big notes appear first - std::sort(z_sprout_inputs_.begin(), z_sprout_inputs_.end(), - [](SendManyInputJSOP i, SendManyInputJSOP j) -> bool { - return i.amount > j.amount; - }); - std::sort(z_sapling_inputs_.begin(), z_sapling_inputs_.end(), - [](SaplingNoteEntry i, SaplingNoteEntry j) -> bool { - return i.note.value() > j.note.value(); - }); - - return true; -} - -UniValue AsyncRPCOperation_sendmany::perform_joinsplit(AsyncJoinSplitInfo & info) { - std::vector> witnesses; - uint256 anchor; - { - LOCK(cs_main); - anchor = pcoinsTip->GetBestAnchor(SPROUT); // As there are no inputs, ask the wallet for the best anchor - } - return perform_joinsplit(info, witnesses, anchor); -} - - -UniValue AsyncRPCOperation_sendmany::perform_joinsplit(AsyncJoinSplitInfo & info, std::vector & outPoints) { - std::vector> witnesses; - uint256 anchor; - { - LOCK(cs_main); - pwalletMain->GetSproutNoteWitnesses(outPoints, witnesses, anchor); - } - return perform_joinsplit(info, witnesses, anchor); -} - -UniValue AsyncRPCOperation_sendmany::perform_joinsplit( - AsyncJoinSplitInfo & info, - std::vector> witnesses, - uint256 anchor) -{ - if (anchor.IsNull()) { - throw std::runtime_error("anchor is null"); - } - - if (!(witnesses.size() == info.notes.size())) { - throw runtime_error("number of notes and witnesses do not match"); - } - - for (size_t i = 0; i < witnesses.size(); i++) { - if (!witnesses[i]) { - throw runtime_error("joinsplit input could not be found in tree"); - } - auto sk = std::visit(GetSproutKeyForPaymentAddress(pwalletMain), frompaymentaddress_).value(); - info.vjsin.push_back(JSInput(*witnesses[i], info.notes[i], sk)); - } - - // Make sure there are two inputs and two outputs - while (info.vjsin.size() < ZC_NUM_JS_INPUTS) { - info.vjsin.push_back(JSInput()); - } - - while (info.vjsout.size() < ZC_NUM_JS_OUTPUTS) { - info.vjsout.push_back(JSOutput()); - } - - if (info.vjsout.size() != ZC_NUM_JS_INPUTS || info.vjsin.size() != ZC_NUM_JS_OUTPUTS) { - throw runtime_error("unsupported joinsplit input/output counts"); - } - - CMutableTransaction mtx(tx_); - - LogPrint("zrpcunsafe", "%s: creating joinsplit at index %d (vpub_old=%s, vpub_new=%s, in[0]=%s, in[1]=%s, out[0]=%s, out[1]=%s)\n", - getId(), - tx_.vJoinSplit.size(), - FormatMoney(info.vpub_old), FormatMoney(info.vpub_new), - FormatMoney(info.vjsin[0].note.value()), FormatMoney(info.vjsin[1].note.value()), - FormatMoney(info.vjsout[0].value), FormatMoney(info.vjsout[1].value) - ); - - // Generate the proof, this can take over a minute. - std::array inputs - {info.vjsin[0], info.vjsin[1]}; - std::array outputs - {info.vjsout[0], info.vjsout[1]}; - std::array inputMap; - std::array outputMap; - - uint256 esk; // payment disclosure - secret - - assert(mtx.fOverwintered && (mtx.nVersion >= SAPLING_TX_VERSION)); - JSDescription jsdesc = JSDescriptionInfo( - joinSplitPubKey_, - anchor, - inputs, - outputs, - info.vpub_old, - info.vpub_new - ).BuildRandomized( - inputMap, - outputMap, - !this->testmode, - &esk); // parameter expects pointer to esk, so pass in address - { - auto verifier = ProofVerifier::Strict(); - if (!(verifier.VerifySprout(jsdesc, joinSplitPubKey_))) { - throw std::runtime_error("error verifying joinsplit"); - } - } - - mtx.vJoinSplit.push_back(jsdesc); - - // Empty output script. - CScript scriptCode; - CTransaction signTx(mtx); - uint256 dataToBeSigned = SignatureHash(scriptCode, signTx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId_); - - // Add the signature - if (!ed25519_sign( - &joinSplitPrivKey_, - dataToBeSigned.begin(), 32, - &mtx.joinSplitSig)) - { - throw std::runtime_error("ed25519_sign failed"); - } - - // Sanity check - if (!ed25519_verify( - &mtx.joinSplitPubKey, - &mtx.joinSplitSig, - dataToBeSigned.begin(), 32)) - { - throw std::runtime_error("ed25519_verify failed"); - } - - CTransaction rawTx(mtx); - tx_ = rawTx; - - CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << rawTx; - - std::string encryptedNote1; - std::string encryptedNote2; - { - CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION); - ss2 << ((unsigned char) 0x00); - ss2 << jsdesc.ephemeralKey; - ss2 << jsdesc.ciphertexts[0]; - ss2 << ZCJoinSplit::h_sig(jsdesc.randomSeed, jsdesc.nullifiers, joinSplitPubKey_); - - encryptedNote1 = HexStr(ss2.begin(), ss2.end()); - } - { - CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION); - ss2 << ((unsigned char) 0x01); - ss2 << jsdesc.ephemeralKey; - ss2 << jsdesc.ciphertexts[1]; - ss2 << ZCJoinSplit::h_sig(jsdesc.randomSeed, jsdesc.nullifiers, joinSplitPubKey_); - - encryptedNote2 = HexStr(ss2.begin(), ss2.end()); - } - - UniValue arrInputMap(UniValue::VARR); - UniValue arrOutputMap(UniValue::VARR); - for (size_t i = 0; i < ZC_NUM_JS_INPUTS; i++) { - arrInputMap.push_back(static_cast(inputMap[i])); - } - for (size_t i = 0; i < ZC_NUM_JS_OUTPUTS; i++) { - arrOutputMap.push_back(static_cast(outputMap[i])); - } - - KeyIO keyIO(Params()); - - // !!! Payment disclosure START - size_t js_index = tx_.vJoinSplit.size() - 1; - uint256 placeholder; - for (int i = 0; i < ZC_NUM_JS_OUTPUTS; i++) { - uint8_t mapped_index = outputMap[i]; - // placeholder for txid will be filled in later when tx has been finalized and signed. - PaymentDisclosureKey pdKey = {placeholder, js_index, mapped_index}; - JSOutput output = outputs[mapped_index]; - libzcash::SproutPaymentAddress zaddr = output.addr; // randomized output - PaymentDisclosureInfo pdInfo = {PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL, esk, joinSplitPrivKey_, zaddr}; - paymentDisclosureData_.push_back(PaymentDisclosureKeyInfo(pdKey, pdInfo)); - - LogPrint("paymentdisclosure", "%s: Payment Disclosure: js=%d, n=%d, zaddr=%s\n", getId(), js_index, int(mapped_index), keyIO.EncodePaymentAddress(zaddr)); - } - // !!! Payment disclosure END - - UniValue obj(UniValue::VOBJ); - obj.pushKV("encryptednote1", encryptedNote1); - obj.pushKV("encryptednote2", encryptedNote2); - obj.pushKV("rawtxn", HexStr(ss.begin(), ss.end())); - obj.pushKV("inputmap", arrInputMap); - obj.pushKV("outputmap", arrOutputMap); - return obj; -} - -void AsyncRPCOperation_sendmany::add_taddr_outputs_to_tx() { - - CMutableTransaction rawTx(tx_); - - KeyIO keyIO(Params()); - - for (SendManyRecipient & r : t_outputs_) { - std::string outputAddress = r.address; - CAmount nAmount = r.amount; - - CTxDestination address = keyIO.DecodeDestination(outputAddress); - if (!IsValidDestination(address)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid output address, not a valid taddr."); - } - - CScript scriptPubKey = GetScriptForDestination(address); - - CTxOut out(nAmount, scriptPubKey); - rawTx.vout.push_back(out); - } - - tx_ = CTransaction(rawTx); -} - -void AsyncRPCOperation_sendmany::add_taddr_change_output_to_tx(CReserveKey& keyChange, CAmount amount) { - - LOCK2(cs_main, pwalletMain->cs_wallet); - - EnsureWalletIsUnlocked(); - CPubKey vchPubKey; - bool ret = keyChange.GetReservedKey(vchPubKey); - if (!ret) { - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Could not generate a taddr to use as a change address"); // should never fail, as we just unlocked - } - CScript scriptPubKey = GetScriptForDestination(vchPubKey.GetID()); - CTxOut out(amount, scriptPubKey); - - CMutableTransaction rawTx(tx_); - rawTx.vout.push_back(out); - tx_ = CTransaction(rawTx); } std::array AsyncRPCOperation_sendmany::get_memo_from_hex_string(std::string s) { diff --git a/src/wallet/asyncrpcoperation_sendmany.h b/src/wallet/asyncrpcoperation_sendmany.h index 6ef704cd9..d5b3ec532 100644 --- a/src/wallet/asyncrpcoperation_sendmany.h +++ b/src/wallet/asyncrpcoperation_sendmany.h @@ -24,52 +24,72 @@ #include using namespace libzcash; -class TxValues; + +class FromAnyTaddr { +public: + friend bool operator==(const FromAnyTaddr &a, const FromAnyTaddr &b) { return true; } +}; + +typedef std::variant PaymentSource; class SendManyRecipient { public: - std::string address; + PaymentAddress address; CAmount amount; - std::string memo; + std::optional memo; - SendManyRecipient(std::string address_, CAmount amount_, std::string memo_) : + SendManyRecipient(PaymentAddress address_, CAmount amount_, std::optional memo_) : address(address_), amount(amount_), memo(memo_) {} }; -class SendManyInputJSOP { +class SpendableInputs { public: - JSOutPoint point; - SproutNote note; - CAmount amount; + std::vector utxos; + std::vector sproutNoteEntries; + std::vector saplingNoteEntries; - SendManyInputJSOP(JSOutPoint point_, SproutNote note_, CAmount amount_) : - point(point_), note(note_), amount(amount_) {} -}; + /** + * Selectively discard notes that are not required to obtain the desired + * amount. Returns `false` if the available inputs do not add up to the + * desired amount. + */ + bool LimitToAmount(CAmount amount); -// Package of info which is passed to perform_joinsplit methods. -struct AsyncJoinSplitInfo -{ - std::vector vjsin; - std::vector vjsout; - std::vector notes; - CAmount vpub_old = 0; - CAmount vpub_new = 0; -}; + /** + * Compute the total ZEC amount of spendable inputs. + */ + CAmount Total() const { + CAmount result = 0; + for (const auto& t : utxos) { + result += t.Value(); + } + for (const auto& t : sproutNoteEntries) { + result += t.note.value(); + } + for (const auto& t : saplingNoteEntries) { + result += t.note.value(); + } + return result; + } -// A struct to help us track the witness and anchor for a given JSOutPoint -struct WitnessAnchorData { - std::optional witness; - uint256 anchor; + /** + * Return whether or not the set of selected UTXOs contains + * coinbase outputs. + */ + bool HasTransparentCoinbase() const; + + /** + * List spendable inputs in zrpcunsafe log entries. + */ + void LogInputs(const AsyncRPCOperationId& id) const; }; class AsyncRPCOperation_sendmany : public AsyncRPCOperation { public: AsyncRPCOperation_sendmany( - std::optional builder, - CMutableTransaction contextualTx, - std::string fromAddress, - std::vector tOutputs, - std::vector zOutputs, + TransactionBuilder builder, + PaymentSource paymentSource, + std::vector recipients, int minDepth, CAmount fee = DEFAULT_FEE, UniValue contextInfo = NullUniValue); @@ -85,64 +105,26 @@ public: virtual UniValue getStatus() const; - bool testmode = false; // Set to true to disable sending txs and generating proofs - - bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database. + bool testmode{false}; // Set to true to disable sending txs and generating proofs private: friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing + TransactionBuilder builder_; + PaymentSource paymentSource_; + std::vector recipients_; + int mindepth_{1}; + CAmount fee_; UniValue contextinfo_; // optional data to include in return value from getStatus() - bool isUsingBuilder_{false}; // Indicates that no Sprout addresses are involved - uint32_t consensusBranchId_; - CAmount fee_; - int mindepth_{1}; - std::string fromaddress_; - bool useanyutxo_{false}; bool isfromtaddr_{false}; bool isfromzaddr_{false}; - CTxDestination fromtaddr_; - PaymentAddress frompaymentaddress_; - Ed25519VerificationKey joinSplitPubKey_; - Ed25519SigningKey joinSplitPrivKey_; + SpendableInputs FindSpendableInputs(bool fAcceptCoinbase); - // The key is the result string from calling JSOutPoint::ToString() - std::unordered_map jsopWitnessAnchorMap; - - std::vector t_outputs_; - std::vector z_outputs_; - std::vector t_inputs_; - std::vector z_sprout_inputs_; - std::vector z_sapling_inputs_; - - TransactionBuilder builder_; - CTransaction tx_; - - void add_taddr_change_output_to_tx(CReserveKey& keyChange, CAmount amount); - void add_taddr_outputs_to_tx(); - bool find_unspent_notes(); - bool find_utxos(bool fAcceptCoinbase, TxValues& txValues); - // Load transparent inputs into the transaction or the transactionBuilder (in case of have it) - bool load_inputs(TxValues& txValues); std::array get_memo_from_hex_string(std::string s); - bool main_impl(); - // JoinSplit without any input notes to spend - UniValue perform_joinsplit(AsyncJoinSplitInfo &); - - // JoinSplit with input notes to spend (JSOutPoints)) - UniValue perform_joinsplit(AsyncJoinSplitInfo &, std::vector & ); - - // JoinSplit where you have the witnesses and anchor - UniValue perform_joinsplit( - AsyncJoinSplitInfo & info, - std::vector> witnesses, - uint256 anchor); - - // payment disclosure! - std::vector paymentDisclosureData_; + uint256 main_impl(); }; @@ -153,52 +135,14 @@ public: TEST_FRIEND_AsyncRPCOperation_sendmany(std::shared_ptr ptr) : delegate(ptr) {} - CTransaction getTx() { - return delegate->tx_; - } - - void setTx(CTransaction tx) { - delegate->tx_ = tx; - } - - // Delegated methods - - void add_taddr_change_output_to_tx(CReserveKey& keyChange, CAmount amount) { - delegate->add_taddr_change_output_to_tx(keyChange, amount); - } - - void add_taddr_outputs_to_tx() { - delegate->add_taddr_outputs_to_tx(); - } - - bool find_unspent_notes() { - return delegate->find_unspent_notes(); - } - std::array get_memo_from_hex_string(std::string s) { return delegate->get_memo_from_hex_string(s); } - bool main_impl() { + uint256 main_impl() { return delegate->main_impl(); } - UniValue perform_joinsplit(AsyncJoinSplitInfo &info) { - return delegate->perform_joinsplit(info); - } - - UniValue perform_joinsplit(AsyncJoinSplitInfo &info, std::vector &v ) { - return delegate->perform_joinsplit(info, v); - } - - UniValue perform_joinsplit( - AsyncJoinSplitInfo & info, - std::vector> witnesses, - uint256 anchor) - { - return delegate->perform_joinsplit(info, witnesses, anchor); - } - void set_state(OperationStatus state) { delegate->state_.store(state); } diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3c2b2b560..bb5bb6f62 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3667,7 +3667,6 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO return ret; } - // JSDescription size depends on the transaction version #define V3_JS_DESCRIPTION_SIZE (GetSerializeSize(JSDescription(), SER_NETWORK, (OVERWINTER_TX_VERSION | (1 << 31)))) // Here we define the maximum number of zaddr outputs that can be included in a transaction. @@ -3680,6 +3679,82 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO #define CTXIN_SPEND_DUST_SIZE 148 #define CTXOUT_REGULAR_SIZE 34 +size_t EstimateTxSize( + const PaymentSource& paymentSource, + const std::vector& recipients) { + int nextBlockHeight = chainActive.Height() + 1; + CMutableTransaction mtx; + mtx.fOverwintered = true; + mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; + mtx.nVersion = SAPLING_TX_VERSION; + + bool fromTaddr = std::visit(match { + [&](const FromAnyTaddr& any) { + return true; + }, + [&](const PaymentAddress& addr) { + return std::visit(match { + [&](const CKeyID& keyId) { + return true; + }, + [&](const CScriptID& scriptId) { + return true; + }, + [&](const libzcash::SproutPaymentAddress& addr) { + return false; + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + return false; + }, + [&](const libzcash::UnifiedAddress& addr) { + // TODO UA + throw JSONRPCError(RPC_INVALID_PARAMETER, "Unified addresses not yet supported."); + return false; // compiler is dumb + } + }, addr); + } + }, paymentSource); + + // As a sanity check, estimate and verify that the size of the transaction will be valid. + // Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid. + size_t txsize = 0; + size_t taddrRecipientCount = 0; + for (const SendManyRecipient& recipient : recipients) { + std::visit(match { + [&](const CKeyID&) { + taddrRecipientCount += 1; + }, + [&](const CScriptID&) { + taddrRecipientCount += 1; + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + mtx.vShieldedOutput.push_back(OutputDescription()); + }, + [&](const libzcash::SproutPaymentAddress& addr) { + JSDescription jsdesc; + if (mtx.fOverwintered && (mtx.nVersion >= SAPLING_TX_VERSION)) { + jsdesc.proof = GrothProof(); + } + mtx.vJoinSplit.push_back(jsdesc); + }, + [&](const libzcash::UnifiedAddress& ua) { + // FIXME + throw JSONRPCError(RPC_INVALID_PARAMETER, "Unified addresses not yet supported."); + } + }, recipient.address); + } + + CTransaction tx(mtx); + txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion); + if (fromTaddr) { + txsize += CTXIN_SPEND_DUST_SIZE; + txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change + } + txsize += CTXOUT_REGULAR_SIZE * taddrRecipientCount; + + return txsize; +} + UniValue z_sendmany(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) @@ -3720,15 +3795,16 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); ThrowIfInitialBlockDownload(); + KeyIO keyIO(Params()); // Check that the from address is valid. auto fromaddress = params[0].get_str(); + PaymentSource paymentSource; bool fromTaddr = false; - bool fromSapling = false; bool fromSprout = false; - KeyIO keyIO(Params()); + bool fromSapling = false; if (fromaddress == "ANY_TADDR") { - fromTaddr = true; + paymentSource = FromAnyTaddr(); } else { auto addr = keyIO.DecodePaymentAddress(fromaddress); if (!addr.has_value()) { @@ -3737,13 +3813,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) "Invalid from address: should be a taddr, a zaddr, or the string 'ANY_TADDR'."); } - // This is a sanity check; the actual checks will come later when the spend is attempted. - if (!std::visit(HaveSpendingKeyForPaymentAddress(pwalletMain), addr.value())) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - "Invalid from address: does not belong to this node, spending key not found."); - } - // Remember what sort of address this is std::visit(match { [&](const CKeyID&) { @@ -3764,190 +3833,80 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) "Invalid from address: unified addresses are not yet supported."); } }, addr.value()); + + paymentSource = addr.value(); } UniValue outputs = params[1].get_array(); - if (outputs.size()==0) + if (outputs.size() == 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty."); + } - // Keep track of addresses to spot duplicates - set setAddress; - - // Track whether we see any Sprout addresses - bool noSproutAddrs = !fromSprout; - - // Recipients - std::vector taddrRecipients; - std::vector zaddrRecipients; + std::vector recipients; CAmount nTotalOut = 0; - - bool containsSproutOutput = false; - bool containsSaplingOutput = false; - for (const UniValue& o : outputs.getValues()) { if (!o.isObject()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object"); // sanity check, report error if unknown key-value pairs - for (const string& name_ : o.getKeys()) { - std::string s = name_; - if (s != "address" && s != "amount" && s!="memo") - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ")+s); + for (const std::string& s : o.getKeys()) { + if (s != "address" && s != "amount" && s != "memo") + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ") + s); } - string address = find_value(o, "address").get_str(); - - bool isZaddr = false; - auto toAddr = keyIO.DecodePaymentAddress(address); - if (toAddr.has_value()) { - bool toSprout = false; - bool toSapling = false; - std::visit(match { - [&](const CKeyID&) { }, - [&](const CScriptID&) { }, - [&](const libzcash::SaplingPaymentAddress& addr) { - isZaddr = true; - toSapling = true; - containsSaplingOutput = true; - }, - [&](const libzcash::SproutPaymentAddress& addr) { - isZaddr = true; - toSprout = true; - containsSproutOutput = true; - noSproutAddrs = false; - }, - [&](const libzcash::UnifiedAddress& ua) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - "Invalid recipient address: unified addresses are not yet supported."); - } - }, toAddr.value()); - - // Sending to both Sprout and Sapling is currently unsupported using z_sendmany - if (containsSproutOutput && containsSaplingOutput) { + std::string addrStr = find_value(o, "address").get_str(); + auto addr = keyIO.DecodePaymentAddress(addrStr); + if (addr.has_value()) { + // TODO: If we want to continue to support sending to Sprout, we'll simply relax the + // restriction here to allow sprout->sprout; these transfers will not be forbidden + // by later code. + bool toSprout = std::holds_alternative(addr.value()); + if (toSprout) { // && !fromSprout) { throw JSONRPCError( RPC_INVALID_PARAMETER, - "Cannot send to both Sprout and Sapling addresses using z_sendmany"); - } - - // If sending between shielded addresses, they must be the same type - if ((fromSprout && toSapling) || (fromSapling && toSprout)) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "Cannot send between Sprout and Sapling addresses using z_sendmany"); - } - - int nextBlockHeight = chainActive.Height() + 1; - - if (fromTaddr && toSprout) { - const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY); - if (canopyActive) { - throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy"); - } + "Sending funds into the Sprout pool is not supported by z_sendmany"); } } else { - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ")+address ); + throw JSONRPCError( + RPC_INVALID_PARAMETER, + std::string("Invalid parameter, unknown address format: ") + addrStr); } - if (setAddress.count(address)) - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ")+address); - setAddress.insert(address); - UniValue memoValue = find_value(o, "memo"); - string memo; + std::optional memo; if (!memoValue.isNull()) { memo = memoValue.get_str(); - if (!isZaddr) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo cannot be used with a taddr. It can only be used with a zaddr."); - } else if (!IsHex(memo)) { + if (!std::visit(libzcash::HasShieldedRecipient(), addr.value())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, memos cannot be sent to transparent addresses."); + } else if (!IsHex(memo.value())) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format."); } - if (memo.length() > ZC_MEMO_SIZE*2) { + + if (memo.value().length() > ZC_MEMO_SIZE*2) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE )); } } UniValue av = find_value(o, "amount"); CAmount nAmount = AmountFromValue( av ); - if (nAmount < 0) + if (nAmount < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); - - if (isZaddr) { - zaddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) ); - } else { - taddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) ); } + recipients.push_back(SendManyRecipient(addr.value(), nAmount, memo) ); nTotalOut += nAmount; } - - int nextBlockHeight = chainActive.Height() + 1; - CMutableTransaction mtx; - mtx.fOverwintered = true; - mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; - mtx.nVersion = SAPLING_TX_VERSION; - unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING; - if (!Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) { - if (Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER)) { - mtx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID; - mtx.nVersion = OVERWINTER_TX_VERSION; - } else { - mtx.fOverwintered = false; - mtx.nVersion = 2; - } - - max_tx_size = MAX_TX_SIZE_BEFORE_SAPLING; - - // Check the number of zaddr outputs does not exceed the limit. - if (zaddrRecipients.size() > Z_SENDMANY_MAX_ZADDR_OUTPUTS_BEFORE_SAPLING) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, too many zaddr outputs"); - } - // If Sapling is not active, do not allow sending from or sending to Sapling addresses. - if (fromSapling || containsSaplingOutput) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated"); - } + if (recipients.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients"); } - // As a sanity check, estimate and verify that the size of the transaction will be valid. - // Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid. - size_t txsize = 0; - for (int i = 0; i < zaddrRecipients.size(); i++) { - auto address = zaddrRecipients[i].address; - auto decoded = keyIO.DecodePaymentAddress(address); - - if (decoded.has_value()) { - std::visit(match { - [&](const CKeyID&) { - // Handled elsewhere - }, - [&](const CScriptID&) { - // Handled elsewhere - }, - [&](const libzcash::SaplingPaymentAddress& addr) { - mtx.vShieldedOutput.push_back(OutputDescription()); - }, - [&](const libzcash::SproutPaymentAddress& addr) { - JSDescription jsdesc; - if (mtx.fOverwintered && (mtx.nVersion >= SAPLING_TX_VERSION)) { - jsdesc.proof = GrothProof(); - } - mtx.vJoinSplit.push_back(jsdesc); - }, - [&](const libzcash::UnifiedAddress& ua) { - // TODO UNIFIED - } - }, decoded.value()); - } - } - CTransaction tx(mtx); - txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion); - if (fromTaddr) { - txsize += CTXIN_SPEND_DUST_SIZE; - txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change - } - txsize += CTXOUT_REGULAR_SIZE * taddrRecipients.size(); - if (txsize > max_tx_size) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", max_tx_size )); + // Sanity check for transaction size + // TODO: move this to the builder? + auto txsize = EstimateTxSize(paymentSource, recipients); + if (txsize > MAX_TX_SIZE_AFTER_SAPLING) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", MAX_TX_SIZE_AFTER_SAPLING)); } // Minimum confirmations @@ -3960,9 +3919,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) } // Fee in Zatoshis, not currency format) - CAmount nFee = DEFAULT_FEE; - CAmount nDefaultFee = nFee; - + CAmount nFee = DEFAULT_FEE; if (params.size() > 3) { if (params[3].get_real() == 0.0) { nFee = 0; @@ -3971,16 +3928,20 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) } // Check that the user specified fee is not absurd. - // This allows amount=0 (and all amount < nDefaultFee) transactions to use the default network fee - // or anything less than nDefaultFee instead of being forced to use a custom fee and leak metadata - if (nTotalOut < nDefaultFee) { - if (nFee > nDefaultFee) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Small transaction amount %s has fee %s that is greater than the default fee %s", FormatMoney(nTotalOut), FormatMoney(nFee), FormatMoney(nDefaultFee))); + // This allows amount=0 (and all amount < DEFAULT_FEE) transactions to use the default network fee + // or anything less than DEFAULT_FEE instead of being forced to use a custom fee and leak metadata + if (nTotalOut < DEFAULT_FEE) { + if (nFee > DEFAULT_FEE) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Small transaction amount %s has fee %s that is greater than the default fee %s", FormatMoney(nTotalOut), FormatMoney(nFee), FormatMoney(DEFAULT_FEE))); } } else { // Check that the user specified fee is not absurd. if (nFee > nTotalOut) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Fee %s is greater than the sum of outputs %s and also greater than the default fee", FormatMoney(nFee), FormatMoney(nTotalOut))); + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Fee %s is greater than the sum of outputs %s and also greater than the default fee", FormatMoney(nFee), FormatMoney(nTotalOut))); } } } @@ -3993,33 +3954,14 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) o.pushKV("fee", std::stod(FormatMoney(nFee))); UniValue contextInfo = o; - if (!fromTaddr || !zaddrRecipients.empty()) { - // We have shielded inputs or outputs, and therefore cannot create - // transactions before Sapling activates. - if (!Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated"); - } - } - - // Builder (used if Sapling addresses are involved) - std::optional builder; - if (noSproutAddrs) { - builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain); - } - - // Contextual transaction we will build on - // (used if no Sapling addresses are involved) - CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction( - Params().GetConsensus(), nextBlockHeight, !noSproutAddrs); - bool isShielded = !fromTaddr || zaddrRecipients.size() > 0; - if (contextualTx.nVersion == 1 && isShielded) { - contextualTx.nVersion = 2; // Tx format should support vJoinSplits - } + int nextBlockHeight = chainActive.Height() + 1; + TransactionBuilder builder(Params().GetConsensus(), nextBlockHeight, pwalletMain); // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); - std::shared_ptr operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) ); + std::shared_ptr operation( + new AsyncRPCOperation_sendmany(builder, paymentSource, recipients, nMinDepth, nFee, contextInfo) + ); q->addOperation(operation); AsyncRPCOperationId operationId = operation->getId(); return operationId; diff --git a/src/wallet/test/rpc_wallet_tests.cpp b/src/wallet/test/rpc_wallet_tests.cpp index f96d15e7e..ca796c5b4 100644 --- a/src/wallet/test/rpc_wallet_tests.cpp +++ b/src/wallet/test/rpc_wallet_tests.cpp @@ -1177,41 +1177,12 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters) // Mutable tx containing contextual information we need to build tx UniValue retValue = CallRPC("getblockcount"); int nHeight = retValue.get_int(); - CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1); - if (mtx.nVersion == 1) { - mtx.nVersion = 2; - } - - // Test constructor of AsyncRPCOperation_sendmany - try { - std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, "",{}, {}, -1)); - } catch (const UniValue& objError) { - BOOST_CHECK( find_error(objError, "Minconf cannot be negative")); - } + TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, pwalletMain); try { - std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, "",{}, {}, 1)); - } catch (const UniValue& objError) { - BOOST_CHECK( find_error(objError, "From address parameter missing")); - } - - try { - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ", {}, {}, 1) ); - } catch (const UniValue& objError) { - BOOST_CHECK( find_error(objError, "No recipients")); - } - - try { - std::vector recipients = { SendManyRecipient("dummy", 1*COIN, "") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "INVALID", recipients, {}, 1) ); - } catch (const UniValue& objError) { - BOOST_CHECK( find_error(objError, "Invalid from address")); - } - - // Testnet payment addresses begin with 'zt'. This test detects an incorrect prefix. - try { - std::vector recipients = { SendManyRecipient("dummy", 1*COIN, "") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", recipients, {}, 1) ); + libzcash::UnifiedAddress ua; //dummy + std::vector recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) }; + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, ua, recipients, 1)); } catch (const UniValue& objError) { BOOST_CHECK( find_error(objError, "Invalid from address")); } @@ -1219,8 +1190,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters) // Note: The following will crash as a google test because AsyncRPCOperation_sendmany // invokes a method on pwalletMain, which is undefined in the google test environment. try { - std::vector recipients = { SendManyRecipient("dummy", 1*COIN, "") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP", recipients, {}, 1) ); + KeyIO keyIO(Params()); + auto sender = keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value(); + libzcash::UnifiedAddress ua; //dummy + std::vector recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) }; + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, sender, recipients, 1)); } catch (const UniValue& objError) { BOOST_CHECK( find_error(objError, "no spending key found for address")); } @@ -1243,6 +1217,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) { SelectParams(CBaseChainParams::TESTNET); const Consensus::Params& consensusParams = Params().GetConsensus(); + KeyIO keyIO(Params()); LOCK2(cs_main, pwalletMain->cs_wallet); @@ -1252,54 +1227,55 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) // We removed the ability to create pre-Sapling Sprout proofs, so we can // only create Sapling-onwards transactions. int nHeight = consensusParams.vUpgrades[Consensus::UPGRADE_SAPLING].nActivationHeight; - CMutableTransaction mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight + 1); - if (mtx.nVersion == 1) { - mtx.nVersion = 2; - } // add keys manually BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress")); - std::string taddr1 = retValue.get_str(); - auto pa = pwalletMain->GenerateNewSproutZKey(); - KeyIO keyIO(Params()); - std::string zaddr1 = keyIO.EncodePaymentAddress(pa); + auto taddr1 = keyIO.DecodePaymentAddress(retValue.get_str()).value(); + + if (!pwalletMain->HaveHDSeed()) { + pwalletMain->GenerateNewSeed(); + } + auto zaddr1 = pwalletMain->GenerateNewSaplingZKey(); // there are no utxos to spend { + TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); std::vector recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, taddr1, {}, recipients, 1) ); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, taddr1, recipients, 1)); operation->main(); BOOST_CHECK(operation->isFailed()); std::string msg = operation->getErrorMessage(); - BOOST_CHECK( msg.find("Insufficient transparent funds") != string::npos); + BOOST_CHECK( msg.find("Insufficient funds") != string::npos); } // minconf cannot be zero when sending from zaddr { + TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); try { std::vector recipients = {SendManyRecipient(taddr1, 100*COIN, "DEADBEEF")}; - std::shared_ptr operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 0)); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 0)); BOOST_CHECK(false); // Fail test if an exception is not thrown } catch (const UniValue& objError) { BOOST_CHECK(find_error(objError, "Minconf cannot be zero when sending from zaddr")); } } - // there are no unspent notes to spend { + TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); std::vector recipients = { SendManyRecipient(taddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) ); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 1)); operation->main(); BOOST_CHECK(operation->isFailed()); std::string msg = operation->getErrorMessage(); - BOOST_CHECK( msg.find("Insufficient funds, no unspent notes") != string::npos); + BOOST_CHECK(msg.find("Insufficient funds, have 0.00") != string::npos); } // get_memo_from_hex_string()) { + TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain); std::vector recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) ); + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 1)); std::shared_ptr ptr = std::dynamic_pointer_cast (operation); TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr); @@ -1345,106 +1321,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals) BOOST_CHECK( find_error(objError, "hexadecimal format")); } } - - - // add_taddr_change_output_to_tx() will append a vout to a raw transaction - { - std::vector recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) ); - std::shared_ptr ptr = std::dynamic_pointer_cast (operation); - TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr); - - CTransaction tx = proxy.getTx(); - BOOST_CHECK(tx.vout.size() == 0); - - CReserveKey keyChange(pwalletMain); - CAmount amount = 12345600000; - proxy.add_taddr_change_output_to_tx(keyChange, amount); - tx = proxy.getTx(); - BOOST_CHECK(tx.vout.size() == 1); - CTxOut out = tx.vout[0]; - BOOST_CHECK_EQUAL(out.nValue, amount); - - amount = 111100000; - proxy.add_taddr_change_output_to_tx(keyChange, amount); - tx = proxy.getTx(); - BOOST_CHECK(tx.vout.size() == 2); - out = tx.vout[1]; - BOOST_CHECK_EQUAL(out.nValue, amount); - } - - // add_taddr_outputs_to_tx() will append many vouts to a raw transaction - { - std::vector recipients = { - SendManyRecipient("tmTGScYwiLMzHe4uGZtBYmuqoW4iEoYNMXt", 123000000, ""), - SendManyRecipient("tmUSbHz3vxnwLvRyNDXbwkZxjVyDodMJEhh", 456000000, ""), - SendManyRecipient("tmYZAXYPCP56Xa5JQWWPZuK7o7bfUQW6kkd", 789000000, ""), - }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) ); - std::shared_ptr ptr = std::dynamic_pointer_cast (operation); - TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr); - - proxy.add_taddr_outputs_to_tx(); - - CTransaction tx = proxy.getTx(); - BOOST_CHECK(tx.vout.size() == 3); - BOOST_CHECK_EQUAL(tx.vout[0].nValue, 123000000); - BOOST_CHECK_EQUAL(tx.vout[1].nValue, 456000000); - BOOST_CHECK_EQUAL(tx.vout[2].nValue, 789000000); - } - - // Test the perform_joinsplit methods. - { - // Dummy input so the operation object can be instantiated. - std::vector recipients = { SendManyRecipient(zaddr1, 50000, "ABCD") }; - - std::shared_ptr operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, {}, recipients, 1) ); - std::shared_ptr ptr = std::dynamic_pointer_cast (operation); - TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr); - - // Enable test mode so tx is not sent and proofs are not generated - static_cast(operation.get())->testmode = true; - - AsyncJoinSplitInfo info; - std::vector> witnesses; - uint256 anchor; - try { - proxy.perform_joinsplit(info, witnesses, anchor); - } catch (const std::runtime_error & e) { - BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos); - } - - try { - std::vector v; - proxy.perform_joinsplit(info, v); - } catch (const std::runtime_error & e) { - BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos); - } - - info.notes.push_back(SproutNote()); - try { - proxy.perform_joinsplit(info); - } catch (const std::runtime_error & e) { - BOOST_CHECK( string(e.what()).find("number of notes")!= string::npos); - } - - info.notes.clear(); - info.vjsin.push_back(JSInput()); - info.vjsin.push_back(JSInput()); - info.vjsin.push_back(JSInput()); - try { - proxy.perform_joinsplit(info); - } catch (const std::runtime_error & e) { - BOOST_CHECK( string(e.what()).find("unsupported joinsplit input")!= string::npos); - } - - info.vjsin.clear(); - try { - proxy.perform_joinsplit(info); - } catch (const std::runtime_error & e) { - BOOST_CHECK( string(e.what()).find("error verifying joinsplit")!= string::npos); - } - } } @@ -1463,9 +1339,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling) KeyIO keyIO(Params()); // add keys manually auto taddr = pwalletMain->GenerateNewKey().GetID(); - std::string taddr1 = keyIO.EncodeDestination(taddr); auto pa = pwalletMain->GenerateNewSaplingZKey(); - std::string zaddr1 = keyIO.EncodePaymentAddress(pa); const Consensus::Params& consensusParams = Params().GetConsensus(); retValue = CallRPC("getblockcount"); @@ -1498,8 +1372,8 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling) auto builder = TransactionBuilder(consensusParams, nextBlockHeight, pwalletMain); mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight); - std::vector recipients = { SendManyRecipient(zaddr1, 1*COIN, "ABCD") }; - std::shared_ptr operation( new AsyncRPCOperation_sendmany(builder, mtx, taddr1, {}, recipients, 0) ); + std::vector recipients = { SendManyRecipient(pa, 1*COIN, "ABCD") }; + std::shared_ptr operation(new AsyncRPCOperation_sendmany(builder, taddr, recipients, 1)); std::shared_ptr ptr = std::dynamic_pointer_cast (operation); // Enable test mode so tx is not sent @@ -1613,8 +1487,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_sapzkeys) UniValue retValue; int n = 100; - if(!pwalletMain->HaveHDSeed()) - { + if (!pwalletMain->HaveHDSeed()) { pwalletMain->GenerateNewSeed(); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0d37b8e34..590bc8594 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -5084,6 +5084,12 @@ bool CWallet::HasSpendingKeys(const AddrSet& addrSet) const { * Find notes in the wallet filtered by payment addresses, min depth, max depth, * if the note is spent, if a spending key is required, and if the notes are locked. * These notes are decrypted and added to the output parameter vector, outEntries. + * + * For the `noteFilter` argument, `std::nullopt` will return every address; if a + * value is provided, all returned notes will correspond to the addresses in + * that address set. If the empty address set is provided, this function will + * return early and the return arguments `sproutEntries` and `saplingEntries` + * will be unmodified. */ void CWallet::GetFilteredNotes( std::vector& sproutEntries, @@ -5095,6 +5101,10 @@ void CWallet::GetFilteredNotes( bool requireSpendingKey, bool ignoreLocked) { + // Don't bother to do anything if the note filter would reject all notes + if (noteFilter.has_value() && noteFilter.value().IsEmpty()) + return; + LOCK2(cs_main, cs_wallet); KeyIO keyIO(Params()); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f32c4873b..cdd2ae956 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -630,6 +630,7 @@ private: AddrSet() {} public: + static AddrSet Empty() { return AddrSet(); } static AddrSet ForPaymentAddresses(const std::vector& addrs); const std::set& GetSproutAddresses() const { @@ -640,6 +641,10 @@ public: return saplingAddresses; } + bool IsEmpty() const { + return sproutAddresses.empty() && saplingAddresses.empty(); + } + bool HasSproutAddress(libzcash::SproutPaymentAddress addr) const { return sproutAddresses.count(addr) > 0; } diff --git a/src/zcash/Address.hpp b/src/zcash/Address.hpp index 11e4f5bfe..f8e6c02b8 100644 --- a/src/zcash/Address.hpp +++ b/src/zcash/Address.hpp @@ -133,6 +133,17 @@ typedef std::variant< SproutSpendingKey, SaplingExtendedSpendingKey> SpendingKey; +class HasShieldedRecipient { +public: + bool operator()(const CKeyID& p2pkh) { return false; } + bool operator()(const CScriptID& p2sh) { return false; } + bool operator()(const SproutPaymentAddress& addr) { return true; } + bool operator()(const SaplingPaymentAddress& addr) { return true; } + // unified addresses must contain a shielded receiver, so we + // consider this to be safe by construction + bool operator()(const UnifiedAddress& addr) { return true; } +}; + class AddressInfoFromSpendingKey { public: std::pair operator()(const SproutSpendingKey&) const;