From 4c53499f11801483ec14434bbf4196867c5463ed Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 3 Aug 2021 16:43:47 -0600 Subject: [PATCH] Add GetFilteredNotes to Orchard wallet. --- src/primitives/transaction.h | 10 ++ src/rust/include/rust/orchard/keys.h | 9 ++ src/rust/include/rust/orchard/wallet.h | 45 +++++++ src/rust/src/orchard_keys_ffi.rs | 12 +- src/rust/src/wallet.rs | 125 +++++++++++++++++- .../asyncrpcoperation_saplingmigration.cpp | 3 +- src/wallet/gtest/test_wallet.cpp | 39 ++++-- src/wallet/orchard.h | 79 +++++++++++ src/wallet/rpcwallet.cpp | 30 +++-- src/wallet/wallet.cpp | 52 ++++++-- src/wallet/wallet.h | 31 +++-- src/zcash/address/orchard.hpp | 4 + 12 files changed, 393 insertions(+), 46 deletions(-) diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index b21803720..e66f448cc 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -523,6 +523,16 @@ public: std::string ToString() const; }; +/** An outpoint - a combination of a transaction hash and an index n into its orchard + * actions */ +class OrchardOutPoint : public BaseOutPoint +{ +public: + OrchardOutPoint() : BaseOutPoint() {}; + OrchardOutPoint(uint256 hashIn, uint32_t nIn) : BaseOutPoint(hashIn, nIn) {}; + std::string ToString() const; +}; + /** An input of a transaction. It contains the location of the previous * transaction's output that it claims and a signature that matches the * output's public key. diff --git a/src/rust/include/rust/orchard/keys.h b/src/rust/include/rust/orchard/keys.h index 506d7b391..f80c0160b 100644 --- a/src/rust/include/rust/orchard/keys.h +++ b/src/rust/include/rust/orchard/keys.h @@ -32,6 +32,15 @@ OrchardRawAddressPtr* orchard_address_clone( */ void orchard_address_free(OrchardRawAddressPtr* ptr); +/** + * Implements the "less than" operation `k0 < k1` for comparing two Orchard + * addresses. This is a comparison of the raw bytes, only useful for cases + * where a semantically irrelevant ordering is needed (such as for map keys.) + */ +bool orchard_address_lt( + const OrchardRawAddressPtr* k0, + const OrchardRawAddressPtr* k1); + // // Incoming Viewing Keys // diff --git a/src/rust/include/rust/orchard/wallet.h b/src/rust/include/rust/orchard/wallet.h index 6e4f5c575..18b5fb76f 100644 --- a/src/rust/include/rust/orchard/wallet.h +++ b/src/rust/include/rust/orchard/wallet.h @@ -106,6 +106,51 @@ bool orchard_wallet_add_raw_address( const OrchardRawAddressPtr* addr, const OrchardIncomingViewingKeyPtr* ivk); +/** + * Returns a pointer to the Orchard incoming viewing key + * corresponding to the specified raw address, if it is + * known to the wallet, or `nullptr` otherwise. + */ +OrchardIncomingViewingKeyPtr* orchard_wallet_get_ivk_for_address( + const OrchardWalletPtr* wallet, + const OrchardRawAddressPtr* addr); + +/** + * A C struct used to transfer note metadata information across the Rust FFI + * boundary. This must have the same in-memory representation as the + * `NoteMetadata` type in orchard_ffi/wallet.rs. + */ +struct RawOrchardNoteMetadata { + unsigned char txid[32]; + uint32_t actionIdx; + OrchardRawAddressPtr* addr; + CAmount noteValue; + unsigned char memo[512]; +}; + +typedef void (*push_callback_t)(void* resultVector, const RawOrchardNoteMetadata noteMeta); + +/** + * Finds notes that belong to the wallet that were sent to addresses derived + * from the specified incoming viewing key, subject to the specified flags, and + * uses the provided callback to push RawOrchardNoteMetadata values + * corresponding to those notes on to the provided result vector. Note that + * the push_cb callback can perform any necessary conversion from a + * RawOrchardNoteMetadata value prior in addition to modifying the provided + * result vector. + * + * If `ivk` is null, all notes belonging to the wallet will be returned. + */ +void orchard_wallet_get_filtered_notes( + const OrchardWalletPtr* wallet, + const OrchardIncomingViewingKeyPtr* ivk, + bool ignoreSpent, + bool ignoreLocked, + bool requireSpendingKey, + void* resultVector, + push_callback_t push_cb + ); + #ifdef __cplusplus } #endif diff --git a/src/rust/src/orchard_keys_ffi.rs b/src/rust/src/orchard_keys_ffi.rs index d068857d3..93c21c1fe 100644 --- a/src/rust/src/orchard_keys_ffi.rs +++ b/src/rust/src/orchard_keys_ffi.rs @@ -5,7 +5,10 @@ use tracing::error; use orchard::keys::{DiversifierIndex, FullViewingKey, IncomingViewingKey, SpendingKey}; use orchard::Address; -use crate::streams_ffi::{CppStreamReader, CppStreamWriter, ReadCb, StreamObj, WriteCb}; +use crate::{ + streams_ffi::{CppStreamReader, CppStreamWriter, ReadCb, StreamObj, WriteCb}, + zcashd_orchard::OrderedAddress, +}; // // Addresses @@ -25,6 +28,13 @@ pub extern "C" fn orchard_address_free(addr: *mut Address) { } } +#[no_mangle] +pub extern "C" fn orchard_address_lt(a0: *const Address, a1: *const Address) -> bool { + let a0 = unsafe { a0.as_ref() }; + let a1 = unsafe { a1.as_ref() }; + a0.map(|a| OrderedAddress::new(*a)) < a1.map(|a| OrderedAddress::new(*a)) +} + // // Incoming viewing keys // diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs index f92b61c71..fd821a900 100644 --- a/src/rust/src/wallet.rs +++ b/src/rust/src/wallet.rs @@ -1,6 +1,6 @@ use incrementalmerkletree::{bridgetree::BridgeTree, Frontier, Tree}; use libc::c_uchar; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use tracing::error; use zcash_primitives::{ @@ -29,6 +29,12 @@ pub struct LastObserved { block_tx_idx: usize, } +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct OutPoint { + txid: TxId, + action_idx: usize, +} + #[derive(Debug, Clone)] pub struct DecryptedNote { note: Note, @@ -36,9 +42,20 @@ pub struct DecryptedNote { } struct TxNotes { + txid: TxId, decrypted_notes: BTreeMap, } +/// A type used to pass note metadata across the FFI boundary +#[repr(C)] +pub struct NoteMetadata { + txid: [u8; 32], + action_idx: u32, + recipient: *const Address, + note_value: i64, + memo: [u8; 512], +} + struct KeyStore { payment_addresses: BTreeMap, viewing_keys: BTreeMap, @@ -73,6 +90,16 @@ impl KeyStore { .insert(OrderedAddress::new(addr), ivk); has_fvk } + + pub fn spending_key_for_ivk(&self, ivk: &IncomingViewingKey) -> Option<&SpendingKey> { + self.viewing_keys + .get(ivk) + .and_then(|fvk| self.spending_keys.get(fvk)) + } + + pub fn ivk_for_address(&self, addr: &Address) -> Option<&IncomingViewingKey> { + self.payment_addresses.get(&OrderedAddress::new(*addr)) + } } pub struct Wallet { @@ -86,6 +113,10 @@ pub struct Wallet { witness_tree: BridgeTree, /// The block height and transaction index of the note most recently added to `witness_tree` last_observed: Option, + /// Notes marked as locked (currently reserved for pending transactions) + locked_notes: HashSet, + /// Notes marked as spent + spent_notes: HashSet, } #[derive(Debug, Clone)] @@ -101,6 +132,8 @@ impl Wallet { wallet_tx_notes: HashMap::new(), witness_tree: BridgeTree::new(MAX_CHECKPOINTS), last_observed: None, + locked_notes: HashSet::new(), + spent_notes: HashSet::new(), } } @@ -121,6 +154,7 @@ impl Wallet { bundle: &Bundle, ) -> Vec { let mut tx_notes = TxNotes { + txid: *txid, decrypted_notes: BTreeMap::new(), }; @@ -199,6 +233,45 @@ impl Wallet { pub fn tx_contains_my_notes(&self, txid: &TxId) -> bool { self.wallet_tx_notes.get(txid).is_some() } + + pub fn get_filtered_notes( + &self, + ivk: Option<&IncomingViewingKey>, + ignore_spent: bool, + ignore_locked: bool, + require_spending_key: bool, + ) -> Vec<(OutPoint, DecryptedNote)> { + self.wallet_tx_notes + .values() + .flat_map(|tx_notes| { + tx_notes + .decrypted_notes + .iter() + .filter_map(move |(idx, dnote)| { + let outpoint = OutPoint { + txid: tx_notes.txid, + action_idx: *idx, + }; + + self.key_store + .ivk_for_address(&dnote.note.recipient()) + // if `ivk` is `None`, return all notes that match the other conditions + .filter(|dnote_ivk| ivk.iter().all(|ivk| ivk == dnote_ivk)) + .and_then(|dnote_ivk| { + if (ignore_spent && self.spent_notes.contains(&outpoint)) + || (ignore_locked && self.locked_notes.contains(&outpoint)) + || (require_spending_key + && self.key_store.spending_key_for_ivk(dnote_ivk).is_none()) + { + None + } else { + Some((outpoint, (*dnote).clone())) + } + }) + }) + }) + .collect() + } } #[no_mangle] @@ -293,6 +366,21 @@ pub extern "C" fn orchard_wallet_add_raw_address( wallet.key_store.add_raw_address(*addr, ivk.clone()) } +#[no_mangle] +pub extern "C" fn orchard_wallet_get_ivk_for_address( + wallet: *const Wallet, + addr: *const Address, +) -> *mut IncomingViewingKey { + let wallet = unsafe { wallet.as_ref() }.expect("Wallet pointer may not be null."); + let addr = unsafe { addr.as_ref() }.expect("Address may not be null."); + + wallet + .key_store + .ivk_for_address(addr) + .map(|ivk| Box::into_raw(Box::new(ivk.clone()))) + .unwrap_or(std::ptr::null_mut()) +} + #[no_mangle] pub extern "C" fn orchard_wallet_tx_contains_my_notes( wallet: *const Wallet, @@ -303,3 +391,38 @@ pub extern "C" fn orchard_wallet_tx_contains_my_notes( wallet.tx_contains_my_notes(&txid) } + +pub type VecObj = std::ptr::NonNull; +pub type PushCb = unsafe extern "C" fn(obj: Option, meta: NoteMetadata); + +#[no_mangle] +pub extern "C" fn orchard_wallet_get_filtered_notes( + wallet: *const Wallet, + ivk: *const IncomingViewingKey, + ignore_spent: bool, + ignore_locked: bool, + require_spending_key: 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() }; + + for (outpoint, dnote) in + wallet.get_filtered_notes(ivk, ignore_spent, ignore_locked, require_spending_key) + { + let recipient = Box::new(dnote.note.recipient()); + unsafe { + (push_cb.unwrap())( + result, + NoteMetadata { + txid: *outpoint.txid.as_ref(), + action_idx: outpoint.action_idx as u32, + recipient: Box::into_raw(recipient), + note_value: dnote.note.value().inner() as i64, + memo: dnote.memo, + }, + ) + }; + } +} diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp index 9ba4dca20..0a7467484 100644 --- a/src/wallet/asyncrpcoperation_saplingmigration.cpp +++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp @@ -80,12 +80,13 @@ bool AsyncRPCOperation_saplingmigration::main_impl() { std::vector sproutEntries; std::vector saplingEntries; + std::vector orchardEntries; { LOCK2(cs_main, pwalletMain->cs_wallet); // We set minDepth to 11 to avoid unconfirmed notes and in anticipation of specifying // an anchor at height N-10 for each Sprout JoinSplit description // Consider, should notes be sorted? - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 11); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 11); } CAmount availableFunds = 0; for (const SproutNoteEntry& sproutEntry : sproutEntries) { diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index 137cfa606..36a0696a7 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -207,11 +207,13 @@ TEST(WalletTests, FindUnspentSproutNotes) { // We currently have an unspent and unconfirmed note in the wallet (depth of -1) std::vector sproutEntries; std::vector saplingEntries; - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0); + std::vector orchardEntries; + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, -1); + orchardEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, -1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); @@ -234,18 +236,21 @@ TEST(WalletTests, FindUnspentSproutNotes) { // We now have an unspent and confirmed note in the wallet (depth of 1) - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1); + orchardEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2); + orchardEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Let's spend the note. @@ -272,25 +277,29 @@ TEST(WalletTests, FindUnspentSproutNotes) { EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); // The note has been spent. By default, GetFilteredNotes() ignores spent notes. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Let's include spent notes to retrieve it. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // The spent note has two confirmations. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // It does not have 3 confirmations. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 3, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 3, INT_MAX, false); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Let's receive a new note @@ -330,25 +339,29 @@ TEST(WalletTests, FindUnspentSproutNotes) { wallet.AddToWallet(wtx3, true, NULL); // We now have an unspent note which has one confirmation, in addition to our spent note. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Let's return the spent note too. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 1, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 1, INT_MAX, false); EXPECT_EQ(2, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Increasing number of confirmations will exclude our new unspent note. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, false); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, false); EXPECT_EQ(1, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // If we also ignore spent notes at this depth, we won't find any notes. - wallet.GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 2, INT_MAX, true); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 2, INT_MAX, true); EXPECT_EQ(0, sproutEntries.size()); sproutEntries.clear(); saplingEntries.clear(); + orchardEntries.clear(); // Tear down chainActive.SetTip(NULL); diff --git a/src/wallet/orchard.h b/src/wallet/orchard.h index ef590ece6..02f40a3ad 100644 --- a/src/wallet/orchard.h +++ b/src/wallet/orchard.h @@ -5,10 +5,46 @@ #ifndef ZCASH_WALLET_ORCHARD_H #define ZCASH_WALLET_ORCHARD_H +#include + +#include "primitives/transaction.h" #include "rust/orchard/keys.h" #include "rust/orchard/wallet.h" #include "zcash/address/orchard.hpp" +class OrchardNoteMetadata +{ +private: + OrchardOutPoint op; + libzcash::OrchardRawAddress address; + CAmount noteValue; + std::array memo; + int confirmations; +public: + OrchardNoteMetadata( + OrchardOutPoint op, + const libzcash::OrchardRawAddress& address, + CAmount noteValue, + const std::array& memo): + op(op), address(address), noteValue(noteValue), memo(memo), confirmations(0) {} + + const OrchardOutPoint& GetOutPoint() const { + return op; + } + + void SetConfirmations(int c) { + confirmations = c; + } + + int GetConfirmations() const { + return confirmations; + } + + CAmount GetNoteValue() const { + return noteValue; + } +}; + class OrchardWallet { private: @@ -85,6 +121,13 @@ public: orchard_wallet_add_full_viewing_key(inner.get(), fvk.inner.get()); } + std::optional GetIncomingViewingKeyForAddress( + const libzcash::OrchardRawAddress& addr) const { + auto ivkPtr = orchard_wallet_get_ivk_for_address(inner.get(), addr.inner.get()); + if (ivkPtr == nullptr) return std::nullopt; + return libzcash::OrchardIncomingViewingKey(ivkPtr); + } + /** * Adds an address/IVK pair to the wallet, and returns `true` if the * IVK corresponds to a full viewing key known to the wallet, `false` @@ -95,6 +138,42 @@ public: const libzcash::OrchardIncomingViewingKey& ivk) { return orchard_wallet_add_raw_address(inner.get(), addr.inner.get(), ivk.inner.get()); } + + static void PushOrchardNoteMeta(void* orchardNotesRet, RawOrchardNoteMetadata rawNoteMeta) { + uint256 txid; + std::move(std::begin(rawNoteMeta.txid), std::end(rawNoteMeta.txid), txid.begin()); + OrchardOutPoint op(txid, rawNoteMeta.actionIdx); + // TODO: what's the efficient way to copy the memo in the OrchardNoteMetadata + // constructor? + std::array memo; + std::move(std::begin(rawNoteMeta.memo), std::end(rawNoteMeta.memo), memo.begin()); + OrchardNoteMetadata noteMeta( + op, + libzcash::OrchardRawAddress(rawNoteMeta.addr), + rawNoteMeta.noteValue, + memo); + // TODO: noteMeta.confirmations is only available from the C++ wallet + + reinterpret_cast*>(orchardNotesRet)->push_back(noteMeta); + } + + void GetFilteredNotes( + std::vector& orchardNotesRet, + const std::optional& ivk, + bool ignoreSpent, + bool ignoreLocked, + bool requireSpendingKey) const { + + orchard_wallet_get_filtered_notes( + inner.get(), + ivk.has_value() ? ivk.value().inner.get() : nullptr, + ignoreSpent, + ignoreLocked, + requireSpendingKey, + &orchardNotesRet, + PushOrchardNoteMeta + ); + } }; #endif // ZCASH_ORCHARD_WALLET_H diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index d8d038091..90f89d9b7 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2313,7 +2313,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); - std::optional noteFilter = std::nullopt; + std::optional noteFilter = std::nullopt; std::set> sproutNullifiers; std::set> saplingNullifiers; @@ -2340,7 +2340,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) sourceAddrs.push_back(zaddr.value()); } - noteFilter = AddrSet::ForPaymentAddresses(sourceAddrs); + noteFilter = NoteFilter::ForPaymentAddresses(sourceAddrs); sproutNullifiers = pwalletMain->GetSproutNullifiers(noteFilter.value().GetSproutAddresses()); saplingNullifiers = pwalletMain->GetSaplingNullifiers(noteFilter.value().GetSaplingAddresses()); @@ -2365,7 +2365,8 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) std::vector sproutEntries; std::vector saplingEntries; - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false); + std::vector orchardEntries; + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, nMaxDepth, true, !fIncludeWatchonly, false); for (auto & entry : sproutEntries) { UniValue obj(UniValue::VOBJ); @@ -3382,14 +3383,15 @@ CAmount getBalanceZaddr(std::optional address, int min CAmount balance = 0; std::vector sproutEntries; std::vector saplingEntries; + std::vector orchardEntries; LOCK2(cs_main, pwalletMain->cs_wallet); - std::optional noteFilter = std::nullopt; + std::optional noteFilter = std::nullopt; if (address.has_value()) { - noteFilter = AddrSet::ForPaymentAddresses(std::vector({address.value()})); + noteFilter = NoteFilter::ForPaymentAddresses(std::vector({address.value()})); } - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, minDepth, maxDepth, true, ignoreUnspendable); for (auto & entry : sproutEntries) { balance += CAmount(entry.note.value()); } @@ -3477,8 +3479,10 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) std::vector sproutEntries; std::vector saplingEntries; - auto noteFilter = AddrSet::ForPaymentAddresses(std::vector({decoded.value()})); - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, nMinDepth, INT_MAX, false, false); + std::vector orchardEntries; + + auto noteFilter = NoteFilter::ForPaymentAddresses(std::vector({decoded.value()})); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter, nMinDepth, INT_MAX, false, false); auto push_transparent_result = [&](const CTxDestination& dest) -> void { const CScript scriptPubKey{GetScriptForDestination(dest)}; @@ -4698,9 +4702,10 @@ UniValue z_getmigrationstatus(const UniValue& params, bool fHelp) { { std::vector sproutEntries; std::vector saplingEntries; + std::vector orchardEntries; // Here we are looking for any and all Sprout notes for which we have the spending key, including those // which are locked and/or only exist in the mempool, as they should be included in the unmigrated amount. - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, std::nullopt, 0, INT_MAX, true, true, false); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, std::nullopt, 0, INT_MAX, true, true, false); CAmount unmigratedAmount = 0; for (const auto& sproutEntry : sproutEntries) { unmigratedAmount += sproutEntry.note.value(); @@ -5304,11 +5309,12 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) // Get available notes std::vector sproutEntries; std::vector saplingEntries; - std::optional noteFilter = + std::vector orchardEntries; + std::optional noteFilter = useAnySprout || useAnySapling ? std::nullopt : - std::optional(AddrSet::ForPaymentAddresses(zaddrs)); - pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter); + std::optional(NoteFilter::ForPaymentAddresses(zaddrs)); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, orchardEntries, noteFilter); // If Sapling is not active, do not allow sending from a sapling addresses. if (!saplingActive && saplingEntries.size() > 0) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index a3d556cd8..d2e0435d5 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -27,6 +27,7 @@ #include "timedata.h" #include "utilmoneystr.h" #include "util/match.h" +#include "zcash/Address.hpp" #include "zcash/JoinSplit.hpp" #include "zcash/Note.hpp" #include "crypter.h" @@ -6014,8 +6015,8 @@ bool CMerkleTx::AcceptToMemoryPool(bool fLimitFree, bool fRejectAbsurdFee) return ::AcceptToMemoryPool(Params(), mempool, state, *this, fLimitFree, NULL, fRejectAbsurdFee); } -AddrSet AddrSet::ForPaymentAddresses(const std::vector& paymentAddrs) { - AddrSet addrs; +NoteFilter NoteFilter::ForPaymentAddresses(const std::vector& paymentAddrs) { + NoteFilter addrs; for (const auto& addr: paymentAddrs) { std::visit(match { [&](const CKeyID& keyId) { }, @@ -6041,12 +6042,13 @@ AddrSet AddrSet::ForPaymentAddresses(const std::vector return addrs; } -bool CWallet::HasSpendingKeys(const AddrSet& addrSet) const { +bool CWallet::HasSpendingKeys(const NoteFilter& addrSet) const { for (const auto& zaddr : addrSet.GetSproutAddresses()) { if (!HaveSproutSpendingKey(zaddr)) { return false; } } + for (const auto& zaddr : addrSet.GetSaplingAddresses()) { if (!HaveSaplingSpendingKeyForAddress(zaddr)) { return false; @@ -6068,9 +6070,10 @@ bool CWallet::HasSpendingKeys(const AddrSet& addrSet) const { * will be unmodified. */ void CWallet::GetFilteredNotes( - std::vector& sproutEntries, - std::vector& saplingEntries, - const std::optional& noteFilter, + std::vector& sproutEntriesRet, + std::vector& saplingEntriesRet, + std::vector& orchardNotesRet, + const std::optional& noteFilter, int minDepth, int maxDepth, bool ignoreSpent, @@ -6095,7 +6098,7 @@ void CWallet::GetFilteredNotes( } // Filter coinbase transactions that don't have Sapling outputs - if (wtx.IsCoinBase() && wtx.mapSaplingNoteData.empty()) { + if (wtx.IsCoinBase() && wtx.mapSaplingNoteData.empty() && true/* TODO ORCHARD */) { continue; } @@ -6147,7 +6150,7 @@ void CWallet::GetFilteredNotes( hSig, (unsigned char) j); - sproutEntries.push_back(SproutNoteEntry { + sproutEntriesRet.push_back(SproutNoteEntry { jsop, pa, plaintext.note(pa), plaintext.memo(), wtx.GetDepthInMainChain() }); } catch (const note_decryption_failed &err) { @@ -6194,10 +6197,41 @@ void CWallet::GetFilteredNotes( } auto note = notePt.note(nd.ivk).value(); - saplingEntries.push_back(SaplingNoteEntry { + saplingEntriesRet.push_back(SaplingNoteEntry { op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() }); } } + + if (noteFilter.has_value()) { + for (const OrchardRawAddress& addr: noteFilter.value().GetOrchardAddresses()) { + auto ivk = orchardWallet.GetIncomingViewingKeyForAddress(addr); + if (ivk.has_value()) { + orchardWallet.GetFilteredNotes( + orchardNotesRet, + ivk.value(), + ignoreSpent, + ignoreLocked, + requireSpendingKey); + } + } + } else { + // return all Orchard notes + orchardWallet.GetFilteredNotes( + orchardNotesRet, + std::nullopt, + ignoreSpent, + ignoreLocked, + requireSpendingKey); + } + + for (auto &orchardNoteMeta : orchardNotesRet) { + auto wtx = GetWalletTx(orchardNoteMeta.GetOutPoint().hash); + if (wtx) { + orchardNoteMeta.SetConfirmations(wtx->GetDepthInMainChain()); + } else { + throw std::runtime_error("Wallet inconsistency: We have an Orchard WalletTx without a corresponding CWalletTx"); + } + } } std::optional CWallet::FindUnifiedFullViewingKey(const libzcash::UnifiedAddress& addr) const { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 7ecae56fe..95853eb8f 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -645,15 +645,16 @@ public: std::set GetConflicts() const; }; -class AddrSet { +class NoteFilter { private: std::set sproutAddresses; std::set saplingAddresses; + std::set orchardAddresses; - AddrSet() {} + NoteFilter() {} public: - static AddrSet Empty() { return AddrSet(); } - static AddrSet ForPaymentAddresses(const std::vector& addrs); + static NoteFilter Empty() { return NoteFilter(); } + static NoteFilter ForPaymentAddresses(const std::vector& addrs); const std::set& GetSproutAddresses() const { return sproutAddresses; @@ -663,8 +664,15 @@ public: return saplingAddresses; } + const std::set& GetOrchardAddresses() const { + return orchardAddresses; + } + bool IsEmpty() const { - return sproutAddresses.empty() && saplingAddresses.empty(); + return + sproutAddresses.empty() && + saplingAddresses.empty() && + orchardAddresses.empty(); } bool HasSproutAddress(libzcash::SproutPaymentAddress addr) const { @@ -674,6 +682,10 @@ public: bool HasSaplingAddress(libzcash::SaplingPaymentAddress addr) const { return saplingAddresses.count(addr) > 0; } + + bool HasOrchardAddress(libzcash::OrchardRawAddress addr) const { + return orchardAddresses.count(addr) > 0; + } }; class COutput @@ -1751,13 +1763,14 @@ public: * Check whether the wallet contains spending keys for all the addresses * contained in the given address set. */ - bool HasSpendingKeys(const AddrSet& noteFilter) const; + bool HasSpendingKeys(const NoteFilter& noteFilter) const; /* Find notes filtered by payment addresses, min depth, max depth, if they are spent, if a spending key is required, and if they are locked */ - void GetFilteredNotes(std::vector& sproutEntries, - std::vector& saplingEntries, - const std::optional& noteFilter, + void GetFilteredNotes(std::vector& sproutEntriesRet, + std::vector& saplingEntriesRet, + std::vector& orchardNotesRet, + const std::optional& noteFilter, int minDepth=1, int maxDepth=INT_MAX, bool ignoreSpent=true, diff --git a/src/zcash/address/orchard.hpp b/src/zcash/address/orchard.hpp index 6f18019ee..0e0233fa3 100644 --- a/src/zcash/address/orchard.hpp +++ b/src/zcash/address/orchard.hpp @@ -51,6 +51,10 @@ public: } return *this; } + + friend bool operator<(const OrchardRawAddress& c1, const OrchardRawAddress& c2) { + return orchard_address_lt(c1.inner.get(), c2.inner.get()); + } }; class OrchardIncomingViewingKey