Use transaction builder for asyncrpcoperation_sendmany.

This replaces the old implementation of asyncrpcoperation_sendmany
with one where all transaction construction is delegated to the
transaction builder. The capabilities of z_sendmany are somewhat
modified in the process:

* z_sendmany now permits sending funds from a Sprout address to
  both transparent and Sapling addresses. PRIVACY NOTE: When
  user sends a Sprout->Sapling transaction, the amount of the
  transaction is publicly revealed.
* z_sendmany no longer supports transactions sending funds into
  the Sprout pool, with the exception of change amounts when
  sending from a Sprout address.
* When sending transparent coinbase funds to a set of shielded
  addresses, the amount sent to recipients must fully consume
  the input value and no change is permitted. This is a slightly
  weaker constraint than was previously implemented; in the past,
  only a single shielded recipient was allowed.
This commit is contained in:
Kris Nuttycombe 2021-12-28 09:30:02 -07:00
parent 005a8624bf
commit f9477a4499
21 changed files with 663 additions and 1740 deletions

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

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; if you are attempting to shield transparent coinbase funds, ensure that you have specified only a single recipient address.',
)
# Prepare some non-coinbase UTXOs

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;
}

File diff suppressed because it is too large Load Diff

View File

@ -24,52 +24,72 @@
#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);
// 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;
};
/**
* 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<SproutWitness> 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<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,
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<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_;
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;
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_;
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 +135,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,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<SendManyRecipient>& 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<std::string> setAddress;
// Track whether we see any Sprout addresses
bool noSproutAddrs = !fromSprout;
// Recipients
std::vector<SendManyRecipient> taddrRecipients;
std::vector<SendManyRecipient> zaddrRecipients;
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) { // && !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<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);
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<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
}
int nextBlockHeight = chainActive.Height() + 1;
TransactionBuilder builder(Params().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, 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"));
}
}
// 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, 1));
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

@ -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;
}

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;