Update z_mergetoaddress to use WalletTxBuilder

Co-authored-by: Kris Nuttycombe <kris@nutty.land>
Co-authored-by: Jack Grigg <jack@z.cash>
This commit is contained in:
Greg Pfeil 2023-04-17 04:21:54 -06:00
parent 250a3f049b
commit 94f5bfc595
No known key found for this signature in database
GPG Key ID: 1193ACD196ED61F2
11 changed files with 335 additions and 1185 deletions

View File

@ -32,6 +32,13 @@ RPC Changes
- The `estimatepriority` RPC call has been removed.
- The `priority_delta` argument to the `prioritisetransaction` RPC call now has
no effect and must be set to a dummy value (0 or null).
- The `z_shieldcoinbase` and `z_mergetoaddress` RPC methods no longer support
transfers of funds to Sprout recipients. While Sprout change may still be
produced in the process of spending Sprout funds, it is no longer possible
to transfer funds between Sprout addresses. Also, some failures have moved
from synchronous to asynchronous, so while you should already be checking
the async operation status, there are now more cases that may trigger
failure at that stage.
Changes to Transaction Fee Selection
------------------------------------

View File

@ -21,6 +21,7 @@ class MergeToAddressMixedNotes(BitcoinTestFramework):
'-minrelaytxfee=0',
'-anchorconfirmations=1',
'-allowdeprecated=getnewaddress',
'-allowdeprecated=legacy_privacy',
'-allowdeprecated=z_getnewaddress',
'-allowdeprecated=z_getbalance',
'-allowdeprecated=z_gettotalbalance',

View File

@ -14,7 +14,10 @@ class MergeToAddressSapling (BitcoinTestFramework):
self.helper.setup_chain(self)
def setup_network(self, split=False):
self.helper.setup_network(self, ['-anchorconfirmations=1'])
self.helper.setup_network(self, [
'-anchorconfirmations=1',
'-allowdeprecated=legacy_privacy',
])
def run_test(self):
self.helper.run_test(self)

View File

@ -49,7 +49,6 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
self.sync_all()
n0_sprout_addr0 = self.nodes[0].listaddresses()[0]['sprout']['addresses'][0]
n2_sprout_addr = self.nodes[2].listaddresses()[0]['sprout']['addresses'][0]
# Attempt to shield coinbase to Sprout on node 0. Should fail;
# transfers to Sprout are no longer supported
@ -77,20 +76,21 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
self.sync_all()
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('3'))
# Create mergetoaddress taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass. This will spend the available funds in taddr0
# Create mergetoaddress taddr -> Sprout transaction, should fail
n1_sprout_addr0 = self.nodes[1].z_getnewaddress('sprout')
merge_tx_0 = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], n1_sprout_addr0, 0)
wait_and_assert_operationid_status(self.nodes[0], merge_tx_0['opid'])
print("taddr -> Sprout z_mergetoaddress tx accepted before Canopy on node 0")
assert_raises_message(
JSONRPCException,
"Sending funds into the Sprout pool is no longer supported.",
self.nodes[0].z_mergetoaddress,
["ANY_TADDR"], n1_sprout_addr0, 0)
self.nodes[0].generate(1)
self.sync_all()
assert_equal(self.nodes[1].z_getbalance(n1_sprout_addr0), Decimal('3'))
# Send some funds back to n0_taddr0
recipients = [{"address": n0_taddr0, "amount": Decimal('1')}]
myopid = self.nodes[1].z_sendmany(n1_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[1], myopid)
myopid = self.nodes[0].z_sendmany(n0_sprout_addr0, recipients, 1, 0, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[0], myopid)
# Mine to one block before Canopy activation on node 0; adding value
# to the Sprout pool will fail now since the transaction must be
@ -99,7 +99,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
self.nodes[0].generate(4)
self.sync_all()
assert_equal(self.nodes[0].getblockchaininfo()['upgrades']['e9ff75a6']['status'], 'pending')
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('1'))
assert_equal(self.nodes[0].z_getbalance(n0_taddr0), Decimal('4'))
# Shield coinbase to Sprout on node 0. Should fail
n0_coinbase_taddr = get_coinbase_address(self.nodes[0])
@ -121,15 +121,9 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
# Create z_mergetoaddress [taddr, Sprout] -> Sprout transaction on node 0. Should fail
assert_raises_message(
JSONRPCException,
"Sprout shielding is not supported after Canopy",
"Sending funds into the Sprout pool is no longer supported.",
self.nodes[0].z_mergetoaddress,
["ANY_TADDR", "ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
print("[taddr, Sprout] -> Sprout z_mergetoaddress tx rejected at Canopy activation on node 0")
# Create z_mergetoaddress Sprout -> Sprout transaction on node 0. Should pass
merge_tx_1 = self.nodes[0].z_mergetoaddress(["ANY_SPROUT"], self.nodes[1].z_getnewaddress('sprout'))
wait_and_assert_operationid_status(self.nodes[0], merge_tx_1['opid'])
print("Sprout -> Sprout z_mergetoaddress tx accepted at Canopy activation on node 0")
# Activate Canopy
self.nodes[0].generate(1)
@ -149,17 +143,5 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
wait_and_assert_operationid_status(self.nodes[0], myopid)
print("taddr -> Sapling z_shieldcoinbase tx accepted after Canopy on node 0")
# Mine to one block before NU5 activation.
self.nodes[0].generate(4)
self.sync_all()
# Create z_mergetoaddress Sprout -> Sprout transaction on node 1. Should pass
merge_tx_2 = self.nodes[1].z_mergetoaddress(["ANY_SPROUT"], n2_sprout_addr)
wait_and_assert_operationid_status(self.nodes[1], merge_tx_2['opid'])
print("Sprout -> Sprout z_mergetoaddress tx accepted at NU5 activation on node 1")
self.nodes[1].generate(1)
self.sync_all()
if __name__ == '__main__':
RemoveSproutShieldingTest().main()

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,10 @@
#include "asyncrpcoperation.h"
#include "primitives/transaction.h"
#include "transaction_builder.h"
#include "uint256.h"
#include "wallet.h"
#include "wallet/paymentdisclosure.h"
#include "wallet/wallet_tx_builder.h"
#include "zcash/Address.hpp"
#include "zcash/JoinSplit.hpp"
@ -25,16 +27,8 @@
using namespace libzcash;
// Input UTXO is a tuple of txid, vout, amount, script
typedef std::tuple<COutPoint, CAmount, CScript> MergeToAddressInputUTXO;
// Input JSOP is a tuple of JSOutpoint, note, amount, spending key
typedef std::tuple<JSOutPoint, SproutNote, CAmount, SproutSpendingKey> MergeToAddressInputSproutNote;
typedef std::tuple<SaplingOutPoint, SaplingNote, CAmount, SaplingExpandedSpendingKey> MergeToAddressInputSaplingNote;
// A recipient is a tuple of address, memo (optional if zaddr)
typedef std::pair<libzcash::PaymentAddress, std::string> MergeToAddressRecipient;
typedef std::pair<libzcash::PaymentAddress, std::optional<Memo>> MergeToAddressRecipient;
// Package of info which is passed to perform_joinsplit methods.
struct MergeToAddressJSInfo {
@ -56,12 +50,11 @@ class AsyncRPCOperation_mergetoaddress : public AsyncRPCOperation
{
public:
AsyncRPCOperation_mergetoaddress(
std::optional<TransactionBuilder> builder,
CMutableTransaction contextualTx,
std::vector<MergeToAddressInputUTXO> utxoInputs,
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs,
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs,
WalletTxBuilder builder,
ZTXOSelector ztxoSelector,
SpendableInputs allInputs,
MergeToAddressRecipient recipient,
TransactionStrategy strategy,
CAmount fee = DEFAULT_FEE,
UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_mergetoaddress();
@ -72,6 +65,8 @@ public:
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy assign
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress&&) = delete; // Move assign
tl::expected<void, InputSelectionError> prepare(const CChainParams& chainparams, CWallet& wallet);
virtual void main();
virtual UniValue getStatus() const;
@ -85,15 +80,13 @@ private:
UniValue contextinfo_; // optional data to include in return value from getStatus()
bool isUsingBuilder_; // Indicates that no Sprout addresses are involved
uint32_t consensusBranchId_;
CAmount fee_;
int mindepth_;
bool isToTaddr_;
bool isToZaddr_;
CTxDestination toTaddr_;
PaymentAddress toPaymentAddress_;
std::string memo_;
std::optional<Memo> memo_;
ed25519::VerificationKey joinSplitPubKey_;
ed25519::SigningKey joinSplitPrivKey_;
@ -101,35 +94,14 @@ private:
// The key is the result string from calling JSOutPoint::ToString()
std::unordered_map<std::string, MergeToAddressWitnessAnchorData> jsopWitnessAnchorMap;
std::vector<MergeToAddressInputUTXO> utxoInputs_;
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs_;
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs_;
WalletTxBuilder builder_;
ZTXOSelector ztxoSelector_;
SpendableInputs allInputs_;
TransactionStrategy strategy_;
TransactionBuilder builder_;
CTransaction tx_;
std::optional<TransactionEffects> effects_;
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(MergeToAddressJSInfo&);
// JoinSplit with input notes to spend (JSOutPoints))
UniValue perform_joinsplit(MergeToAddressJSInfo&, std::vector<JSOutPoint>&);
// JoinSplit where you have the witnesses and anchor
UniValue perform_joinsplit(
MergeToAddressJSInfo& info,
std::vector<std::optional<SproutWitness>> witnesses,
uint256 anchor);
void lock_utxos();
void unlock_utxos();
void lock_notes();
void unlock_notes();
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams);
// payment disclosure!
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
@ -144,44 +116,11 @@ public:
TEST_FRIEND_AsyncRPCOperation_mergetoaddress(std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr) : delegate(ptr) {}
CTransaction getTx()
{
return delegate->tx_;
}
void setTx(CTransaction tx)
{
delegate->tx_ = tx;
}
// Delegated methods
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s)
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams)
{
return delegate->get_memo_from_hex_string(s);
}
bool main_impl()
{
return delegate->main_impl();
}
UniValue perform_joinsplit(MergeToAddressJSInfo& info)
{
return delegate->perform_joinsplit(info);
}
UniValue perform_joinsplit(MergeToAddressJSInfo& info, std::vector<JSOutPoint>& v)
{
return delegate->perform_joinsplit(info, v);
}
UniValue perform_joinsplit(
MergeToAddressJSInfo& info,
std::vector<std::optional<SproutWitness>> witnesses,
uint256 anchor)
{
return delegate->perform_joinsplit(info, witnesses, anchor);
return delegate->main_impl(wallet, chainparams);
}
void set_state(OperationStatus state)

View File

@ -17,10 +17,21 @@
#include <librustzcash.h>
#include <rust/ed25519.h>
namespace {
bool find_error(const UniValue& objError, const std::string& expected) {
return find_value(objError, "message").get_str().find(expected) != string::npos;
}
CWalletTx FakeWalletTx() {
CMutableTransaction mtx;
mtx.vout.resize(1);
mtx.vout[0].nValue = 1;
return CWalletTx(nullptr, mtx);
}
}
// TODO: test private methods
TEST(WalletRPCTests, RPCZMergeToAddressInternals)
{
@ -46,128 +57,26 @@ TEST(WalletRPCTests, RPCZMergeToAddressInternals)
auto taddr = pwalletMain->GenerateNewKey(true).GetID();
std::string taddr_string = keyIO.EncodeDestination(taddr);
MergeToAddressRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), "");
MergeToAddressRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), Memo());
auto pa = pwalletMain->GenerateNewSproutZKey();
MergeToAddressRecipient zaddr1(pa, "DEADBEEF");
MergeToAddressRecipient zaddr1(pa, Memo());
WalletTxBuilder builder(Params(), minRelayTxFee);
auto selector = CWallet::LegacyTransparentZTXOSelector(
true,
TransparentCoinbasePolicy::Disallow);
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
// Insufficient funds
{
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},0, CScript()} };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
operation->main();
EXPECT_TRUE(operation->isFailed());
std::string msg = operation->getErrorMessage();
EXPECT_TRUE(msg.find("Insufficient funds, have 0.00 and miners fee is 0.00001") != string::npos);
}
// get_memo_from_hex_string())
{
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000, CScript()} };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_mergetoaddress> (operation);
TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr);
std::string memo = "DEADBEEF";
std::array<unsigned char, ZC_MEMO_SIZE> array = proxy.get_memo_from_hex_string(memo);
EXPECT_EQ(array[0], 0xDE);
EXPECT_EQ(array[1], 0xAD);
EXPECT_EQ(array[2], 0xBE);
EXPECT_EQ(array[3], 0xEF);
for (int i=4; i<ZC_MEMO_SIZE; i++) {
EXPECT_EQ(array[i], 0x00); // zero padding
}
// memo is longer than allowed
std::vector<char> v (2 * (ZC_MEMO_SIZE+1));
std::fill(v.begin(), v.end(), 'A');
std::string bigmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(bigmemo);
FAIL() << "Should have caused an error";
} catch (const UniValue& objError) {
EXPECT_TRUE(find_error(objError, "too big"));
}
// invalid hexadecimal string
std::fill(v.begin(), v.end(), '@'); // not a hex character
std::string badmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(badmemo);
FAIL() << "Should have caused an error";
} catch (const UniValue& objError) {
EXPECT_TRUE(find_error(objError, "hexadecimal format"));
}
// odd length hexadecimal string
std::fill(v.begin(), v.end(), 'A');
v.resize(v.size() - 1);
assert(v.size() % 2 == 1); // odd length
std::string oddmemo(v.begin(), v.end());
try {
proxy.get_memo_from_hex_string(oddmemo);
FAIL() << "Should have caused an error";
} catch (const UniValue& objError) {
EXPECT_TRUE(find_error(objError, "hexadecimal format"));
}
}
// Test the perform_joinsplit methods.
{
// Dummy input so the operation object can be instantiated.
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000, CScript()} };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, {}, {}, zaddr1) );
std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_mergetoaddress> (operation);
TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr);
// Enable test mode so tx is not sent and proofs are not generated
static_cast<AsyncRPCOperation_mergetoaddress *>(operation.get())->testmode = true;
MergeToAddressJSInfo info;
std::vector<std::optional < SproutWitness>> witnesses;
uint256 anchor;
try {
proxy.perform_joinsplit(info, witnesses, anchor);
FAIL() << "Should have caused an error";
} catch (const std::runtime_error & e) {
EXPECT_TRUE(string(e.what()).find("anchor is null") != string::npos);
}
try {
std::vector<JSOutPoint> v;
proxy.perform_joinsplit(info, v);
FAIL() << "Should have caused an error";
} catch (const std::runtime_error & e) {
EXPECT_TRUE(string(e.what()).find("anchor is null") != string::npos);
}
info.notes.push_back(SproutNote());
try {
proxy.perform_joinsplit(info);
FAIL() << "Should have caused an error";
} catch (const std::runtime_error & e) {
EXPECT_TRUE(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);
FAIL() << "Should have caused an error";
} catch (const std::runtime_error & e) {
EXPECT_TRUE(string(e.what()).find("unsupported joinsplit input") != string::npos);
}
info.vjsin.clear();
try {
proxy.perform_joinsplit(info);
FAIL() << "Should have caused an error";
} catch (const std::runtime_error & e) {
EXPECT_TRUE(string(e.what()).find("error verifying joinsplit") != string::npos);
}
SpendableInputs inputs;
auto wtx = FakeWalletTx();
inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true));
auto operation = AsyncRPCOperation_mergetoaddress(std::move(builder), selector, inputs, zaddr1, strategy, 0);
operation.main();
EXPECT_TRUE(operation.isFailed());
std::string msg = operation.getErrorMessage();
EXPECT_EQ(msg, "Sending funds into the Sprout pool is no longer supported.");
}
}
UnloadGlobalWallet();

View File

@ -38,6 +38,7 @@
#include "util/time.h"
#include "asyncrpcoperation.h"
#include "asyncrpcqueue.h"
#include "wallet/asyncrpcoperation_common.h"
#include "wallet/asyncrpcoperation_mergetoaddress.h"
#include "wallet/asyncrpcoperation_saplingmigration.h"
#include "wallet/asyncrpcoperation_sendmany.h"
@ -5342,9 +5343,9 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() < 2 || params.size() > 6)
if (fHelp || params.size() < 2 || params.size() > 7)
throw runtime_error(
"z_mergetoaddress [\"fromaddress\", ... ] \"toaddress\" ( fee ) ( transparent_limit ) ( shielded_limit ) ( memo )\n"
"z_mergetoaddress [\"fromaddress\", ... ] \"toaddress\" ( fee ) ( transparent_limit ) ( shielded_limit ) ( memo ) ( privacyPolicy )\n"
"\nMerge multiple UTXOs and notes into a single UTXO or note. Coinbase UTXOs are ignored; use `z_shieldcoinbase`"
"\nto combine those into a single note."
"\n\nThis is an asynchronous operation, and UTXOs selected for merging will be locked. If there is an error, they"
@ -5376,6 +5377,26 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
"5. shielded_limit (numeric, optional, default="
+ strprintf("%d Sprout or %d Sapling Notes", MERGE_TO_ADDRESS_DEFAULT_SPROUT_LIMIT, MERGE_TO_ADDRESS_DEFAULT_SAPLING_LIMIT) + ") Limit on the maximum number of notes to merge. Set to 0 to merge as many as will fit in the transaction.\n"
"6. \"memo\" (string, optional) Encoded as hex. When toaddress is a zaddr, this will be stored in the memo field of the new note.\n"
"7. privacyPolicy (string, optional, default=\"LegacyCompat\") Policy for what information leakage is acceptable.\n"
" One of the following strings:\n"
" - \"FullPrivacy\": Only allow fully-shielded transactions (involving a single shielded value pool).\n"
" - \"LegacyCompat\": If the transaction involves any Unified Addresses, this is equivalent to\n"
" \"FullPrivacy\". Otherwise, this is equivalent to \"AllowFullyTransparent\".\n"
" - \"AllowRevealedAmounts\": Allow funds to cross between shielded value pools, revealing the amount\n"
" that crosses pools.\n"
" - \"AllowRevealedRecipients\": Allow transparent recipients. This also implies revealing\n"
" information described under \"AllowRevealedAmounts\".\n"
" - \"AllowRevealedSenders\": Allow transparent funds to be spent, revealing the sending\n"
" addresses and amounts. This implies revealing information described under \"AllowRevealedAmounts\".\n"
" - \"AllowFullyTransparent\": Allow transaction to both spend transparent funds and have\n"
" transparent recipients. This implies revealing information described under \"AllowRevealedSenders\"\n"
" and \"AllowRevealedRecipients\".\n"
" - \"AllowLinkingAccountAddresses\": Allow selecting transparent coins from the full account,\n"
" rather than just the funds sent to the transparent receiver in the provided Unified Address.\n"
" This implies revealing information described under \"AllowRevealedSenders\".\n"
" - \"NoPrivacy\": Allow the transaction to reveal any information necessary to create it.\n"
" This implies revealing information described under \"AllowFullyTransparent\" and\n"
" \"AllowLinkingAccountAddresses\".\n"
"\nResult:\n"
"{\n"
" \"remainingUTXOs\": xxx (numeric) Number of UTXOs still available for merging.\n"
@ -5409,10 +5430,10 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
// Keep track of addresses to spot duplicates
std::set<std::string> setAddress;
std::set<ReceiverType> receiverTypes;
bool isFromNonSprout = false;
KeyIO keyIO(Params());
const auto chainparams = Params();
KeyIO keyIO(chainparams);
// Sources
for (const UniValue& o : addresses.getValues()) {
if (!o.isStr())
@ -5421,28 +5442,27 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
std::string address = o.get_str();
if (address == "ANY_TADDR") {
receiverTypes.insert({ReceiverType::P2PKH, ReceiverType::P2SH});
useAnyUTXO = true;
isFromNonSprout = true;
} else if (address == "ANY_SPROUT") {
// TODO: How can we add sprout addresses?
// receiverTypes.insert(ReceiverType::Sprout);
useAnySprout = true;
} else if (address == "ANY_SAPLING") {
receiverTypes.insert(ReceiverType::Sapling);
useAnySapling = true;
isFromNonSprout = true;
} else {
auto addr = keyIO.DecodePaymentAddress(address);
if (addr.has_value()) {
examine(addr.value(), match {
[&](const CKeyID& taddr) {
taddrs.insert(taddr);
isFromNonSprout = true;
},
[&](const CScriptID& taddr) {
taddrs.insert(taddr);
isFromNonSprout = true;
},
[&](const libzcash::SaplingPaymentAddress& zaddr) {
zaddrs.push_back(zaddr);
isFromNonSprout = true;
},
[&](const libzcash::SproutPaymentAddress& zaddr) {
zaddrs.push_back(zaddr);
@ -5450,7 +5470,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
[&](libzcash::UnifiedAddress) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Unified addresses are not supported in z_mergetoaddress");
"Funds belonging to unified addresses can not be merged in z_mergetoaddress");
}
});
} else {
@ -5471,16 +5491,16 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
}
const int nextBlockHeight = chainActive.Height() + 1;
const bool overwinterActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER);
const bool saplingActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
const bool overwinterActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER);
const bool saplingActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING);
const bool canopyActive = chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
// Validate the destination address
auto destStr = params[1].get_str();
auto destaddress = keyIO.DecodePaymentAddress(destStr);
bool isToTaddr = false;
bool isToSproutZaddr = false;
bool isToSaplingZaddr = false;
if (destaddress.has_value()) {
examine(destaddress.value(), match {
[&](CKeyID addr) {
@ -5496,10 +5516,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated");
}
},
[&](libzcash::SproutPaymentAddress addr) {
isToSproutZaddr = true;
},
[&](libzcash::UnifiedAddress) {
[](libzcash::SproutPaymentAddress) { },
[](libzcash::UnifiedAddress) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, unified addresses are not yet supported.");
@ -5511,11 +5529,6 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
string("Invalid parameter, unknown address format: ") + destStr);
}
if (canopyActive && isFromNonSprout && isToSproutZaddr) {
// Value can be moved within Sprout, but not into Sprout.
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
}
// Convert fee from currency format to zatoshis
CAmount nFee = DEFAULT_FEE;
if (params.size() > 2) {
@ -5536,34 +5549,24 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
int sproutNoteLimit = MERGE_TO_ADDRESS_DEFAULT_SPROUT_LIMIT;
int saplingNoteLimit = MERGE_TO_ADDRESS_DEFAULT_SAPLING_LIMIT;
int shieldedNoteLimit = 0;
if (params.size() > 4) {
int nNoteLimit = params[4].get_int();
if (nNoteLimit < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Limit on maximum number of notes cannot be negative");
}
sproutNoteLimit = nNoteLimit;
saplingNoteLimit = nNoteLimit;
shieldedNoteLimit = nNoteLimit;
}
std::string memo;
std::optional<Memo> memo;
if (params.size() > 5) {
memo = params[5].get_str();
if (!(isToSproutZaddr || isToSaplingZaddr)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo can not be used with a taddr. It can only be used with a zaddr.");
} else if (!IsHex(memo)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format.");
}
if (memo.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 ));
}
memo = Memo::FromHexOrThrow(params[5].get_str());
}
MergeToAddressRecipient recipient(destaddress.value(), memo);
// Prepare to get UTXOs and notes
std::vector<MergeToAddressInputUTXO> utxoInputs;
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs;
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs;
SpendableInputs allInputs;
CAmount mergedUTXOValue = 0;
CAmount mergedNoteValue = 0;
CAmount remainingUTXOValue = 0;
@ -5576,9 +5579,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
unsigned int max_tx_size = saplingActive ? MAX_TX_SIZE_AFTER_SAPLING : MAX_TX_SIZE_BEFORE_SAPLING;
size_t estimatedTxSize = 200; // tx overhead + wiggle room
if (isToSproutZaddr) {
estimatedTxSize += JOINSPLIT_SIZE(SAPLING_TX_VERSION); // We assume that sapling has activated
} else if (isToSaplingZaddr) {
if (isToSaplingZaddr) {
estimatedTxSize += OUTPUTDESCRIPTION_SIZE;
}
@ -5615,8 +5616,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
maxedOutUTXOsFlag = true;
} else {
estimatedTxSize += increase;
COutPoint utxo(out.tx->GetHash(), out.i);
utxoInputs.emplace_back(utxo, nValue, scriptPubKey);
allInputs.utxos.push_back(out);
mergedUTXOValue += nValue;
}
}
@ -5629,51 +5629,59 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
if (useAnySprout || useAnySapling || zaddrs.size() > 0) {
// Get available notes
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
std::vector<OrchardNoteMetadata> orchardEntries;
std::optional<NoteFilter> noteFilter =
useAnySprout || useAnySapling ?
std::nullopt :
std::optional(NoteFilter::ForPaymentAddresses(zaddrs));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, std::nullopt, nAnchorConfirmations);
std::vector<SproutNoteEntry> sproutCandidateNotes;
std::vector<SaplingNoteEntry> saplingCandidateNotes;
std::vector<OrchardNoteMetadata> orchardCandidateNotes;
pwalletMain->GetFilteredNotes(
sproutCandidateNotes,
saplingCandidateNotes,
orchardCandidateNotes,
noteFilter,
std::nullopt,
nAnchorConfirmations);
// If Sapling is not active, do not allow sending from a sapling addresses.
if (!saplingActive && saplingEntries.size() > 0) {
if (!saplingActive && saplingCandidateNotes.size() > 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated");
}
// Do not include Sprout/Sapling notes if using "ANY_SAPLING"/"ANY_SPROUT" respectively
if (useAnySprout) {
saplingEntries.clear();
saplingCandidateNotes.clear();
}
if (useAnySapling) {
sproutEntries.clear();
sproutCandidateNotes.clear();
}
// Sending from both Sprout and Sapling is currently unsupported using z_mergetoaddress
if ((sproutEntries.size() > 0 && saplingEntries.size() > 0) || (useAnySprout && useAnySapling)) {
if ((sproutCandidateNotes.size() > 0 && saplingCandidateNotes.size() > 0) || (useAnySprout && useAnySapling)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Cannot send from both Sprout and Sapling addresses using z_mergetoaddress");
}
// If sending between shielded addresses, they must be within the same value pool
if ((saplingEntries.size() > 0 && isToSproutZaddr) || (sproutEntries.size() > 0 && isToSaplingZaddr)) {
if (sproutCandidateNotes.size() > 0 && isToSaplingZaddr) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Cannot send between Sprout and Sapling addresses using z_mergetoaddress");
}
// Find unspent notes and update estimated size
for (const SproutNoteEntry& entry : sproutEntries) {
for (const SproutNoteEntry& entry : sproutCandidateNotes) {
noteCounter++;
CAmount nValue = entry.note.value();
if (!maxedOutNotesFlag) {
// If we haven't added any notes yet and the merge is to a
// z-address, we have already accounted for the first JoinSplit.
size_t increase = (sproutNoteInputs.empty() && !isToSproutZaddr) || (sproutNoteInputs.size() % 2 == 0) ?
size_t increase = allInputs.sproutNoteEntries.empty() || allInputs.sproutNoteEntries.size() % 2 == 0 ?
JOINSPLIT_SIZE(SAPLING_TX_VERSION) : 0;
if (estimatedTxSize + increase >= max_tx_size ||
(sproutNoteLimit > 0 && noteCounter > sproutNoteLimit))
(sproutNoteLimit > 0 && noteCounter > sproutNoteLimit) ||
(shieldedNoteLimit > 0 && noteCounter > shieldedNoteLimit))
{
maxedOutNotesFlag = true;
} else {
@ -5681,7 +5689,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
auto zaddr = entry.address;
SproutSpendingKey zkey;
pwalletMain->GetSproutSpendingKey(zaddr, zkey);
sproutNoteInputs.emplace_back(entry.jsop, entry.note, nValue, zkey);
allInputs.sproutNoteEntries.push_back(entry);
mergedNoteValue += nValue;
}
}
@ -5691,13 +5699,14 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
}
}
for (const SaplingNoteEntry& entry : saplingEntries) {
for (const SaplingNoteEntry& entry : saplingCandidateNotes) {
noteCounter++;
CAmount nValue = entry.note.value();
if (!maxedOutNotesFlag) {
size_t increase = SPENDDESCRIPTION_SIZE;
if (estimatedTxSize + increase >= max_tx_size ||
(saplingNoteLimit > 0 && noteCounter > saplingNoteLimit))
(saplingNoteLimit > 0 && noteCounter > saplingNoteLimit) ||
(shieldedNoteLimit > 0 && noteCounter > shieldedNoteLimit))
{
maxedOutNotesFlag = true;
} else {
@ -5706,7 +5715,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
if (!pwalletMain->GetSaplingExtendedSpendingKey(entry.address, extsk)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Could not find spending key for payment address.");
}
saplingNoteInputs.emplace_back(entry.op, entry.note, nValue, extsk.expsk);
allInputs.saplingNoteEntries.push_back(entry);
mergedNoteValue += nValue;
}
}
@ -5717,8 +5726,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
}
}
size_t numUtxos = utxoInputs.size();
size_t numNotes = sproutNoteInputs.size() + saplingNoteInputs.size();
size_t numUtxos = allInputs.utxos.size();
size_t numNotes = allInputs.sproutNoteEntries.size() + allInputs.saplingNoteEntries.size();
if (numUtxos == 0 && numNotes == 0) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any funds to merge.");
@ -5751,45 +5760,80 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
contextInfo.pushKV("toaddress", params[1]);
contextInfo.pushKV("fee", ValueFromAmount(nFee));
if (!sproutNoteInputs.empty() || !saplingNoteInputs.empty() || !isToTaddr) {
// We have shielded inputs or the recipient is a shielded address, and
// therefore we cannot create transactions before Sapling activates.
if (!saplingActive) {
throw JSONRPCError(
RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated");
}
}
// The privacy policy is determined early so as to be able to use it
// for selector construction.
auto strategy =
ResolveTransactionStrategy(
ReifyPrivacyPolicy(
std::nullopt,
params.size() > 6 ? std::optional(params[6].get_str()) : std::nullopt),
InterpretLegacyCompat(std::nullopt, {recipient.first}));
bool isSproutShielded = sproutNoteInputs.size() > 0 || isToSproutZaddr;
bool isSproutShielded = allInputs.sproutNoteEntries.size() > 0;
// Contextual transaction we will build on
CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(
Params().GetConsensus(),
chainparams.GetConsensus(),
nextBlockHeight,
isSproutShielded || nPreferredTxVersion < ZIP225_MIN_TX_VERSION);
if (contextualTx.nVersion == 1 && isSproutShielded) {
contextualTx.nVersion = 2; // Tx format should support vJoinSplit
}
// Builder (used if Sapling addresses are involved)
std::optional<TransactionBuilder> builder;
if (isToSaplingZaddr || saplingNoteInputs.size() > 0) {
if (isToSaplingZaddr || allInputs.saplingNoteEntries.size() > 0) {
std::optional<uint256> orchardAnchor;
if (!isSproutShielded && nPreferredTxVersion >= ZIP225_MIN_TX_VERSION && nAnchorConfirmations > 0) {
// Allow Orchard recipients by setting an Orchard anchor.
auto orchardAnchorHeight = nextBlockHeight - nAnchorConfirmations;
if (Params().GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
if (chainparams.GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
auto anchorBlockIndex = chainActive[orchardAnchorHeight];
assert(anchorBlockIndex != nullptr);
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
}
}
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
}
WalletTxBuilder builder(Params(), minRelayTxFee);
// This currently isnt too critical since we dont yet use it for note selection and we never
// need a change address. The one thing this does is indicate whether or not Sprout can be
// selected. However, we eventually want a `ZTXOSelector` that can support this operation fully.
// I.e., be able to select from multiple pools in the legacy account.
std::optional<ZTXOSelector> ztxoSelector;
if (isSproutShielded) {
ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
allInputs.sproutNoteEntries[0].address,
true,
TransparentCoinbasePolicy::Disallow,
strategy.AllowLinkingAccountAddresses());
} else if (allInputs.saplingNoteEntries.size() > 0) {
ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
allInputs.saplingNoteEntries[0].address,
true,
TransparentCoinbasePolicy::Disallow,
strategy.AllowLinkingAccountAddresses());
} else {
ztxoSelector = CWallet::LegacyTransparentZTXOSelector(true, TransparentCoinbasePolicy::Disallow);
}
if (!ztxoSelector.has_value()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing spending key for an address to be merged.");
}
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_mergetoaddress(
std::move(builder), contextualTx, utxoInputs, sproutNoteInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
auto mergeOp = new AsyncRPCOperation_mergetoaddress(
std::move(builder),
ztxoSelector.value(),
allInputs,
recipient,
strategy,
nFee,
contextInfo);
mergeOp->prepare(chainparams, *pwalletMain).map_error([&](const InputSelectionError& err) {
ThrowInputSelectionError(err, ztxoSelector.value(), strategy);
});
std::shared_ptr<AsyncRPCOperation> operation(mergeOp);
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();

View File

@ -69,6 +69,13 @@ private:
fs::path old_cwd;
};
CWalletTx FakeWalletTx() {
CMutableTransaction mtx;
mtx.vout.resize(1);
mtx.vout[0].nValue = 1;
return CWalletTx(nullptr, mtx);
}
}
static UniValue ValueFromString(const std::string &str)
@ -1620,7 +1627,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
std::fill(v.begin(),v.end(), 'A');
std::string badmemo(v.begin(), v.end());
CheckRPCThrows("z_mergetoaddress [\"" + taddr1 + "\"] " + aSproutAddr + " 0.00001 100 100 " + badmemo,
"Invalid parameter, size of memo is larger than maximum allowed 512");
strprintf("Invalid parameter, memo is longer than the maximum allowed %d characters.", ZC_MEMO_SIZE));
// Mutable tx containing contextual information we need to build tx
UniValue retValue = CallRPC("getblockcount");
@ -1631,42 +1638,36 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
KeyIO keyIO(Params());
MergeToAddressRecipient testnetzaddr(
keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value(),
"testnet memo");
Memo());
WalletTxBuilder builder(Params(), minRelayTxFee);
auto selector = CWallet::LegacyTransparentZTXOSelector(
true,
TransparentCoinbasePolicy::Disallow);
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
try {
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, {}, {}, {}, testnetzaddr, -1 ));
BOOST_FAIL("Should have caused an error");
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, -1);
BOOST_FAIL("Fee value of -1 expected to be out of the valid range of values.");
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "Fee is out of range"));
}
try {
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, {}, {}, {}, testnetzaddr, 1));
BOOST_FAIL("Should have caused an error");
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "No inputs"));
{
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, 1);
operation.main();
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have -0.00000001, need 0.00000001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.");
}
std::vector<MergeToAddressInputUTXO> inputs = { MergeToAddressInputUTXO{ COutPoint{uint256(), 0}, 0, CScript()} };
auto wtx = FakeWalletTx();
SpendableInputs inputs;
inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true));
inputs.sproutNoteEntries.emplace_back(SproutNoteEntry {JSOutPoint(), SproutPaymentAddress(), SproutNote(), "", 0});
inputs.saplingNoteEntries.emplace_back(SaplingNoteEntry {SaplingOutPoint(), SaplingPaymentAddress(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), "", 0});
std::vector<MergeToAddressInputSproutNote> sproutNoteInputs =
{MergeToAddressInputSproutNote{JSOutPoint(), SproutNote(), 0, SproutSpendingKey()}};
std::vector<MergeToAddressInputSaplingNote> saplingNoteInputs =
{MergeToAddressInputSaplingNote{SaplingOutPoint(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), 0, SaplingExpandedSpendingKey()}};
// Sprout and Sapling inputs -> throw
try {
auto operation = new AsyncRPCOperation_mergetoaddress(std::nullopt, mtx, inputs, sproutNoteInputs, saplingNoteInputs, testnetzaddr, 1);
BOOST_FAIL("Should have caused an error");
} catch (const UniValue& objError) {
BOOST_CHECK(find_error(objError, "Cannot send from both Sprout and Sapling addresses using z_mergetoaddress"));
}
// Sprout inputs and TransactionBuilder -> throw
try {
auto operation = new AsyncRPCOperation_mergetoaddress(TransactionBuilder(), mtx, inputs, sproutNoteInputs, {}, testnetzaddr, 1);
BOOST_FAIL("Should have caused an error");
} catch (const UniValue& objError) {
BOOST_CHECK(find_error(objError, "Sprout notes are not supported by the TransactionBuilder"));
{
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, inputs, testnetzaddr, strategy, 1);
operation.main();
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have 0.00, need 0.00000001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.");
}
}

View File

@ -11,7 +11,7 @@ using namespace libzcash;
int GetAnchorHeight(const CChain& chain, uint32_t anchorConfirmations)
{
int nextBlockHeight = chain.Height() + 1;
return nextBlockHeight - anchorConfirmations;
return std::max(0, nextBlockHeight - (int) anchorConfirmations);
}
static size_t PadCount(size_t n)

View File

@ -49,7 +49,7 @@ public:
CAmount amount,
std::optional<Memo> memo) :
address(address), amount(amount), memo(memo) {
assert(amount >= 0);
assert(MoneyRange(amount));
}
const PaymentAddress& GetAddress() const {