diff --git a/Cargo.lock b/Cargo.lock index 710f255a3b6..2ddd48864c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,7 +1353,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.5.0" -source = "git+https://github.com/QED-it/orchard?branch=ivk-to-bytes-visibility-downgrade#3ba61462638a5c0e39a022cb8ddc16559c24dd06" +source = "git+https://github.com/QED-it/orchard?branch=ivk-to-bytes-visibility-downgrade#089df65fbb17a053b3060af1b704401c1ee86a7a" dependencies = [ "aes", "bitvec", diff --git a/qa/rpc-tests/asset_issuance.py b/qa/rpc-tests/asset_issuance.py new file mode 100755 index 00000000000..7a7e2daa9f2 --- /dev/null +++ b/qa/rpc-tests/asset_issuance.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 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.util import ( + NU5_BRANCH_ID, + assert_equal, + nuparams, + start_nodes, +) +from test_framework.mininode import CTransaction +from io import BytesIO +from binascii import unhexlify + +def issue_asset(self, node, address, asset_descr, amount, finalize): + txid = self.nodes[node].issue(0, address, asset_descr, amount, finalize) + + tx = CTransaction() + f = BytesIO(unhexlify(self.nodes[node].getrawtransaction(txid).encode('ascii'))) + tx.deserialize(f) + + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + return tx.issueBundle.actions[0].notes[0].asset.hex() + + +def check_asset_balance(self, node, asset, expected_balance): + walletinfo = self.nodes[node].getwalletinfo() + assert_equal(walletinfo['asset_balances'][asset]['confirmed_balance'], expected_balance) + + +def orchard_address(node): + acct = node.z_getnewaccount()['account'] + return node.z_getaddressforaccount(acct, ['orchard'])['address'] + + +class IssueTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.num_nodes = 4 + + def setup_nodes(self): + return start_nodes(self.num_nodes, self.options.tmpdir, [[ + nuparams(NU5_BRANCH_ID, 205), + '-regtestwalletsetbestchaineveryblock' + ]] * self.num_nodes) + + def run_test(self): + # Sanity-check the test harness + assert_equal(self.nodes[0].getblockcount(), 200) + + # Activate NU5 + self.nodes[0].generate(5) + self.sync_all() + + address0 = orchard_address(self.nodes[0]) + address1 = orchard_address(self.nodes[1]) + + # Issue assets to an address on node 0 + asset0 = issue_asset(self, 0, address0, "WBTC", 4000, False) + issue_asset(self, 0, address0, "WBTC", 2, False) + issue_asset(self, 0, address1, "WBTC", 23, False) + + # Issue assets to an address on node 1 + asset1 = issue_asset(self, 0, address1, "WETH", 42, False) + + check_asset_balance(self, 0, asset0, 4002) + check_asset_balance(self, 1, asset0, 23) + check_asset_balance(self, 1, asset1, 42) + +if __name__ == '__main__': + IssueTest().main() + diff --git a/qa/rpc-tests/issuance.py b/qa/rpc-tests/asset_transfer.py similarity index 50% rename from qa/rpc-tests/issuance.py rename to qa/rpc-tests/asset_transfer.py index ee6b54ed418..ecdf1381918 100755 --- a/qa/rpc-tests/issuance.py +++ b/qa/rpc-tests/asset_transfer.py @@ -9,7 +9,10 @@ assert_equal, nuparams, start_nodes, + wait_and_assert_operationid_status ) +from asset_issuance import issue_asset, check_asset_balance, orchard_address + class IssueTest(BitcoinTestFramework): def __init__(self): @@ -26,41 +29,34 @@ def run_test(self): # Sanity-check the test harness assert_equal(self.nodes[0].getblockcount(), 200) - # Get a new Orchard account on node 0 - acct0 = self.nodes[0].z_getnewaccount()['account'] - ua0 = self.nodes[0].z_getaddressforaccount(acct0, ['orchard'])['address'] - - # Get a new Orchard account on node 1 - acct1 = self.nodes[1].z_getnewaccount()['account'] - ua1 = self.nodes[1].z_getaddressforaccount(acct1, ['orchard'])['address'] - # Activate NU5 self.nodes[0].generate(5) self.sync_all() + address0 = orchard_address(self.nodes[0]) + address1 = orchard_address(self.nodes[1]) + address2 = orchard_address(self.nodes[2]) + + issued = 4003 + transfer1 = 1 + transfer2 = 2 + # Issue assets to an address on node 0 - self.nodes[0].issue(0, ua0, "WBTC", 4001, True) + asset = issue_asset(self, 0, address0, "WBTC", issued, True) + check_asset_balance(self, 0, asset, issued) - # Issue assets to an address on node 1 - self.nodes[0].issue(0, ua1, "WBTC", 42, True) + # Send assets from node 0 to node 1 + recipients = [{"address": address1, "amount": transfer1, "asset": asset}, {"address": address2, "amount": transfer2, "asset": asset}] + opid = self.nodes[0].z_sendassets(address0, recipients, 1) + wait_and_assert_operationid_status(self.nodes[0], opid) self.sync_all() self.nodes[0].generate(1) self.sync_all() - walletinfo0 = self.nodes[0].getwalletinfo() - print(walletinfo0) - assert_equal(len(walletinfo0['asset_balances'].items()), 1) - for key, value in walletinfo0['asset_balances'].items(): - assert_equal(value['confirmed_balance'], 4001) - - - walletinfo1 = self.nodes[1].getwalletinfo() - print(walletinfo1) - assert_equal(len(walletinfo1['asset_balances'].items()), 1) - for key, value in walletinfo1['asset_balances'].items(): - assert_equal(value['confirmed_balance'], 42) - + check_asset_balance(self, 0, asset, issued - transfer1 - transfer2) + check_asset_balance(self, 1, asset, transfer1) + check_asset_balance(self, 2, asset, transfer2) if __name__ == '__main__': diff --git a/qa/rpc-tests/test_framework/mininode.py b/qa/rpc-tests/test_framework/mininode.py index 682ee2d0443..9bb206c4fe0 100755 --- a/qa/rpc-tests/test_framework/mininode.py +++ b/qa/rpc-tests/test_framework/mininode.py @@ -524,20 +524,79 @@ def __repr__(self): ) +class Note(object): + def __init__(self): + self.recipient = None + self.value = 0 + self.asset = None + self.rho = None + self.rseed = None + + def deserialize(self, f): + self.recipient = f.read(43) + self.value = struct.unpack(" 0: + self.ik = f.read(32) + self.authorization = f.read(64) def serialize(self): r = b"" - r += ser_compact_size(0) + r += ser_vector(self.actions) + if len(self.actions) > 0: + r += self.ik + r += self.authorization return r def __repr__(self): - return "IssueBundle(Empty)" + return "IssueBundle(actions=%r)" % self.actions class Groth16Proof(object): def __init__(self): diff --git a/qa/zcash/smoke_tests.py b/qa/zcash/smoke_tests.py index 59871255ced..f43e777cd2b 100755 --- a/qa/zcash/smoke_tests.py +++ b/qa/zcash/smoke_tests.py @@ -229,6 +229,16 @@ def z_sendmany(results, case, zcash, from_addr, recipients, privacy_policy): def issue(results, case, zcash, account, addr, asset, amount, finalize): return async_txid_cmd(results, case, zcash, 'issue', [account, addr, asset, amount, finalize]) +def z_sendassets(results, case, zcash, from_addr, recipients): + return async_txid_cmd(results, case, zcash, 'z_sendassets', [ + from_addr, + [{ + 'address': to_addr, + 'amount': amount, + 'asset': asset, + } for (to_addr, amount, asset) in recipients] + ]) + def check_z_sendmany(results, case, zcash, from_addr, recipients, privacy_policy): txid = z_sendmany(results, case, zcash, from_addr, recipients, privacy_policy) if txid is None: diff --git a/src/Asset.h b/src/Asset.h index f10e139d98d..0f0bedddabe 100644 --- a/src/Asset.h +++ b/src/Asset.h @@ -25,6 +25,16 @@ class Asset { std::copy(id, id + ZC_ORCHARD_ZSA_ASSET_ID_SIZE, this->id); } + Asset(std::string hex) { + std::vector bytes; + for (unsigned int i = 0; i < hex.length(); i += 2) { + std::string byteString = hex.substr(i, 2); + unsigned char byte = (unsigned char) strtol(byteString.c_str(), NULL, 16); + bytes.push_back(byte); + } + std::copy(bytes.begin(), bytes.end(), this->id); + } + /** * Similar to 'derive' method from Rust * @param ik asset issuance key diff --git a/src/Makefile.am b/src/Makefile.am index 9bd6d2a91ca..7bb258b903c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -312,6 +312,7 @@ BITCOIN_CORE_H = \ wallet/asyncrpcoperation_mergetoaddress.h \ wallet/asyncrpcoperation_saplingmigration.h \ wallet/asyncrpcoperation_sendmany.h \ + wallet/asyncrpcoperation_sendassets.h \ wallet/asyncrpcoperation_shieldcoinbase.h \ wallet/wallet_tx_builder.h \ wallet/crypter.h \ @@ -411,6 +412,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/asyncrpcoperation_mergetoaddress.cpp \ wallet/asyncrpcoperation_saplingmigration.cpp \ wallet/asyncrpcoperation_sendmany.cpp \ + wallet/asyncrpcoperation_sendassets.cpp \ wallet/asyncrpcoperation_shieldcoinbase.cpp \ wallet/wallet_tx_builder.cpp \ wallet/crypter.cpp \ diff --git a/src/gtest/test_checktransaction.cpp b/src/gtest/test_checktransaction.cpp index f6e60622870..1f7578915fe 100644 --- a/src/gtest/test_checktransaction.cpp +++ b/src/gtest/test_checktransaction.cpp @@ -1415,14 +1415,14 @@ TEST(ChecktransactionTests, NU5AcceptsOrchardShieldedCoinbase) { .ToIncomingViewingKey() .Address(0); uint256 ovk; - builder.AddOutput(ovk, to, CAmount(123456), std::nullopt); + builder.AddOutput(ovk, to, CAmount(123456), std::nullopt, Asset::Native()); // orchard::Builder pads to two Actions, but does so using a "no OVK" policy for // dummy outputs, which violates coinbase rules requiring all shielded outputs to // be recoverable. We manually add a dummy output to sidestep this issue. // TODO: If/when we have funding streams going to Orchard recipients, this dummy // output can be removed. - builder.AddOutput(ovk, to, 0, std::nullopt); + builder.AddOutput(ovk, to, 0, std::nullopt, Asset::Native()); auto bundle = builder .Build().value() @@ -1537,14 +1537,14 @@ TEST(ChecktransactionTests, NU5EnforcesOrchardRulesOnShieldedCoinbase) { .ToIncomingViewingKey() .Address(0); uint256 ovk; - builder.AddOutput(ovk, to, CAmount(1000), std::nullopt); + builder.AddOutput(ovk, to, CAmount(1000), std::nullopt, Asset::Native()); // orchard::Builder pads to two Actions, but does so using a "no OVK" policy for // dummy outputs, which violates coinbase rules requiring all shielded outputs to // be recoverable. We manually add a dummy output to sidestep this issue. // TODO: If/when we have funding streams going to Orchard recipients, this dummy // output can be removed. - builder.AddOutput(ovk, to, 0, std::nullopt); + builder.AddOutput(ovk, to, 0, std::nullopt, Asset::Native()); auto bundle = builder .Build().value() diff --git a/src/gtest/test_mempoollimit.cpp b/src/gtest/test_mempoollimit.cpp index 9abffbf2d4c..9cf2d473d69 100644 --- a/src/gtest/test_mempoollimit.cpp +++ b/src/gtest/test_mempoollimit.cpp @@ -138,7 +138,7 @@ TEST(MempoolLimitTests, MempoolCostAndEvictionWeight) auto builder = TransactionBuilder(Params(), 1, std::nullopt); builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness()); builder.AddSaplingOutput(fvk.ovk, pa, 25000, {}); - static_assert(MINIMUM_FEE == 10000); + static_assert(MINIMUM_FEE == 0); // TODO re-enable fees builder.SetFee(MINIMUM_FEE-1); auto [cost, evictionWeight] = MempoolCostAndEvictionWeight(builder.Build().GetTxOrThrow(), MINIMUM_FEE-1); diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp index be3c4d5e31b..131e9a1d6c0 100644 --- a/src/gtest/test_transaction_builder.cpp +++ b/src/gtest/test_transaction_builder.cpp @@ -250,7 +250,7 @@ TEST(TransactionBuilder, TransparentToOrchard) // 0.00005 t-ZEC in, 0.00004 z-ZEC out, default fee auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore); builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 5000); - builder.AddOrchardOutput(std::nullopt, recipient, 4000, std::nullopt); + builder.AddOrchardOutput(std::nullopt, recipient, 4000, std::nullopt, Asset::Native()); auto maybeTx = builder.Build(); EXPECT_TRUE(maybeTx.IsTx()); if (maybeTx.IsError()) { diff --git a/src/main.cpp b/src/main.cpp index 0648487078e..374f6b12f08 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1767,9 +1767,6 @@ bool AcceptToMemoryPool( return false; } - LogPrint("mempool", "Checking txid %s", tx.GetHash().ToString()); - LogPrint("mempool", "issue bundle num actions = %d, actions num = %d", tx.GetIssueBundle().GetDetails()->num_actions(), tx.GetIssueBundle().GetDetails()->actions().size()); - auto verifier = ProofVerifier::Strict(); if (!CheckTransaction(tx, state, verifier)) return false; @@ -1903,33 +1900,32 @@ bool AcceptToMemoryPool( CTxMemPoolEntry entry(tx, nFees, GetTime(), chainActive.Height(), pool.HasNoInputsOf(tx), fSpendsCoinbase, nSigOps, consensusBranchId); unsigned int nSize = entry.GetTxSize(); - // TODO return fees -// // No transactions are allowed with modified fee below the minimum relay fee, -// // except from disconnected blocks. The minimum relay fee will never be more -// // than LEGACY_DEFAULT_FEE zatoshis. -// CAmount minRelayFee = ::minRelayTxFee.GetFeeForRelay(nSize); -// if (fLimitFree && nModifiedFees < minRelayFee) { -// LogPrint("mempool", -// "Not accepting transaction with txid %s, size %d bytes, effective fee %d " + MINOR_CURRENCY_UNIT + -// ", and fee delta %d " + MINOR_CURRENCY_UNIT + " to the mempool due to insufficient fee. " + -// " The minimum acceptance/relay fee for this transaction is %d " + MINOR_CURRENCY_UNIT, -// tx.GetHash().ToString(), nSize, nModifiedFees, nModifiedFees - nFees, minRelayFee); -// return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "min relay fee not met"); -// } -// -// // Transactions with more than `-txunpaidactionlimit` unpaid actions (calculated -// // using the modified fee) are not accepted to the mempool or relayed. -// // -// size_t nUnpaidActionCount = entry.GetUnpaidActionCount(); -// if (nUnpaidActionCount > nTxUnpaidActionLimit) { -// LogPrint("mempool", -// "Not accepting transaction with txid %s, size %d bytes, effective fee %d " + MINOR_CURRENCY_UNIT + -// ", and fee delta %d " + MINOR_CURRENCY_UNIT + " to the mempool because it has %d unpaid actions" -// ", which is over the limit of %d. The conventional fee for this transaction is %d " + MINOR_CURRENCY_UNIT, -// tx.GetHash().ToString(), nSize, nModifiedFees, nModifiedFees - nFees, nUnpaidActionCount, -// nTxUnpaidActionLimit, tx.GetConventionalFee()); -// return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "tx unpaid action limit exceeded"); -// } + // No transactions are allowed with modified fee below the minimum relay fee, + // except from disconnected blocks. The minimum relay fee will never be more + // than LEGACY_DEFAULT_FEE zatoshis. + CAmount minRelayFee = ::minRelayTxFee.GetFeeForRelay(nSize); + if (fLimitFree && nModifiedFees < minRelayFee) { + LogPrint("mempool", + "Not accepting transaction with txid %s, size %d bytes, effective fee %d " + MINOR_CURRENCY_UNIT + + ", and fee delta %d " + MINOR_CURRENCY_UNIT + " to the mempool due to insufficient fee. " + + " The minimum acceptance/relay fee for this transaction is %d " + MINOR_CURRENCY_UNIT, + tx.GetHash().ToString(), nSize, nModifiedFees, nModifiedFees - nFees, minRelayFee); + return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "min relay fee not met"); + } + + // Transactions with more than `-txunpaidactionlimit` unpaid actions (calculated + // using the modified fee) are not accepted to the mempool or relayed. + // + size_t nUnpaidActionCount = entry.GetUnpaidActionCount(); + if (nUnpaidActionCount > nTxUnpaidActionLimit) { + LogPrint("mempool", + "Not accepting transaction with txid %s, size %d bytes, effective fee %d " + MINOR_CURRENCY_UNIT + + ", and fee delta %d " + MINOR_CURRENCY_UNIT + " to the mempool because it has %d unpaid actions" + ", which is over the limit of %d. The conventional fee for this transaction is %d " + MINOR_CURRENCY_UNIT, + tx.GetHash().ToString(), nSize, nModifiedFees, nModifiedFees - nFees, nUnpaidActionCount, + nTxUnpaidActionLimit, tx.GetConventionalFee()); + return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "tx unpaid action limit exceeded"); + } if (fRejectAbsurdFee && nFees > maxTxFee) { return state.Invalid(false, @@ -2614,22 +2610,24 @@ bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoins } - nValueIn += tx.GetShieldedValueIn(); - if (!MoneyRange(nValueIn)) - return state.DoS(100, error("CheckInputs(): shielded input to transparent value pool out of range"), - REJECT_INVALID, "bad-txns-inputvalues-outofrange"); - - if (nValueIn < tx.GetValueOut()) - return state.DoS(100, false, REJECT_INVALID, "bad-txns-in-belowout", false, - strprintf("value in (%s) < value out (%s)", FormatMoney(nValueIn), FormatMoney(tx.GetValueOut()))); - - // Tally transaction fees - CAmount nTxFee = nValueIn - tx.GetValueOut(); - if (nTxFee < 0) - return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-negative"); - nFees += nTxFee; - if (!MoneyRange(nFees)) - return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-outofrange"); + // TODO Implement fees and inputs check per asset +// nValueIn += tx.GetShieldedValueIn(); +// +// if (!MoneyRange(nValueIn)) +// return state.DoS(100, error("CheckInputs(): shielded input to transparent value pool out of range"), +// REJECT_INVALID, "bad-txns-inputvalues-outofrange"); +// +// if (nValueIn < tx.GetValueOut()) +// return state.DoS(100, false, REJECT_INVALID, "bad-txns-in-belowout", false, +// strprintf("value in (%s) < value out (%s)", FormatMoney(nValueIn), FormatMoney(tx.GetValueOut()))); +// +// // Tally transaction fees +// CAmount nTxFee = nValueIn - tx.GetValueOut(); +// if (nTxFee < 0) +// return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-negative"); +// nFees += nTxFee; +// if (!MoneyRange(nFees)) +// return state.DoS(100, false, REJECT_INVALID, "bad-txns-fee-outofrange"); } return true; } @@ -3462,6 +3460,28 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin } } + if (tx.GetIssueBundle().IsPresent()) { + try { + auto appendResult = orchard_tree.AppendIssueBundle(tx.GetIssueBundle()); + if (fUpdateOrchardSubtrees && appendResult.has_subtree_boundary) { + libzcash::SubtreeData subtree(appendResult.completed_subtree_root, pindex->nHeight); + + view.PushSubtree(ORCHARD, subtree); + auto latest = view.GetLatestSubtree(ORCHARD); + + // The latest subtree, according to the view, should now be one + // less than the "current" subtree index according to the tree + // itself, after the append takes place earlier in this loop. + assert(latest.has_value()); + assert((latest->index + 1) == orchard_tree.current_subtree_index()); + } + } catch (const rust::Error& e) { + return state.DoS(100, + error("ConnectBlock(): block would overfill the Orchard commitment tree."), + REJECT_INVALID, "orchard-commitment-tree-full"); + } + } + for (const auto& out : tx.vout) { transparentValueDelta += out.nValue; } @@ -8155,9 +8175,6 @@ CMutableTransaction CreateNewContextualCMutableTransaction( mtx.nVersionGroupId = txVersionInfo.nVersionGroupId; mtx.nVersion = txVersionInfo.nVersion; - LogPrint("mempool", "TCreateNewContextualCMutableTransaction-nVersionGroupId: %d\n", mtx.nVersionGroupId); - LogPrint("mempool", "CreateNewContextualCMutableTransaction-nVersion: %d\n", mtx.nVersion); - if (mtx.fOverwintered) { if (mtx.nVersion >= ZIP225_TX_VERSION) { mtx.nConsensusBranchId = CurrentEpochBranchId(nHeight, consensusParams); diff --git a/src/main.h b/src/main.h index 1698c2bf24f..fa238e4588a 100644 --- a/src/main.h +++ b/src/main.h @@ -69,7 +69,7 @@ static const bool DEFAULT_WHITELISTRELAY = true; /** Default for DEFAULT_WHITELISTFORCERELAY. */ static const bool DEFAULT_WHITELISTFORCERELAY = true; /** Default for -minrelaytxfee, minimum relay fee rate for transactions in zatoshis per 1000 bytes. TODO(misnamed, this is a rate) */ -static const unsigned int DEFAULT_MIN_RELAY_TX_FEE = 100; +static const unsigned int DEFAULT_MIN_RELAY_TX_FEE = 0; // TODO re-enable fees = 100 /** Default for -maxtxfee in zatoshis. */ static const CAmount DEFAULT_TRANSACTION_MAXFEE = 0.1 * COIN; /** Discourage users from setting fee rates higher than this in zatoshis per 1000 bytes. */ diff --git a/src/miner.cpp b/src/miner.cpp index ae598ffc90f..41417fb0fa6 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -225,7 +225,7 @@ class AddOutputsToCoinbaseTxAndSign // Shielded coinbase outputs must be recoverable with an all-zeroes ovk. uint256 ovk; auto miner_reward = SetFoundersRewardAndGetMinerValue(*saplingBuilder); - builder.AddOutput(ovk, to, miner_reward, std::nullopt); + builder.AddOutput(ovk, to, miner_reward, std::nullopt, Asset::Native()); // orchard::Builder pads to two Actions, but does so using a "no OVK" policy for // dummy outputs, which violates coinbase rules requiring all shielded outputs to @@ -238,7 +238,7 @@ class AddOutputsToCoinbaseTxAndSign .ToFullViewingKey() .ToIncomingViewingKey() .Address(0); - builder.AddOutput(ovk, dummyTo, 0, std::nullopt); + builder.AddOutput(ovk, dummyTo, 0, std::nullopt, Asset::Native()); auto bundle = builder.Build(); if (!bundle.has_value()) { diff --git a/src/rpc/common.h b/src/rpc/common.h index 3a0418e309f..8896c8db579 100644 --- a/src/rpc/common.h +++ b/src/rpc/common.h @@ -182,7 +182,8 @@ static const CRPCConvertTable rpcCvtTable = { "z_mergetoaddress", {{o, s}, {o, o, o, n, s}} }, { "z_listoperationids", {{}, {s}} }, { "z_getnotescount", {{}, {o, o}} }, - { "issue", { {s, s, s, s, s}, {} }}, + { "issue", {{s, s, s, s, s}, {} }}, + { "z_sendassets", {{s, o}, {o} }}, // server { "help", {{}, {s}} }, { "setlogfilter", {{s}, {}} }, diff --git a/src/rust/include/rust/builder.h b/src/rust/include/rust/builder.h index 8e16782151f..45605b61cb4 100644 --- a/src/rust/include/rust/builder.h +++ b/src/rust/include/rust/builder.h @@ -63,6 +63,7 @@ bool orchard_builder_add_recipient( const unsigned char* ovk, const OrchardRawAddressPtr* recipient, uint64_t value, + const unsigned char* asset, const unsigned char* memo); /// Builds a bundle containing the given spent notes and recipients. diff --git a/src/rust/include/rust/orchard/wallet.h b/src/rust/include/rust/orchard/wallet.h index 44428b561c5..c49c3b7b092 100644 --- a/src/rust/include/rust/orchard/wallet.h +++ b/src/rust/include/rust/orchard/wallet.h @@ -298,9 +298,10 @@ typedef void (*push_note_callback_t)(void* resultVector, const RawOrchardNoteMet void orchard_wallet_get_filtered_notes( const OrchardWalletPtr* wallet, const OrchardIncomingViewingKeyPtr* ivk, + const unsigned char* asset_bytes, bool ignoreMined, bool requireSpendingKey, - bool nativeOnly, + bool allAssets, void* resultVector, push_note_callback_t push_cb ); diff --git a/src/rust/src/bridge.rs b/src/rust/src/bridge.rs index 50cb17016db..7eae94284cc 100644 --- a/src/rust/src/bridge.rs +++ b/src/rust/src/bridge.rs @@ -386,6 +386,10 @@ pub(crate) mod ffi { fn root(self: &Orchard) -> [u8; 32]; fn size(self: &Orchard) -> u64; fn append_bundle(self: &mut Orchard, bundle: &Bundle) -> Result; + fn append_issue_bundle( + self: &mut Orchard, + bundle: &IssueBundle, + ) -> Result; unsafe fn init_wallet(self: &Orchard, wallet: *mut OrchardWallet) -> bool; } diff --git a/src/rust/src/builder_ffi.rs b/src/rust/src/builder_ffi.rs index 1f0e2957fd1..8c00495ac99 100644 --- a/src/rust/src/builder_ffi.rs +++ b/src/rust/src/builder_ffi.rs @@ -97,6 +97,7 @@ pub extern "C" fn orchard_builder_add_recipient( ovk: *const [u8; 32], recipient: *const orchard::Address, value: u64, + asset_bytes: *const [u8; 32], memo: *const [u8; 512], ) -> bool { let builder = unsafe { builder.as_mut() }.expect("Builder may not be null."); @@ -106,8 +107,12 @@ pub extern "C" fn orchard_builder_add_recipient( let recipient = unsafe { recipient.as_ref() }.expect("Recipient may not be null."); let value = NoteValue::from_raw(value); let memo = unsafe { memo.as_ref() }.copied(); + let safe_asset_bytes = unsafe { asset_bytes.as_ref() } + .copied() + .expect("Asset may not be null"); + let asset = AssetBase::from_bytes(&safe_asset_bytes).unwrap(); - match builder.add_recipient(ovk, *recipient, value, AssetBase::native(), memo) { + match builder.add_recipient(ovk, *recipient, value, asset, memo) { Ok(()) => true, Err(e) => { error!("Failed to add Orchard recipient: {}", e); diff --git a/src/rust/src/issue_bundle.rs b/src/rust/src/issue_bundle.rs index 088e3d9dbd3..a5b4e79f7e4 100644 --- a/src/rust/src/issue_bundle.rs +++ b/src/rust/src/issue_bundle.rs @@ -1,14 +1,14 @@ -use std::{mem, ptr}; use crate::{bridge::ffi, streams::CppStream}; +use std::{mem, ptr}; -use orchard::{Address, issuance as issuance}; use issuance::Signed; use memuse::DynamicUsage; -use orchard::value::NoteValue; use orchard::issuance::IssueInfo; use orchard::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey}; +use orchard::value::NoteValue; +use orchard::{issuance, Address}; use rand_core::OsRng; -use zcash_primitives::transaction::components::{issuance as issue_serialization}; +use zcash_primitives::transaction::components::issuance as issue_serialization; pub struct IssueNote(orchard::note::Note); @@ -74,30 +74,36 @@ pub(crate) fn create_issue_bundle( recipient: *const ffi::OrchardRawAddressPtr, asset_descr: String, ) -> Box { - let recipient: Address = *unsafe { - recipient - .cast::
() - .as_ref() - }.expect("IssuanceAuthorizingKey may not be null."); + let recipient: Address = *unsafe { recipient.cast::
().as_ref() } + .expect("IssuanceAuthorizingKey may not be null."); - let isk = unsafe { - isk - .cast::() - .as_ref() - }.expect("IssuanceAuthorizingKey may not be null."); + let isk = unsafe { isk.cast::().as_ref() } + .expect("IssuanceAuthorizingKey may not be null."); let ik: IssuanceValidatingKey = IssuanceValidatingKey::from(isk); let rng = OsRng; - let bundle = issuance::IssueBundle::new(ik, asset_descr, Some(IssueInfo { recipient, value: NoteValue::from_raw(value) }), rng).unwrap().0; - + let bundle = issuance::IssueBundle::new( + ik, + asset_descr, + Some(IssueInfo { + recipient, + value: NoteValue::from_raw(value), + }), + rng, + ) + .unwrap() + .0; + + // TODO use real tx sighash let sighash: [u8; 32] = bundle.commitment().into(); - Box::new(IssueBundle(Some(bundle.prepare(sighash).sign(rng, isk).unwrap()))) + Box::new(IssueBundle(Some( + bundle.prepare(sighash).sign(rng, isk).unwrap(), + ))) } impl IssueBundle { - pub(crate) unsafe fn from_raw_box(bundle: *mut ffi::IssueBundlePtr) -> Box { Box::new(IssueBundle(if bundle.is_null() { None diff --git a/src/rust/src/merkle_frontier.rs b/src/rust/src/merkle_frontier.rs index ae4b3b7d71a..15650026e6b 100644 --- a/src/rust/src/merkle_frontier.rs +++ b/src/rust/src/merkle_frontier.rs @@ -4,13 +4,15 @@ use incrementalmerkletree::{ frontier::{CommitmentTree, Frontier}, Hashable, Level, }; +use nonempty::NonEmpty; +use orchard::note::ExtractedNoteCommitment; use orchard::tree::MerkleHashOrchard; use zcash_primitives::{ merkle_tree::{read_frontier_v1, write_commitment_tree, write_frontier_v1, HashSer}, sapling::NOTE_COMMITMENT_TREE_DEPTH, }; -use crate::{bridge::ffi, orchard_bundle, streams::CppStream, wallet::Wallet}; +use crate::{bridge::ffi, issue_bundle, orchard_bundle, streams::CppStream, wallet::Wallet}; // This is also defined in `IncrementalMerkleTree.hpp` pub const TRACKED_SUBTREE_HEIGHT: u8 = 16; @@ -104,40 +106,67 @@ impl Orchard { bundle: &orchard_bundle::Bundle, ) -> Result { if let Some(bundle) = bundle.inner() { - // A single bundle can't contain 2^TRACKED_SUBTREE_HEIGHT actions, so we'll never cross - // more than one subtree boundary while processing that bundle. This means we only need - // to find a single subtree root while processing an individual bundle, so `Option` is - // sufficient; we don't need a `Vec`. - let mut tracked_root: Option = None; - for action in bundle.actions().iter() { - if !self.0.append(MerkleHashOrchard::from_cmx(action.cmx())) { - return Err("Orchard note commitment tree is full."); - } + let actions = bundle.actions().clone(); + self.append_commitments(actions.map(|action| *action.cmx())) + } else { + Err("null Orchard bundle pointer") + } + } - if let Some(non_empty_frontier) = self.0.value() { - let level = Level::from(TRACKED_SUBTREE_HEIGHT); - let pos = non_empty_frontier.position(); - if pos.is_complete_subtree(level) { - assert_eq!(tracked_root, None); - tracked_root = Some(non_empty_frontier.root(Some(level))) - } - } + /// Appends the note commitments in the given issue bundle to this frontier. + pub(crate) fn append_issue_bundle( + &mut self, + bundle: &issue_bundle::IssueBundle, + ) -> Result { + if let Some(bundle) = bundle.inner() { + let commitments: Vec = bundle + .actions() + .iter() + .flat_map(|a| a.notes()) + .map(|note| note.commitment().into()) + .collect(); + self.append_commitments(NonEmpty::from_vec(commitments).unwrap()) + } else { + Err("null Orchard bundle pointer") + } + } + + /// Appends the note commitments to this frontier. + fn append_commitments( + &mut self, + commitments: NonEmpty, + ) -> Result { + // A single bundle can't contain 2^TRACKED_SUBTREE_HEIGHT actions, so we'll never cross + // more than one subtree boundary while processing that bundle. This means we only need + // to find a single subtree root while processing an individual bundle, so `Option` is + // sufficient; we don't need a `Vec`. + let mut tracked_root: Option = None; + for commitment in commitments { + if !self.0.append(MerkleHashOrchard::from_cmx(&commitment)) { + return Err("Orchard note commitment tree is full."); } - Ok(if let Some(root_hash) = tracked_root { - ffi::OrchardAppendResult { - has_subtree_boundary: true, - completed_subtree_root: root_hash.to_bytes(), + if let Some(non_empty_frontier) = self.0.value() { + let level = Level::from(TRACKED_SUBTREE_HEIGHT); + let pos = non_empty_frontier.position(); + if pos.is_complete_subtree(level) { + assert_eq!(tracked_root, None); + tracked_root = Some(non_empty_frontier.root(Some(level))) } - } else { - ffi::OrchardAppendResult { - has_subtree_boundary: false, - completed_subtree_root: [0u8; 32], - } - }) - } else { - Err("null Orchard bundle pointer") + } } + + Ok(if let Some(root_hash) = tracked_root { + ffi::OrchardAppendResult { + has_subtree_boundary: true, + completed_subtree_root: root_hash.to_bytes(), + } + } else { + ffi::OrchardAppendResult { + has_subtree_boundary: false, + completed_subtree_root: [0u8; 32], + } + }) } /// Overwrites the first bridge of the Orchard wallet's note commitment tree to have diff --git a/src/rust/src/note_encryption.rs b/src/rust/src/note_encryption.rs index 2136b689803..0afb1946a62 100644 --- a/src/rust/src/note_encryption.rs +++ b/src/rust/src/note_encryption.rs @@ -1,8 +1,7 @@ use std::convert::TryInto; -use zcash_note_encryption::{ - try_output_recovery_with_ovk, EphemeralKeyBytes, ShieldedOutput, -}; +use zcash_note_encryption::{try_output_recovery_with_ovk, EphemeralKeyBytes, ShieldedOutput}; +use zcash_primitives::sapling::note_encryption::{CompactNoteCiphertextBytes, NoteCiphertextBytes}; use zcash_primitives::{ consensus::BlockHeight, keys::OutgoingViewingKey, @@ -14,7 +13,6 @@ use zcash_primitives::{ SaplingIvk, }, }; -use zcash_primitives::sapling::note_encryption::{CompactNoteCiphertextBytes, NoteCiphertextBytes}; use crate::{bridge::ffi::SaplingShieldedOutput, params::Network}; diff --git a/src/rust/src/rustzcash.rs b/src/rust/src/rustzcash.rs index 8c21ca0a7b0..e001675fc09 100644 --- a/src/rust/src/rustzcash.rs +++ b/src/rust/src/rustzcash.rs @@ -79,11 +79,11 @@ mod bundlecache; mod history_ffi; mod incremental_merkle_tree; mod init_ffi; +mod issue_bundle; +mod issue_ffi; mod merkle_frontier; mod note_encryption; -mod issue_ffi; mod orchard_bundle; -mod issue_bundle; mod orchard_ffi; mod orchard_keys_ffi; mod params; diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs index 449fee23ed1..3336c950535 100644 --- a/src/rust/src/wallet.rs +++ b/src/rust/src/wallet.rs @@ -19,7 +19,7 @@ use zcash_primitives::{ use orchard::issuance::{IssueBundle, Signed}; use orchard::keys::IssuanceAuthorizingKey; -use orchard::note::ExtractedNoteCommitment; +use orchard::note::{AssetBase, ExtractedNoteCommitment}; use orchard::{ bundle::Authorized, keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, Scope, SpendingKey}, @@ -735,7 +735,7 @@ impl Wallet { ivk: Option<&IncomingViewingKey>, ignore_mined: bool, require_spending_key: bool, - native_only: bool, + asset: Option, ) -> Vec<(OutPoint, DecryptedNote)> { tracing::trace!("Filtering notes"); self.wallet_received_notes @@ -745,7 +745,7 @@ impl Wallet { .decrypted_notes .iter() .filter_map(move |(idx, dnote)| { - if native_only && dnote.note.asset().is_native().unwrap_u8() == 0 { + if asset.is_some() && asset.unwrap() != dnote.note.asset() { return None; } @@ -1186,17 +1186,26 @@ pub type NotePushCb = unsafe extern "C" fn(obj: Option, met pub extern "C" fn orchard_wallet_get_filtered_notes( wallet: *const Wallet, ivk: *const IncomingViewingKey, + asset_bytes: *const [u8; 32], ignore_mined: bool, require_spending_key: bool, - native_only: bool, + all_assets: bool, result: Option, push_cb: Option, ) { let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null."); let ivk = unsafe { ivk.as_ref() }; + let asset = if all_assets { + None + } else { + let safe_asset_bytes = unsafe { asset_bytes.as_ref() } + .copied() + .expect("Asset may not be null"); + Some(AssetBase::from_bytes(&safe_asset_bytes).unwrap()) + }; for (outpoint, dnote) in - wallet.get_filtered_notes(ivk, ignore_mined, require_spending_key, native_only) + wallet.get_filtered_notes(ivk, ignore_mined, require_spending_key, asset) { let metadata = FFINoteMetadata { txid: *outpoint.txid.as_ref(), diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp index 70592b1732c..4712c05e57c 100644 --- a/src/transaction_builder.cpp +++ b/src/transaction_builder.cpp @@ -77,17 +77,22 @@ void Builder::AddOutput( const std::optional& ovk, const libzcash::OrchardRawAddress& to, CAmount value, - const std::optional& memo) + const std::optional& memo, + Asset asset) { if (!inner) { throw std::logic_error("orchard::Builder has already been used"); } + // TODO implement multi-asset transfers + primaryAsset = asset; + orchard_builder_add_recipient( inner.get(), ovk.has_value() ? ovk->begin() : nullptr, to.inner.get(), value, + asset.id, memo.has_value() ? memo.value().ToBytes().data() : nullptr); hasActions = true; @@ -291,7 +296,8 @@ void TransactionBuilder::AddOrchardOutput( const std::optional& ovk, const libzcash::OrchardRawAddress& to, CAmount value, - const std::optional& memo) + const std::optional& memo, + Asset asset) { if (!orchardBuilder.has_value()) { // Try to give a useful error. @@ -303,7 +309,7 @@ void TransactionBuilder::AddOrchardOutput( throw std::runtime_error("TransactionBuilder cannot add Orchard output without Orchard anchor"); } } - orchardBuilder.value().AddOutput(ovk, to, value, memo); + orchardBuilder.value().AddOutput(ovk, to, value, memo, asset); valueBalanceOrchard -= value; } @@ -476,12 +482,13 @@ TransactionBuilderResult TransactionBuilder::Build() // if (change > 0) { + auto asset = orchardBuilder.value().primaryAsset.value(); // Send change to the specified change address. If no change address // was set, send change to the first Sapling address given as input // if any; otherwise the first Sprout address given as input. // (A t-address can only be used as the change address if explicitly set.) if (orchardChangeAddr) { - AddOrchardOutput(orchardChangeAddr->first, orchardChangeAddr->second, change, std::nullopt); + AddOrchardOutput(orchardChangeAddr->first, orchardChangeAddr->second, change, std::nullopt, asset); } else if (saplingChangeAddr) { AddSaplingOutput(saplingChangeAddr->first, saplingChangeAddr->second, change, std::nullopt); } else if (sproutChangeAddr) { @@ -491,7 +498,7 @@ TransactionBuilderResult TransactionBuilder::Build() AddTransparentOutput(tChangeAddr.value(), change); } else if (firstOrchardSpendAddr.has_value()) { auto ovk = orchardSpendingKeys[0].ToFullViewingKey().ToInternalOutgoingViewingKey(); - AddOrchardOutput(ovk, firstOrchardSpendAddr.value(), change, std::nullopt); + AddOrchardOutput(ovk, firstOrchardSpendAddr.value(), change, std::nullopt, asset); } else if (firstSaplingSpendAddr.has_value()) { uint256 ovk; libzcash::SaplingPaymentAddress changeAddr; diff --git a/src/transaction_builder.h b/src/transaction_builder.h index 4d29afbd938..5d7512855ce 100644 --- a/src/transaction_builder.h +++ b/src/transaction_builder.h @@ -106,6 +106,9 @@ class Builder { return *this; } + // TODO temporary measure to ensure single-asset bundles + std::optional primaryAsset; + /// Adds a note to be spent in this bundle. /// /// Returns `false` if the given Merkle path does not have the required anchor @@ -117,7 +120,8 @@ class Builder { const std::optional& ovk, const libzcash::OrchardRawAddress& to, CAmount value, - const std::optional& memo); + const std::optional& memo, + Asset asset); /// Returns `true` if any spends or outputs have been added to this builder. This can /// be used to avoid calling `Build()` and creating a dummy Orchard bundle. @@ -349,7 +353,8 @@ class TransactionBuilder const std::optional& ovk, const libzcash::OrchardRawAddress& to, CAmount value, - const std::optional& memo); + const std::optional& memo, + Asset asset); // Throws if the anchor does not match the anchor used by // previously-added Sapling spends. diff --git a/src/wallet/asyncrpcoperation_sendassets.cpp b/src/wallet/asyncrpcoperation_sendassets.cpp new file mode 100644 index 00000000000..8c96c4d9dfd --- /dev/null +++ b/src/wallet/asyncrpcoperation_sendassets.cpp @@ -0,0 +1,216 @@ +// Copyright (c) 2016-2023 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +#include "asyncrpcoperation_sendassets.h" + +#include "amount.h" +#include "asyncrpcoperation_common.h" +#include "asyncrpcqueue.h" +#include "consensus/upgrades.h" +#include "core_io.h" +#include "experimental_features.h" +#include "init.h" +#include "key_io.h" +#include "main.h" +#include "net.h" +#include "netbase.h" +#include "proof_verifier.h" +#include "rpc/protocol.h" +#include "rpc/server.h" +#include "timedata.h" +#include "util/system.h" +#include "util/match.h" +#include "util/moneystr.h" +#include "wallet.h" +#include "walletdb.h" +#include "script/interpreter.h" +#include "util/time.h" +#include "zcash/IncrementalMerkleTree.hpp" +#include "miner.h" +#include "wallet/wallet_tx_builder.h" +#include "Asset.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace libzcash; + +AsyncRPCOperation_sendassets::AsyncRPCOperation_sendassets( + WalletTxBuilder builder, + ZTXOSelector ztxoSelector, + std::vector recipients, + int minDepth, + unsigned int anchorDepth, + UniValue contextInfo) : + builder_(std::move(builder)), ztxoSelector_(ztxoSelector), recipients_(recipients), + mindepth_(minDepth), anchordepth_(anchorDepth), strategy_(TransactionStrategy(PrivacyPolicy::FullPrivacy)), + contextinfo_(contextInfo), fee_(std::nullopt) +{ + assert(mindepth_ >= 0); + assert(!recipients_.empty()); + assert(ztxoSelector.RequireSpendingKeys()); + + // Log the context info i.e. the call parameters to z_sendassets + if (LogAcceptCategory("zrpcunsafe")) { + LogPrint("zrpcunsafe", "%s: z_sendassets initialized (params=%s)\n", getId(), contextInfo.write()); + } else { + LogPrint("zrpc", "%s: z_sendassets initialized\n", getId()); + } +} + +AsyncRPCOperation_sendassets::~AsyncRPCOperation_sendassets() { +} + +void AsyncRPCOperation_sendassets::main() { + if (isCancelled()) + return; + + set_state(OperationStatus::EXECUTING); + start_execution_clock(); + +#ifdef ENABLE_MINING + GenerateBitcoins(false, 0, Params()); +#endif + + std::optional txid; + try { + txid = main_impl(*pwalletMain) + .map_error([&](const InputSelectionError& err) { + ThrowInputSelectionError(err, ztxoSelector_, strategy_); + }) + .value(); + } catch (const UniValue& objError) { + int code = find_value(objError, "code").get_int(); + std::string message = find_value(objError, "message").get_str(); + set_error_code(code); + set_error_message(message); + } catch (const runtime_error& e) { + set_error_code(-1); + set_error_message("runtime error: " + string(e.what())); + } catch (const logic_error& e) { + set_error_code(-1); + set_error_message("logic error: " + string(e.what())); + } catch (const exception& e) { + set_error_code(-1); + set_error_message("general exception: " + string(e.what())); + } catch (...) { + set_error_code(-2); + set_error_message("unknown error"); + } + +#ifdef ENABLE_MINING + GenerateBitcoins(GetBoolArg("-gen", false), GetArg("-genproclimit", 1), Params()); +#endif + + stop_execution_clock(); + + if (txid.has_value()) { + set_state(OperationStatus::SUCCESS); + } else { + set_state(OperationStatus::FAILED); + } + + std::string s = strprintf("%s: z_sendassets finished (status=%s", getId(), getStateAsString()); + if (txid.has_value()) { + s += strprintf(", txid=%s)\n", txid.value().ToString()); + } else { + s += strprintf(", error=%s)\n", getErrorMessage()); + } + LogPrintf("%s",s); +} + +// Construct and send the transaction, returning the resulting txid. +// Errors in transaction construction will throw. +// +// Notes: +// 1. #1159 Currently there is no limit set on the number of elements, which could +// make the tx too large. +// 2. #1360 Note selection is not optimal. +// 3. #1277 Spendable notes are not locked, so an operation running in parallel +// could also try to use them. +// 4. #3615 There is no padding of inputs or outputs, which may leak information. +// +// At least #4 differs from the Rust transaction builder. +tl::expected +AsyncRPCOperation_sendassets::main_impl(CWallet& wallet) { + + auto asset = recipients_[0].GetAsset(); // TODO support multiple assets + + auto spendable = builder_.FindAllSpendableAssets(wallet, asset, ztxoSelector_, mindepth_); + + auto preparedTx = builder_.PrepareTransaction( + wallet, + ztxoSelector_, + spendable, + recipients_, + chainActive, + strategy_, + fee_, + anchordepth_); + + return preparedTx + .map([&](const TransactionEffects& effects) { + effects.LockSpendable(wallet); + try { + const auto& spendable = effects.GetSpendable(); + const auto& payments = effects.GetPayments(); + spendable.LogInputs(getId()); + + LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n", getId(), + FormatMoney(payments.Total()), + FormatMoney(spendable.Total()), + FormatMoney(effects.GetFee())); + LogPrint("zrpc", "%s: total transparent input: %s (to choose from)\n", getId(), + FormatMoney(spendable.GetTransparentTotal())); + LogPrint("zrpcunsafe", "%s: total shielded input: %s (to choose from)\n", getId(), + FormatMoney(spendable.GetSaplingTotal() + spendable.GetOrchardTotal())); + LogPrint("zrpc", "%s: total transparent output: %s\n", getId(), + FormatMoney(payments.GetTransparentTotal())); + LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", getId(), + FormatMoney(payments.GetSaplingTotal())); + LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(), + FormatMoney(payments.GetOrchardTotal())); + LogPrint("zrpcunsafe", "%s: requested fee: %s\n", getId(), + fee_.has_value() ? FormatMoney(fee_.value()) : "default"); + LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(effects.GetFee())); + + auto buildResult = effects.ApproveAndBuild( + Params(), + wallet, + chainActive, + strategy_); + auto tx = buildResult.GetTxOrThrow(); + LogPrint("zrpc", "%s, conventional fee: %s\n", getId(), FormatMoney(tx.GetConventionalFee())); + + UniValue sendResult = SendTransaction(tx, payments.GetResolvedPayments(), std::nullopt, testmode); + set_result(sendResult); + + effects.UnlockSpendable(wallet); + return tx.GetHash(); + } catch (...) { + effects.UnlockSpendable(wallet); + throw; + } + }); +} + +/** + * Override getStatus() to append the operation's input parameters to the default status object. + */ +UniValue AsyncRPCOperation_sendassets::getStatus() const { + UniValue v = AsyncRPCOperation::getStatus(); + if (contextinfo_.isNull()) { + return v; + } + + UniValue obj = v.get_obj(); + obj.pushKV("method", "z_sendassets"); + obj.pushKV("params", contextinfo_ ); + return obj; +} diff --git a/src/wallet/asyncrpcoperation_sendassets.h b/src/wallet/asyncrpcoperation_sendassets.h new file mode 100644 index 00000000000..36cdaf006be --- /dev/null +++ b/src/wallet/asyncrpcoperation_sendassets.h @@ -0,0 +1,83 @@ +// Copyright (c) 2016-2023 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +#ifndef ZCASH_WALLET_ASYNCRPCOPERATION_SENDASSETS_H +#define ZCASH_WALLET_ASYNCRPCOPERATION_SENDASSETS_H + +#include "asyncrpcoperation.h" +#include "amount.h" +#include "primitives/transaction.h" +#include "transaction_builder.h" +#include "zcash/JoinSplit.hpp" +#include "zcash/Address.hpp" +#include "wallet.h" +#include "wallet/paymentdisclosure.h" +#include "wallet/wallet_tx_builder.h" + +#include +#include +#include +#include + +#include + +using namespace libzcash; + +class AsyncRPCOperation_sendassets : public AsyncRPCOperation { +public: + AsyncRPCOperation_sendassets( + WalletTxBuilder builder, + ZTXOSelector ztxoSelector, + std::vector recipients, + int minDepth, + unsigned int anchorDepth, + UniValue contextInfo = NullUniValue); + + virtual ~AsyncRPCOperation_sendassets(); + + // We don't want to be copied or moved around + AsyncRPCOperation_sendassets(AsyncRPCOperation_sendassets const&) = delete; // Copy construct + AsyncRPCOperation_sendassets(AsyncRPCOperation_sendassets&&) = delete; // Move construct + AsyncRPCOperation_sendassets& operator=(AsyncRPCOperation_sendassets const&) = delete; // Copy assign + AsyncRPCOperation_sendassets& operator=(AsyncRPCOperation_sendassets &&) = delete; // Move assign + + virtual void main(); + + virtual UniValue getStatus() const; + + bool testmode{false}; // Set to true to disable sending txs and generating proofs + +private: + friend class TEST_FRIEND_AsyncRPCOperation_sendassets; // class for unit testing + + WalletTxBuilder builder_; + ZTXOSelector ztxoSelector_; + std::vector recipients_; + int mindepth_{1}; + unsigned int anchordepth_{nAnchorConfirmations}; + TransactionStrategy strategy_; + std::optional fee_; + UniValue contextinfo_; // optional data to include in return value from getStatus() + + tl::expected main_impl(CWallet& wallet); +}; + +// To test private methods, a friend class can act as a proxy +class TEST_FRIEND_AsyncRPCOperation_sendassets { +public: + std::shared_ptr delegate; + + TEST_FRIEND_AsyncRPCOperation_sendassets(std::shared_ptr ptr) : delegate(ptr) {} + + tl::expected main_impl(CWallet& wallet) { + return delegate->main_impl(wallet); + } + + void set_state(OperationStatus state) { + delegate->state_.store(state); + } +}; + + +#endif // ZCASH_WALLET_ASYNCRPCOPERATION_SENDASSETS_H diff --git a/src/wallet/gtest/test_orchard_wallet.cpp b/src/wallet/gtest/test_orchard_wallet.cpp index 6e326b8f62c..718c93bce7e 100644 --- a/src/wallet/gtest/test_orchard_wallet.cpp +++ b/src/wallet/gtest/test_orchard_wallet.cpp @@ -36,7 +36,7 @@ CTransaction FakeOrchardTx(const OrchardSpendingKey& sk, libzcash::diversifier_i auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore); builder.SetFee(10000); builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000); - builder.AddOrchardOutput(std::nullopt, recipient, 40000, std::nullopt); + builder.AddOrchardOutput(std::nullopt, recipient, 40000, std::nullopt, Asset::Native()); auto maybeTx = builder.Build(); EXPECT_TRUE(maybeTx.IsTx()); @@ -106,7 +106,7 @@ TEST(TransactionBuilder, OrchardToOrchard) { // Select the one note in the wallet for spending. std::vector notes; wallet.GetFilteredNotes( - notes, sk.ToFullViewingKey().ToIncomingViewingKey(), true, true, true); + notes, sk.ToFullViewingKey().ToIncomingViewingKey(), true, true); ASSERT_EQ(notes.size(), 1); // If we attempt to get spend info now, it will fail because the note hasn't @@ -132,7 +132,7 @@ TEST(TransactionBuilder, OrchardToOrchard) { // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00014 z-ZEC change auto builder = TransactionBuilder(Params(), 2, orchardAnchor); EXPECT_TRUE(builder.AddOrchardSpend(sk, std::move(spendInfo[0].second))); - builder.AddOrchardOutput(std::nullopt, recipient, 25000, std::nullopt); + builder.AddOrchardOutput(std::nullopt, recipient, 25000, std::nullopt, Asset::Native()); auto maybeTx = builder.Build(); EXPECT_TRUE(maybeTx.IsTx()); if (maybeTx.IsError()) { diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index 8b0682df024..851230f7323 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -824,7 +824,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) { // Generate a bundle containing output note A. auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore); builder.AddTransparentInput(COutPoint(uint256(), 0), scriptPubKey, 5000); - builder.AddOrchardOutput(std::nullopt, recipient, 4000, {}); + builder.AddOrchardOutput(std::nullopt, recipient, 4000, {}, Asset::Native()); auto maybeTx = builder.Build(); EXPECT_TRUE(maybeTx.IsTx()); if (maybeTx.IsError()) { diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index 9bf755be411..7c185b1a7ba 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -401,7 +401,7 @@ class OrchardWallet orchard_wallet_add_full_viewing_key(inner.get(), fvk.inner.get()); } - void AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey& isk) { + void AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey& isk) const { orchard_wallet_add_issuance_authorizing_key(inner.get(), accountId, isk.inner.get()); } @@ -451,14 +451,21 @@ class OrchardWallet const std::optional& ivk, bool ignoreMined, bool requireSpendingKey, - bool nativeOnly) const { + std::optional asset = Asset::Native()) const { + + bool allAssets = !asset.has_value(); + unsigned char *assetId = nullptr; + if (asset.has_value()) { + assetId = (unsigned char *)asset.value().id; + } orchard_wallet_get_filtered_notes( inner.get(), ivk.has_value() ? ivk.value().inner.get() : nullptr, + assetId, ignoreMined, requireSpendingKey, - nativeOnly, + allAssets, &orchardNotesRet, PushOrchardNoteMeta ); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 816df67658f..861f50dc6f7 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -45,6 +45,7 @@ #include "wallet/asyncrpcoperation_mergetoaddress.h" #include "wallet/asyncrpcoperation_saplingmigration.h" #include "wallet/asyncrpcoperation_sendmany.h" +#include "wallet/asyncrpcoperation_sendassets.h" #include "wallet/asyncrpcoperation_shieldcoinbase.h" #include "wallet/wallet_tx_builder.h" @@ -350,6 +351,298 @@ UniValue issue(const UniValue ¶ms, bool fHelp) { return wtx.GetHash().GetHex(); } +static std::optional ParseMemo(const UniValue& memoValue) +{ + if (memoValue.isNull()) { + return std::nullopt; + } else { + auto memoStr = memoValue.get_str(); + auto rawMemo = ParseHex(memoStr); + + // If ParseHex comes across a non-hex char, it will stop but still return results so far. + if (memoStr.size() != rawMemo.size() * 2) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Invalid parameter, expected memo data in hexadecimal format."); + } else { + return libzcash::Memo::FromBytes(rawMemo) + .map_error([](auto err) { + switch (err) { + case libzcash::Memo::ConversionError::MemoTooLong: + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf( + "Invalid parameter, memo is longer than the maximum allowed %d bytes.", + libzcash::Memo::SIZE)); + default: + assert(false); + } + }) + .value(); + } + } +} + + +size_t EstimateTxSize( + const ZTXOSelector& ztxoSelector, + const std::vector& recipients, + int nextBlockHeight) { + CMutableTransaction mtx; + mtx.fOverwintered = true; + mtx.nConsensusBranchId = CurrentEpochBranchId(nextBlockHeight, Params().GetConsensus()); + + bool fromSprout = ztxoSelector.SelectsSprout(); + bool fromTaddr = ztxoSelector.SelectsTransparent(); + + // 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; + size_t saplingRecipientCount = 0; + size_t orchardRecipientCount = 0; + for (const Payment& recipient : recipients) { + examine(recipient.GetAddress(), match { + [&](const CKeyID&) { + taddrRecipientCount += 1; + }, + [&](const CScriptID&) { + taddrRecipientCount += 1; + }, + [&](const libzcash::SaplingPaymentAddress& addr) { + saplingRecipientCount += 1; + }, + [&](const libzcash::SproutPaymentAddress& addr) { + JSDescription jsdesc; + jsdesc.proof = GrothProof(); + mtx.vJoinSplit.push_back(jsdesc); + }, + [&](const libzcash::UnifiedAddress& addr) { + if (addr.GetOrchardReceiver().has_value()) { + orchardRecipientCount += 1; + } else if (addr.GetSaplingReceiver().has_value()) { + saplingRecipientCount += 1; + } else if (addr.GetP2PKHReceiver().has_value() + || addr.GetP2SHReceiver().has_value()) { + taddrRecipientCount += 1; + } + } + }); + } + + bool nu5Active = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_NU5); + + if (fromSprout || !nu5Active) { + mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; + mtx.nVersion = SAPLING_TX_VERSION; + } else { + mtx.nVersionGroupId = ZIP225_VERSION_GROUP_ID; + mtx.nVersion = ZIP225_TX_VERSION; + } + // Fine to call this because we are only testing that `mtx` is a valid size. + mtx.saplingBundle = sapling::test_only_invalid_bundle(0, saplingRecipientCount, 0); + + CTransaction tx(mtx); + txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion); + if (fromTaddr) { + txsize += CTXIN_SPEND_P2PKH_SIZE; + txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change + } + txsize += CTXOUT_REGULAR_SIZE * taddrRecipientCount; + + if (orchardRecipientCount > 0) { + // - The Orchard transaction builder pads to a minimum of 2 actions. + // - We subtract 1 because `GetSerializeSize(tx, ...)` already counts + // `ZC_ZIP225_ORCHARD_NUM_ACTIONS_BASE_SIZE`. + txsize += ZC_ZIP225_ORCHARD_BASE_SIZE - 1 + ZC_ZIP225_ORCHARD_MARGINAL_SIZE * std::max(2, (int) orchardRecipientCount) + ZC_ISSUE_BASE_SIZE; + } + return txsize; +} + + +UniValue z_sendassets(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() < 2 || params.size() > 3) + throw runtime_error( + "z_sendassets \"fromaddress\" [{\"address\":... ,\"amount\":... ,\"asset\":...},...] ( minconf )\n" + "\nSend a transaction with multiple recipients. Amounts are decimal numbers with at" + "\nmost 8 digits of precision. When sending from a unified address," + "\nchange is returned to the internal-only address for the associated unified account." + + HelpRequiringPassphrase() + "\n" + "\nArguments:\n" + "1. \"fromaddress\" (string, required) The shielded address to send the funds from.\n" + " If a unified address is provided for this argument, the TXOs to be spent will be selected from those\n" + " associated with the account corresponding to that unified address, from value pools corresponding\n" + " to the receivers included in the UA.\n" + "2. \"amounts\" (array, required) An array of json objects representing the amounts to send.\n" + " [{\n" + " \"address\":address (string, required) The address is a taddr, zaddr, or Unified Address\n" + " \"amount\":amount (numeric, required) The numeric amount in " + CURRENCY_UNIT + " is the value\n" + " \"asset\":asset (string, required) AssetBase in hex\n" + " \"memo\":memo (string, optional) If the address is a zaddr, raw data represented in hexadecimal string format. If\n" + " the output is being sent to a transparent address, it’s an error to include this field.\n" + " }, ... ]\n" + "3. minconf (numeric, optional, default=" + strprintf("%u", DEFAULT_NOTE_CONFIRMATIONS) + ") Only use funds confirmed at least this many times.\n" + "\nResult:\n" + "\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" + "\nExamples:\n" + + HelpExampleCli("z_sendassets", "\"ANY_TADDR\" '[{\"address\": \"t1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\", \"amount\": 2.0}]'") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + const auto& chainparams = Params(); + int nextBlockHeight = chainActive.Height() + 1; + + ThrowIfInitialBlockDownload(); + if (!chainparams.GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated"); + } + + KeyIO keyIO(chainparams); + + UniValue outputs = params[1].get_array(); + if (outputs.size() == 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty."); + } + + std::set recipientAddrs; + std::vector recipients; + + CAmount nTotalOut = 0; + size_t nOrchardOutputs = 0; + 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 std::string& s : o.getKeys()) { + if (s != "address" && s != "amount" && s != "memo" && s != "asset") + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ") + s); + } + + // TODO check that we only accept Orchard adresses + std::string addrStr = find_value(o, "address").get_str(); + auto addr = keyIO.DecodePaymentAddress(addrStr); + if (!addr.has_value()) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + std::string("Invalid parameter, unknown address format: ") + addrStr); + } + + if (!recipientAddrs.insert(addr.value()).second) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient address: ") + addrStr); + } + + auto memo = ParseMemo(find_value(o, "memo")); + + UniValue av = find_value(o, "amount"); + CAmount nAmount = AmountFromValue( av ); + if (nAmount < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); + } + + std::string assetStr = find_value(o, "asset").get_str(); + Asset asset = Asset(assetStr); + + recipients.push_back(Payment(addr.value(), nAmount, asset, memo)); + nTotalOut += nAmount; + } + if (recipients.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients"); + } + + // Check that the from address is valid. + // Unified address (UA) allowed here (#5185) + auto fromaddress = params[0].get_str(); + std::optional sender; + if (fromaddress != "ANY_TADDR") { + auto decoded = keyIO.DecodePaymentAddress(fromaddress); + if (decoded.has_value()) { + sender = decoded.value(); + } else { + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Invalid from address: should be a taddr, zaddr, UA, or the string 'ANY_TADDR'."); + } + } + + auto ztxoSelector = [&]() { + if (!sender.has_value()) { + return CWallet::LegacyTransparentZTXOSelector(true, TransparentCoinbasePolicy::Disallow); + } else { + auto ztxoSelectorOpt = pwalletMain->ZTXOSelectorForAddress( + sender.value(), + true, + TransparentCoinbasePolicy::Disallow, + UnifiedAccountSpendingPolicy::ShieldedOnly); + if (!ztxoSelectorOpt.has_value()) { + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Invalid from address, no payment source found for address."); + } + + auto selectorAccount = pwalletMain->FindAccountForSelector(ztxoSelectorOpt.value()); + bool unknownOrLegacy = !selectorAccount.has_value() || selectorAccount.value() == ZCASH_LEGACY_ACCOUNT; + examine(sender.value(), match { + [&](const libzcash::UnifiedAddress& ua) { + if (unknownOrLegacy) { + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Invalid from address, UA does not correspond to a known account."); + } + }, + [&](const auto& other) { + if (!unknownOrLegacy) { + throw JSONRPCError( + RPC_INVALID_ADDRESS_OR_KEY, + "Invalid from address: is a bare receiver from a Unified Address in this wallet. Provide the UA as returned by z_getaddressforaccount instead."); + } + } + }); + + return ztxoSelectorOpt.value(); + } + }(); + + // Sanity check for transaction size + // TODO: move this to the builder? + auto txsize = EstimateTxSize(ztxoSelector, recipients, nextBlockHeight); + if (txsize > MAX_TX_SIZE_AFTER_SAPLING) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", MAX_TX_SIZE_AFTER_SAPLING)); + } + + // Minimum confirmations + int nMinDepth = parseMinconf(DEFAULT_NOTE_CONFIRMATIONS, params, 2, std::nullopt); + + // Use input parameters as the optional context info to be returned by z_getoperationstatus and z_getoperationresult. + UniValue o(UniValue::VOBJ); + o.pushKV("fromaddress", params[0]); + o.pushKV("amounts", params[1]); + o.pushKV("minconf", nMinDepth); + + UniValue contextInfo = o; + + // Create operation and add to global queue + auto nAnchorDepth = std::min((unsigned int) nMinDepth, nAnchorConfirmations); + WalletTxBuilder builder(chainparams, minRelayTxFee); + + std::shared_ptr q = getAsyncRPCQueue(); + std::shared_ptr operation( + new AsyncRPCOperation_sendassets( + std::move(builder), ztxoSelector, recipients, nMinDepth, nAnchorDepth, contextInfo) + ); + q->addOperation(operation); + AsyncRPCOperationId operationId = operation->getId(); + return operationId; +} + static void SendMoney(const CTxDestination &address, CAmount nValue, bool fSubtractFeeFromAmount, CWalletTx& wtxNew) { CAmount curBalance = pwalletMain->GetBalance(std::nullopt); @@ -4827,113 +5120,6 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO return ret; } -size_t EstimateTxSize( - const ZTXOSelector& ztxoSelector, - const std::vector& recipients, - int nextBlockHeight) { - CMutableTransaction mtx; - mtx.fOverwintered = true; - mtx.nConsensusBranchId = CurrentEpochBranchId(nextBlockHeight, Params().GetConsensus()); - - bool fromSprout = ztxoSelector.SelectsSprout(); - bool fromTaddr = ztxoSelector.SelectsTransparent(); - - // 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; - size_t saplingRecipientCount = 0; - size_t orchardRecipientCount = 0; - for (const Payment& recipient : recipients) { - examine(recipient.GetAddress(), match { - [&](const CKeyID&) { - taddrRecipientCount += 1; - }, - [&](const CScriptID&) { - taddrRecipientCount += 1; - }, - [&](const libzcash::SaplingPaymentAddress& addr) { - saplingRecipientCount += 1; - }, - [&](const libzcash::SproutPaymentAddress& addr) { - JSDescription jsdesc; - jsdesc.proof = GrothProof(); - mtx.vJoinSplit.push_back(jsdesc); - }, - [&](const libzcash::UnifiedAddress& addr) { - if (addr.GetOrchardReceiver().has_value()) { - orchardRecipientCount += 1; - } else if (addr.GetSaplingReceiver().has_value()) { - saplingRecipientCount += 1; - } else if (addr.GetP2PKHReceiver().has_value() - || addr.GetP2SHReceiver().has_value()) { - taddrRecipientCount += 1; - } - } - }); - } - - bool nu5Active = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_NU5); - - if (fromSprout || !nu5Active) { - mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID; - mtx.nVersion = SAPLING_TX_VERSION; - } else { - mtx.nVersionGroupId = ZIP225_VERSION_GROUP_ID; - mtx.nVersion = ZIP225_TX_VERSION; - } - // Fine to call this because we are only testing that `mtx` is a valid size. - mtx.saplingBundle = sapling::test_only_invalid_bundle(0, saplingRecipientCount, 0); - - CTransaction tx(mtx); - txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion); - if (fromTaddr) { - txsize += CTXIN_SPEND_P2PKH_SIZE; - txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change - } - txsize += CTXOUT_REGULAR_SIZE * taddrRecipientCount; - - if (orchardRecipientCount > 0) { - // - The Orchard transaction builder pads to a minimum of 2 actions. - // - We subtract 1 because `GetSerializeSize(tx, ...)` already counts - // `ZC_ZIP225_ORCHARD_NUM_ACTIONS_BASE_SIZE`. - txsize += ZC_ZIP225_ORCHARD_BASE_SIZE - 1 + ZC_ZIP225_ORCHARD_MARGINAL_SIZE * std::max(2, (int) orchardRecipientCount) + ZC_ISSUE_BASE_SIZE; - } - return txsize; -} - -static std::optional ParseMemo(const UniValue& memoValue) -{ - if (memoValue.isNull()) { - return std::nullopt; - } else { - auto memoStr = memoValue.get_str(); - auto rawMemo = ParseHex(memoStr); - - // If ParseHex comes across a non-hex char, it will stop but still return results so far. - if (memoStr.size() != rawMemo.size() * 2) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "Invalid parameter, expected memo data in hexadecimal format."); - } else { - return libzcash::Memo::FromBytes(rawMemo) - .map_error([](auto err) { - switch (err) { - case libzcash::Memo::ConversionError::MemoTooLong: - throw JSONRPCError( - RPC_INVALID_PARAMETER, - strprintf( - "Invalid parameter, memo is longer than the maximum allowed %d bytes.", - libzcash::Memo::SIZE)); - default: - assert(false); - } - }) - .value(); - } - } -} - UniValue z_sendmany(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) @@ -6129,6 +6315,7 @@ static const CRPCCommand commands[] = { "wallet", "z_viewtransaction", &z_viewtransaction, false }, { "wallet", "z_getnotescount", &z_getnotescount, false }, { "wallet", "issue", &issue, true }, + { "wallet", "z_sendassets", &z_sendassets, true }, // TODO: rearrange into another category { "disclosure", "z_getpaymentdisclosure", &z_getpaymentdisclosure, true }, { "disclosure", "z_validatepaymentdisclosure", &z_validatepaymentdisclosure, true } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 16b29c381a5..99777c7e102 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -272,7 +272,7 @@ bool CWallet::AddOrchardFullViewingKey(const libzcash::OrchardFullViewingKey &fv return true; // TODO ORCHARD: persist fvk } -bool CWallet::AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey &isk) +bool CWallet::AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey &isk) const { AssertLockHeld(cs_wallet); // orchardWallet orchardWallet.AddIssuanceAuthorizingKey(accountId, isk); @@ -2437,7 +2437,8 @@ SpendableInputs CWallet::FindSpendableInputs( for (const auto& ivk : orchardIvks) { std::vector incomingNotes; - orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true, true); + + orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true, Asset::Native()); for (auto& noteMeta : incomingNotes) { if (IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) { @@ -2463,6 +2464,83 @@ SpendableInputs CWallet::FindSpendableInputs( return unspent; } +SpendableInputs CWallet::FindSpendableAssets( + const Asset asset, + ZTXOSelector selector, + uint32_t minDepth, + const std::optional &asOfHeight) const { + AssertLockHeld(cs_main); + AssertLockHeld(cs_wallet); + + KeyIO keyIO(Params()); + + SpendableInputs unspent; + + // for Orchard, we select both the internal and external IVKs. + auto orchardIvks = examine(selector.GetPattern(), match{ + [&](const libzcash::UnifiedAddress &selectorUA) -> std::vector { + auto orchardReceiver = selectorUA.GetOrchardReceiver(); + if (orchardReceiver.has_value()) { + auto meta = GetUFVKMetadataForReceiver(orchardReceiver.value()); + if (meta.has_value()) { + auto ufvk = GetUnifiedFullViewingKey(meta.value().GetUFVKId()); + if (ufvk.has_value()) { + auto fvk = ufvk->GetOrchardKey(); + if (fvk.has_value()) { + return {fvk->ToIncomingViewingKey(), fvk->ToInternalIncomingViewingKey()}; + } + } + } + } + return {}; + }, + [&](const libzcash::UnifiedFullViewingKey &ufvk) -> std::vector { + auto fvk = ufvk.GetOrchardKey(); + if (fvk.has_value()) { + return {fvk->ToIncomingViewingKey(), fvk->ToInternalIncomingViewingKey()}; + } + return {}; + }, + [&](const AccountZTXOPattern &acct) -> std::vector { + auto ufvk = GetUnifiedFullViewingKeyByAccount(acct.GetAccountId()); + if (ufvk.has_value()) { + auto fvk = ufvk->GetOrchardKey(); + if (fvk.has_value()) { + return {fvk->ToIncomingViewingKey(), fvk->ToInternalIncomingViewingKey()}; + } + } + return {}; + }, + [&](const auto &addr) -> std::vector { return {}; } + }); + + for (const auto &ivk: orchardIvks) { + std::vector incomingNotes; + + orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true, asset); + + for (auto ¬eMeta: incomingNotes) { + if (IsOrchardSpent(noteMeta.GetOutPoint(), asOfHeight)) { + continue; + } + + auto mit = mapWallet.find(noteMeta.GetOutPoint().hash); + + // We should never get an outpoint from the Orchard wallet where + // the transaction does not exist in the main wallet. + assert(mit != mapWallet.end()); + + int confirmations = mit->second.GetDepthInMainChain(asOfHeight); + if (confirmations < 0) continue; + if (confirmations >= minDepth) { + noteMeta.SetConfirmations(confirmations); + unspent.orchardNoteMetadata.push_back(noteMeta); + } + } + } + return unspent; +} + /** * Outpoint is spent if any non-conflicted transaction * spends it: @@ -5136,7 +5214,7 @@ map CWallet::getAssetBalances(std::optionalGetFilteredOrchardNotes(orchardEntries, orchardUnconfirmedEntries, noteFilter, asOfHeight, 1, INT_MAX, true, ignoreUnspendable, false); + pwalletMain->GetFilteredOrchardNotes(orchardEntries, orchardUnconfirmedEntries, noteFilter, asOfHeight, 1, INT_MAX, true, ignoreUnspendable, std::nullopt); for (auto & entry : orchardEntries) { std::string asset = entry.GetAsset().ToHexString(); @@ -7040,20 +7118,21 @@ bool CWallet::HaveOrchardSpendingKeyForAddress( return orchardWallet.GetSpendingKeyForAddress(addr).has_value(); } -IssuanceAuthorizingKey generateDummyIssuanceAuthorizingKey() { - auto coinType = Params().BIP44CoinType(); - auto seed = MnemonicSeed::Random(coinType); - auto ik = IssuanceKey::ForAccount(seed, coinType, 0); - auto isk = ik.ToIssuanceAuthorizingKey(); - return isk; -} - IssuanceAuthorizingKey CWallet::GetIssuanceAuthorizingKey(const int accountId) const { auto isk_opt = orchardWallet.GetIssuanceAuthorizingKeyForAccountId(accountId); if (isk_opt.has_value()) { return isk_opt.value(); } else { - return generateDummyIssuanceAuthorizingKey(); // TODO orchardWallet.GenerateNewIssuanceAuthorizingKey(); + auto seed = GetMnemonicSeed(); + if (!seed.has_value()) { + throw std::runtime_error(std::string(__func__) + ": Wallet has no mnemonic HD seed."); + } + auto isk = IssuanceKey::ForAccount(seed.value(), Params().BIP44CoinType(), accountId).ToIssuanceAuthorizingKey(); + bool success = AddIssuanceAuthorizingKey(accountId, isk); + if (!success) + return nullptr; + else + return isk; } } @@ -7079,7 +7158,7 @@ void CWallet::GetFilteredNotes( bool ignoreSpent, bool requireSpendingKey, bool ignoreLocked, - bool nativeOnly) const + const std::optional asset) const { // Don't bother to do anything if the note filter would reject all notes if (noteFilter.has_value() && noteFilter.value().IsEmpty()) @@ -7211,7 +7290,7 @@ void CWallet::GetFilteredNotes( ivk.value(), ignoreSpent, requireSpendingKey, - nativeOnly); + asset); } } } else { @@ -7221,7 +7300,7 @@ void CWallet::GetFilteredNotes( std::nullopt, ignoreSpent, requireSpendingKey, - nativeOnly); + asset); } for (auto& noteMeta : orchardNotes) { @@ -7252,7 +7331,7 @@ void CWallet::GetFilteredOrchardNotes( int maxDepth, bool ignoreSpent, bool requireSpendingKey, - bool nativeOnly) const + std::optional asset) const { bool ignoreLocked = true; @@ -7274,7 +7353,7 @@ void CWallet::GetFilteredOrchardNotes( ivk.value(), ignoreSpent, requireSpendingKey, - nativeOnly); + asset); } } } else { @@ -7284,7 +7363,7 @@ void CWallet::GetFilteredOrchardNotes( std::nullopt, ignoreSpent, requireSpendingKey, - nativeOnly); + asset); } for (auto& noteMeta : orchardNotes) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f60ea77e33b..07073cb073a 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1661,6 +1661,12 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface uint32_t minDepth, const std::optional& asOfHeight) const; + SpendableInputs FindSpendableAssets( + const Asset asset, + ZTXOSelector selector, + uint32_t minDepth, + const std::optional& asOfHeight) const; + bool SelectorMatchesAddress(const ZTXOSelector& source, const CTxDestination& a0) const; bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SproutPaymentAddress& a0) const; bool SelectorMatchesAddress(const ZTXOSelector& source, const libzcash::SaplingPaymentAddress& a0) const; @@ -1802,7 +1808,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool AddOrchardZKey(const libzcash::OrchardSpendingKey &sk); bool AddOrchardFullViewingKey(const libzcash::OrchardFullViewingKey &fvk); - bool AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey &isk); + bool AddIssuanceAuthorizingKey(const int accountId, const IssuanceAuthorizingKey &isk) const; /** * Adds an address/ivk mapping to the in-memory wallet. Returns `false` if @@ -2244,7 +2250,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface bool ignoreSpent=true, bool requireSpendingKey=true, bool ignoreLocked=true, - bool nativeOnly=true) const; + const std::optional asset = Asset::Native()) const; /** * Similar to GetFilteredNotes but only for Orchard notes @@ -2258,7 +2264,7 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface int maxDepth=INT_MAX, bool ignoreSpent=true, bool requireSpendingKey=true, - bool nativeOnly=true) const; + const std::optional asset = Asset::Native()) const; /** * Returns confirmed and unconfirmed balances per asset diff --git a/src/wallet/wallet_tx_builder.cpp b/src/wallet/wallet_tx_builder.cpp index 6b84589cea4..dd279bbcdd6 100644 --- a/src/wallet/wallet_tx_builder.cpp +++ b/src/wallet/wallet_tx_builder.cpp @@ -136,7 +136,7 @@ ResolvePayment( [&](const UnifiedAddress& ua) -> tl::expected { if (canResolveOrchard && ua.GetOrchardReceiver().has_value() - && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxOrchardAvailable) + && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxOrchardAvailable || true) // TODO check max per asset ) { if (!strategy.AllowRevealedAmounts()) { maxOrchardAvailable -= payment.GetAmount(); @@ -147,7 +147,8 @@ ResolvePayment( ua.GetOrchardReceiver().value(), payment.GetAmount(), payment.GetMemo(), - false + false, + payment.GetAsset() }}; } else if (ua.GetSaplingReceiver().has_value() && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) @@ -495,6 +496,16 @@ SpendableInputs WalletTxBuilder::FindAllSpendableInputs( return wallet.FindSpendableInputs(selector, minDepth, std::nullopt); } +SpendableInputs WalletTxBuilder::FindAllSpendableAssets( + const CWallet& wallet, + Asset asset, + const ZTXOSelector& selector, + int32_t minDepth) const +{ + LOCK2(cs_main, wallet.cs_wallet); + return wallet.FindSpendableAssets(asset, selector, minDepth, std::nullopt); +} + CAmount GetConstrainedFee( const CWallet& wallet, const std::optional& inputs, @@ -502,14 +513,17 @@ CAmount GetConstrainedFee( const std::optional& changeAddr, uint32_t consensusBranchId) { + // TODO re-enable fees + return CAmount(0); + // We know that minRelayFee <= MINIMUM_FEE <= conventional_fee, so we can use an arbitrary // transaction size when constraining the fee, because we are guaranteed to already satisfy the // lower bound. - constexpr unsigned int DUMMY_TX_SIZE = 1; - - return CWallet::ConstrainFee( - CalcZIP317Fee(wallet, inputs, payments, changeAddr, consensusBranchId), - DUMMY_TX_SIZE); +// constexpr unsigned int DUMMY_TX_SIZE = 1; +// +// return CWallet::ConstrainFee( +// CalcZIP317Fee(wallet, inputs, payments, changeAddr, consensusBranchId), +// DUMMY_TX_SIZE); } static tl::expected @@ -520,6 +534,9 @@ AddChangePayment( CAmount changeAmount, CAmount targetAmount) { + // TODO: This is a hack to get the asset from the first spendable input. We need to implement transaction with multiple assets + auto asset = spendable.orchardNoteMetadata[0].GetAsset(); + assert(changeAmount > 0); // When spending transparent coinbase outputs, all inputs must be fully consumed. @@ -534,7 +551,7 @@ AddChangePayment( [](const libzcash::SproutViewingKey&) {}, [&](const auto& sendTo) { resolvedPayments.AddPayment( - ResolvedPayment(std::nullopt, sendTo, changeAmount, std::nullopt, true)); + ResolvedPayment(std::nullopt, sendTo, changeAmount, std::nullopt, true, asset)); } }); @@ -1041,7 +1058,8 @@ TransactionBuilderResult TransactionEffects::ApproveAndBuild( r.isInternal ? internalOVK : externalOVK, addr, r.amount, - r.memo); + r.memo, + r.asset); }, }); if (result.has_value()) { diff --git a/src/wallet/wallet_tx_builder.h b/src/wallet/wallet_tx_builder.h index 51dd364832f..fac134c5423 100644 --- a/src/wallet/wallet_tx_builder.h +++ b/src/wallet/wallet_tx_builder.h @@ -22,6 +22,7 @@ int GetAnchorHeight(const CChain& chain, int anchorConfirmations); class ResolvedPayment : public RecipientMapping { public: CAmount amount; + Asset asset; std::optional memo; bool isInternal; @@ -30,8 +31,9 @@ class ResolvedPayment : public RecipientMapping { libzcash::RecipientAddress address, CAmount amount, std::optional memo, - bool isInternal) : - RecipientMapping(ua, address), amount(amount), memo(memo), isInternal(isInternal) {} + bool isInternal, + Asset asset = Asset::Native()) : + RecipientMapping(ua, address), amount(amount), memo(memo), isInternal(isInternal), asset(asset) {} }; /** @@ -42,13 +44,23 @@ class Payment { private: PaymentAddress address; CAmount amount; + Asset asset; std::optional memo; public: Payment( PaymentAddress address, CAmount amount, std::optional memo) : - address(address), amount(amount), memo(memo) { + address(address), amount(amount), asset(Asset::Native()), memo(memo) { + assert(MoneyRange(amount)); + } + + Payment( + PaymentAddress address, + CAmount amount, + Asset asset, + std::optional memo) : + address(address), amount(amount), asset(asset), memo(memo) { assert(MoneyRange(amount)); } @@ -60,6 +72,10 @@ class Payment { return amount; } + Asset GetAsset() const { + return asset; + } + const std::optional& GetMemo() const { return memo; } @@ -438,6 +454,12 @@ class WalletTxBuilder { const ZTXOSelector& selector, int32_t minDepth) const; + SpendableInputs FindAllSpendableAssets( + const CWallet& wallet, + Asset asset, + const ZTXOSelector& selector, + int32_t minDepth) const; + tl::expected PrepareTransaction( CWallet& wallet, diff --git a/src/zcash/IncrementalMerkleTree.hpp b/src/zcash/IncrementalMerkleTree.hpp index c55cf964b7d..81c613b2727 100644 --- a/src/zcash/IncrementalMerkleTree.hpp +++ b/src/zcash/IncrementalMerkleTree.hpp @@ -13,6 +13,7 @@ #include "zcash/util.h" #include +#include #include namespace libzcash { @@ -394,6 +395,10 @@ class OrchardMerkleFrontier return inner->append_bundle(*bundle.GetDetails()); } + merkle_frontier::OrchardAppendResult AppendIssueBundle(const IssueBundle& bundle) { + return inner->append_issue_bundle(*bundle.GetDetails()); + } + const uint256 root() const { return uint256::FromRawBytes(inner->root()); } diff --git a/src/zip317.h b/src/zip317.h index 296ce1e2c8f..e8df3dd713b 100644 --- a/src/zip317.h +++ b/src/zip317.h @@ -27,7 +27,7 @@ static const size_t DEFAULT_BLOCK_UNPAID_ACTION_LIMIT = 50; static const size_t DEFAULT_TX_UNPAID_ACTION_LIMIT = DEFAULT_BLOCK_UNPAID_ACTION_LIMIT; /// This is the lowest the conventional fee can be in ZIP 317. -static const CAmount MINIMUM_FEE = MARGINAL_FEE * GRACE_ACTIONS; +static const CAmount MINIMUM_FEE = 0; // MARGINAL_FEE * GRACE_ACTIONS; TODO implement ZSA fees /// Return the conventional fee for the given `logicalActionCount` calculated according to /// .