diff --git a/zcash_client_memory/src/error.rs b/zcash_client_memory/src/error.rs index 8548404a56..dc3b90e325 100644 --- a/zcash_client_memory/src/error.rs +++ b/zcash_client_memory/src/error.rs @@ -1,8 +1,11 @@ +use std::convert::Infallible; + +use shardtree::error::ShardTreeError; use zcash_keys::keys::{AddressGenerationError, DerivationError}; use zcash_primitives::transaction::TxId; -use zcash_protocol::memo; +use zcash_protocol::{consensus::BlockHeight, memo}; -use crate::mem_wallet::AccountId; +use crate::AccountId; type Type = AddressGenerationError; @@ -16,7 +19,7 @@ pub enum Error { MemoDecryption(memo::Error), #[error("Error deriving key: {0}")] KeyDerivation(DerivationError), - #[error("Unknown ZIP32 derivation ")] + #[error("Unknown ZIP32 derivation")] UnknownZip32Derivation, #[error("Error generating address: {0}")] AddressGeneration(Type), @@ -31,11 +34,17 @@ pub enum Error { #[error("Conflicting Tx Locator map entry")] ConflictingTxLocator, #[error("Io Error: {0}")] - IoError(std::io::Error), + Io(std::io::Error), #[error("Corrupted Data: {0}")] CorruptedData(String), #[error("An error occurred while processing an account due to a failure in deriving the account's keys: {0}")] BadAccountData(String), + #[error("Blocks are non sequental")] + NonSequentialBlocks, + #[error("Invalid scan range start {0}, end {1}: {2}")] + InvalidScanRange(BlockHeight, BlockHeight, String), + #[error("ShardTree error: {0}")] + ShardTree(ShardTreeError), #[error("Other error: {0}")] Other(String), } @@ -60,6 +69,12 @@ impl From for Error { impl From for Error { fn from(value: std::io::Error) -> Self { - Error::IoError(value) + Error::Io(value) + } +} + +impl From> for Error { + fn from(value: ShardTreeError) -> Self { + Error::ShardTree(value) } } diff --git a/zcash_client_memory/src/mem_wallet/input_source.rs b/zcash_client_memory/src/input_source.rs similarity index 53% rename from zcash_client_memory/src/mem_wallet/input_source.rs rename to zcash_client_memory/src/input_source.rs index 512eb34bad..119c41e6c3 100644 --- a/zcash_client_memory/src/mem_wallet/input_source.rs +++ b/zcash_client_memory/src/input_source.rs @@ -1,17 +1,17 @@ use zcash_client_backend::data_api::InputSource; -use crate::mem_wallet::{AccountId, MemoryWalletDb}; +use crate::{AccountId, MemoryWalletDb, NoteId}; impl InputSource for MemoryWalletDb { type Error = crate::error::Error; - type AccountId = crate::mem_wallet::AccountId; - type NoteRef = crate::mem_wallet::NoteId; + type AccountId = AccountId; + type NoteRef = NoteId; fn get_spendable_note( &self, - txid: &zcash_primitives::transaction::TxId, - protocol: zcash_protocol::ShieldedProtocol, - index: u32, + _txid: &zcash_primitives::transaction::TxId, + _protocol: zcash_protocol::ShieldedProtocol, + _index: u32, ) -> Result< Option< zcash_client_backend::wallet::ReceivedNote< @@ -26,11 +26,11 @@ impl InputSource for MemoryWalletDb { fn select_spendable_notes( &self, - account: Self::AccountId, - target_value: zcash_protocol::value::Zatoshis, - sources: &[zcash_protocol::ShieldedProtocol], - anchor_height: zcash_protocol::consensus::BlockHeight, - exclude: &[Self::NoteRef], + _account: Self::AccountId, + _target_value: zcash_protocol::value::Zatoshis, + _sources: &[zcash_protocol::ShieldedProtocol], + _anchor_height: zcash_protocol::consensus::BlockHeight, + _exclude: &[Self::NoteRef], ) -> Result, Self::Error> { todo!() } diff --git a/zcash_client_memory/src/lib.rs b/zcash_client_memory/src/lib.rs index 81cca52c82..448ec9c4ae 100644 --- a/zcash_client_memory/src/lib.rs +++ b/zcash_client_memory/src/lib.rs @@ -1,2 +1,242 @@ +use scanning::ScanQueue; + +use shardtree::{store::memory::MemoryShardStore, ShardTree}; +use std::{ + collections::{hash_map::Entry, BTreeMap}, + hash::Hash, + ops::Deref, +}; +use subtle::ConditionallySelectable; +use zip32::fingerprint::SeedFingerprint; + +use zcash_primitives::{ + consensus::{BlockHeight, Network}, + transaction::TxId, +}; + +use zcash_client_backend::{ + data_api::{Account as _, AccountSource}, + wallet::{NoteId, WalletSaplingOutput}, +}; + +use zcash_client_backend::data_api::SAPLING_SHARD_HEIGHT; + +#[cfg(feature = "orchard")] +use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, wallet::WalletOrchardOutput}; + +use crate::error::Error; mod error; -pub mod mem_wallet; +pub mod input_source; +pub mod types; +pub mod wallet_commitment_trees; +pub mod wallet_read; +pub mod wallet_write; +pub(crate) use types::*; + +/// The ID type for accounts. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct AccountId(u32); + +impl Deref for AccountId { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ConditionallySelectable for AccountId { + fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self { + AccountId(ConditionallySelectable::conditional_select( + &a.0, &b.0, choice, + )) + } +} + +/// The main in-memory wallet database. Implements all the traits needed to be used as a backend. +pub struct MemoryWalletDb { + network: Network, + accounts: Vec, + blocks: BTreeMap, + + tx_table: TransactionTable, + + received_notes: ReceivedNoteTable, + receieved_note_spends: ReceievdNoteSpends, + nullifiers: NullifierMap, + + tx_locator: TxLocatorMap, + + scan_queue: ScanQueue, + + sapling_tree: ShardTree< + MemoryShardStore, + { SAPLING_SHARD_HEIGHT * 2 }, + SAPLING_SHARD_HEIGHT, + >, + #[cfg(feature = "orchard")] + orchard_tree: ShardTree< + MemoryShardStore, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, +} + +impl MemoryWalletDb { + pub fn new(network: Network, max_checkpoints: usize) -> Self { + Self { + network, + accounts: Vec::new(), + blocks: BTreeMap::new(), + sapling_tree: ShardTree::new(MemoryShardStore::empty(), max_checkpoints), + #[cfg(feature = "orchard")] + orchard_tree: ShardTree::new(MemoryShardStore::empty(), max_checkpoints), + tx_table: TransactionTable::new(), + received_notes: ReceivedNoteTable::new(), + nullifiers: NullifierMap::new(), + tx_locator: TxLocatorMap::new(), + receieved_note_spends: ReceievdNoteSpends::new(), + scan_queue: ScanQueue::new(), + } + } + pub(crate) fn mark_sapling_note_spent( + &mut self, + nf: sapling::Nullifier, + txid: TxId, + ) -> Result<(), Error> { + let note_id = self + .received_notes + .0 + .iter() + .filter(|v| v.nullifier() == Some(&Nullifier::Sapling(nf))) + .map(|v| v.note_id()) + .next() + .ok_or_else(|| Error::NoteNotFound)?; + self.receieved_note_spends.insert_spend(note_id, txid); + Ok(()) + } + + pub(crate) fn get_account_mut(&mut self, account_id: AccountId) -> Option<&mut Account> { + self.accounts.get_mut(*account_id as usize) + } + + #[cfg(feature = "orchard")] + pub(crate) fn mark_orchard_note_spent( + &mut self, + nf: orchard::note::Nullifier, + txid: TxId, + ) -> Result<(), Error> { + let note_id = self + .received_notes + .0 + .iter() + .filter(|v| v.nullifier() == Some(&Nullifier::Orchard(nf))) + .map(|v| v.note_id()) + .next() + .ok_or_else(|| Error::NoteNotFound)?; + self.receieved_note_spends.insert_spend(note_id, txid); + Ok(()) + } + + pub(crate) fn max_zip32_account_index( + &self, + seed_fingerprint: &SeedFingerprint, + ) -> Result, Error> { + Ok(self + .accounts + .iter() + .filter_map(|a| match a.source() { + AccountSource::Derived { + seed_fingerprint: sf, + account_index, + } => { + if &sf == seed_fingerprint { + Some(account_index) + } else { + None + } + } + _ => None, + }) + .max()) + } + pub(crate) fn insert_received_sapling_note( + &mut self, + note_id: NoteId, + output: &WalletSaplingOutput, + spent_in: Option, + ) { + self.received_notes + .insert_received_note(ReceivedNote::from_wallet_sapling_output(note_id, output)); + if let Some(spent_in) = spent_in { + self.receieved_note_spends.insert_spend(note_id, spent_in); + } + } + #[cfg(feature = "orchard")] + pub(crate) fn insert_received_orchard_note( + &mut self, + note_id: NoteId, + output: &WalletOrchardOutput, + spent_in: Option, + ) { + self.received_notes + .insert_received_note(ReceivedNote::from_wallet_orchard_output(note_id, output)); + if let Some(spent_in) = spent_in { + self.receieved_note_spends.insert_spend(note_id, spent_in); + } + } + pub(crate) fn insert_sapling_nullifier_map( + &mut self, + block_height: BlockHeight, + new_entries: &[(TxId, u16, Vec)], + ) -> Result<(), Error> { + for (txid, tx_index, nullifiers) in new_entries { + match self.tx_locator.entry((block_height, *tx_index as u32)) { + Entry::Occupied(x) => { + if txid == x.get() { + // This is a duplicate entry + continue; + } else { + return Err(Error::ConflictingTxLocator); + } + } + Entry::Vacant(entry) => { + entry.insert(*txid); + } + } + for nf in nullifiers.iter() { + self.nullifiers + .insert(block_height, *tx_index as u32, Nullifier::Sapling(*nf)); + } + } + Ok(()) + } + + #[cfg(feature = "orchard")] + pub(crate) fn insert_orchard_nullifier_map( + &mut self, + block_height: BlockHeight, + new_entries: &[(TxId, u16, Vec)], + ) -> Result<(), Error> { + for (txid, tx_index, nullifiers) in new_entries { + match self.tx_locator.entry((block_height, *tx_index as u32)) { + Entry::Occupied(x) => { + if txid == x.get() { + // This is a duplicate entry + continue; + } else { + return Err(Error::ConflictingTxLocator); + } + } + Entry::Vacant(entry) => { + entry.insert(*txid); + } + } + for nf in nullifiers.iter() { + self.nullifiers + .insert(block_height, *tx_index as u32, Nullifier::Orchard(*nf)); + } + } + Ok(()) + } +} diff --git a/zcash_client_memory/src/mem_wallet/mod.rs b/zcash_client_memory/src/mem_wallet/mod.rs deleted file mode 100644 index 2036449cb9..0000000000 --- a/zcash_client_memory/src/mem_wallet/mod.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![allow(unused)] -use core::time; -use incrementalmerkletree::{Address, Marking, Retention}; -use sapling::NullifierDerivingKey; -use secrecy::{ExposeSecret, SecretVec}; -use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - cmp::Ordering, - collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, - ops::Deref, -}; -use subtle::ConditionallySelectable; -use zcash_keys::keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; - -use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network}, - transaction::{components::OutPoint, txid, Authorized, Transaction, TransactionData, TxId}, -}; -use zcash_protocol::{ - memo::{self, Memo, MemoBytes}, - value::{ZatBalance, Zatoshis}, - PoolType, - ShieldedProtocol::{Orchard, Sapling}, -}; - -use zcash_client_backend::{ - address::UnifiedAddress, - data_api::{ - chain::ChainState, Account as _, AccountPurpose, AccountSource, SeedRelevance, - TransactionDataRequest, TransactionStatus, - }, - keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - proto::service::ShieldedProtocol, - wallet::{Note, NoteId, WalletSaplingOutput, WalletSpend, WalletTransparentOutput, WalletTx}, -}; - -use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, - DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, -}; - -#[cfg(feature = "transparent-inputs")] -use { - zcash_client_backend::wallet::TransparentAddressMetadata, - zcash_primitives::legacy::TransparentAddress, -}; - -#[cfg(feature = "orchard")] -use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, wallet::WalletOrchardOutput}; - -use crate::error::Error; - -mod input_source; -mod tables; -mod wallet_commitment_trees; -mod wallet_read; -mod wallet_write; -use tables::*; - -struct MemoryWalletBlock { - height: BlockHeight, - hash: BlockHash, - block_time: u32, - // Just the transactions that involve an account in this wallet - transactions: HashSet, - memos: HashMap, -} - -pub struct MemoryWalletDb { - network: Network, - accounts: Vec, - blocks: BTreeMap, - - tx_table: TransactionTable, - - received_notes: ReceivedNoteTable, - receieved_note_spends: ReceievdNoteSpends, - nullifiers: NullifierMap, - - tx_locator: TxLocatorMap, - - sapling_tree: ShardTree< - MemoryShardStore, - { SAPLING_SHARD_HEIGHT * 2 }, - SAPLING_SHARD_HEIGHT, - >, - #[cfg(feature = "orchard")] - orchard_tree: ShardTree< - MemoryShardStore, - { ORCHARD_SHARD_HEIGHT * 2 }, - ORCHARD_SHARD_HEIGHT, - >, -} -impl MemoryWalletDb { - pub fn new(network: Network, max_checkpoints: usize) -> Self { - Self { - network, - accounts: Vec::new(), - blocks: BTreeMap::new(), - sapling_tree: ShardTree::new(MemoryShardStore::empty(), max_checkpoints), - #[cfg(feature = "orchard")] - orchard_tree: ShardTree::new(MemoryShardStore::empty(), max_checkpoints), - tx_table: TransactionTable::new(), - received_notes: ReceivedNoteTable::new(), - nullifiers: NullifierMap::new(), - tx_locator: TxLocatorMap::new(), - receieved_note_spends: ReceievdNoteSpends::new(), - } - } - fn mark_sapling_note_spent(&mut self, nf: sapling::Nullifier, txid: TxId) -> Result<(), Error> { - let note_id = self - .received_notes - .0 - .iter() - .filter(|v| v.nullifier() == Some(&Nullifier::Sapling(nf))) - .map(|v| v.note_id()) - .next() - .ok_or_else(|| Error::NoteNotFound)?; - self.receieved_note_spends.insert_spend(note_id, txid); - Ok(()) - } - - // fn get_account(&self, account_id: AccountId) -> Option<&Account> { - // self.accounts.get(*account_id as usize) - // } - fn get_account_mut(&mut self, account_id: AccountId) -> Option<&mut Account> { - self.accounts.get_mut(*account_id as usize) - } - - #[cfg(feature = "orchard")] - fn mark_orchard_note_spent( - &mut self, - nf: orchard::note::Nullifier, - txid: TxId, - ) -> Result<(), Error> { - let note_id = self - .received_notes - .0 - .iter() - .filter(|v| v.nullifier() == Some(&Nullifier::Orchard(nf))) - .map(|v| v.note_id()) - .next() - .ok_or_else(|| Error::NoteNotFound)?; - self.receieved_note_spends.insert_spend(note_id, txid); - Ok(()) - } - - fn max_zip32_account_index( - &self, - seed_fingerprint: &SeedFingerprint, - ) -> Result, Error> { - Ok(self - .accounts - .iter() - .filter_map(|a| match a.source() { - AccountSource::Derived { - seed_fingerprint: sf, - account_index, - } => { - if &sf == seed_fingerprint { - Some(account_index) - } else { - None - } - } - _ => None, - }) - .max()) - } - pub fn insert_received_sapling_note( - &mut self, - note_id: NoteId, - output: &WalletSaplingOutput, - spent_in: Option, - ) { - self.received_notes - .insert_received_note(ReceivedNote::from_wallet_sapling_output(note_id, output)); - if let Some(spent_in) = spent_in { - self.receieved_note_spends.insert_spend(note_id, spent_in); - } - } - #[cfg(feature = "orchard")] - pub fn insert_received_orchard_note( - &mut self, - note_id: NoteId, - output: &WalletOrchardOutput, - spent_in: Option, - ) { - self.received_notes - .insert_received_note(ReceivedNote::from_wallet_orchard_output(note_id, output)); - if let Some(spent_in) = spent_in { - self.receieved_note_spends.insert_spend(note_id, spent_in); - } - } - fn insert_sapling_nullifier_map( - &mut self, - block_height: BlockHeight, - new_entries: &[(TxId, u16, Vec)], - ) -> Result<(), Error> { - for (txid, tx_index, nullifiers) in new_entries { - match self.tx_locator.entry((block_height, *tx_index as u32)) { - Entry::Occupied(x) => { - if txid == x.get() { - // This is a duplicate entry - continue; - } else { - return Err(Error::ConflictingTxLocator); - } - } - Entry::Vacant(entry) => { - entry.insert(*txid); - } - } - for nf in nullifiers.iter() { - self.nullifiers - .insert(block_height, *tx_index as u32, Nullifier::Sapling(*nf)); - } - } - Ok(()) - } - - #[cfg(feature = "orchard")] - fn insert_orchard_nullifier_map( - &mut self, - block_height: BlockHeight, - new_entries: &[(TxId, u16, Vec)], - ) -> Result<(), Error> { - for (txid, tx_index, nullifiers) in new_entries { - match self.tx_locator.entry((block_height, *tx_index as u32)) { - Entry::Occupied(x) => { - if txid == x.get() { - // This is a duplicate entry - continue; - } else { - return Err(Error::ConflictingTxLocator); - } - } - Entry::Vacant(entry) => { - entry.insert(*txid); - } - } - for nf in nullifiers.iter() { - self.nullifiers - .insert(block_height, *tx_index as u32, Nullifier::Orchard(*nf)); - } - } - Ok(()) - } -} - -/// The viewing key that an [`Account`] has available to it. -#[derive(Debug, Clone)] -pub(crate) enum ViewingKey { - /// A full viewing key. - /// - /// This is available to derived accounts, as well as accounts directly imported as - /// full viewing keys. - Full(Box), - - /// An incoming viewing key. - /// - /// Accounts that have this kind of viewing key cannot be used in wallet contexts, - /// because they are unable to maintain an accurate balance. - Incoming(Box), -} - -/// The ID type for accounts. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] -pub struct AccountId(u32); - -impl Deref for AccountId { - type Target = u32; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ConditionallySelectable for AccountId { - fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self { - AccountId(ConditionallySelectable::conditional_select( - &a.0, &b.0, choice, - )) - } -} - -/// An account stored in a `zcash_client_sqlite` database. -#[derive(Debug, Clone)] -pub struct Account { - account_id: AccountId, - kind: AccountSource, - viewing_key: ViewingKey, - birthday: AccountBirthday, - purpose: AccountPurpose, // TODO: Remove this. AccountSource should be sufficient. - addresses: BTreeMap, - notes: HashSet, -} - -impl Account { - fn new( - account_id: AccountId, - kind: AccountSource, - viewing_key: ViewingKey, - birthday: AccountBirthday, - purpose: AccountPurpose, - ) -> Result { - let mut acc = Self { - account_id, - kind, - viewing_key, - birthday, - purpose, - addresses: BTreeMap::new(), - notes: HashSet::new(), - }; - let ua_request = acc - .viewing_key - .uivk() - .to_address_request() - .and_then(|ua_request| ua_request.intersect(&UnifiedAddressRequest::all().unwrap())) - .ok_or_else(|| { - Error::AddressGeneration(AddressGenerationError::ShieldedReceiverRequired) - })?; - - let (addr, diversifier_index) = acc.default_address(ua_request)?; - acc.addresses.insert(diversifier_index, addr); - Ok(acc) - } - /// Returns the default Unified Address for the account, - /// along with the diversifier index that generated it. - /// - /// The diversifier index may be non-zero if the Unified Address includes a Sapling - /// receiver, and there was no valid Sapling receiver at diversifier index zero. - pub(crate) fn default_address( - &self, - request: UnifiedAddressRequest, - ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { - self.uivk().default_address(request) - } - - fn birthday(&self) -> &AccountBirthday { - &self.birthday - } - - fn addresses(&self) -> &BTreeMap { - &self.addresses - } - - fn current_address(&self) -> Option<(DiversifierIndex, UnifiedAddress)> { - self.addresses - .last_key_value() - .map(|(diversifier_index, address)| (*diversifier_index, address.clone())) - } - fn kind(&self) -> &AccountSource { - &self.kind - } - fn viewing_key(&self) -> &ViewingKey { - &self.viewing_key - } - fn next_available_address( - &mut self, - request: UnifiedAddressRequest, - ) -> Result, Error> { - match self.ufvk() { - Some(ufvk) => { - let search_from = match self.current_address() { - Some((mut last_diversifier_index, _)) => { - last_diversifier_index - .increment() - .map_err(|_| AddressGenerationError::DiversifierSpaceExhausted)?; - last_diversifier_index - } - None => DiversifierIndex::default(), - }; - let (addr, diversifier_index) = ufvk.find_address(search_from, request)?; - self.addresses.insert(diversifier_index, addr.clone()); - Ok(Some(addr)) - } - None => Ok(None), - } - } -} - -impl zcash_client_backend::data_api::Account for Account { - fn id(&self) -> AccountId { - self.account_id - } - - fn source(&self) -> AccountSource { - self.kind - } - - fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { - self.viewing_key.ufvk() - } - - fn uivk(&self) -> UnifiedIncomingViewingKey { - self.viewing_key.uivk() - } -} - -impl ViewingKey { - fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { - match self { - ViewingKey::Full(ufvk) => Some(ufvk), - ViewingKey::Incoming(_) => None, - } - } - - fn uivk(&self) -> UnifiedIncomingViewingKey { - match self { - ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(), - ViewingKey::Incoming(uivk) => uivk.as_ref().clone(), - } - } -} - -impl PartialEq for MemoryWalletBlock { - fn eq(&self, other: &Self) -> bool { - (self.height, self.block_time) == (other.height, other.block_time) - } -} - -impl Eq for MemoryWalletBlock {} - -impl PartialOrd for MemoryWalletBlock { - fn partial_cmp(&self, other: &Self) -> Option { - Some((self.height, self.block_time).cmp(&(other.height, other.block_time))) - } -} - -impl Ord for MemoryWalletBlock { - fn cmp(&self, other: &Self) -> Ordering { - (self.height, self.block_time).cmp(&(other.height, other.block_time)) - } -} diff --git a/zcash_client_memory/src/mem_wallet/tables.rs b/zcash_client_memory/src/mem_wallet/tables.rs deleted file mode 100644 index c16ca4d229..0000000000 --- a/zcash_client_memory/src/mem_wallet/tables.rs +++ /dev/null @@ -1,422 +0,0 @@ -#![allow(unused)] -use core::time; -use incrementalmerkletree::{Address, Marking, Position, Retention}; -use sapling::NullifierDerivingKey; -use secrecy::{ExposeSecret, SecretVec}; -use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - cell::RefCell, - cmp::Ordering, - collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, - ops::Deref, - rc::Rc, -}; -use zcash_keys::keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; - -use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network}, - transaction::{components::OutPoint, txid, Authorized, Transaction, TransactionData, TxId}, -}; -use zcash_protocol::{ - memo::{self, Memo, MemoBytes}, - value::{ZatBalance, Zatoshis}, - PoolType, - ShieldedProtocol::{self, Orchard, Sapling}, -}; - -use zcash_client_backend::{ - address::UnifiedAddress, - data_api::{ - chain::ChainState, Account as _, AccountPurpose, AccountSource, SeedRelevance, - SentTransactionOutput, TransactionDataRequest, TransactionStatus, - }, - keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ - Note, NoteId, Recipient, WalletSaplingOutput, WalletSpend, WalletTransparentOutput, - WalletTx, - }, -}; - -use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, - DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, -}; - -use super::AccountId; - -#[cfg(feature = "transparent-inputs")] -use { - zcash_client_backend::wallet::TransparentAddressMetadata, - zcash_primitives::legacy::TransparentAddress, -}; - -#[cfg(feature = "orchard")] -use { - zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, - zcash_client_backend::wallet::WalletOrchardOutput, -}; - -use crate::error::Error; - -/// Maps a block height and transaction index to a transaction ID. -pub struct TxLocatorMap(HashMap<(BlockHeight, u32), TxId>); - -/// Maps a block height and transaction (i.e. transaction locator) index to a nullifier. -pub struct NullifierMap(BTreeMap); - -/// Keeps track of notes that are spent in which transaction -pub struct ReceievdNoteSpends(HashMap); - -pub struct ReceivedNoteTable(pub Vec); - -pub struct ReceivedNote { - // Uniquely identifies this note - pub note_id: NoteId, - pub txid: TxId, - // output_index: sapling, action_index: orchard - pub output_index: u32, - pub account_id: AccountId, - //sapling: (diversifier, value, rcm) orchard: (diversifier, value, rho, rseed) - pub note: Note, - pub nf: Option, - pub is_change: bool, - pub memo: Memo, - pub commitment_tree_position: Option, - pub recipient_key_scope: Option, -} - -/// A table of received notes. Corresponds to sapling_received_notes and orchard_received_notes tables. -pub struct TransactionEntry { - // created: String, - /// Combines block height and mined_height into a txn status - tx_status: TransactionStatus, - tx_index: Option, - expiry_height: Option, - raw: Vec, - fee: Option, - /// - `target_height`: stores the target height for which the transaction was constructed, if - /// known. This will ordinarily be null for transactions discovered via chain scanning; it - /// will only be set for transactions created using this wallet specifically, and not any - /// other wallet that uses the same seed (including previous installations of the same - /// wallet application.) - target_height: Option, -} -impl TransactionEntry { - pub fn new_from_tx_meta(tx_meta: WalletTx, height: BlockHeight) -> Self { - Self { - tx_status: TransactionStatus::Mined(height), - tx_index: Some(tx_meta.block_index() as u32), - expiry_height: None, - raw: Vec::new(), - fee: None, - target_height: None, - } - } - pub fn expiry_height(&self) -> Option { - self.expiry_height - } - pub fn status(&self) -> TransactionStatus { - self.tx_status - } - pub fn tx_index(&self) -> Option { - self.tx_index - } - pub fn fee(&self) -> Option { - self.fee - } - pub fn target_height(&self) -> Option { - self.target_height - } - pub fn raw(&self) -> &[u8] { - self.raw.as_slice() - } -} -pub struct TransactionTable(HashMap); -impl TransactionTable { - pub fn new() -> Self { - Self(HashMap::new()) - } - /// Returns transaction status for a given transaction ID. None if the transaction is not known. - pub fn tx_status(&self, txid: &TxId) -> Option { - self.0.get(txid).map(|entry| entry.tx_status) - } - pub fn expiry_height(&self, txid: &TxId) -> Option { - self.0.get(txid).and_then(|entry| entry.expiry_height) - } - pub fn get_transaction(&self, txid: TxId) -> Option<&TransactionEntry> { - self.0.get(&txid) - } - /// Inserts information about a MINED transaction that was observed to - /// contain a note related to this wallet - pub fn put_tx_meta(&mut self, tx_meta: WalletTx, height: BlockHeight) { - match self.0.entry(tx_meta.txid()) { - Entry::Occupied(mut entry) => { - entry.get_mut().tx_index = Some(tx_meta.block_index() as u32); - entry.get_mut().tx_status = TransactionStatus::Mined(height); - } - Entry::Vacant(entry) => { - entry.insert(TransactionEntry::new_from_tx_meta(tx_meta, height)); - } - } - } - /// Inserts full transaction data - pub fn put_tx_data( - &mut self, - tx: &Transaction, - fee: Option, - target_height: Option, - ) { - match self.0.entry(tx.txid()) { - Entry::Occupied(mut entry) => { - entry.get_mut().fee = fee; - entry.get_mut().expiry_height = Some(tx.expiry_height()); - entry.get_mut().raw = Vec::new(); - tx.write(&mut entry.get_mut().raw).unwrap(); - } - Entry::Vacant(entry) => { - let mut raw = Vec::new(); - tx.write(&mut raw).unwrap(); - entry.insert(TransactionEntry { - tx_status: TransactionStatus::NotInMainChain, - tx_index: None, - expiry_height: Some(tx.expiry_height()), - raw, - fee, - target_height, - }); - } - } - } - pub fn set_transaction_status( - &mut self, - txid: &TxId, - status: TransactionStatus, - ) -> Result<(), Error> { - if let Some(entry) = self.0.get_mut(txid) { - entry.tx_status = status; - Ok(()) - } else { - return Err(Error::TransactionNotFound(*txid)); - } - } - pub fn get_tx_raw(&self, txid: &TxId) -> Option<&[u8]> { - self.0.get(txid).map(|entry| entry.raw.as_slice()) - } -} - -impl ReceivedNote { - pub fn pool(&self) -> PoolType { - match self.note { - Note::Sapling { .. } => PoolType::SAPLING, - #[cfg(feature = "orchard")] - Note::Orchard { .. } => PoolType::ORCHARD, - } - } - pub fn account_id(&self) -> AccountId { - self.account_id - } - pub fn nullifier(&self) -> Option<&Nullifier> { - self.nf.as_ref() - } - pub fn txid(&self) -> TxId { - self.txid - } - pub fn note_id(&self) -> NoteId { - self.note_id - } - pub fn from_sent_tx_output( - txid: TxId, - output: &SentTransactionOutput, - ) -> Result { - match output.recipient() { - Recipient::InternalAccount { - receiving_account, - note: Note::Sapling(note), - .. - } => Ok(ReceivedNote { - note_id: NoteId::new(txid, Sapling, output.output_index() as u16), - txid: txid, - output_index: output.output_index() as u32, - account_id: *receiving_account, - note: Note::Sapling(note.clone()), - nf: None, - is_change: true, - memo: output.memo().map(|m| Memo::try_from(m).unwrap()).unwrap(), - commitment_tree_position: None, - recipient_key_scope: Some(Scope::Internal), - }), - #[cfg(feature = "orchard")] - Recipient::InternalAccount { - receiving_account, - note: Note::Orchard(note), - .. - } => Ok(ReceivedNote { - note_id: NoteId::new(txid, Orchard, output.output_index() as u16), - txid: txid, - output_index: output.output_index() as u32, - account_id: *receiving_account, - note: Note::Orchard(note.clone()), - nf: None, - is_change: true, - memo: output.memo().map(|m| Memo::try_from(m).unwrap()).unwrap(), - commitment_tree_position: None, - recipient_key_scope: Some(Scope::Internal), - }), - _ => Err(Error::Other( - "Recipient is not an internal shielded account".to_owned(), - )), - } - } - pub fn from_wallet_sapling_output( - note_id: NoteId, - output: &WalletSaplingOutput, - ) -> Self { - ReceivedNote { - note_id, - txid: *note_id.txid(), - output_index: output.index() as u32, - account_id: *output.account_id(), - note: Note::Sapling(output.note().clone()), - nf: output.nf().map(|nf| Nullifier::Sapling(*nf)), - is_change: output.is_change(), - memo: Memo::Empty, - commitment_tree_position: Some(output.note_commitment_tree_position()), - recipient_key_scope: output.recipient_key_scope(), - } - } - #[cfg(feature = "orchard")] - pub fn from_wallet_orchard_output( - note_id: NoteId, - output: &WalletOrchardOutput, - ) -> Self { - ReceivedNote { - note_id, - txid: *note_id.txid(), - output_index: output.index() as u32, - account_id: *output.account_id(), - note: Note::Orchard(output.note().clone()), - nf: output.nf().map(|nf| Nullifier::Orchard(*nf)), - is_change: output.is_change(), - memo: Memo::Empty, - commitment_tree_position: Some(output.note_commitment_tree_position()), - recipient_key_scope: output.recipient_key_scope(), - } - } -} - -impl ReceivedNoteTable { - pub fn new() -> Self { - Self(Vec::new()) - } - - pub fn get_sapling_nullifiers( - &self, - ) -> impl Iterator + '_ { - self.0.iter().filter_map(|entry| { - if let Some(Nullifier::Sapling(nf)) = entry.nullifier() { - Some((entry.account_id(), entry.txid(), *nf)) - } else { - None - } - }) - } - #[cfg(feature = "orchard")] - pub fn get_orchard_nullifiers( - &self, - ) -> impl Iterator + '_ { - self.0.iter().filter_map(|entry| { - if let Some(Nullifier::Orchard(nf)) = entry.nullifier() { - Some((entry.account_id(), entry.txid(), *nf)) - } else { - None - } - }) - } - - pub fn insert_received_note(&mut self, note: ReceivedNote) { - self.0.push(note); - } -} - -impl TransactionTable { - pub fn get(&self, txid: &TxId) -> Option<&TransactionEntry> { - self.0.get(txid) - } - - pub fn get_mut(&mut self, txid: &TxId) -> Option<&mut TransactionEntry> { - self.0.get_mut(txid) - } - - pub fn remove(&mut self, txid: &TxId) -> Option { - self.0.remove(txid) - } -} -impl NullifierMap { - pub fn new() -> Self { - Self(BTreeMap::new()) - } - pub fn insert(&mut self, height: BlockHeight, index: u32, nullifier: Nullifier) { - self.0.insert(nullifier, (height, index)); - } - - pub fn get(&self, nullifier: &Nullifier) -> Option<&(BlockHeight, u32)> { - self.0.get(nullifier) - } -} -impl TxLocatorMap { - pub fn new() -> Self { - Self(HashMap::new()) - } - pub fn insert(&mut self, height: BlockHeight, index: u32, txid: TxId) { - self.0.insert((height, index), txid); - } - - pub fn get(&self, height: BlockHeight, index: u32) -> Option<&TxId> { - self.0.get(&(height, index)) - } - pub fn entry(&mut self, k: (BlockHeight, u32)) -> Entry<(BlockHeight, u32), TxId> { - self.0.entry(k) - } -} -impl ReceievdNoteSpends { - pub fn new() -> Self { - Self(HashMap::new()) - } - pub fn insert_spend(&mut self, note_id: NoteId, txid: TxId) -> Option { - self.0.insert(note_id, txid) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Nullifier { - #[cfg(feature = "orchard")] - Orchard(orchard::note::Nullifier), - Sapling(sapling::Nullifier), -} - -impl Nullifier { - pub fn pool(&self) -> PoolType { - match self { - #[cfg(feature = "orchard")] - Nullifier::Orchard(_) => PoolType::ORCHARD, - Nullifier::Sapling(_) => PoolType::SAPLING, - } - } -} -#[cfg(feature = "orchard")] -impl From for Nullifier { - fn from(n: orchard::note::Nullifier) -> Self { - Nullifier::Orchard(n) - } -} -impl From for Nullifier { - fn from(n: sapling::Nullifier) -> Self { - Nullifier::Sapling(n) - } -} diff --git a/zcash_client_memory/src/types/account.rs b/zcash_client_memory/src/types/account.rs new file mode 100644 index 0000000000..d1de8d2e7b --- /dev/null +++ b/zcash_client_memory/src/types/account.rs @@ -0,0 +1,163 @@ +use crate::AccountId; + +use std::collections::{BTreeMap, HashSet}; +use zcash_keys::keys::{AddressGenerationError, UnifiedIncomingViewingKey}; +use zip32::DiversifierIndex; + +use zcash_client_backend::{ + address::UnifiedAddress, + data_api::{Account as _, AccountPurpose, AccountSource}, + keys::{UnifiedAddressRequest, UnifiedFullViewingKey}, + wallet::NoteId, +}; + +use zcash_client_backend::data_api::AccountBirthday; + +use crate::error::Error; + +/// An account stored in a `zcash_client_sqlite` database. +#[derive(Debug, Clone)] +pub struct Account { + account_id: AccountId, + kind: AccountSource, + viewing_key: ViewingKey, + birthday: AccountBirthday, + _purpose: AccountPurpose, // TODO: Remove this. AccountSource should be sufficient. + addresses: BTreeMap, + _notes: HashSet, +} + +/// The viewing key that an [`Account`] has available to it. +#[derive(Debug, Clone)] +pub(crate) enum ViewingKey { + /// A full viewing key. + /// + /// This is available to derived accounts, as well as accounts directly imported as + /// full viewing keys. + Full(Box), + + /// An incoming viewing key. + /// + /// Accounts that have this kind of viewing key cannot be used in wallet contexts, + /// because they are unable to maintain an accurate balance. + _Incoming(Box), +} + +impl Account { + pub(crate) fn new( + account_id: AccountId, + kind: AccountSource, + viewing_key: ViewingKey, + birthday: AccountBirthday, + purpose: AccountPurpose, + ) -> Result { + let mut acc = Self { + account_id, + kind, + viewing_key, + birthday, + _purpose: purpose, + addresses: BTreeMap::new(), + _notes: HashSet::new(), + }; + let ua_request = acc + .viewing_key + .uivk() + .to_address_request() + .and_then(|ua_request| ua_request.intersect(&UnifiedAddressRequest::all().unwrap())) + .ok_or_else(|| { + Error::AddressGeneration(AddressGenerationError::ShieldedReceiverRequired) + })?; + + let (addr, diversifier_index) = acc.default_address(ua_request)?; + acc.addresses.insert(diversifier_index, addr); + Ok(acc) + } + /// Returns the default Unified Address for the account, + /// along with the diversifier index that generated it. + /// + /// The diversifier index may be non-zero if the Unified Address includes a Sapling + /// receiver, and there was no valid Sapling receiver at diversifier index zero. + pub(crate) fn default_address( + &self, + request: UnifiedAddressRequest, + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { + self.uivk().default_address(request) + } + + pub(crate) fn birthday(&self) -> &AccountBirthday { + &self.birthday + } + + pub(crate) fn _addresses(&self) -> &BTreeMap { + &self.addresses + } + + pub(crate) fn current_address(&self) -> Option<(DiversifierIndex, UnifiedAddress)> { + self.addresses + .last_key_value() + .map(|(diversifier_index, address)| (*diversifier_index, address.clone())) + } + pub(crate) fn kind(&self) -> &AccountSource { + &self.kind + } + pub(crate) fn _viewing_key(&self) -> &ViewingKey { + &self.viewing_key + } + pub(crate) fn next_available_address( + &mut self, + request: UnifiedAddressRequest, + ) -> Result, Error> { + match self.ufvk() { + Some(ufvk) => { + let search_from = match self.current_address() { + Some((mut last_diversifier_index, _)) => { + last_diversifier_index + .increment() + .map_err(|_| AddressGenerationError::DiversifierSpaceExhausted)?; + last_diversifier_index + } + None => DiversifierIndex::default(), + }; + let (addr, diversifier_index) = ufvk.find_address(search_from, request)?; + self.addresses.insert(diversifier_index, addr.clone()); + Ok(Some(addr)) + } + None => Ok(None), + } + } +} + +impl zcash_client_backend::data_api::Account for Account { + fn id(&self) -> AccountId { + self.account_id + } + + fn source(&self) -> AccountSource { + self.kind + } + + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + self.viewing_key.ufvk() + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + self.viewing_key.uivk() + } +} + +impl ViewingKey { + fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { + match self { + ViewingKey::Full(ufvk) => Some(ufvk), + ViewingKey::_Incoming(_) => None, + } + } + + fn uivk(&self) -> UnifiedIncomingViewingKey { + match self { + ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(), + ViewingKey::_Incoming(uivk) => uivk.as_ref().clone(), + } + } +} diff --git a/zcash_client_memory/src/types/block.rs b/zcash_client_memory/src/types/block.rs new file mode 100644 index 0000000000..d165675f39 --- /dev/null +++ b/zcash_client_memory/src/types/block.rs @@ -0,0 +1,45 @@ +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, +}; + +use zcash_primitives::{block::BlockHash, consensus::BlockHeight, transaction::TxId}; +use zcash_protocol::memo::MemoBytes; + +use zcash_client_backend::wallet::NoteId; + +/// Internal wallet representation of a Block. +pub(crate) struct MemoryWalletBlock { + pub(crate) height: BlockHeight, + pub(crate) hash: BlockHash, + pub(crate) block_time: u32, + // Just the transactions that involve an account in this wallet + pub(crate) _transactions: HashSet, + pub(crate) _memos: HashMap, + pub(crate) sapling_commitment_tree_size: Option, + pub(crate) _sapling_output_count: Option, + #[cfg(feature = "orchard")] + pub(crate) orchard_commitment_tree_size: Option, + #[cfg(feature = "orchard")] + pub(crate) _orchard_action_count: Option, +} + +impl PartialEq for MemoryWalletBlock { + fn eq(&self, other: &Self) -> bool { + (self.height, self.block_time) == (other.height, other.block_time) + } +} + +impl Eq for MemoryWalletBlock {} + +impl PartialOrd for MemoryWalletBlock { + fn partial_cmp(&self, other: &Self) -> Option { + Some((self.height, self.block_time).cmp(&(other.height, other.block_time))) + } +} + +impl Ord for MemoryWalletBlock { + fn cmp(&self, other: &Self) -> Ordering { + (self.height, self.block_time).cmp(&(other.height, other.block_time)) + } +} diff --git a/zcash_client_memory/src/types/mod.rs b/zcash_client_memory/src/types/mod.rs new file mode 100644 index 0000000000..9f864e5825 --- /dev/null +++ b/zcash_client_memory/src/types/mod.rs @@ -0,0 +1,13 @@ +pub(crate) mod account; +pub(crate) mod block; +pub(crate) mod notes; +pub(crate) mod nullifier; +pub(crate) mod scanning; +pub(crate) mod transaction; + +pub(crate) use account::*; +pub(crate) use block::*; +pub(crate) use notes::*; +pub(crate) use nullifier::*; + +pub(crate) use transaction::*; diff --git a/zcash_client_memory/src/types/notes.rs b/zcash_client_memory/src/types/notes.rs new file mode 100644 index 0000000000..2a1b9a1829 --- /dev/null +++ b/zcash_client_memory/src/types/notes.rs @@ -0,0 +1,187 @@ +use incrementalmerkletree::Position; + +use std::collections::HashMap; + +use zip32::Scope; + +use zcash_primitives::transaction::TxId; +use zcash_protocol::{memo::Memo, PoolType, ShieldedProtocol::Sapling}; + +use zcash_client_backend::{ + data_api::SentTransactionOutput, + wallet::{Note, NoteId, Recipient, WalletSaplingOutput}, +}; + +use crate::AccountId; + +#[cfg(feature = "orchard")] +use { + zcash_client_backend::wallet::WalletOrchardOutput, zcash_protocol::ShieldedProtocol::Orchard, +}; + +use crate::{error::Error, Nullifier}; + +/// Keeps track of notes that are spent in which transaction +pub(crate) struct ReceievdNoteSpends(HashMap); + +impl ReceievdNoteSpends { + pub fn new() -> Self { + Self(HashMap::new()) + } + pub fn insert_spend(&mut self, note_id: NoteId, txid: TxId) -> Option { + self.0.insert(note_id, txid) + } +} + +/// A note that has been received by the wallet +/// TODO: Instead of Vec, perhaps we should identify by some unique ID +pub(crate) struct ReceivedNoteTable(pub Vec); + +pub(crate) struct ReceivedNote { + // Uniquely identifies this note + pub(crate) note_id: NoteId, + pub(crate) txid: TxId, + // output_index: sapling, action_index: orchard + pub(crate) _output_index: u32, + pub(crate) account_id: AccountId, + //sapling: (diversifier, value, rcm) orchard: (diversifier, value, rho, rseed) + pub(crate) _note: Note, + pub(crate) nf: Option, + pub(crate) _is_change: bool, + pub(crate) _memo: Memo, + pub(crate) _commitment_tree_position: Option, + pub(crate) _recipient_key_scope: Option, +} +impl ReceivedNote { + pub fn _pool(&self) -> PoolType { + match self._note { + Note::Sapling { .. } => PoolType::SAPLING, + #[cfg(feature = "orchard")] + Note::Orchard { .. } => PoolType::ORCHARD, + } + } + pub fn account_id(&self) -> AccountId { + self.account_id + } + pub fn nullifier(&self) -> Option<&Nullifier> { + self.nf.as_ref() + } + pub fn txid(&self) -> TxId { + self.txid + } + pub fn note_id(&self) -> NoteId { + self.note_id + } + pub fn from_sent_tx_output( + txid: TxId, + output: &SentTransactionOutput, + ) -> Result { + match output.recipient() { + Recipient::InternalAccount { + receiving_account, + note: Note::Sapling(note), + .. + } => Ok(ReceivedNote { + note_id: NoteId::new(txid, Sapling, output.output_index() as u16), + txid, + _output_index: output.output_index() as u32, + account_id: *receiving_account, + _note: Note::Sapling(note.clone()), + nf: None, + _is_change: true, + _memo: output.memo().map(|m| Memo::try_from(m).unwrap()).unwrap(), + _commitment_tree_position: None, + _recipient_key_scope: Some(Scope::Internal), + }), + #[cfg(feature = "orchard")] + Recipient::InternalAccount { + receiving_account, + note: Note::Orchard(note), + .. + } => Ok(ReceivedNote { + note_id: NoteId::new(txid, Orchard, output.output_index() as u16), + txid, + _output_index: output.output_index() as u32, + account_id: *receiving_account, + _note: Note::Orchard(*note), + nf: None, + _is_change: true, + _memo: output.memo().map(|m| Memo::try_from(m).unwrap()).unwrap(), + _commitment_tree_position: None, + _recipient_key_scope: Some(Scope::Internal), + }), + _ => Err(Error::Other( + "Recipient is not an internal shielded account".to_owned(), + )), + } + } + pub fn from_wallet_sapling_output( + note_id: NoteId, + output: &WalletSaplingOutput, + ) -> Self { + ReceivedNote { + note_id, + txid: *note_id.txid(), + _output_index: output.index() as u32, + account_id: *output.account_id(), + _note: Note::Sapling(output.note().clone()), + nf: output.nf().map(|nf| Nullifier::Sapling(*nf)), + _is_change: output.is_change(), + _memo: Memo::Empty, + _commitment_tree_position: Some(output.note_commitment_tree_position()), + _recipient_key_scope: output.recipient_key_scope(), + } + } + #[cfg(feature = "orchard")] + pub fn from_wallet_orchard_output( + note_id: NoteId, + output: &WalletOrchardOutput, + ) -> Self { + ReceivedNote { + note_id, + txid: *note_id.txid(), + _output_index: output.index() as u32, + account_id: *output.account_id(), + _note: Note::Orchard(*output.note()), + nf: output.nf().map(|nf| Nullifier::Orchard(*nf)), + _is_change: output.is_change(), + _memo: Memo::Empty, + _commitment_tree_position: Some(output.note_commitment_tree_position()), + _recipient_key_scope: output.recipient_key_scope(), + } + } +} + +impl ReceivedNoteTable { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn get_sapling_nullifiers( + &self, + ) -> impl Iterator + '_ { + self.0.iter().filter_map(|entry| { + if let Some(Nullifier::Sapling(nf)) = entry.nullifier() { + Some((entry.account_id(), entry.txid(), *nf)) + } else { + None + } + }) + } + #[cfg(feature = "orchard")] + pub fn get_orchard_nullifiers( + &self, + ) -> impl Iterator + '_ { + self.0.iter().filter_map(|entry| { + if let Some(Nullifier::Orchard(nf)) = entry.nullifier() { + Some((entry.account_id(), entry.txid(), *nf)) + } else { + None + } + }) + } + + pub fn insert_received_note(&mut self, note: ReceivedNote) { + self.0.push(note); + } +} diff --git a/zcash_client_memory/src/types/nullifier.rs b/zcash_client_memory/src/types/nullifier.rs new file mode 100644 index 0000000000..86049549e0 --- /dev/null +++ b/zcash_client_memory/src/types/nullifier.rs @@ -0,0 +1,48 @@ +use std::collections::BTreeMap; + +use zcash_primitives::consensus::BlockHeight; +use zcash_protocol::PoolType; + +/// Maps a block height and transaction (i.e. transaction locator) index to a nullifier. +pub(crate) struct NullifierMap(BTreeMap); + +impl NullifierMap { + pub fn new() -> Self { + Self(BTreeMap::new()) + } + pub fn insert(&mut self, height: BlockHeight, index: u32, nullifier: Nullifier) { + self.0.insert(nullifier, (height, index)); + } + + pub fn get(&self, nullifier: &Nullifier) -> Option<&(BlockHeight, u32)> { + self.0.get(nullifier) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Nullifier { + #[cfg(feature = "orchard")] + Orchard(orchard::note::Nullifier), + Sapling(sapling::Nullifier), +} + +impl Nullifier { + pub(crate) fn _pool(&self) -> PoolType { + match self { + #[cfg(feature = "orchard")] + Nullifier::Orchard(_) => PoolType::ORCHARD, + Nullifier::Sapling(_) => PoolType::SAPLING, + } + } +} +#[cfg(feature = "orchard")] +impl From for Nullifier { + fn from(n: orchard::note::Nullifier) -> Self { + Nullifier::Orchard(n) + } +} +impl From for Nullifier { + fn from(n: sapling::Nullifier) -> Self { + Nullifier::Sapling(n) + } +} diff --git a/zcash_client_memory/src/types/scanning.rs b/zcash_client_memory/src/types/scanning.rs new file mode 100644 index 0000000000..eb970913de --- /dev/null +++ b/zcash_client_memory/src/types/scanning.rs @@ -0,0 +1,156 @@ +use std::ops::{Deref, DerefMut, Range}; + +use zcash_primitives::consensus::BlockHeight; + +use zcash_client_backend::data_api::scanning::{spanning_tree::SpanningTree, ScanPriority}; + +use zcash_client_backend::data_api::scanning::ScanRange; + +use crate::error::Error; + +/// A queue of scanning ranges. Contains the start and end heights of each range, along with the +/// priority of scanning that range. +pub(crate) struct ScanQueue(Vec<(BlockHeight, BlockHeight, ScanPriority)>); + +impl ScanQueue { + pub(crate) fn new() -> Self { + ScanQueue(Vec::new()) + } + + pub(crate) fn suggest_scan_ranges(&self, min_priority: ScanPriority) -> Vec { + let mut priorities: Vec<_> = self + .0 + .iter() + .filter(|(_, _, p)| *p >= min_priority) + .collect(); + priorities.sort_by(|(_, _, a), (_, _, b)| b.cmp(a)); + + priorities + .into_iter() + .map(|(start, end, priority)| { + let range = Range { + start: *start, + end: *end, + }; + ScanRange::from_parts(range, *priority) + }) + .collect() + } + fn _insert_queue_entries<'a>( + &mut self, + entries: impl Iterator, + ) -> Result<(), Error> { + for entry in entries { + if entry.block_range().start >= entry.block_range().end { + return Err(Error::InvalidScanRange( + entry.block_range().start, + entry.block_range().end, + "start must be less than end".to_string(), + )); + } + + for (start, end, _) in &self.0 { + if *start == entry.block_range().start || *end == entry.block_range().end { + return Err(Error::InvalidScanRange( + entry.block_range().start, + entry.block_range().end, + "at least part of range is already covered by another range".to_string(), + )); + } + } + + self.0.push(( + entry.block_range().start, + entry.block_range().end, + entry.priority(), + )); + } + Ok(()) + } + pub(crate) fn _replace_queue_entries( + &mut self, + query_range: &Range, + entries: impl Iterator, + force_rescans: bool, + ) -> Result<(), Error> { + let (to_create, to_delete_ends) = { + let mut q_ranges: Vec<_> = self + .0 + .iter() + .filter(|(start, end, _)| { + // Ignore ranges that do not overlap and are not adjacent to the query range. + !(start > &query_range.end || &query_range.start > end) + }) + .collect(); + q_ranges.sort_by(|(_, end_a, _), (_, end_b, _)| end_a.cmp(end_b)); + + // Iterate over the ranges in the scan queue that overlap the range that we have + // identified as needing to be fully scanned. For each such range add it to the + // spanning tree (these should all be nonoverlapping ranges, but we might coalesce + // some in the process). + let mut to_create: Option = None; + let mut to_delete_ends: Vec = vec![]; + + let q_ranges = q_ranges.into_iter(); + for (start, end, priority) in q_ranges { + let entry = ScanRange::from_parts( + Range { + start: *start, + end: *end, + }, + *priority, + ); + to_delete_ends.push(entry.block_range().end); + to_create = if let Some(cur) = to_create { + Some(cur.insert(entry, force_rescans)) + } else { + Some(SpanningTree::Leaf(entry)) + }; + } + + // Update the tree that we read from the database, or if we didn't find any ranges + // start with the scanned range. + for entry in entries { + to_create = if let Some(cur) = to_create { + Some(cur.insert(entry, force_rescans)) + } else { + Some(SpanningTree::Leaf(entry)) + }; + } + (to_create, to_delete_ends) + }; + + if let Some(tree) = to_create { + self.0.retain(|(_, block_range_end, _)| { + // if the block_range_end is equal to any in to_delete_ends, remove it + !to_delete_ends.contains(block_range_end) + }); + let scan_ranges = tree.into_vec(); + self._insert_queue_entries(scan_ranges.iter())?; + } + Ok(()) + } +} + +impl IntoIterator for ScanQueue { + type Item = (BlockHeight, BlockHeight, ScanPriority); + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +// We deref to slice so that we can reuse the slice impls +impl Deref for ScanQueue { + type Target = [(BlockHeight, BlockHeight, ScanPriority)]; + + fn deref(&self) -> &Self::Target { + &self.0[..] + } +} +impl DerefMut for ScanQueue { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0[..] + } +} diff --git a/zcash_client_memory/src/types/transaction.rs b/zcash_client_memory/src/types/transaction.rs new file mode 100644 index 0000000000..64cfa6d8e2 --- /dev/null +++ b/zcash_client_memory/src/types/transaction.rs @@ -0,0 +1,156 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use zcash_primitives::{ + consensus::BlockHeight, + transaction::{Transaction, TxId}, +}; +use zcash_protocol::value::Zatoshis; + +use zcash_client_backend::{data_api::TransactionStatus, wallet::WalletTx}; + +use crate::AccountId; + +use crate::error::Error; + +/// Maps a block height and transaction index to a transaction ID. +pub(crate) struct TxLocatorMap(HashMap<(BlockHeight, u32), TxId>); + +/// A table of received notes. Corresponds to sapling_received_notes and orchard_received_notes tables. +pub(crate) struct TransactionEntry { + // created: String, + /// Combines block height and mined_height into a txn status + tx_status: TransactionStatus, + tx_index: Option, + expiry_height: Option, + raw: Vec, + fee: Option, + /// - `target_height`: stores the target height for which the transaction was constructed, if + /// known. This will ordinarily be null for transactions discovered via chain scanning; it + /// will only be set for transactions created using this wallet specifically, and not any + /// other wallet that uses the same seed (including previous installations of the same + /// wallet application.) + _target_height: Option, +} +impl TransactionEntry { + pub fn new_from_tx_meta(tx_meta: WalletTx, height: BlockHeight) -> Self { + Self { + tx_status: TransactionStatus::Mined(height), + tx_index: Some(tx_meta.block_index() as u32), + expiry_height: None, + raw: Vec::new(), + fee: None, + _target_height: None, + } + } + pub(crate) fn expiry_height(&self) -> Option { + self.expiry_height + } + pub(crate) fn status(&self) -> TransactionStatus { + self.tx_status + } + pub(crate) fn raw(&self) -> &[u8] { + self.raw.as_slice() + } +} +pub(crate) struct TransactionTable(HashMap); +impl TransactionTable { + pub(crate) fn new() -> Self { + Self(HashMap::new()) + } + /// Returns transaction status for a given transaction ID. None if the transaction is not known. + pub(crate) fn tx_status(&self, txid: &TxId) -> Option { + self.0.get(txid).map(|entry| entry.tx_status) + } + pub(crate) fn expiry_height(&self, txid: &TxId) -> Option { + self.0.get(txid).and_then(|entry| entry.expiry_height) + } + pub(crate) fn _get_transaction(&self, txid: TxId) -> Option<&TransactionEntry> { + self.0.get(&txid) + } + /// Inserts information about a MINED transaction that was observed to + /// contain a note related to this wallet + pub(crate) fn put_tx_meta(&mut self, tx_meta: WalletTx, height: BlockHeight) { + match self.0.entry(tx_meta.txid()) { + Entry::Occupied(mut entry) => { + entry.get_mut().tx_index = Some(tx_meta.block_index() as u32); + entry.get_mut().tx_status = TransactionStatus::Mined(height); + } + Entry::Vacant(entry) => { + entry.insert(TransactionEntry::new_from_tx_meta(tx_meta, height)); + } + } + } + /// Inserts full transaction data + pub(crate) fn put_tx_data( + &mut self, + tx: &Transaction, + fee: Option, + target_height: Option, + ) { + match self.0.entry(tx.txid()) { + Entry::Occupied(mut entry) => { + entry.get_mut().fee = fee; + entry.get_mut().expiry_height = Some(tx.expiry_height()); + entry.get_mut().raw = Vec::new(); + tx.write(&mut entry.get_mut().raw).unwrap(); + } + Entry::Vacant(entry) => { + let mut raw = Vec::new(); + tx.write(&mut raw).unwrap(); + entry.insert(TransactionEntry { + tx_status: TransactionStatus::NotInMainChain, + tx_index: None, + expiry_height: Some(tx.expiry_height()), + raw, + fee, + _target_height: target_height, + }); + } + } + } + pub(crate) fn set_transaction_status( + &mut self, + txid: &TxId, + status: TransactionStatus, + ) -> Result<(), Error> { + if let Some(entry) = self.0.get_mut(txid) { + entry.tx_status = status; + Ok(()) + } else { + Err(Error::TransactionNotFound(*txid)) + } + } + pub(crate) fn get_tx_raw(&self, txid: &TxId) -> Option<&[u8]> { + self.0.get(txid).map(|entry| entry.raw.as_slice()) + } +} + +impl TransactionTable { + pub(crate) fn get(&self, txid: &TxId) -> Option<&TransactionEntry> { + self.0.get(txid) + } + + pub(crate) fn _get_mut(&mut self, txid: &TxId) -> Option<&mut TransactionEntry> { + self.0.get_mut(txid) + } + + pub(crate) fn _remove(&mut self, txid: &TxId) -> Option { + self.0.remove(txid) + } +} + +impl TxLocatorMap { + pub(crate) fn new() -> Self { + Self(HashMap::new()) + } + pub(crate) fn _insert(&mut self, height: BlockHeight, index: u32, txid: TxId) { + self.0.insert((height, index), txid); + } + + pub(crate) fn get(&self, height: BlockHeight, index: u32) -> Option<&TxId> { + self.0.get(&(height, index)) + } + pub(crate) fn entry(&mut self, k: (BlockHeight, u32)) -> Entry<(BlockHeight, u32), TxId> { + self.0.entry(k) + } +} diff --git a/zcash_client_memory/src/mem_wallet/wallet_commitment_trees.rs b/zcash_client_memory/src/wallet_commitment_trees.rs similarity index 64% rename from zcash_client_memory/src/mem_wallet/wallet_commitment_trees.rs rename to zcash_client_memory/src/wallet_commitment_trees.rs index 2c2f5dfdbe..37825f9e95 100644 --- a/zcash_client_memory/src/mem_wallet/wallet_commitment_trees.rs +++ b/zcash_client_memory/src/wallet_commitment_trees.rs @@ -1,50 +1,18 @@ -use incrementalmerkletree::{Address, Marking, Retention}; -use sapling::NullifierDerivingKey; -use secrecy::{ExposeSecret, SecretVec}; -use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - cmp::Ordering, - collections::{BTreeMap, HashMap, HashSet}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, -}; -use zcash_keys::keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; +use incrementalmerkletree::Address; -use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network}, - transaction::{Transaction, TxId}, - zip32::AccountId, -}; -use zcash_protocol::{ - memo::{self, Memo, MemoBytes}, - value::Zatoshis, - ShieldedProtocol::{Orchard, Sapling}, -}; +use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; +use std::convert::Infallible; -use zcash_client_backend::{ - address::UnifiedAddress, - data_api::{ - chain::ChainState, AccountPurpose, AccountSource, SeedRelevance, TransactionDataRequest, - TransactionStatus, - }, - keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{NoteId, WalletSpend, WalletTransparentOutput, WalletTx}, -}; +use zcash_primitives::consensus::BlockHeight; use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, scanning::ScanRange, Account as _, AccountBirthday, BlockMetadata, - DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + chain::CommitmentTreeRoot, WalletCommitmentTrees, SAPLING_SHARD_HEIGHT, }; #[cfg(feature = "orchard")] use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; -use super::{Account, MemoryWalletBlock, MemoryWalletDb}; -use crate::error::Error; +use super::MemoryWalletDb; impl WalletCommitmentTrees for MemoryWalletDb { type Error = Infallible; diff --git a/zcash_client_memory/src/mem_wallet/wallet_read.rs b/zcash_client_memory/src/wallet_read.rs similarity index 73% rename from zcash_client_memory/src/mem_wallet/wallet_read.rs rename to zcash_client_memory/src/wallet_read.rs index 8b82987696..af3b1e4ff6 100644 --- a/zcash_client_memory/src/mem_wallet/wallet_read.rs +++ b/zcash_client_memory/src/wallet_read.rs @@ -1,47 +1,33 @@ -use incrementalmerkletree::{Address, Marking, Retention}; use nonempty::NonEmpty; -use sapling::NullifierDerivingKey; + use secrecy::{ExposeSecret, SecretVec}; -use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - clone, - cmp::Ordering, - collections::{BTreeMap, HashMap, HashSet}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, -}; -use zcash_keys::keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; -use std::ops::Add; +use std::{collections::HashMap, num::NonZeroU32}; +use zcash_keys::keys::UnifiedIncomingViewingKey; +use zip32::fingerprint::SeedFingerprint; + use zcash_client_backend::{ address::UnifiedAddress, data_api::{ - chain::ChainState, Account as _, AccountPurpose, AccountSource, SeedRelevance, - TransactionDataRequest, TransactionStatus, + scanning::ScanPriority, Account as _, AccountSource, SeedRelevance, TransactionDataRequest, + TransactionStatus, }, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{NoteId, WalletSpend, WalletTransparentOutput, WalletTx}, + wallet::NoteId, }; use zcash_primitives::{ block::BlockHash, - consensus::{BlockHeight, Network}, + consensus::BlockHeight, transaction::{Transaction, TransactionData, TxId}, }; use zcash_protocol::{ consensus::{self, BranchId}, - memo::{self, Memo, MemoBytes}, - value::Zatoshis, - ShieldedProtocol::{Orchard, Sapling}, + memo::Memo, }; use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, - DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + scanning::ScanRange, BlockMetadata, NullifierQuery, WalletRead, WalletSummary, }; -use zcash_primitives::transaction::components::OutPoint; #[cfg(feature = "transparent-inputs")] use { @@ -50,7 +36,7 @@ use { }; use super::{Account, AccountId, MemoryWalletDb}; -use crate::error::Error; +use crate::{error::Error, MemoryWalletBlock}; impl WalletRead for MemoryWalletDb { type Error = Error; @@ -65,7 +51,7 @@ impl WalletRead for MemoryWalletDb { &self, account_id: Self::AccountId, ) -> Result, Self::Error> { - Ok(self.accounts.get(*account_id as usize).map(|a| a.clone())) + Ok(self.accounts.get(*account_id as usize).cloned()) } fn get_derived_account( @@ -84,7 +70,7 @@ impl WalletRead for MemoryWalletDb { None } } - AccountSource::Imported { purpose } => None, + AccountSource::Imported { purpose: _ } => None, })) } @@ -214,7 +200,11 @@ impl WalletRead for MemoryWalletDb { } fn chain_height(&self) -> Result, Self::Error> { - todo!() + Ok(self + .scan_queue + .iter() + .max_by(|(_, end_a, _), (_, end_b, _)| end_a.cmp(end_b)) + .map(|(_, end, _)| end.saturating_sub(1))) } fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { @@ -227,24 +217,92 @@ impl WalletRead for MemoryWalletDb { })) } - fn block_metadata(&self, _height: BlockHeight) -> Result, Self::Error> { - todo!() + fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error> { + Ok(self.blocks.get(&height).map(|block| { + let MemoryWalletBlock { + height, + hash, + sapling_commitment_tree_size, + #[cfg(feature = "orchard")] + orchard_commitment_tree_size, + .. + } = block; + // TODO: Deal with legacy sapling trees + BlockMetadata::from_parts( + *height, + *hash, + *sapling_commitment_tree_size, + #[cfg(feature = "orchard")] + *orchard_commitment_tree_size, + ) + })) } fn block_fully_scanned(&self) -> Result, Self::Error> { - todo!() + if let Some(birthday_height) = self.get_wallet_birthday()? { + // We assume that the only way we get a contiguous range of block heights in the `blocks` table + // starting with the birthday block, is if all scanning operations have been performed on those + // blocks. This holds because the `blocks` table is only altered by `WalletDb::put_blocks` via + // `put_block`, and the effective combination of intra-range linear scanning and the nullifier + // map ensures that we discover all wallet-related information within the contiguous range. + // + // We also assume that every contiguous range of block heights in the `blocks` table has a + // single matching entry in the `scan_queue` table with priority "Scanned". This requires no + // bugs in the scan queue update logic, which we have had before. However, a bug here would + // mean that we return a more conservative fully-scanned height, which likely just causes a + // performance regression. + // + // The fully-scanned height is therefore the last height that falls within the first range in + // the scan queue with priority "Scanned". + // SQL query problems. + + let mut scanned_ranges: Vec<_> = self + .scan_queue + .iter() + .filter(|(_, _, p)| p == &ScanPriority::Scanned) + .collect(); + scanned_ranges.sort_by(|(start_a, _, _), (start_b, _, _)| start_a.cmp(start_b)); + if let Some(fully_scanned_height) = scanned_ranges.first().and_then( + |(block_range_start, block_range_end, _priority)| { + // If the start of the earliest scanned range is greater than + // the birthday height, then there is an unscanned range between + // the wallet birthday and that range, so there is no fully + // scanned height. + if *block_range_start <= birthday_height { + // Scan ranges are end-exclusive. + Some(*block_range_end - 1) + } else { + None + } + }, + ) { + self.block_metadata(fully_scanned_height) + } else { + Ok(None) + } + } else { + Ok(None) + } } fn get_max_height_hash(&self) -> Result, Self::Error> { - todo!() + Ok(self + .blocks + .last_key_value() + .map(|(height, block)| (*height, block.hash))) } fn block_max_scanned(&self) -> Result, Self::Error> { - todo!() + Ok(self + .blocks + .last_key_value() + .map(|(height, _)| self.block_metadata(*height)) + .transpose()? + .flatten()) } fn suggest_scan_ranges(&self) -> Result, Self::Error> { - Ok(vec![]) + Ok(self.scan_queue.suggest_scan_ranges(ScanPriority::Historic)) } fn get_target_and_anchor_heights( @@ -272,24 +330,21 @@ impl WalletRead for MemoryWalletDb { Ok(self .accounts .iter() - .filter_map(|account| match account.ufvk() { - Some(ufvk) => Some((account.id(), ufvk.clone())), - None => None, - }) + .filter_map(|account| account.ufvk().map(|ufvk| (account.id(), ufvk.clone()))) .collect()) } - fn get_memo(&self, id_note: NoteId) -> Result, Self::Error> { + fn get_memo(&self, _id_note: NoteId) -> Result, Self::Error> { todo!() } fn get_transaction(&self, txid: TxId) -> Result, Self::Error> { - let raw = self.tx_table.get_tx_raw(&txid); - let status = self.tx_table.tx_status(&txid); - let expiry_height = self.tx_table.expiry_height(&txid); + let _raw = self.tx_table.get_tx_raw(&txid); + let _status = self.tx_table.tx_status(&txid); + let _expiry_height = self.tx_table.expiry_height(&txid); self.tx_table .get(&txid) - .and_then(|tx| Some((tx.status(), tx.expiry_height(), tx.raw()))) + .map(|tx| (tx.status(), tx.expiry_height(), tx.raw())) .map(|(status, expiry_height, raw)| { // We need to provide a consensus branch ID so that pre-v5 `Transaction` structs // (which don't commit directly to one) can store it internally. @@ -302,18 +357,18 @@ impl WalletRead for MemoryWalletDb { // height or return an error. if let TransactionStatus::Mined(height) = status { return Ok(Some( - Transaction::read(&raw[..], BranchId::for_height(&self.network, height)) + Transaction::read(raw, BranchId::for_height(&self.network, height)) .map(|t| (height, t)), )); } if let Some(height) = expiry_height.filter(|h| h > &BlockHeight::from(0)) { return Ok(Some( - Transaction::read(&raw[..], BranchId::for_height(&self.network, height)) + Transaction::read(raw, BranchId::for_height(&self.network, height)) .map(|t| (height, t)), )); } - let tx_data = Transaction::read(&raw[..], BranchId::Sprout) + let tx_data = Transaction::read(raw, BranchId::Sprout) .map_err(Self::Error::from)? .into_data(); @@ -405,9 +460,9 @@ impl WalletRead for MemoryWalletDb { #[cfg(feature = "transparent-inputs")] fn get_transparent_balances( &self, - account: Self::AccountId, - max_height: BlockHeight, - ) -> Result, Self::Error> { + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { todo!() } diff --git a/zcash_client_memory/src/mem_wallet/wallet_write.rs b/zcash_client_memory/src/wallet_write.rs similarity index 77% rename from zcash_client_memory/src/mem_wallet/wallet_write.rs rename to zcash_client_memory/src/wallet_write.rs index ae195904d7..f0a00f54c1 100644 --- a/zcash_client_memory/src/mem_wallet/wallet_write.rs +++ b/zcash_client_memory/src/wallet_write.rs @@ -1,56 +1,33 @@ -use incrementalmerkletree::{Address, Marking, Retention}; -use sapling::NullifierDerivingKey; +use incrementalmerkletree::{Marking, Retention}; + use secrecy::{ExposeSecret, SecretVec}; -use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - cmp::Ordering, - collections::{BTreeMap, HashMap, HashSet}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, -}; -use zcash_keys::{ - address::Receiver, - keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}, -}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; -use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network}, - transaction::{Transaction, TxId}, -}; -use zcash_protocol::{ - memo::{self, Memo, MemoBytes}, - value::Zatoshis, - PoolType, - ShieldedProtocol::{Orchard, Sapling}, -}; +use std::collections::HashMap; + +use zip32::fingerprint::SeedFingerprint; + +use zcash_primitives::{consensus::BlockHeight, transaction::TxId}; +use zcash_protocol::ShieldedProtocol::Sapling; use zcash_client_backend::{ address::UnifiedAddress, - data_api::{ - chain::ChainState, AccountPurpose, AccountSource, SeedRelevance, TransactionDataRequest, - TransactionStatus, - }, + data_api::{chain::ChainState, AccountPurpose, AccountSource, TransactionStatus}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{ - Note, NoteId, Recipient, WalletSaplingOutput, WalletSpend, WalletTransparentOutput, - WalletTx, - }, - TransferType, + wallet::{NoteId, Recipient, WalletTransparentOutput}, }; use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, scanning::ScanRange, Account as _, AccountBirthday, BlockMetadata, - DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + Account as _, AccountBirthday, DecryptedTransaction, ScannedBlock, SentTransaction, WalletRead, + WalletWrite, }; -use super::{ +use crate::error::Error; +use crate::{ Account, AccountId, MemoryWalletBlock, MemoryWalletDb, Nullifier, ReceivedNote, ViewingKey, }; -use crate::error::Error; + +#[cfg(feature = "orchard")] +use zcash_protocol::ShieldedProtocol::Orchard; impl WalletWrite for MemoryWalletDb { type UtxoRef = u32; @@ -117,68 +94,77 @@ impl WalletWrite for MemoryWalletDb { // - Make sure blocks are coming in order. // - Make sure the first block in the sequence is tip + 1? // - Add a check to make sure the blocks are not already in the data store. + let _start_height = blocks.first().map(|b| b.height()); + let mut last_scanned_height = None; + for block in blocks.into_iter() { let mut transactions = HashMap::new(); let mut memos = HashMap::new(); + if last_scanned_height + .iter() + .any(|prev| block.height() != *prev + 1) + { + return Err(Error::NonSequentialBlocks); + } + for transaction in block.transactions().iter() { let txid = transaction.txid(); // Mark the Sapling nullifiers of the spent notes as spent in the `sapling_spends` map. - transaction - .sapling_spends() - .iter() - .map(|s| self.mark_sapling_note_spent(*s.nf(), txid)); + for spend in transaction.sapling_spends() { + self.mark_sapling_note_spent(*spend.nf(), txid)?; + } - #[cfg(feature = "orchard")] // Mark the Orchard nullifiers of the spent notes as spent in the `orchard_spends` map. - transaction - .orchard_spends() - .iter() - .map(|s| self.mark_orchard_note_spent(*s.nf(), txid)); + #[cfg(feature = "orchard")] + for spend in transaction.orchard_spends() { + self.mark_orchard_note_spent(*spend.nf(), txid)?; + } - transaction.sapling_outputs().iter().map(|o| { + for output in transaction.sapling_outputs() { // Insert the memo into the `memos` map. let note_id = NoteId::new( txid, Sapling, - u16::try_from(o.index()).expect("output indices are representable as u16"), + u16::try_from(output.index()) + .expect("output indices are representable as u16"), ); if let Ok(Some(memo)) = self.get_memo(note_id) { memos.insert(note_id, memo.encode()); } // Check whether this note was spent in a later block range that // we previously scanned. - let spent_in = o + let spent_in = output .nf() .and_then(|nf| self.nullifiers.get(&Nullifier::Sapling(*nf))) .and_then(|(height, tx_idx)| self.tx_locator.get(*height, *tx_idx)) - .map(|x| *x); + .copied(); - self.insert_received_sapling_note(note_id, &o, spent_in); - }); + self.insert_received_sapling_note(note_id, output, spent_in); + } #[cfg(feature = "orchard")] - transaction.orchard_outputs().iter().map(|o| { + for output in transaction.orchard_outputs().iter() { // Insert the memo into the `memos` map. let note_id = NoteId::new( txid, Orchard, - u16::try_from(o.index()).expect("output indices are representable as u16"), + u16::try_from(output.index()) + .expect("output indices are representable as u16"), ); if let Ok(Some(memo)) = self.get_memo(note_id) { memos.insert(note_id, memo.encode()); } // Check whether this note was spent in a later block range that // we previously scanned. - let spent_in = o + let spent_in = output .nf() - .and_then(|nf| self.nullifiers.get(&&Nullifier::Orchard(*nf))) + .and_then(|nf| self.nullifiers.get(&Nullifier::Orchard(*nf))) .and_then(|(height, tx_idx)| self.tx_locator.get(*height, *tx_idx)) - .map(|x| *x); - - self.insert_received_orchard_note(note_id, &o, spent_in) - }); + .copied(); + self.insert_received_orchard_note(note_id, output, spent_in) + } // Add frontier to the sapling tree self.sapling_tree.insert_frontier( from_state.final_sapling_tree().clone(), @@ -186,7 +172,7 @@ impl WalletWrite for MemoryWalletDb { id: from_state.block_height(), marking: Marking::Reference, }, - ); + )?; #[cfg(feature = "orchard")] // Add frontier to the orchard tree @@ -196,8 +182,8 @@ impl WalletWrite for MemoryWalletDb { id: from_state.block_height(), marking: Marking::Reference, }, - ); - + )?; + last_scanned_height = Some(block.height()); transactions.insert(txid, transaction.clone()); } @@ -210,14 +196,26 @@ impl WalletWrite for MemoryWalletDb { height: block.height(), hash: block.block_hash(), block_time: block.block_time(), - transactions: transactions.keys().cloned().collect(), - memos, + _transactions: transactions.keys().cloned().collect(), + _memos: memos, + sapling_commitment_tree_size: Some(block.sapling().final_tree_size()), + _sapling_output_count: Some( + block.sapling().commitments().len().try_into().unwrap(), + ), + #[cfg(feature = "orchard")] + orchard_commitment_tree_size: Some(block.orchard().final_tree_size()), + #[cfg(feature = "orchard")] + _orchard_action_count: Some( + block.orchard().commitments().len().try_into().unwrap(), + ), }; + // Insert transaction metadata into the transaction table transactions .into_iter() .for_each(|(_id, tx)| self.tx_table.put_tx_meta(tx, block.height())); + // Insert the block into the block map self.blocks.insert(block.height(), memory_block); // Add the Sapling commitments to the sapling tree. @@ -227,7 +225,7 @@ impl WalletWrite for MemoryWalletDb { .value() .map_or(0.into(), |t| t.position() + 1); self.sapling_tree - .batch_insert(start_position, block_commitments.sapling.into_iter()); + .batch_insert(start_position, block_commitments.sapling.into_iter())?; #[cfg(feature = "orchard")] { @@ -237,9 +235,10 @@ impl WalletWrite for MemoryWalletDb { .value() .map_or(0.into(), |t| t.position() + 1); self.orchard_tree - .batch_insert(start_position, block_commitments.orchard.into_iter()); + .batch_insert(start_position, block_commitments.orchard.into_iter())?; } } + // We can do some pruning of the tx_locator_map here Ok(()) } @@ -327,7 +326,7 @@ impl WalletWrite for MemoryWalletDb { // Mark sapling notes as spent if let Some(bundle) = sent_tx.tx().sapling_bundle() { for spend in bundle.shielded_spends() { - self.mark_sapling_note_spent(*spend.nullifier(), sent_tx.tx().txid()); + self.mark_sapling_note_spent(*spend.nullifier(), sent_tx.tx().txid())?; } } // Mark orchard notes as spent @@ -335,7 +334,7 @@ impl WalletWrite for MemoryWalletDb { #[cfg(feature = "orchard")] { for action in _bundle.actions() { - self.mark_orchard_note_spent(*action.nullifier(), sent_tx.tx().txid()); + self.mark_orchard_note_spent(*action.nullifier(), sent_tx.tx().txid())?; } } @@ -344,7 +343,7 @@ impl WalletWrite for MemoryWalletDb { } // Mark transparent UTXOs as spent #[cfg(feature = "transparent-inputs")] - for utxo_outpoint in sent_tx.utxos_spent() { + for _utxo_outpoint in sent_tx.utxos_spent() { // self.mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?; todo!() } @@ -353,21 +352,15 @@ impl WalletWrite for MemoryWalletDb { // TODO: insert sent output match output.recipient() { - Recipient::InternalAccount { .. } => { - self.received_notes.insert_received_note( - ReceivedNote::from_sent_tx_output(sent_tx.tx().txid(), output)?, - ); - } - #[cfg(feature = "orchard")] Recipient::InternalAccount { .. } => { self.received_notes.insert_received_note( ReceivedNote::from_sent_tx_output(sent_tx.tx().txid(), output)?, ); } Recipient::EphemeralTransparent { - receiving_account, - ephemeral_address, - outpoint_metadata, + receiving_account: _, + ephemeral_address: _, + outpoint_metadata: _, } => { // mark ephemeral address as used }