diff --git a/Cargo.lock b/Cargo.lock index ac142ccd1f..b0114d2795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,8 +1476,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "orchard" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c801aeaccd19bb6916d71f25694b62d223061872900e8022221c1ad8dcad2d" +source = "git+https://github.com/nuttycom/orchard?rev=9729cd8d266a6121bd8c7f3b43053440b787d413#9729cd8d266a6121bd8c7f3b43053440b787d413" dependencies = [ "aes", "bitvec", diff --git a/Cargo.toml b/Cargo.toml index c129eb377d..162fccfd48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,3 +115,6 @@ zip32 = "0.1" lto = true panic = 'abort' codegen-units = 1 + +[patch.crates-io] +orchard = { git = "https://github.com/nuttycom/orchard", rev = "9729cd8d266a6121bd8c7f3b43053440b787d413" } \ No newline at end of file diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index ad2a56fa9d..9c564b5f99 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -693,7 +693,7 @@ pub struct ScannedBlockCommitments { /// The ordered vector of note commitments for Orchard outputs of the block. /// Present only when the `orchard` feature is enabled. #[cfg(feature = "orchard")] - pub orchard: Vec<(orchard::note::NoteCommitment, Retention)>, + pub orchard: Vec<(orchard::tree::MerkleHashOrchard, Retention)>, } /// The subset of information that is relevant to this wallet that has been @@ -707,7 +707,7 @@ pub struct ScannedBlock { transactions: Vec, sapling: ScannedBundles, #[cfg(feature = "orchard")] - orchard: ScannedBundles, + orchard: ScannedBundles, } impl ScannedBlock { @@ -719,7 +719,7 @@ impl ScannedBlock { transactions: Vec, sapling: ScannedBundles, #[cfg(feature = "orchard")] orchard: ScannedBundles< - orchard::note::NoteCommitment, + orchard::tree::MerkleHashOrchard, orchard::note::Nullifier, >, ) -> Self { @@ -763,7 +763,7 @@ impl ScannedBlock { #[cfg(feature = "orchard")] pub fn orchard( &self, - ) -> &ScannedBundles { + ) -> &ScannedBundles { &self.orchard } diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs index 2dac470978..b6221ef6b0 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -150,8 +150,7 @@ use zcash_primitives::consensus::{self, BlockHeight}; use crate::{ data_api::{NullifierQuery, WalletWrite}, proto::compact_formats::CompactBlock, - scan::BatchRunner, - scanning::{add_block_to_runner, scan_block_with_runner, ScanningKeys}, + scanning::{scan_block_with_runners, BatchRunners, ScanningKeys}, }; pub mod error; @@ -213,22 +212,26 @@ pub trait BlockSource { /// blocks. #[derive(Clone, Debug)] pub struct ScanSummary { - scanned_range: Range, - spent_sapling_note_count: usize, - received_sapling_note_count: usize, + pub(crate) scanned_range: Range, + pub(crate) spent_sapling_note_count: usize, + pub(crate) received_sapling_note_count: usize, + #[cfg(feature = "orchard")] + pub(crate) spent_orchard_note_count: usize, + #[cfg(feature = "orchard")] + pub(crate) received_orchard_note_count: usize, } impl ScanSummary { - /// Constructs a new [`ScanSummary`] from its constituent parts. - pub fn from_parts( - scanned_range: Range, - spent_sapling_note_count: usize, - received_sapling_note_count: usize, - ) -> Self { + /// Constructs a new [`ScanSummary`] for the provided block range. + pub fn for_range(scanned_range: Range) -> Self { Self { scanned_range, - spent_sapling_note_count, - received_sapling_note_count, + spent_sapling_note_count: 0, + received_sapling_note_count: 0, + #[cfg(feature = "orchard")] + spent_orchard_note_count: 0, + #[cfg(feature = "orchard")] + received_orchard_note_count: 0, } } @@ -252,6 +255,16 @@ impl ScanSummary { pub fn received_sapling_note_count(&self) -> usize { self.received_sapling_note_count } + + #[cfg(feature = "orchard")] + pub fn spent_orchard_note_count(&self) -> usize { + self.spent_orchard_note_count + } + + #[cfg(feature = "orchard")] + pub fn received_orchard_note_count(&self) -> usize { + self.received_orchard_note_count + } } /// Scans at most `limit` blocks from the provided block source for in order to find transactions @@ -275,31 +288,23 @@ where DbT: WalletWrite, { // Fetch the UnifiedFullViewingKeys we are tracking - let mut scanning_keys = ScanningKeys::from_account_ufvks( - data_db - .get_unified_full_viewing_keys() - .map_err(Error::Wallet)?, - ); - - let mut sapling_runner = BatchRunner::<_, _, _, _, ()>::new( - 100, - scanning_keys - .sapling_keys() - .iter() - .map(|(id, key)| (*id, key.prepare())), - ); + let account_ufvks = data_db + .get_unified_full_viewing_keys() + .map_err(Error::Wallet)?; + let mut scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks); + let mut runners = BatchRunners::<_, (), ()>::for_keys(100, &scanning_keys); block_source.with_blocks::<_, DbT::Error>( Some(from_height), Some(limit), |block: CompactBlock| { - add_block_to_runner(params, block, &mut sapling_runner); + runners.add_block(params, block); Ok(()) }, )?; - sapling_runner.flush(); + runners.flush(); let mut prior_block_metadata = if from_height > BlockHeight::from(0) { data_db @@ -317,42 +322,37 @@ where ); let mut scanned_blocks = vec![]; - let mut scan_end_height = from_height; - let mut received_note_count = 0; - let mut spent_note_count = 0; + let mut scan_summary = ScanSummary::for_range(from_height..from_height); block_source.with_blocks::<_, DbT::Error>( Some(from_height), Some(limit), |block: CompactBlock| { - scan_end_height = block.height() + 1; - let scanned_block = scan_block_with_runner( + scan_summary.scanned_range.end = block.height() + 1; + let scanned_block = scan_block_with_runners::<_, _, (), ()>( params, block, &scanning_keys, prior_block_metadata.as_ref(), - Some(&mut sapling_runner), + Some(&mut runners), ) .map_err(Error::Scan)?; - let (s, r) = scanned_block - .transactions - .iter() - .fold((0, 0), |(s, r), wtx| { - ( - s + wtx.sapling_spends().len(), - r + wtx.sapling_outputs().len(), - ) - }); - spent_note_count += s; - received_note_count += r; - - let spent_nf: Vec<&sapling::Nullifier> = scanned_block + for wtx in &scanned_block.transactions { + scan_summary.spent_sapling_note_count += wtx.sapling_spends().len(); + scan_summary.received_sapling_note_count += wtx.sapling_outputs().len(); + #[cfg(feature = "orchard")] + { + scan_summary.spent_orchard_note_count += wtx.orchard_spends().len(); + scan_summary.received_orchard_note_count += wtx.orchard_outputs().len(); + } + } + + let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block .transactions .iter() .flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf())) .collect(); - - scanning_keys.retain_sapling_nullifiers(|(_, nf)| !spent_nf.contains(&nf)); + scanning_keys.retain_sapling_nullifiers(|(_, nf)| !sapling_spent_nf.contains(&nf)); scanning_keys.extend_sapling_nullifiers(scanned_block.transactions.iter().flat_map( |tx| { tx.sapling_outputs().iter().flat_map(|out| { @@ -364,6 +364,27 @@ where }, )); + #[cfg(feature = "orchard")] + { + let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block + .transactions + .iter() + .flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf())) + .collect(); + + scanning_keys.retain_orchard_nullifiers(|(_, nf)| !orchard_spent_nf.contains(&nf)); + scanning_keys.extend_orchard_nullifiers( + scanned_block.transactions.iter().flat_map(|tx| { + tx.orchard_outputs().iter().flat_map(|out| { + out.key_source() + .account() + .zip(out.nf().copied()) + .into_iter() + }) + }), + ); + } + prior_block_metadata = Some(scanned_block.to_block_metadata()); scanned_blocks.push(scanned_block); @@ -372,11 +393,7 @@ where )?; data_db.put_blocks(scanned_blocks).map_err(Error::Wallet)?; - Ok(ScanSummary::from_parts( - from_height..scan_end_height, - spent_note_count, - received_note_count, - )) + Ok(scan_summary) } #[cfg(feature = "test-dependencies")] diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs index 5ca56bf784..6975e62586 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -10,7 +10,7 @@ use std::{ use incrementalmerkletree::frontier::CommitmentTree; use nonempty::NonEmpty; -use sapling::{self, note::ExtractedNoteCommitment, Node, Nullifier, NOTE_COMMITMENT_TREE_DEPTH}; +use sapling::{self, note::ExtractedNoteCommitment, Node, NOTE_COMMITMENT_TREE_DEPTH}; use zcash_primitives::{ block::{BlockHash, BlockHeader}, consensus::{self, BlockHeight, Parameters}, @@ -169,8 +169,49 @@ impl TryFrom<&compact_formats::CompactSaplingOutput> } impl compact_formats::CompactSaplingSpend { - pub fn nf(&self) -> Result { - Nullifier::from_slice(&self.nf).map_err(|_| ()) + pub fn nf(&self) -> Result { + sapling::Nullifier::from_slice(&self.nf).map_err(|_| ()) + } +} + +#[cfg(feature = "orchard")] +impl TryFrom<&compact_formats::CompactOrchardAction> for orchard::note_encryption::CompactAction { + type Error = (); + + fn try_from(value: &compact_formats::CompactOrchardAction) -> Result { + Ok(orchard::note_encryption::CompactAction::from_parts( + value.nf()?, + value.cmx()?, + value.ephemeral_key()?, + value.ciphertext[..].try_into().map_err(|_| ())?, + )) + } +} + +#[cfg(feature = "orchard")] +impl compact_formats::CompactOrchardAction { + pub fn cmx(&self) -> Result { + Option::from(orchard::note::ExtractedNoteCommitment::from_bytes( + &self.cmx[..].try_into().map_err(|_| ())?, + )) + .ok_or(()) + } + + pub fn nf(&self) -> Result { + let nf_bytes: [u8; 32] = self.nullifier[..].try_into().map_err(|_| ())?; + Option::from(orchard::note::Nullifier::from_bytes(&nf_bytes)).ok_or(()) + } + + /// Returns the ephemeral public key for this output. + /// + /// A convenience method that parses [`CompactOutput.epk`]. + /// + /// [`CompactOutput.epk`]: #structfield.epk + pub fn ephemeral_key(&self) -> Result { + self.ephemeral_key[..] + .try_into() + .map(EphemeralKeyBytes) + .map_err(|_| ()) } } diff --git a/zcash_client_backend/src/scan.rs b/zcash_client_backend/src/scan.rs index 4ee4820df2..31ea6f5059 100644 --- a/zcash_client_backend/src/scan.rs +++ b/zcash_client_backend/src/scan.rs @@ -404,12 +404,16 @@ where /// `replier` will be called with the result of every output. fn add_outputs( &mut self, - domain: impl Fn() -> D, + domain: impl Fn(&Output) -> D, outputs: &[Output], replier: channel::Sender>, ) { - self.outputs - .extend(outputs.iter().cloned().map(|output| (domain(), output))); + self.outputs.extend( + outputs + .iter() + .cloned() + .map(|output| (domain(&output), output)), + ); self.repliers.extend((0..outputs.len()).map(|output_index| { OutputReplier(OutputIndex { output_index, @@ -539,7 +543,7 @@ where &mut self, block_tag: BlockHash, txid: TxId, - domain: impl Fn() -> D, + domain: impl Fn(&Output) -> D, outputs: &[Output], ) { let (tx, rx) = channel::unbounded(); diff --git a/zcash_client_backend/src/scanning.rs b/zcash_client_backend/src/scanning.rs index 38baf17595..633f89e7aa 100644 --- a/zcash_client_backend/src/scanning.rs +++ b/zcash_client_backend/src/scanning.rs @@ -11,7 +11,7 @@ use sapling::{ }; use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; use zcash_keys::keys::UnifiedFullViewingKey; -use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput}; +use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE}; use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade}; use zcash_primitives::transaction::TxId; use zip32::{AccountId, Scope}; @@ -21,10 +21,22 @@ use crate::wallet::KeySource; use crate::{ proto::compact_formats::CompactBlock, scan::{Batch, BatchRunner, CompactDecryptor, DecryptedOutput, Tasks}, - wallet::{WalletSaplingOutput, WalletSaplingSpend, WalletTx}, + wallet::{WalletSaplingOutput, WalletSpend, WalletTx}, ShieldedProtocol, }; +#[cfg(feature = "orchard")] +use { + crate::wallet::WalletOrchardOutput, + orchard::{ + note_encryption::{CompactAction, OrchardDomain}, + tree::MerkleHashOrchard, + }, +}; + +#[cfg(not(feature = "orchard"))] +use std::marker::PhantomData; + /// A key that can be used to perform trial decryption and nullifier /// computation for a [`CompactSaplingOutput`] or [`CompactOrchardAction`]. /// @@ -118,9 +130,34 @@ impl ScanningKeyOps for SaplingIvk { } } +#[cfg(feature = "orchard")] +impl ScanningKeyOps + for ScanningKey +{ + fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey { + orchard::keys::PreparedIncomingViewingKey::new(&self.ivk) + } + + fn nf( + &self, + note: &orchard::note::Note, + _position: Position, + ) -> Option { + self.nk.as_ref().map(|key| note.nullifier(key)) + } + + fn key_source(&self) -> KeySource { + self.key_source + } +} + pub struct ScanningKeys { sapling_keys: HashMap>>, sapling_nullifiers: Vec<(AccountId, sapling::Nullifier)>, + #[cfg(feature = "orchard")] + orchard_keys: HashMap>>, + #[cfg(feature = "orchard")] + orchard_nullifiers: Vec<(AccountId, orchard::note::Nullifier)>, } impl ScanningKeys { @@ -128,6 +165,10 @@ impl ScanningKeys { Self { sapling_keys: HashMap::new(), sapling_nullifiers: vec![], + #[cfg(feature = "orchard")] + orchard_keys: HashMap::new(), + #[cfg(feature = "orchard")] + orchard_nullifiers: vec![], } } @@ -140,55 +181,69 @@ impl ScanningKeys { pub fn sapling_nullifiers(&self) -> &[(AccountId, sapling::Nullifier)] { self.sapling_nullifiers.as_ref() } + + #[cfg(feature = "orchard")] + pub fn orchard_keys( + &self, + ) -> &HashMap>> { + &self.orchard_keys + } + + #[cfg(feature = "orchard")] + pub fn orchard_nullifiers(&self) -> &[(AccountId, orchard::note::Nullifier)] { + self.orchard_nullifiers.as_ref() + } } impl ScanningKeys<(AccountId, Scope)> { pub fn from_account_ufvks( ufvks: impl IntoIterator, ) -> Self { - let sapling_keys = ufvks - .into_iter() - .flat_map(|(account, ufvk)| { - if let Some(dfvk) = ufvk.sapling() { - vec![ - ( - dfvk.to_ivk(Scope::External), - dfvk.to_nk(Scope::External), - account, - Scope::External, - ), - ( - dfvk.to_ivk(Scope::Internal), - dfvk.to_nk(Scope::Internal), - account, - Scope::Internal, - ), - ] - } else { - vec![] + let mut sapling_keys: HashMap< + (AccountId, Scope), + Box>, + > = HashMap::new(); + #[cfg(feature = "orchard")] + let mut orchard_keys: HashMap< + (AccountId, Scope), + Box>, + > = HashMap::new(); + for (account, ufvk) in ufvks { + if let Some(dfvk) = ufvk.sapling() { + for scope in [Scope::External, Scope::Internal] { + sapling_keys.insert( + (account, scope), + Box::new(ScanningKey { + ivk: dfvk.to_ivk(scope), + nk: Some(dfvk.to_nk(scope)), + key_source: KeySource::Seed(account, scope), + }), + ); } - .into_iter() - }) - .map(|(ivk, nk, account, scope)| { - let entry: ( - (AccountId, Scope), - Box>, - ) = ( - (account, scope), - Box::new(ScanningKey { - ivk, - nk: Some(nk), - key_source: KeySource::Seed(account, scope), - }), - ); - - entry - }) - .collect(); + } + + #[cfg(feature = "orchard")] + if let Some(fvk) = ufvk.orchard() { + for scope in [Scope::External, Scope::Internal] { + orchard_keys.insert( + (account, scope), + Box::new(ScanningKey { + ivk: fvk.to_ivk(scope), + nk: Some(fvk.clone()), + key_source: KeySource::Seed(account, scope), + }), + ); + } + } + } Self { sapling_keys, sapling_nullifiers: vec![], + #[cfg(feature = "orchard")] + orchard_keys, + #[cfg(feature = "orchard")] + orchard_nullifiers: vec![], } } @@ -205,6 +260,22 @@ impl ScanningKeys<(AccountId, Scope)> { ) { self.sapling_nullifiers.extend(nfs); } + + #[cfg(feature = "orchard")] + pub(crate) fn retain_orchard_nullifiers( + &mut self, + f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool, + ) { + self.orchard_nullifiers.retain(f); + } + + #[cfg(feature = "orchard")] + pub(crate) fn extend_orchard_nullifiers( + &mut self, + nfs: impl IntoIterator, + ) { + self.orchard_nullifiers.extend(nfs); + } } /// Errors that may occur in chain scanning @@ -330,81 +401,174 @@ pub fn scan_block< scanning_keys: &ScanningKeys, prior_block_metadata: Option<&BlockMetadata>, ) -> Result { - scan_block_with_runner::<_, _, ()>(params, block, scanning_keys, prior_block_metadata, None) + scan_block_with_runners::<_, _, (), ()>( + params, + block, + scanning_keys, + prior_block_metadata, + None, + ) } -type TaggedBatch = Batch; -type TaggedBatchRunner = - BatchRunner; +type TaggedSaplingBatch = Batch< + KeyId, + SaplingDomain, + sapling::note_encryption::CompactOutputDescription, + CompactDecryptor, +>; +type TaggedSaplingBatchRunner = BatchRunner< + KeyId, + SaplingDomain, + sapling::note_encryption::CompactOutputDescription, + CompactDecryptor, + Tasks, +>; + +#[cfg(feature = "orchard")] +type TaggedOrchardBatch = + Batch; +#[cfg(feature = "orchard")] +type TaggedOrchardBatchRunner = BatchRunner< + KeyId, + OrchardDomain, + orchard::note_encryption::CompactAction, + CompactDecryptor, + Tasks, +>; + +pub(crate) trait SaplingTasks: Tasks> {} +impl>> SaplingTasks for T {} + +#[cfg(not(feature = "orchard"))] +pub(crate) trait OrchardTasks {} +#[cfg(not(feature = "orchard"))] +impl OrchardTasks for T {} + +#[cfg(feature = "orchard")] +pub(crate) trait OrchardTasks: Tasks> {} +#[cfg(feature = "orchard")] +impl>> OrchardTasks for T {} + +pub(crate) struct BatchRunners, TO: OrchardTasks> { + sapling: TaggedSaplingBatchRunner, + #[cfg(feature = "orchard")] + orchard: TaggedOrchardBatchRunner, + #[cfg(not(feature = "orchard"))] + orchard: PhantomData, +} -#[tracing::instrument(skip_all, fields(height = block.height))] -pub(crate) fn add_block_to_runner( - params: &P, - block: CompactBlock, - batch_runner: &mut TaggedBatchRunner, -) where - P: consensus::Parameters + Send + 'static, - KeyId: Copy + Send + 'static, - T: Tasks>, +impl, TO: OrchardTasks> + BatchRunners { - let block_hash = block.hash(); - let block_height = block.height(); - let zip212_enforcement = consensus::sapling_zip212_enforcement(params, block_height); - - for tx in block.vtx.into_iter() { - let txid = tx.txid(); - let outputs = tx - .outputs - .iter() - .map(|output| { - CompactOutputDescription::try_from(output) - .expect("Invalid output found in compact block decoding.") - }) - .collect::>(); + pub(crate) fn for_keys( + batch_size_threshold: usize, + scanning_keys: &ScanningKeys, + ) -> Self { + BatchRunners { + sapling: BatchRunner::new( + batch_size_threshold, + scanning_keys + .sapling_keys() + .iter() + .map(|(id, key)| (id.clone(), key.prepare())), + ), + #[cfg(feature = "orchard")] + orchard: BatchRunner::new( + batch_size_threshold, + scanning_keys + .orchard_keys() + .iter() + .map(|(id, key)| (id.clone(), key.prepare())), + ), + #[cfg(not(feature = "orchard"))] + orchard: PhantomData, + } + } - batch_runner.add_outputs( - block_hash, - txid, - || SaplingDomain::new(zip212_enforcement), - &outputs, - ) + pub(crate) fn flush(&mut self) { + self.sapling.flush(); + #[cfg(feature = "orchard")] + self.orchard.flush(); } -} -fn check_hash_continuity( - block: &CompactBlock, - prior_block_metadata: Option<&BlockMetadata>, -) -> Option { - if let Some(prev) = prior_block_metadata { - if block.height() != prev.block_height() + 1 { - return Some(ScanError::BlockHeightDiscontinuity { - prev_height: prev.block_height(), - new_height: block.height(), - }); - } + #[tracing::instrument(skip_all, fields(height = block.height))] + pub(crate) fn add_block

(&mut self, params: &P, block: CompactBlock) + where + P: consensus::Parameters + Send + 'static, + KeyId: Copy + Send + 'static, + { + let block_hash = block.hash(); + let block_height = block.height(); + let zip212_enforcement = consensus::sapling_zip212_enforcement(params, block_height); - if block.prev_hash() != prev.block_hash() { - return Some(ScanError::PrevHashMismatch { - at_height: block.height(), - }); + for tx in block.vtx.into_iter() { + let txid = tx.txid(); + + self.sapling.add_outputs( + block_hash, + txid, + |_| SaplingDomain::new(zip212_enforcement), + &tx.outputs + .iter() + .map(|output| { + CompactOutputDescription::try_from(output) + .expect("Invalid output found in compact block decoding.") + }) + .collect::>(), + ); + + #[cfg(feature = "orchard")] + self.orchard.add_outputs( + block_hash, + txid, + |action| OrchardDomain::for_nullifier(action.nullifier()), + &tx.actions + .iter() + .map(|action| { + CompactAction::try_from(action) + .expect("Invalid action found in compact block decoding.") + }) + .collect::>(), + ); } } - - None } #[tracing::instrument(skip_all, fields(height = block.height))] -pub(crate) fn scan_block_with_runner< +pub(crate) fn scan_block_with_runners< P: consensus::Parameters + Send + 'static, KeyId: Copy + std::hash::Hash + Eq + Send + 'static, - T: Tasks> + Sync, + TS: SaplingTasks + Sync, + TO: OrchardTasks + Sync, >( params: &P, block: CompactBlock, scanning_keys: &ScanningKeys, prior_block_metadata: Option<&BlockMetadata>, - mut sapling_batch_runner: Option<&mut TaggedBatchRunner>, + mut batch_runners: Option<&mut BatchRunners>, ) -> Result { + fn check_hash_continuity( + block: &CompactBlock, + prior_block_metadata: Option<&BlockMetadata>, + ) -> Option { + if let Some(prev) = prior_block_metadata { + if block.height() != prev.block_height() + 1 { + return Some(ScanError::BlockHeightDiscontinuity { + prev_height: prev.block_height(), + new_height: block.height(), + }); + } + + if block.prev_hash() != prev.block_hash() { + return Some(ScanError::PrevHashMismatch { + at_height: block.height(), + }); + } + } + + None + } + if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) { return Err(scan_error); } @@ -508,8 +672,15 @@ pub(crate) fn scan_block_with_runner< let compact_block_tx_count = block.vtx.len(); let mut wtxs: Vec = vec![]; + let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len()); let mut sapling_note_commitments: Vec<(sapling::Node, Retention)> = vec![]; + + #[cfg(feature = "orchard")] + let mut orchard_nullifier_map = Vec::with_capacity(block.vtx.len()); + #[cfg(feature = "orchard")] + let mut orchard_note_commitments: Vec<(MerkleHashOrchard, Retention)> = vec![]; + for (tx_idx, tx) in block.vtx.into_iter().enumerate() { let txid = tx.txid(); let tx_index = @@ -523,13 +694,33 @@ pub(crate) fn scan_block_with_runner< "Could not deserialize nullifier for spend from protobuf representation.", ) }, - WalletSaplingSpend::from_parts, + WalletSpend::from_parts, ); sapling_nullifier_map.push((txid, tx_index, sapling_unlinked_nullifiers)); + #[cfg(feature = "orchard")] + let orchard_spends = { + let (orchard_spends, orchard_unlinked_nullifiers) = find_spent( + &tx.actions, + &scanning_keys.orchard_nullifiers, + |spend| { + spend.nf().expect( + "Could not deserialize nullifier for spend from protobuf representation.", + ) + }, + WalletSpend::from_parts, + ); + orchard_nullifier_map.push((txid, tx_index, orchard_unlinked_nullifiers)); + orchard_spends + }; + // Collect the set of accounts that were spent from in this transaction - let spent_from_accounts: HashSet<_> = - sapling_spends.iter().map(|spend| spend.account()).collect(); + let spent_from_accounts = sapling_spends.iter().map(|spend| spend.account()); + #[cfg(feature = "orchard")] + let spent_from_accounts = + spent_from_accounts.chain(orchard_spends.iter().map(|spend| spend.account())); + + let spent_from_accounts = spent_from_accounts.collect::>(); let (sapling_outputs, mut sapling_nc) = find_received( cur_height, @@ -549,9 +740,9 @@ pub(crate) fn scan_block_with_runner< ) }) .collect::>(), - sapling_batch_runner + batch_runners .as_mut() - .map(|runner| |txid| runner.collect_results(cur_hash, txid)), + .map(|runners| |txid| runners.sapling.collect_results(cur_hash, txid)), |output| sapling::Node::from_cmu(&output.cmu), |output_idx, output, note, is_change, position, nf, key_source| { WalletSaplingOutput::from_parts( @@ -567,13 +758,63 @@ pub(crate) fn scan_block_with_runner< }, ); sapling_note_commitments.append(&mut sapling_nc); + let has_sapling = !(sapling_spends.is_empty() && sapling_outputs.is_empty()); + + #[cfg(feature = "orchard")] + let (orchard_outputs, mut orchard_nc) = find_received( + cur_height, + compact_block_tx_count, + txid, + tx_idx, + orchard_commitment_tree_size, + &scanning_keys.orchard_keys, + &spent_from_accounts, + &tx.actions + .iter() + .map(|action| { + let action = CompactAction::try_from(action) + .expect("Invalid output found in compact block decoding."); + (OrchardDomain::for_nullifier(action.nullifier()), action) + }) + .collect::>(), + batch_runners + .as_mut() + .map(|runners| |txid| runners.orchard.collect_results(cur_hash, txid)), + |output| { + Option::from(MerkleHashOrchard::from_bytes(&output.cmstar_bytes())) + .expect("cmstar_bytes is a valid MerkleHashOrchard") + }, + |output_idx, output, note, is_change, position, nf, key_source| { + WalletOrchardOutput::from_parts( + output_idx, + output.cmx(), + output.ephemeral_key(), + note, + is_change, + position, + nf, + key_source, + ) + }, + ); + #[cfg(feature = "orchard")] + orchard_note_commitments.append(&mut orchard_nc); - if !(sapling_spends.is_empty() && sapling_outputs.is_empty()) { + #[cfg(feature = "orchard")] + let has_orchard = !(orchard_spends.is_empty() && orchard_outputs.is_empty()); + #[cfg(not(feature = "orchard"))] + let has_orchard = false; + + if has_sapling || has_orchard { wtxs.push(WalletTx::new( txid, tx_index as usize, sapling_spends, sapling_outputs, + #[cfg(feature = "orchard")] + orchard_spends, + #[cfg(feature = "orchard")] + orchard_outputs, )); } @@ -620,8 +861,8 @@ pub(crate) fn scan_block_with_runner< #[cfg(feature = "orchard")] ScannedBundles::new( orchard_commitment_tree_size, - vec![], // FIXME: collect the Orchard nullifiers - vec![], // FIXME: collect the Orchard note commitments + orchard_note_commitments, + orchard_nullifier_map, ), )) } @@ -670,7 +911,7 @@ fn find_received< Nf, KeyId: Copy + std::hash::Hash + Eq + Send + 'static, SK: ScanningKeyOps, - Output: ShieldedOutput, + Output: ShieldedOutput, WalletOutput, NoteCommitment, >( @@ -809,7 +1050,7 @@ mod tests { Nullifier, }; use zcash_keys::keys::UnifiedSpendingKey; - use zcash_note_encryption::Domain; + use zcash_note_encryption::{Domain, COMPACT_NOTE_SIZE}; use zcash_primitives::{ block::BlockHash, consensus::{sapling_zip212_enforcement, BlockHeight, Network}, @@ -823,11 +1064,10 @@ mod tests { proto::compact_formats::{ self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, }, - scan::BatchRunner, - scanning::ScanningKeys, + scanning::{BatchRunners, ScanningKeys}, }; - use super::{add_block_to_runner, scan_block, scan_block_with_runner}; + use super::{scan_block, scan_block_with_runners}; fn random_compact_tx(mut rng: impl RngCore) -> CompactTx { let fake_nf = { @@ -850,7 +1090,7 @@ mod tests { let cout = CompactSaplingOutput { cmu: fake_cmu, ephemeral_key: fake_epk, - ciphertext: vec![0; 52], + ciphertext: vec![0; COMPACT_NOTE_SIZE], }; let mut ctx = CompactTx::default(); let mut txid = vec![0; 32]; @@ -968,24 +1208,17 @@ mod tests { ); assert_eq!(cb.vtx.len(), 2); - let mut batch_runner = if scan_multithreaded { - let mut runner = BatchRunner::<_, _, _, _, ()>::new( - 10, - scanning_keys - .sapling_keys() - .iter() - .map(|(key_id, key)| (*key_id, key.prepare())), - ); + let mut batch_runners = if scan_multithreaded { + let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys); + runners.add_block(&Network::TestNetwork, cb.clone()); + runners.flush(); - add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner); - runner.flush(); - - Some(runner) + Some(runners) } else { None }; - let scanned_block = scan_block_with_runner( + let scanned_block = scan_block_with_runners( &network, cb, &scanning_keys, @@ -996,7 +1229,7 @@ mod tests { #[cfg(feature = "orchard")] Some(0), )), - batch_runner.as_mut(), + batch_runners.as_mut(), ) .unwrap(); let txs = scanned_block.transactions(); @@ -1060,25 +1293,18 @@ mod tests { ); assert_eq!(cb.vtx.len(), 3); - let mut batch_runner = if scan_multithreaded { - let mut runner = BatchRunner::<_, _, _, _, ()>::new( - 10, - scanning_keys - .sapling_keys() - .iter() - .map(|(key_id, key)| (*key_id, key.prepare())), - ); - - add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner); - runner.flush(); + let mut batch_runners = if scan_multithreaded { + let mut runners = BatchRunners::<_, (), ()>::for_keys(10, &scanning_keys); + runners.add_block(&Network::TestNetwork, cb.clone()); + runners.flush(); - Some(runner) + Some(runners) } else { None }; let scanned_block = - scan_block_with_runner(&network, cb, &scanning_keys, None, batch_runner.as_mut()) + scan_block_with_runners(&network, cb, &scanning_keys, None, batch_runners.as_mut()) .unwrap(); let txs = scanned_block.transactions(); assert_eq!(txs.len(), 1); diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 422127097a..bc733cf097 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -100,8 +100,12 @@ impl Recipient> { pub struct WalletTx { txid: TxId, block_index: usize, - sapling_spends: Vec, + sapling_spends: Vec>, sapling_outputs: Vec, + #[cfg(feature = "orchard")] + orchard_spends: Vec>, + #[cfg(feature = "orchard")] + orchard_outputs: Vec, } impl WalletTx { @@ -109,14 +113,20 @@ impl WalletTx { pub fn new( txid: TxId, block_index: usize, - sapling_spends: Vec, + sapling_spends: Vec>, sapling_outputs: Vec, + #[cfg(feature = "orchard")] orchard_spends: Vec>, + #[cfg(feature = "orchard")] orchard_outputs: Vec, ) -> Self { Self { txid, block_index, sapling_spends, sapling_outputs, + #[cfg(feature = "orchard")] + orchard_spends, + #[cfg(feature = "orchard")] + orchard_outputs, } } @@ -132,7 +142,7 @@ impl WalletTx { /// Returns a record for each Sapling note belonging to the wallet that was spent in the /// transaction. - pub fn sapling_spends(&self) -> &[WalletSaplingSpend] { + pub fn sapling_spends(&self) -> &[WalletSpend] { self.sapling_spends.as_ref() } @@ -141,6 +151,20 @@ impl WalletTx { pub fn sapling_outputs(&self) -> &[WalletSaplingOutput] { self.sapling_outputs.as_ref() } + + /// Returns a record for each Orchard note belonging to the wallet that was spent in the + /// transaction. + #[cfg(feature = "orchard")] + pub fn orchard_spends(&self) -> &[WalletSpend] { + self.orchard_spends.as_ref() + } + + /// Returns a record for each Orchard note belonging to and/or produced by the wallet in the + /// transaction. + #[cfg(feature = "orchard")] + pub fn orchard_outputs(&self) -> &[WalletOrchardOutput] { + self.orchard_outputs.as_ref() + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -197,24 +221,22 @@ impl transparent_fees::InputView for WalletTransparentOutput { } } -/// A subset of a [`SpendDescription`] relevant to wallets and light clients. -/// -/// [`SpendDescription`]: sapling::bundle::SpendDescription -pub struct WalletSaplingSpend { +/// A reference to a spent note belonging to the wallet within a transaction. +pub struct WalletSpend { index: usize, - nf: sapling::Nullifier, + nf: Nf, account: AccountId, } -impl WalletSaplingSpend { - pub fn from_parts(index: usize, nf: sapling::Nullifier, account: AccountId) -> Self { +impl WalletSpend { + pub fn from_parts(index: usize, nf: Nf, account: AccountId) -> Self { Self { index, nf, account } } pub fn index(&self) -> usize { self.index } - pub fn nf(&self) -> &sapling::Nullifier { + pub fn nf(&self) -> &Nf { &self.nf } pub fn account(&self) -> AccountId { @@ -311,6 +333,70 @@ impl WalletSaplingOutput { } } +#[cfg(feature = "orchard")] +pub struct WalletOrchardOutput { + index: usize, + cmx: orchard::note::ExtractedNoteCommitment, + ephemeral_key: EphemeralKeyBytes, + note: orchard::note::Note, + is_change: bool, + note_commitment_tree_position: Position, + nf: Option, + key_source: KeySource, +} + +#[cfg(feature = "orchard")] +impl WalletOrchardOutput { + /// Constructs a new `WalletOrchardOutput` value from its constituent parts. + #[allow(clippy::too_many_arguments)] + pub fn from_parts( + index: usize, + cmx: orchard::note::ExtractedNoteCommitment, + ephemeral_key: EphemeralKeyBytes, + note: orchard::note::Note, + is_change: bool, + note_commitment_tree_position: Position, + nf: Option, + key_source: KeySource, + ) -> Self { + Self { + index, + cmx, + ephemeral_key, + note, + is_change, + note_commitment_tree_position, + nf, + key_source, + } + } + + pub fn index(&self) -> usize { + self.index + } + pub fn cmx(&self) -> &orchard::note::ExtractedNoteCommitment { + &self.cmx + } + pub fn ephemeral_key(&self) -> &EphemeralKeyBytes { + &self.ephemeral_key + } + pub fn note(&self) -> &orchard::note::Note { + &self.note + } + pub fn is_change(&self) -> bool { + self.is_change + } + pub fn note_commitment_tree_position(&self) -> Position { + self.note_commitment_tree_position + } + pub fn nf(&self) -> Option<&orchard::note::Nullifier> { + self.nf.as_ref() + } + pub fn key_source(&self) -> KeySource { + self.key_source + } +} + /// An enumeration of supported shielded note types for use in [`ReceivedNote`] #[derive(Debug, Clone, PartialEq, Eq)] pub enum Note { diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 5ffa815d9d..92627c0945 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -2033,7 +2033,12 @@ pub(crate) fn query_nullifier_map, S>( // have been created during the same scan that the locator was added to the nullifier // map, but it would not happen if the transaction in question spent the note with no // change or explicit in-wallet recipient. - put_tx_meta(conn, &WalletTx::new(txid, index, vec![], vec![]), height).map(Some) + put_tx_meta( + conn, + &WalletTx::new(txid, index, vec![], vec![], vec![], vec![]), + height, + ) + .map(Some) } /// Deletes from the nullifier map any entries with a locator referencing a block height