Add support for sending Orchard funds in `z_sendmany`
Closes zcash/zcash#5665.
This commit is contained in:
parent
d49c8a2865
commit
c2220f4eb9
|
@ -110,7 +110,8 @@ class WalletAccountsTest(BitcoinTestFramework):
|
|||
self.check_balance(0, 0, ua0, {})
|
||||
self.check_balance(0, 1, ua1, {})
|
||||
|
||||
# Manually send funds to one of the receivers in the UA.
|
||||
# Send coinbase funds to the UA.
|
||||
print('Sending coinbase funds to account')
|
||||
recipients = [{'address': ua0, 'amount': Decimal('10')}]
|
||||
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0)
|
||||
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||
|
@ -133,7 +134,8 @@ class WalletAccountsTest(BitcoinTestFramework):
|
|||
# The default minconf should now detect the balance.
|
||||
self.check_balance(0, 0, ua0, {'sapling': 10})
|
||||
|
||||
# Manually send funds from the UA receiver.
|
||||
# Send Sapling funds from the UA.
|
||||
print('Sending account funds to Sapling address')
|
||||
node1sapling = self.nodes[1].z_getnewaddress('sapling')
|
||||
recipients = [{'address': node1sapling, 'amount': Decimal('1')}]
|
||||
opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
|
||||
|
@ -154,17 +156,19 @@ class WalletAccountsTest(BitcoinTestFramework):
|
|||
self.check_balance(0, 0, ua0, {'sapling': 9}, 0)
|
||||
|
||||
# Activate NU5
|
||||
print('Activating NU5')
|
||||
self.nodes[2].generate(9)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 210)
|
||||
|
||||
# Send more coinbase funds to the UA.
|
||||
print('Sending coinbase funds to account')
|
||||
recipients = [{'address': ua0, 'amount': Decimal('10')}]
|
||||
opid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients, 1, 0)
|
||||
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||
|
||||
# The wallet should detect the new note as belonging to the UA.
|
||||
# TODO: Uncomment once z_viewtransaction shows Orchard details.
|
||||
# TODO: Uncomment once z_viewtransaction shows Orchard details (#5186).
|
||||
#tx_details = self.nodes[0].z_viewtransaction(txid)
|
||||
#assert_equal(len(tx_details['outputs']), 1)
|
||||
#assert_equal(tx_details['outputs'][0]['type'], 'orchard')
|
||||
|
@ -183,6 +187,33 @@ class WalletAccountsTest(BitcoinTestFramework):
|
|||
self.nodes[2].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Send Orchard funds from the UA.
|
||||
print('Sending account funds to Orchard-only UA')
|
||||
node1account = self.nodes[1].z_getnewaccount()['account']
|
||||
node1orchard = self.nodes[1].z_getaddressforaccount(node1account, ['orchard'])['unifiedaddress']
|
||||
recipients = [{'address': node1orchard, 'amount': Decimal('1')}]
|
||||
opid = self.nodes[0].z_sendmany(ua0, recipients, 1, 0)
|
||||
txid = wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||
|
||||
# The wallet should detect the spent note as belonging to the UA.
|
||||
# TODO ORCHARD: Uncomment this once z_viewtransaction shows Orchard details (#5186).
|
||||
# tx_details = self.nodes[0].z_viewtransaction(txid)
|
||||
# assert_equal(len(tx_details['spends']), 1)
|
||||
# assert_equal(tx_details['spends'][0]['type'], 'orchard')
|
||||
# assert_equal(tx_details['spends'][0]['address'], ua0)
|
||||
# assert_equal(len(tx_details['outputs']), 1)
|
||||
# assert_equal(tx_details['outputs'][0]['type'], 'orchard')
|
||||
# assert_equal(tx_details['outputs'][0]['address'], ua0)
|
||||
|
||||
# The balances of the account should reflect whether zero-conf transactions are
|
||||
# being considered. The Sapling balance should remain at 9, while the Orchard
|
||||
# balance will show either 0 (because the spent 10-ZEC note is never shown, as
|
||||
# that transaction has been created and broadcast, and _might_ get mined up until
|
||||
# the transaction expires), or 9 (if we include the unmined transaction).
|
||||
self.sync_all()
|
||||
self.check_balance(0, 0, ua0, {'sapling': 9})
|
||||
self.check_balance(0, 0, ua0, {'sapling': 9, 'orchard': 9}, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletAccountsTest().main()
|
||||
|
|
|
@ -112,8 +112,10 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
self.nodes[2].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# The remaining change from ua2's Sapling note has been sent to the
|
||||
# account's internal Orchard change address.
|
||||
assert_equal(
|
||||
{'pools': {'sapling': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
|
||||
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2))
|
||||
|
||||
assert_equal(
|
||||
|
@ -137,7 +139,7 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
# un-mined and returned to the mempool
|
||||
assert_equal(set([rollback_tx]), set(self.nodes[2].getrawmempool()))
|
||||
|
||||
# acct2's sole Sapling note is spent by a transaction in the mempool, so our
|
||||
# acct2's sole Orchard note is spent by a transaction in the mempool, so our
|
||||
# confirmed balance is currently 0
|
||||
assert_equal(
|
||||
{'pools': {}, 'minimum_confirmations': 1},
|
||||
|
@ -145,7 +147,7 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
|
||||
# acct2's incoming change (unconfirmed, still in the mempool) is 9 zec
|
||||
assert_equal(
|
||||
{'pools': {'sapling': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 0},
|
||||
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 0},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2, 0))
|
||||
|
||||
# The transaction was un-mined, so acct3 should have no confirmed balance
|
||||
|
@ -168,7 +170,7 @@ class WalletOrchardTest(BitcoinTestFramework):
|
|||
|
||||
# The un-mined transaction should now have been re-mined
|
||||
assert_equal(
|
||||
{'pools': {'sapling': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
|
||||
{'pools': {'orchard': {'valueZat': Decimal('900000000')}}, 'minimum_confirmations': 1},
|
||||
self.nodes[2].z_getbalanceforaccount(acct2))
|
||||
|
||||
assert_equal(
|
||||
|
|
|
@ -287,6 +287,10 @@ void TransactionBuilder::SetExpiryHeight(uint32_t nExpiryHeight)
|
|||
mtx.nExpiryHeight = nExpiryHeight;
|
||||
}
|
||||
|
||||
bool TransactionBuilder::SupportsOrchard() const {
|
||||
return orchardBuilder.has_value();
|
||||
}
|
||||
|
||||
bool TransactionBuilder::AddOrchardSpend(
|
||||
libzcash::OrchardSpendingKey sk,
|
||||
orchard::SpendInfo spendInfo)
|
||||
|
|
|
@ -342,6 +342,8 @@ public:
|
|||
|
||||
void SetFee(CAmount fee);
|
||||
|
||||
bool SupportsOrchard() const;
|
||||
|
||||
bool AddOrchardSpend(
|
||||
libzcash::OrchardSpendingKey sk,
|
||||
orchard::SpendInfo spendInfo);
|
||||
|
|
|
@ -78,26 +78,47 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
|
|||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
txOutputAmounts_.sapling_outputs_total += recipient.amount;
|
||||
recipientPools_.insert(OutputPool::Sapling);
|
||||
if (ztxoSelector_.SelectsSprout() && !allowRevealedAmounts_) {
|
||||
if (!(ztxoSelector_.SelectsSapling() || allowRevealedAmounts_)) {
|
||||
if (ztxoSelector_.SelectsSprout()) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Sending between shielded pools is not enabled by default because it will "
|
||||
"Sending from the Sprout shielded pool to the Sapling "
|
||||
"shielded pool is not enabled by default because it will "
|
||||
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
|
||||
"Resubmit with the `allowRevealedAmounts` parameter set to `true` if "
|
||||
"you wish to allow this transaction to proceed anyway.");
|
||||
}
|
||||
},
|
||||
[&](const libzcash::OrchardRawAddress& addr) {
|
||||
txOutputAmounts_.orchard_outputs_total += recipient.amount;
|
||||
// TODO ORCHARD: Add to recipientPools_
|
||||
if ((ztxoSelector_.SelectsSprout() || ztxoSelector_.SelectsSapling()) && !allowRevealedAmounts_) {
|
||||
if (builder_.SupportsOrchard() && ztxoSelector_.SelectsOrchard()) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Sending between shielded pools is not enabled by default because it will "
|
||||
"Sending from the Orchard shielded pool to the Sapling "
|
||||
"shielded pool is not enabled by default because it will "
|
||||
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
|
||||
"Resubmit with the `allowRevealedAmounts` parameter set to `true` if "
|
||||
"you wish to allow this transaction to proceed anyway.");
|
||||
}
|
||||
// If the source selects transparent then we don't show an
|
||||
// error because we are necessarily revealing information.
|
||||
}
|
||||
},
|
||||
[&](const libzcash::OrchardRawAddress& addr) {
|
||||
txOutputAmounts_.orchard_outputs_total += recipient.amount;
|
||||
recipientPools_.insert(OutputPool::Orchard);
|
||||
// No transaction allows sends from Sprout to Orchard.
|
||||
assert(!ztxoSelector_.SelectsSprout());
|
||||
if (!((builder_.SupportsOrchard() && ztxoSelector_.SelectsOrchard()) || allowRevealedAmounts_)) {
|
||||
if (ztxoSelector_.SelectsSapling()) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Sending from the Sapling shielded pool to the Orchard "
|
||||
"shielded pool is not enabled by default because it will "
|
||||
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
|
||||
"Resubmit with the `allowRevealedAmounts` parameter set to `true` if "
|
||||
"you wish to allow this transaction to proceed anyway.");
|
||||
}
|
||||
// If the source selects transparent then we don't show an
|
||||
// error because we are necessarily revealing information.
|
||||
}
|
||||
}
|
||||
}, recipient.address);
|
||||
}
|
||||
|
@ -251,6 +272,9 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
|
|||
for (const auto& t : spendable.saplingNoteEntries) {
|
||||
z_inputs_total += t.note.value();
|
||||
}
|
||||
for (const auto& t : spendable.orchardNoteMetadata) {
|
||||
z_inputs_total += t.GetNoteValue();
|
||||
}
|
||||
|
||||
if (z_inputs_total > 0 && mindepth_ == 0) {
|
||||
throw JSONRPCError(
|
||||
|
@ -365,7 +389,9 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
|
|||
allowedChangeTypes.insert(OutputPool::Sapling);
|
||||
break;
|
||||
case ReceiverType::Orchard:
|
||||
// TODO
|
||||
if (builder_.SupportsOrchard()) {
|
||||
allowedChangeTypes.insert(OutputPool::Orchard);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -402,12 +428,28 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch Sapling anchor and witnesses
|
||||
// Fetch Sapling anchor and witnesses, and Orchard Merkle paths.
|
||||
uint256 anchor;
|
||||
std::vector<std::optional<SaplingWitness>> witnesses;
|
||||
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> orchardSpendInfo;
|
||||
{
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
pwalletMain->GetSaplingNoteWitnesses(saplingOutPoints, witnesses, anchor);
|
||||
orchardSpendInfo = pwalletMain->GetOrchardSpendInfo(spendable.orchardNoteMetadata);
|
||||
}
|
||||
|
||||
// Add Orchard spends
|
||||
for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
|
||||
auto spendInfo = std::move(orchardSpendInfo[i]);
|
||||
if (!builder_.AddOrchardSpend(
|
||||
std::move(spendInfo.first),
|
||||
std::move(spendInfo.second)))
|
||||
{
|
||||
throw JSONRPCError(
|
||||
RPC_WALLET_ERROR,
|
||||
"Failed to add Orchard note to transaction (check debug.log for details)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Sapling spends
|
||||
|
@ -424,7 +466,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
|
|||
builder_.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value());
|
||||
}
|
||||
|
||||
// Add Sapling and transparent outputs
|
||||
// Add outputs
|
||||
for (const auto& r : recipients_) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID& keyId) {
|
||||
|
@ -508,7 +550,25 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
|
|||
std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs(const SpendableInputs& spendable) const {
|
||||
uint256 internalOVK;
|
||||
uint256 externalOVK;
|
||||
if (!spendable.saplingNoteEntries.empty()) {
|
||||
if (!spendable.orchardNoteMetadata.empty()) {
|
||||
std::optional<OrchardFullViewingKey> fvk;
|
||||
std::visit(match {
|
||||
[&](const libzcash::UnifiedFullViewingKey& ufvk) {
|
||||
fvk = ufvk.GetOrchardKey().value();
|
||||
},
|
||||
[&](const AccountZTXOPattern& acct) {
|
||||
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
|
||||
fvk = ufvk.value().GetOrchardKey().value();
|
||||
},
|
||||
[&](const auto& other) {
|
||||
throw std::runtime_error("unreachable");
|
||||
}
|
||||
}, this->ztxoSelector_.GetPattern());
|
||||
assert(fvk.has_value());
|
||||
|
||||
internalOVK = fvk.value().ToInternalOutgoingViewingKey();
|
||||
externalOVK = fvk.value().ToExternalOutgoingViewingKey();
|
||||
} else if (!spendable.saplingNoteEntries.empty()) {
|
||||
std::optional<SaplingDiversifiableFullViewingKey> dfvk;
|
||||
std::visit(match {
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
|
|
|
@ -1937,6 +1937,7 @@ SpendableInputs CWallet::FindSpendableInputs(
|
|||
bool selectTransparent{selector.SelectsTransparent()};
|
||||
bool selectSprout{selector.SelectsSprout()};
|
||||
bool selectSapling{selector.SelectsSapling()};
|
||||
bool selectOrchard{selector.SelectsOrchard()};
|
||||
|
||||
SpendableInputs unspent;
|
||||
for (auto const& [wtxid, wtx] : mapWallet) {
|
||||
|
@ -2063,6 +2064,7 @@ SpendableInputs CWallet::FindSpendableInputs(
|
|||
}
|
||||
}
|
||||
|
||||
if (selectOrchard) {
|
||||
// for Orchard, we select both the internal and external IVKs.
|
||||
auto orchardIvks = std::visit(match {
|
||||
[&](const libzcash::UnifiedFullViewingKey& ufvk) -> std::vector<OrchardIncomingViewingKey> {
|
||||
|
@ -2102,6 +2104,7 @@ SpendableInputs CWallet::FindSpendableInputs(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unspent;
|
||||
}
|
||||
|
@ -6361,6 +6364,9 @@ NoteFilter NoteFilter::ForPaymentAddresses(const std::vector<libzcash::PaymentAd
|
|||
[&](const libzcash::UnifiedAddress& uaddr) {
|
||||
for (auto& receiver : uaddr) {
|
||||
std::visit(match {
|
||||
[&](const libzcash::OrchardRawAddress& addr) {
|
||||
addrs.orchardAddresses.insert(addr);
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
addrs.saplingAddresses.insert(addr);
|
||||
},
|
||||
|
@ -6385,6 +6391,13 @@ bool CWallet::HasSpendingKeys(const NoteFilter& addrSet) const {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& addr : addrSet.GetOrchardAddresses()) {
|
||||
if (!orchardWallet.GetSpendingKeyForAddress(addr).has_value()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -7068,6 +7081,13 @@ bool ZTXOSelector::SelectsSapling() const {
|
|||
[](const auto& addr) { return false; }
|
||||
}, this->pattern);
|
||||
}
|
||||
bool ZTXOSelector::SelectsOrchard() const {
|
||||
return std::visit(match {
|
||||
[](const libzcash::UnifiedFullViewingKey& ufvk) { return ufvk.GetOrchardKey().has_value(); },
|
||||
[](const AccountZTXOPattern& acct) { return acct.IncludesOrchard(); },
|
||||
[](const auto& addr) { return false; }
|
||||
}, this->pattern);
|
||||
}
|
||||
|
||||
bool SpendableInputs::LimitToAmount(
|
||||
const CAmount amountRequired,
|
||||
|
|
|
@ -758,6 +758,10 @@ public:
|
|||
return receiverTypes.empty() || receiverTypes.count(libzcash::ReceiverType::Sapling) > 0;
|
||||
}
|
||||
|
||||
bool IncludesOrchard() const {
|
||||
return receiverTypes.empty() || receiverTypes.count(libzcash::ReceiverType::Orchard) > 0;
|
||||
}
|
||||
|
||||
friend bool operator==(const AccountZTXOPattern &a, const AccountZTXOPattern &b) {
|
||||
return a.accountId == b.accountId && a.receiverTypes == b.receiverTypes;
|
||||
}
|
||||
|
@ -798,6 +802,7 @@ public:
|
|||
bool SelectsTransparent() const;
|
||||
bool SelectsSprout() const;
|
||||
bool SelectsSapling() const;
|
||||
bool SelectsOrchard() const;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue