Add support for sending Orchard funds in `z_sendmany`

Closes zcash/zcash#5665.
This commit is contained in:
Jack Grigg 2022-03-14 17:52:57 +00:00
parent d49c8a2865
commit c2220f4eb9
7 changed files with 181 additions and 57 deletions

View File

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

View File

@ -64,7 +64,7 @@ class WalletOrchardTest(BitcoinTestFramework):
# Check the value sent to saplingAddr2 was received in node 2's account
assert_equal(
{'pools': {'sapling': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
{'pools': {'sapling': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
# Node 0 shields some funds
@ -78,7 +78,7 @@ class WalletOrchardTest(BitcoinTestFramework):
self.sync_all()
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
{'pools': {'orchard': {'valueZat': Decimal('1000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# Split the network
@ -95,7 +95,7 @@ class WalletOrchardTest(BitcoinTestFramework):
self.sync_all()
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# On the other side of the split, send some funds to node 3
@ -112,12 +112,14 @@ 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(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# Check that the mempools are empty
@ -130,32 +132,32 @@ class WalletOrchardTest(BitcoinTestFramework):
# split 0/1's chain should have won, so their wallet balance should be consistent
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
{'pools': {'orchard': {'valueZat': Decimal('2000000000')}}, 'minimum_confirmations': 1},
self.nodes[1].z_getbalanceforaccount(acct1))
# split 2/3's chain should have been rolled back, so their txn should have been
# 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},
{'pools': {}, 'minimum_confirmations': 1},
self.nodes[2].z_getbalanceforaccount(acct2))
# 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
assert_equal(
{'pools': {}, 'minimum_confirmations': 1},
{'pools': {}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
# acct3's unconfirmed balance is 1 zec
assert_equal(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 0},
self.nodes[3].z_getbalanceforaccount(acct3, 0))
# Manually resend the transaction in node 2's mempool
@ -168,11 +170,11 @@ 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(
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
{'pools': {'orchard': {'valueZat': Decimal('100000000')}}, 'minimum_confirmations': 1},
self.nodes[3].z_getbalanceforaccount(acct3))
if __name__ == '__main__':

View File

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

View File

@ -342,6 +342,8 @@ public:
void SetFee(CAmount fee);
bool SupportsOrchard() const;
bool AddOrchardSpend(
libzcash::OrchardSpendingKey sk,
orchard::SpendInfo spendInfo);

View File

@ -78,25 +78,46 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
[&](const libzcash::SaplingPaymentAddress& addr) {
txOutputAmounts_.sapling_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Sapling);
if (ztxoSelector_.SelectsSprout() && !allowRevealedAmounts_) {
throw JSONRPCError(
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.");
}
if (builder_.SupportsOrchard() && ztxoSelector_.SelectsOrchard()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"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;
// TODO ORCHARD: Add to recipientPools_
if ((ztxoSelector_.SelectsSprout() || ztxoSelector_.SelectsSapling()) && !allowRevealedAmounts_) {
throw JSONRPCError(
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 between shielded pools is not enabled by default because it will "
"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) {

View File

@ -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,42 +2064,44 @@ SpendableInputs CWallet::FindSpendableInputs(
}
}
// for Orchard, we select both the internal and external IVKs.
auto orchardIvks = std::visit(match {
[&](const libzcash::UnifiedFullViewingKey& ufvk) -> std::vector<OrchardIncomingViewingKey> {
auto fvk = ufvk.GetOrchardKey();
if (fvk.has_value()) {
return {fvk->ToIncomingViewingKey(), fvk->ToInternalIncomingViewingKey()};
}
return {};
},
[&](const AccountZTXOPattern& acct) -> std::vector<OrchardIncomingViewingKey> {
auto ufvk = GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
if (ufvk.has_value()) {
auto fvk = ufvk->GetOrchardKey();
if (selectOrchard) {
// for Orchard, we select both the internal and external IVKs.
auto orchardIvks = std::visit(match {
[&](const libzcash::UnifiedFullViewingKey& ufvk) -> std::vector<OrchardIncomingViewingKey> {
auto fvk = ufvk.GetOrchardKey();
if (fvk.has_value()) {
return {fvk->ToIncomingViewingKey(), fvk->ToInternalIncomingViewingKey()};
}
}
return {};
},
[&](const auto& addr) -> std::vector<OrchardIncomingViewingKey> { return {}; }
}, selector.GetPattern());
return {};
},
[&](const AccountZTXOPattern& acct) -> std::vector<OrchardIncomingViewingKey> {
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<OrchardIncomingViewingKey> { return {}; }
}, selector.GetPattern());
for (const auto& ivk : orchardIvks) {
std::vector<OrchardNoteMetadata> incomingNotes;
orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true);
for (const auto& ivk : orchardIvks) {
std::vector<OrchardNoteMetadata> incomingNotes;
orchardWallet.GetFilteredNotes(incomingNotes, ivk, true, true);
for (auto& noteMeta : incomingNotes) {
if (IsOrchardSpent(noteMeta.GetOutPoint())) {
continue;
}
for (auto& noteMeta : incomingNotes) {
if (IsOrchardSpent(noteMeta.GetOutPoint())) {
continue;
}
auto mit = mapWallet.find(noteMeta.GetOutPoint().hash);
auto confirmations = mit->second.GetDepthInMainChain();
if (mit != mapWallet.end() && confirmations >= minDepth) {
noteMeta.SetConfirmations(confirmations);
unspent.orchardNoteMetadata.push_back(noteMeta);
auto mit = mapWallet.find(noteMeta.GetOutPoint().hash);
auto confirmations = mit->second.GetDepthInMainChain();
if (mit != mapWallet.end() && confirmations >= minDepth) {
noteMeta.SetConfirmations(confirmations);
unspent.orchardNoteMetadata.push_back(noteMeta);
}
}
}
}
@ -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,

View File

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