diff --git a/zcash_client_memory/src/error.rs b/zcash_client_memory/src/error.rs index 80faa02ca2..c65910b116 100644 --- a/zcash_client_memory/src/error.rs +++ b/zcash_client_memory/src/error.rs @@ -1,4 +1,5 @@ use zcash_keys::keys::{AddressGenerationError, DerivationError}; +use zcash_primitives::transaction::TxId; use zcash_protocol::memo; use crate::mem_wallet::AccountId; @@ -21,6 +22,18 @@ pub enum Error { InvalidSeedLength, #[error("Account out of range.")] AccountOutOfRange, + #[error("Transaction not in table: {0}")] + TransactionNotFound(TxId), + #[error("Note not found")] + NoteNotFound, + #[error("Conflicting Tx Locator map entry")] + ConflictingTxLocator, + #[error("Io Error: {0}")] + IoError(std::io::Error), + #[error("Corrupted Data: {0}")] + CorruptedData(String), + #[error("Other error: {0}")] + Other(String), } impl From for Error { @@ -40,3 +53,9 @@ impl From for Error { Error::MemoDecryption(value) } } + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Error::IoError(value) + } +} diff --git a/zcash_client_memory/src/mem_wallet/mod.rs b/zcash_client_memory/src/mem_wallet/mod.rs index 8040149613..68f8d79e19 100644 --- a/zcash_client_memory/src/mem_wallet/mod.rs +++ b/zcash_client_memory/src/mem_wallet/mod.rs @@ -1,11 +1,12 @@ #![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::{BTreeMap, HashMap, HashSet}, + collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, convert::Infallible, hash::Hash, num::NonZeroU32, @@ -17,11 +18,12 @@ use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network}, - transaction::{components::OutPoint, Transaction, TxId}, + transaction::{components::OutPoint, txid, Authorized, Transaction, TransactionData, TxId}, }; use zcash_protocol::{ memo::{self, Memo, MemoBytes}, - value::Zatoshis, + value::{ZatBalance, Zatoshis}, + PoolType, ShieldedProtocol::{Orchard, Sapling}, }; @@ -32,7 +34,8 @@ use zcash_client_backend::{ TransactionDataRequest, TransactionStatus, }, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{NoteId, WalletSpend, WalletTransparentOutput, WalletTx}, + proto::service::ShieldedProtocol, + wallet::{Note, NoteId, WalletSaplingOutput, WalletSpend, WalletTransparentOutput, WalletTx}, }; use zcash_client_backend::data_api::{ @@ -48,13 +51,15 @@ use { }; #[cfg(feature = "orchard")] -use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; +use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, wallet::WalletOrchardOutput}; use crate::error::Error; +mod tables; mod wallet_commitment_trees; mod wallet_read; mod wallet_write; +use tables::*; struct MemoryWalletBlock { height: BlockHeight, @@ -65,46 +70,18 @@ struct MemoryWalletBlock { memos: HashMap, } -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)) - } -} - pub struct MemoryWalletDb { network: Network, accounts: Vec, blocks: BTreeMap, - tx_idx: HashMap, - /// Tracks transactions relevant to this wallet indexed by their TxId - tx_meta: HashMap>, + tx_table: TransactionTable, - /// Tracks transparent outputs received by this wallet indexed by their OutPoint which defines the - /// transaction and index where the output was created - transparent_received_outputs: HashMap, + received_notes: ReceivedNoteTable, + receieved_note_spends: ReceievdNoteSpends, + nullifiers: NullifierMap, - /// Tracks spends of received outputs. In thix case the TxId is the spending transaction - /// from this wallet. - transparent_received_output_spends: HashMap, - - sapling_spends: BTreeMap, - #[cfg(feature = "orchard")] - orchard_spends: BTreeMap, + tx_locator: TxLocatorMap, sapling_tree: ShardTree< MemoryShardStore, @@ -118,25 +95,52 @@ pub struct MemoryWalletDb { ORCHARD_SHARD_HEIGHT, >, } - impl MemoryWalletDb { pub fn new(network: Network, max_checkpoints: usize) -> Self { Self { network, accounts: Vec::new(), blocks: BTreeMap::new(), - tx_idx: HashMap::new(), - tx_meta: HashMap::new(), - transparent_received_outputs: HashMap::new(), - transparent_received_output_spends: HashMap::new(), - sapling_spends: BTreeMap::new(), - #[cfg(feature = "orchard")] - orchard_spends: 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(()) + } + + #[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, @@ -160,6 +164,85 @@ impl MemoryWalletDb { }) .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. @@ -213,6 +296,10 @@ impl Account { ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { self.uivk().default_address(request) } + + fn birthday(&self) -> &AccountBirthday { + &self.birthday + } } impl zcash_client_backend::data_api::Account for Account { @@ -249,9 +336,22 @@ impl ViewingKey { } } -#[derive(Debug, Clone)] -struct TransparentReceivedOutput { - output: WalletTransparentOutput, - account_id: AccountId, - tx_id: TxId, +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 new file mode 100644 index 0000000000..c16ca4d229 --- /dev/null +++ b/zcash_client_memory/src/mem_wallet/tables.rs @@ -0,0 +1,422 @@ +#![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/mem_wallet/wallet_read.rs b/zcash_client_memory/src/mem_wallet/wallet_read.rs index b8c1166639..47e332b5dd 100644 --- a/zcash_client_memory/src/mem_wallet/wallet_read.rs +++ b/zcash_client_memory/src/mem_wallet/wallet_read.rs @@ -25,9 +25,10 @@ use zcash_client_backend::{ use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network}, - transaction::{Transaction, TxId}, + transaction::{Transaction, TransactionData, TxId}, }; use zcash_protocol::{ + consensus::BranchId, memo::{self, Memo, MemoBytes}, value::Zatoshis, ShieldedProtocol::{Orchard, Sapling}, @@ -46,7 +47,7 @@ use { zcash_primitives::legacy::TransparentAddress, }; -use super::{Account, AccountId, MemoryWalletDb, TransparentReceivedOutput}; +use super::{Account, AccountId, MemoryWalletDb}; use crate::error::Error; impl WalletRead for MemoryWalletDb { @@ -60,9 +61,9 @@ impl WalletRead for MemoryWalletDb { fn get_account( &self, - _account_id: Self::AccountId, + account_id: Self::AccountId, ) -> Result, Self::Error> { - todo!() + Ok(self.accounts.get(*account_id as usize).map(|a| a.clone())) } fn get_derived_account( @@ -125,12 +126,19 @@ impl WalletRead for MemoryWalletDb { .map_err(|e| e.into()) } - fn get_account_birthday(&self, _account: Self::AccountId) -> Result { - Err(Error::AccountUnknown(_account)) + fn get_account_birthday(&self, account: Self::AccountId) -> Result { + self.accounts + .get(*account as usize) + .map(|account| account.birthday().height()) + .ok_or(Error::AccountUnknown(account)) } fn get_wallet_birthday(&self) -> Result, Self::Error> { - todo!() + Ok(self + .accounts + .iter() + .map(|account| account.birthday().height()) + .min()) } fn get_wallet_summary( @@ -185,35 +193,114 @@ impl WalletRead for MemoryWalletDb { todo!() } - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { - todo!() + fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + if let Some(TransactionStatus::Mined(height)) = self.tx_table.tx_status(&txid) { + Ok(Some(height)) + } else { + Ok(None) + } } fn get_unified_full_viewing_keys( &self, ) -> Result, Self::Error> { - Ok(HashMap::new()) + Ok(self + .accounts + .iter() + .filter_map(|account| match account.ufvk() { + Some(ufvk) => Some((account.id(), ufvk.clone())), + None => None, + }) + .collect()) } fn get_memo(&self, id_note: NoteId) -> Result, Self::Error> { - self.tx_idx - .get(id_note.txid()) - .and_then(|height| self.blocks.get(height)) - .and_then(|block| block.memos.get(&id_note)) - .map(Memo::try_from) - .transpose() - .map_err(Error::from) + todo!() } - fn get_transaction(&self, _id_tx: TxId) -> Result, Self::Error> { + 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); + self.tx_table + .get(&txid) + .and_then(|tx| Some((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. + // - If the transaction is mined, we use the block height to get the correct one. + // - If the transaction is unmined and has a cached non-zero expiry height, we use + // that (relying on the invariant that a transaction can't be mined across a network + // upgrade boundary, so the expiry height must be in the same epoch). + // - Otherwise, we use a placeholder for the initial transaction parse (as the + // consensus branch ID is not used there), and then either use its non-zero expiry + // height or return an error. + if let TransactionStatus::Mined(height) = status { + return Ok(Some( + 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)) + .map(|t| (height, t)), + )); + } + + let tx_data = Transaction::read(&raw[..], BranchId::Sprout) + .map_err(Self::Error::from)? + .into_data(); + + let expiry_height = tx_data.expiry_height(); + if expiry_height > BlockHeight::from(0) { + Ok(Some( + TransactionData::from_parts( + tx_data.version(), + BranchId::for_height(&self.network, expiry_height), + tx_data.lock_time(), + expiry_height, + tx_data.transparent_bundle().cloned(), + tx_data.sprout_bundle().cloned(), + tx_data.sapling_bundle().cloned(), + tx_data.orchard_bundle().cloned(), + ) + .freeze() + .map(|t| (expiry_height, t)), + )) + } else { + Err(Self::Error::CorruptedData( + "Consensus branch ID not known, cannot parse this transaction until it is mined" + .to_string(), + )) + } + }); todo!() } fn get_sapling_nullifiers( &self, - _query: NullifierQuery, + query: NullifierQuery, ) -> Result, Self::Error> { - Ok(Vec::new()) + let nullifiers = self.received_notes.get_sapling_nullifiers(); + Ok(match query { + NullifierQuery::All => nullifiers + .map(|(account_id, _, nf)| (account_id, nf)) + .collect(), + NullifierQuery::Unspent => nullifiers + .filter_map(|(account_id, txid, nf)| { + let tx_status = self.tx_table.tx_status(&txid); + let expiry_height = self.tx_table.expiry_height(&txid); + if matches!(tx_status, Some(TransactionStatus::Mined(_))) + || expiry_height.is_none() + { + None + } else { + Some((account_id, nf)) + } + }) + .collect(), + }) } #[cfg(feature = "orchard")] @@ -221,35 +308,25 @@ impl WalletRead for MemoryWalletDb { &self, query: NullifierQuery, ) -> Result, Self::Error> { - Ok(self - .orchard_spends - .iter() - .filter_map(|(nf, (txid, spent))| match query { - NullifierQuery::Unspent => { - if !spent { - Some((txid, self.tx_idx.get(txid).unwrap(), *nf)) - } else { + let nullifiers = self.received_notes.get_orchard_nullifiers(); + Ok(match query { + NullifierQuery::All => nullifiers + .map(|(account_id, _, nf)| (account_id, nf)) + .collect(), + NullifierQuery::Unspent => nullifiers + .filter_map(|(account_id, txid, nf)| { + let tx_status = self.tx_table.tx_status(&txid); + let expiry_height = self.tx_table.expiry_height(&txid); + if matches!(tx_status, Some(TransactionStatus::Mined(_))) + || expiry_height.is_none() + { None + } else { + Some((account_id, nf)) } - } - NullifierQuery::All => Some((txid, self.tx_idx.get(txid).unwrap(), *nf)), - }) - .map(|(txid, height, nf)| { - self.tx_meta.get(txid).and_then(|tx| { - tx.orchard_outputs() - .iter() - .find(|o| o.nf() == Some(&nf)) - .map(|o| (*o.account_id(), *o.nf().unwrap())) - .or_else(|| { - tx.orchard_spends() - .iter() - .find(|s| s.nf() == &nf) - .map(|s| (*s.account_id(), *s.nf())) - }) }) - }) - .flatten() - .collect()) + .collect(), + }) } #[cfg(feature = "transparent-inputs")] @@ -266,39 +343,7 @@ impl WalletRead for MemoryWalletDb { account: Self::AccountId, max_height: BlockHeight, ) -> Result, Self::Error> { - // scan all transparent outputs and return those in a tx belonging to this account - // as a map between the address and the total value received - Ok(self - .transparent_received_outputs - .iter() - .filter(|(_, output)| output.account_id == account) // that belong to this account - .filter(|(outpoint, output)| { - // where the tx creating the output is mined - if let Some(height) = self.tx_idx.get(&output.tx_id) { - height <= &max_height - } else { - false - } - }) - .filter(|(outpoint, _)| { - // that are unspent - !self - .transparent_received_output_spends - .contains_key(&outpoint) - }) - .fold( - HashMap::new(), - |mut res, (_, TransparentReceivedOutput { output, .. })| { - let addr = output.recipient_address().clone(); - let zats = res - .get(&addr) - .unwrap_or(&Zatoshis::ZERO) - .add(output.value()) - .expect("Can always add a non-negative value to zero"); - res.insert(addr, zats); - res - }, - )) + todo!() } fn transaction_data_requests(&self) -> Result, Self::Error> { diff --git a/zcash_client_memory/src/mem_wallet/wallet_write.rs b/zcash_client_memory/src/mem_wallet/wallet_write.rs index 29c14b9d9e..1a01ccfc00 100644 --- a/zcash_client_memory/src/mem_wallet/wallet_write.rs +++ b/zcash_client_memory/src/mem_wallet/wallet_write.rs @@ -9,7 +9,10 @@ use std::{ hash::Hash, num::NonZeroU32, }; -use zcash_keys::keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}; +use zcash_keys::{ + address::Receiver, + keys::{AddressGenerationError, DerivationError, UnifiedIncomingViewingKey}, +}; use zip32::{fingerprint::SeedFingerprint, DiversifierIndex, Scope}; use zcash_primitives::{ @@ -20,6 +23,7 @@ use zcash_primitives::{ use zcash_protocol::{ memo::{self, Memo, MemoBytes}, value::Zatoshis, + PoolType, ShieldedProtocol::{Orchard, Sapling}, }; @@ -30,7 +34,11 @@ use zcash_client_backend::{ TransactionStatus, }, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{NoteId, WalletSpend, WalletTransparentOutput, WalletTx}, + wallet::{ + Note, NoteId, Recipient, WalletSaplingOutput, WalletSpend, WalletTransparentOutput, + WalletTx, + }, + TransferType, }; use zcash_client_backend::data_api::{ @@ -39,7 +47,9 @@ use zcash_client_backend::data_api::{ WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }; -use super::{Account, AccountId, MemoryWalletBlock, MemoryWalletDb, ViewingKey}; +use super::{ + Account, AccountId, MemoryWalletBlock, MemoryWalletDb, Nullifier, ReceivedNote, ViewingKey, +}; use crate::error::Error; impl WalletWrite for MemoryWalletDb { @@ -108,14 +118,21 @@ impl WalletWrite for MemoryWalletDb { let mut memos = HashMap::new(); for transaction in block.transactions().iter() { let txid = transaction.txid(); - transaction.sapling_outputs().iter().map(|o| { - // Insert the Sapling nullifiers of the spent notes into the `sapling_spends` map. - if let Some(nullifier) = o.nf() { - self.sapling_spends - .entry(*nullifier) - .or_insert((txid, false)); - } + // 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)); + + #[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)); + + transaction.sapling_outputs().iter().map(|o| { // Insert the memo into the `memos` map. let note_id = NoteId::new( txid, @@ -125,17 +142,19 @@ impl WalletWrite for MemoryWalletDb { 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 + .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); + + self.insert_received_sapling_note(note_id, &o, spent_in); }); #[cfg(feature = "orchard")] transaction.orchard_outputs().iter().map(|o| { - // Insert the Orchard nullifiers of the spent notes into the `orchard_spends` map. - if let Some(nullifier) = o.nf() { - self.orchard_spends - .entry(*nullifier) - .or_insert((txid, false)); - } - // Insert the memo into the `memos` map. let note_id = NoteId::new( txid, @@ -145,6 +164,15 @@ impl WalletWrite for MemoryWalletDb { 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 + .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) }); // Add frontier to the sapling tree @@ -166,36 +194,26 @@ impl WalletWrite for MemoryWalletDb { }, ); - // Mark the Sapling nullifiers of the spent notes as spent in the `sapling_spends` map. - transaction.sapling_spends().iter().map(|s| { - let nullifier = s.nf(); - if let Some((txid, spent)) = self.sapling_spends.get_mut(nullifier) { - *spent = true; - } - }); - - #[cfg(feature = "orchard")] - // Mark the Orchard nullifiers of the spent notes as spent in the `orchard_spends` map. - transaction.orchard_spends().iter().map(|s| { - let nullifier = s.nf(); - if let Some((txid, spent)) = self.orchard_spends.get_mut(nullifier) { - *spent = true; - } - }); - - self.tx_idx.insert(txid, block.height()); transactions.insert(txid, transaction.clone()); } - self.tx_meta.extend(transactions); + + // Insert the new nullifiers from this block into the nullifier map + self.insert_sapling_nullifier_map(block.height(), block.sapling().nullifier_map())?; + #[cfg(feature = "orchard")] + self.insert_orchard_nullifier_map(block.height(), block.orchard().nullifier_map())?; let memory_block = MemoryWalletBlock { height: block.height(), hash: block.block_hash(), block_time: block.block_time(), - transactions: self.tx_meta.keys().cloned().collect(), + transactions: transactions.keys().cloned().collect(), memos, }; + transactions + .into_iter() + .for_each(|(_id, tx)| self.tx_table.put_tx_meta(tx, block.height())); + self.blocks.insert(block.height(), memory_block); // Add the Sapling commitments to the sapling tree. @@ -232,18 +250,15 @@ impl WalletWrite for MemoryWalletDb { fn store_decrypted_tx( &mut self, - _received_tx: DecryptedTransaction, + d_tx: DecryptedTransaction, ) -> Result<(), Self::Error> { - todo!() + self.tx_table.put_tx_data(d_tx.tx(), None, None); + if let Some(height) = d_tx.mined_height() { + self.set_transaction_status(d_tx.tx().txid(), TransactionStatus::Mined(height))? + } + Ok(()) } - // fn store_sent_tx( - // &mut self, - // _sent_tx: &SentTransaction, - // ) -> Result<(), Self::Error> { - // todo!() - // } - fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { todo!() } @@ -299,14 +314,72 @@ impl WalletWrite for MemoryWalletDb { &mut self, transactions: &[SentTransaction], ) -> Result<(), Self::Error> { - todo!() + for sent_tx in transactions { + self.tx_table.put_tx_data( + sent_tx.tx(), + Some(sent_tx.fee_amount()), + Some(sent_tx.target_height()), + ); + // 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()); + } + } + // Mark orchard notes as spent + if let Some(_bundle) = sent_tx.tx().orchard_bundle() { + #[cfg(feature = "orchard")] + { + for action in _bundle.actions() { + self.mark_orchard_note_spent(*action.nullifier(), sent_tx.tx().txid()); + } + } + + #[cfg(not(feature = "orchard"))] + panic!("Sent a transaction with Orchard Actions without `orchard` enabled?"); + } + // Mark transparent UTXOs as spent + #[cfg(feature = "transparent-inputs")] + for utxo_outpoint in sent_tx.utxos_spent() { + // self.mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?; + todo!() + } + + for output in sent_tx.outputs() { + // 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, + } => { + // mark ephemeral address as used + } + Recipient::External(_, _) => {} + } + } + // in sqlite they que + } + Ok(()) } fn set_transaction_status( &mut self, - _txid: TxId, - _status: TransactionStatus, + txid: TxId, + status: TransactionStatus, ) -> Result<(), Self::Error> { - todo!() + self.tx_table.set_transaction_status(&txid, status) } }