Merge pull request #5458 from nuttycom/cleanup/sendmany_txbuilder

Use the transaction builder to implement z_sendmany.
This commit is contained in:
str4d 2022-01-12 23:50:09 +00:00 committed by GitHub
commit 423489c5e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 859 additions and 1811 deletions

View File

@ -4,3 +4,31 @@ release-notes at release time)
Notable changes
===============
Wallet
------
`z_sendmany`
------------
- The `z_sendmany` RPC call no longer permits Sprout recipients in the
list of recipient addresses. Transactions spending Sprout funds will
still result in change being sent back into the Sprout pool, but no
other `Sprout->Sprout` transactions will be constructed by the Zcashd
wallet.
- The restriction that prohibited `Sprout->Sapling` transactions has been
lifted; however, since such transactions reveal the amount crossing
pool boundaries, they must be explicitly enabled via a parameter to
the `z_sendmany` call.
- A new boolean parameter, `allowRevealedAmounts`, has been added to the
list of arguments accepted by `z_sendmany`. This parameter defaults to
`false` and is only required when the transaction being constructed
would reveal transaction amounts as a consequence of ZEC value crossing
shielded pool boundaries via the turnstile.
- Since Sprout outputs are no longer created (with the exception of change)
`z_sendmany` no longer generates payment disclosures (which were only
available for Sprout outputs) when the `-paymentdisclosure` experimental
feature flag is set.

View File

@ -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',

View File

@ -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)

View File

@ -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'))

View File

@ -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()

View File

@ -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")

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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,13 +61,13 @@ 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}]
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients)
# 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, 1, DEFAULT_FEE, True)
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, 1, DEFAULT_FEE, True)
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

View File

@ -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__':

View File

@ -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()

View File

@ -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()

View File

@ -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; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.',
)
# Prepare some non-coinbase UTXOs

View File

@ -61,10 +61,10 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
assert_equal(self.nodes[2].getbalance(), 0)
assert_equal(self.nodes[3].getbalance(), 0)
check_value_pool(self.nodes[0], 'sprout', 0)
check_value_pool(self.nodes[1], 'sprout', 0)
check_value_pool(self.nodes[2], 'sprout', 0)
check_value_pool(self.nodes[3], 'sprout', 0)
check_value_pool(self.nodes[0], 'sapling', 0)
check_value_pool(self.nodes[1], 'sapling', 0)
check_value_pool(self.nodes[2], 'sapling', 0)
check_value_pool(self.nodes[3], 'sapling', 0)
# Send will fail because we are enforcing the consensus rule that
# coinbase utxos can only be sent to a zaddr.
@ -77,7 +77,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
# Prepare to send taddr->zaddr
mytaddr = get_coinbase_address(self.nodes[0])
myzaddr = self.nodes[0].z_getnewaddress('sprout')
myzaddr = self.nodes[0].z_getnewaddress('sapling')
# Node 3 will test that watch only address utxos are not selected
self.nodes[3].importaddress(mytaddr)
@ -86,17 +86,22 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
except JSONRPCException as e:
errorString = e.error['message']
assert_equal("Invalid from address: does not belong to this node, spending key not found.", errorString);
assert_equal("Invalid from address, no spending key found for address", errorString);
# This send will fail because our wallet does not allow any change when shielding a coinbase utxo,
# as it's currently not possible to specify a change address in z_sendmany.
# This send will fail because our consensus does not allow transparent change when
# shielding a coinbase utxo.
# TODO: After upgrading to unified address support, change will be sent to the most
# recent shielded spend authority corresponding to the account of the source address
# and this send will succeed, causing this test to fail.
recipients = []
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
error_result = wait_and_assert_operationid_status_result(self.nodes[0], myopid, "failed", ("Change 8.76542211 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."), 10)
error_result = wait_and_assert_operationid_status_result(
self.nodes[0],
myopid, "failed",
"When shielding coinbase funds, the wallet does not allow any change. The proposed transaction would result in 8.76542211 in change.",
10)
# Test that the returned status object contains a params field with the operation's input parameters
assert_equal(error_result["method"], "z_sendmany")
@ -167,8 +172,8 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
assert_equal(Decimal(resp["total"]), Decimal('40.0') - DEFAULT_FEE)
# The Sprout value pool should reflect the send
sproutvalue = shieldvalue
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
saplingvalue = shieldvalue
check_value_pool(self.nodes[0], 'sapling', saplingvalue)
# A custom fee of 0 is okay. Here the node will send the note value back to itself.
recipients = []
@ -183,8 +188,8 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
assert_equal(Decimal(resp["private"]), Decimal('20.0') - DEFAULT_FEE)
assert_equal(Decimal(resp["total"]), Decimal('40.0') - DEFAULT_FEE)
# The Sprout value pool should be unchanged
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
# The Sapling value pool should be unchanged
check_value_pool(self.nodes[0], 'sapling', saplingvalue)
# convert note to transparent funds
unshieldvalue = Decimal('10.0')
@ -203,12 +208,12 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
self.sync_all()
# check balances
sproutvalue -= unshieldvalue + DEFAULT_FEE
saplingvalue -= unshieldvalue + DEFAULT_FEE
resp = self.nodes[0].z_gettotalbalance()
assert_equal(Decimal(resp["transparent"]), Decimal('30.0'))
assert_equal(Decimal(resp["private"]), Decimal('10.0') - 2*DEFAULT_FEE)
assert_equal(Decimal(resp["total"]), Decimal('40.0') - 2*DEFAULT_FEE)
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
check_value_pool(self.nodes[0], 'sapling', saplingvalue)
# z_sendmany will return an error if there is transparent change output considered dust.
# UTXO selection in z_sendmany sorts in ascending order, so smallest utxos are consumed first.
@ -217,7 +222,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
amount = Decimal('10.0') - DEFAULT_FEE - Decimal('0.00000001') # this leaves change at 1 zatoshi less than dust threshold
recipients.append({"address":self.nodes[0].getnewaddress(), "amount":amount })
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054)")
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054)")
# Send will fail because send amount is too big, even when including coinbase utxos
errorString = ""
@ -231,9 +236,9 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
recipients = []
recipients.append({"address":self.nodes[1].getnewaddress(), "amount":Decimal('10000.0')})
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 10000.00001")
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.")
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient shielded funds, have 9.99998, need 10000.00001")
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 9.99998, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.")
# Send will fail because of insufficient funds unless sender uses coinbase utxos
try:
@ -284,9 +289,9 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
# check balance
node2balance = amount_per_recipient * num_t_recipients
sproutvalue -= node2balance + DEFAULT_FEE
saplingvalue -= node2balance + DEFAULT_FEE
assert_equal(self.nodes[2].getbalance(), node2balance)
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
check_value_pool(self.nodes[0], 'sapling', saplingvalue)
# Send will fail because fee is negative
try:
@ -332,7 +337,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
custom_fee = Decimal('0.00012345')
zbalance = self.nodes[0].z_getbalance(myzaddr)
for i in range(0,num_recipients):
newzaddr = self.nodes[2].z_getnewaddress('sprout')
newzaddr = self.nodes[2].z_getnewaddress('sapling')
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, custom_fee)
wait_and_assert_operationid_status(self.nodes[0], myopid)
@ -350,8 +355,8 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
resp = self.nodes[0].z_getbalance(myzaddr)
assert_equal(Decimal(resp), zbalance - custom_fee - send_amount)
sproutvalue -= custom_fee
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
saplingvalue -= custom_fee
check_value_pool(self.nodes[0], 'sapling', saplingvalue)
notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr])
sum_of_notes = sum([note["amount"] for note in notes])

View File

@ -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;
}
@ -522,6 +522,7 @@ void TransactionBuilder::CheckOrSetUsingSprout()
auto txVersionInfo = CurrentTxVersionInfo(consensusParams, nHeight, usingSprout.value());
mtx.nVersionGroupId = txVersionInfo.nVersionGroupId;
mtx.nVersion = txVersionInfo.nVersion;
mtx.nConsensusBranchId = std::nullopt;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -24,54 +24,81 @@
#include <rust/ed25519/types.h>
using namespace libzcash;
class TxValues;
class FromAnyTaddr {
public:
friend bool operator==(const FromAnyTaddr &a, const FromAnyTaddr &b) { return true; }
};
typedef std::variant<FromAnyTaddr, PaymentAddress> PaymentSource;
class SendManyRecipient {
public:
std::string address;
PaymentAddress address;
CAmount amount;
std::string memo;
std::optional<std::string> memo;
SendManyRecipient(std::string address_, CAmount amount_, std::string memo_) :
SendManyRecipient(PaymentAddress address_, CAmount amount_, std::optional<std::string> memo_) :
address(address_), amount(amount_), memo(memo_) {}
};
class SendManyInputJSOP {
class SpendableInputs {
public:
JSOutPoint point;
SproutNote note;
CAmount amount;
std::vector<COutput> utxos;
std::vector<SproutNoteEntry> sproutNoteEntries;
std::vector<SaplingNoteEntry> 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, CAmount dustThreshold);
/**
* 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;
}
/**
* 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;
};
// Package of info which is passed to perform_joinsplit methods.
struct AsyncJoinSplitInfo
{
std::vector<JSInput> vjsin;
std::vector<JSOutput> vjsout;
std::vector<SproutNote> notes;
CAmount vpub_old = 0;
CAmount vpub_new = 0;
};
// A struct to help us track the witness and anchor for a given JSOutPoint
struct WitnessAnchorData {
std::optional<SproutWitness> witness;
uint256 anchor;
class TxOutputAmounts {
public:
CAmount t_outputs_total{0};
CAmount z_outputs_total{0};
};
class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
public:
AsyncRPCOperation_sendmany(
std::optional<TransactionBuilder> builder,
CMutableTransaction contextualTx,
std::string fromAddress,
std::vector<SendManyRecipient> tOutputs,
std::vector<SendManyRecipient> zOutputs,
TransactionBuilder builder,
PaymentSource paymentSource,
std::vector<SendManyRecipient> recipients,
int minDepth,
CAmount fee = DEFAULT_FEE,
bool allowRevealedAmounts = false,
UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_sendmany();
@ -85,64 +112,32 @@ 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<SendManyRecipient> 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_;
bool isfromsprout_{false};
bool isfromsapling_{false};
bool allowRevealedAmounts_{false};
uint32_t transparentRecipients_{0};
TxOutputAmounts txOutputAmounts_;
Ed25519VerificationKey joinSplitPubKey_;
Ed25519SigningKey joinSplitPrivKey_;
SpendableInputs FindSpendableInputs(bool fAcceptCoinbase);
// The key is the result string from calling JSOutPoint::ToString()
std::unordered_map<std::string, WitnessAnchorData> jsopWitnessAnchorMap;
static CAmount DefaultDustThreshold();
std::vector<SendManyRecipient> t_outputs_;
std::vector<SendManyRecipient> z_outputs_;
std::vector<COutput> t_inputs_;
std::vector<SendManyInputJSOP> z_sprout_inputs_;
std::vector<SaplingNoteEntry> z_sapling_inputs_;
static std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s);
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<unsigned char, ZC_MEMO_SIZE> 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<JSOutPoint> & );
// JoinSplit where you have the witnesses and anchor
UniValue perform_joinsplit(
AsyncJoinSplitInfo & info,
std::vector<std::optional < SproutWitness>> witnesses,
uint256 anchor);
// payment disclosure!
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
uint256 main_impl();
};
@ -153,52 +148,14 @@ public:
TEST_FRIEND_AsyncRPCOperation_sendmany(std::shared_ptr<AsyncRPCOperation_sendmany> 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<unsigned char, ZC_MEMO_SIZE> 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<JSOutPoint> &v ) {
return delegate->perform_joinsplit(info, v);
}
UniValue perform_joinsplit(
AsyncJoinSplitInfo & info,
std::vector<std::optional < SproutWitness>> witnesses,
uint256 anchor)
{
return delegate->perform_joinsplit(info, witnesses, anchor);
}
void set_state(OperationStatus state) {
delegate->state_.store(state);
}

View File

@ -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,14 +3679,88 @@ 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<SendManyRecipient>& recipients,
int nextBlockHeight) {
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;
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))
return NullUniValue;
if (fHelp || params.size() < 2 || params.size() > 4)
if (fHelp || params.size() < 2 || params.size() > 5)
throw runtime_error(
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee )\n"
"z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) ( allowRevealedAmounts )\n"
"\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision."
"\nChange generated from one or more transparent addresses flows to a new transparent"
"\naddress, while change generated from a shielded address returns to itself."
@ -3707,8 +3780,8 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
" \"memo\":memo (string, optional) If the address is a zaddr, raw data represented in hexadecimal string format\n"
" }, ... ]\n"
"3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n"
"4. fee (numeric, optional, default="
+ strprintf("%s", FormatMoney(DEFAULT_FEE)) + ") The fee amount to attach to this transaction.\n"
"4. fee (numeric, optional, default=" + strprintf("%s", FormatMoney(DEFAULT_FEE)) + ") The fee amount to attach to this transaction.\n"
"5. allowRevealedAmounts (bool, optional, default=false) Permit cross-shielded-pool transfers, which will publicly reveal the amount(s) crossing pool boundaries.\n"
"\nResult:\n"
"\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n"
"\nExamples:\n"
@ -3719,16 +3792,22 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet);
const auto& chainparams = Params();
int nextBlockHeight = chainActive.Height() + 1;
ThrowIfInitialBlockDownload();
if (!chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated");
}
KeyIO keyIO(chainparams);
// Check that the from address is valid.
auto fromaddress = params[0].get_str();
bool fromTaddr = false;
bool fromSapling = false;
bool fromSprout = false;
KeyIO keyIO(Params());
PaymentSource paymentSource;
if (fromaddress == "ANY_TADDR") {
fromTaddr = true;
paymentSource = FromAnyTaddr();
} else {
auto addr = keyIO.DecodePaymentAddress(fromaddress);
if (!addr.has_value()) {
@ -3737,217 +3816,91 @@ 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())) {
// Unified addresses are not yet supported.
if (std::holds_alternative<libzcash::UnifiedAddress>(addr.value())) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: does not belong to this node, spending key not found.");
"Invalid from address: unified addresses are not yet supported.");
}
// Remember what sort of address this is
std::visit(match {
[&](const CKeyID&) {
fromTaddr = true;
},
[&](const CScriptID&) {
fromTaddr = true;
},
[&](const libzcash::SaplingPaymentAddress& addr) {
fromSapling = true;
},
[&](const libzcash::SproutPaymentAddress& addr) {
fromSprout = true;
},
[&](const libzcash::UnifiedAddress& ua) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"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<std::string> setAddress;
// Track whether we see any Sprout addresses
bool noSproutAddrs = !fromSprout;
// Recipients
std::vector<SendManyRecipient> taddrRecipients;
std::vector<SendManyRecipient> zaddrRecipients;
std::set<std::string> addrStrings;
std::vector<SendManyRecipient> 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<libzcash::SproutPaymentAddress>(addr.value());
if (toSprout) {
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);
if (!addrStrings.insert(addrStr).second) {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ") + addrStr);
};
UniValue memoValue = find_value(o, "memo");
string memo;
std::optional<std::string> 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, nextBlockHeight);
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 +3913,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,20 +3922,29 @@ 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)));
}
}
}
bool allowRevealedAmounts{false};
if (params.size() > 4) {
allowRevealedAmounts = params[4].get_bool();
}
// Use input parameters as the optional context info to be returned by z_getoperationstatus and z_getoperationresult.
UniValue o(UniValue::VOBJ);
o.pushKV("fromaddress", params[0]);
@ -3993,33 +3953,13 @@ 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<TransactionBuilder> 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
}
TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, pwalletMain);
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) );
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_sendmany(builder, paymentSource, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo)
);
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();
return operationId;

View File

@ -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<AsyncRPCOperation> 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<AsyncRPCOperation> 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<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ", {}, {}, 1) );
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "No recipients"));
}
try {
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", recipients, {}, 1) );
libzcash::UnifiedAddress ua; //dummy
std::vector<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, taddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = {SendManyRecipient(taddr1, 100*COIN, "DEADBEEF")};
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 0));
std::shared_ptr<AsyncRPCOperation> 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"));
BOOST_CHECK(find_error(objError, "Minconf cannot be zero when sending from a shielded address"));
}
}
// there are no unspent notes to spend
{
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
std::vector<SendManyRecipient> recipients = { SendManyRecipient(taddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> 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<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 1));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (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<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (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<SendManyRecipient> recipients = {
SendManyRecipient("tmTGScYwiLMzHe4uGZtBYmuqoW4iEoYNMXt", 123000000, ""),
SendManyRecipient("tmUSbHz3vxnwLvRyNDXbwkZxjVyDodMJEhh", 456000000, ""),
SendManyRecipient("tmYZAXYPCP56Xa5JQWWPZuK7o7bfUQW6kkd", 789000000, ""),
};
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (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<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 50000, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
// Enable test mode so tx is not sent and proofs are not generated
static_cast<AsyncRPCOperation_sendmany *>(operation.get())->testmode = true;
AsyncJoinSplitInfo info;
std::vector<std::optional < SproutWitness>> 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<JSOutPoint> 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<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 1*COIN, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, mtx, taddr1, {}, recipients, 0) );
std::vector<SendManyRecipient> recipients = { SendManyRecipient(pa, 1*COIN, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, taddr, recipients, 0));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (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();
}

View File

@ -2033,7 +2033,7 @@ bool CWallet::IsSaplingNullifierFromMe(const uint256& nullifier) const
return false;
}
void CWallet::GetSproutNoteWitnesses(std::vector<JSOutPoint> notes,
void CWallet::GetSproutNoteWitnesses(const std::vector<JSOutPoint>& notes,
std::vector<std::optional<SproutWitness>>& witnesses,
uint256 &final_anchor)
{
@ -2060,7 +2060,7 @@ void CWallet::GetSproutNoteWitnesses(std::vector<JSOutPoint> notes,
}
}
void CWallet::GetSaplingNoteWitnesses(std::vector<SaplingOutPoint> notes,
void CWallet::GetSaplingNoteWitnesses(const std::vector<SaplingOutPoint>& notes,
std::vector<std::optional<SaplingWitness>>& witnesses,
uint256 &final_anchor)
{
@ -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<SproutNoteEntry>& 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());

View File

@ -630,6 +630,7 @@ private:
AddrSet() {}
public:
static AddrSet Empty() { return AddrSet(); }
static AddrSet ForPaymentAddresses(const std::vector<libzcash::PaymentAddress>& addrs);
const std::set<libzcash::SproutPaymentAddress>& 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;
}
@ -1190,11 +1195,11 @@ public:
bool IsSaplingNullifierFromMe(const uint256& nullifier) const;
void GetSproutNoteWitnesses(
std::vector<JSOutPoint> notes,
const std::vector<JSOutPoint>& notes,
std::vector<std::optional<SproutWitness>>& witnesses,
uint256 &final_anchor);
void GetSaplingNoteWitnesses(
std::vector<SaplingOutPoint> notes,
const std::vector<SaplingOutPoint>& notes,
std::vector<std::optional<SaplingWitness>>& witnesses,
uint256 &final_anchor);

View File

@ -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<std::string, PaymentAddress> operator()(const SproutSpendingKey&) const;