diff --git a/components/zcash_note_encryption/src/batch.rs b/components/zcash_note_encryption/src/batch.rs index 656726d26..9742277a7 100644 --- a/components/zcash_note_encryption/src/batch.rs +++ b/components/zcash_note_encryption/src/batch.rs @@ -47,6 +47,10 @@ fn batch_note_decryption, F, FR, c where F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, &D::SymmetricKey) -> Option, { + if ivks.is_empty() { + return (0..outputs.len()).map(|_| None).collect(); + }; + // Fetch the ephemeral keys for each output and batch-parse them. let ephemeral_keys = D::batch_epk(outputs.iter().map(|(_, output)| output.ephemeral_key())); diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 4e2bac272..7f7fd13e2 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -6,6 +6,7 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + ### Added - Functionality that enables the receiving and spending of transparent funds, behind the new `transparent-inputs` feature flag. @@ -60,7 +61,7 @@ and this library adheres to Rust's notion of been replaced by `ephemeral_key`. - `zcash_client_backend::proto::compact_formats::CompactSaplingOutput`: the `epk` method has been replaced by `ephemeral_key`. -- `data_api::wallet::spend_to_address` now takes a `min_confirmations` +- `data_api::wallet::spend_to_address` now takes a `min_confirmations` parameter, which the caller can provide to specify the minimum number of confirmations required for notes being selected. A default value of 10 confirmations is recommended. @@ -107,6 +108,12 @@ and this library adheres to Rust's notion of - `Zip321Error::TransparentMemo(usize)` - `Zip321Error::RecipientMissing(usize)` - `Zip321Error::ParseError(String)` +- The api of `welding_rig::ScanningKey` has changed to accommodate batch + decryption and to correctly handle scanning with the internal (change) keys + derived from ZIP 316 UFVKs and UIVKs. +- `welding_rig::scan_block` now uses batching for trial-decryption of + transaction outputs. + ### Removed - `zcash_client_backend::data_api`: diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs index cd3ab9707..bf7116730 100644 --- a/zcash_client_backend/src/welding_rig.rs +++ b/zcash_client_backend/src/welding_rig.rs @@ -4,15 +4,15 @@ use ff::PrimeField; use std::collections::HashSet; use std::convert::TryFrom; use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; -use zcash_note_encryption::{ShieldedOutput, COMPACT_NOTE_SIZE}; +use zcash_note_encryption::batch; use zcash_primitives::{ - consensus::{self, BlockHeight}, + consensus, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{ self, - keys::DiversifiableFullViewingKey, - note_encryption::{try_sapling_compact_note_decryption, SaplingDomain}, - Node, Note, Nullifier, PaymentAddress, SaplingIvk, + keys::{DiversifiableFullViewingKey, Scope}, + note_encryption::SaplingDomain, + Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk, }, transaction::components::sapling::CompactOutputDescription, zip32::{AccountId, ExtendedFullViewingKey}, @@ -21,58 +21,6 @@ use zcash_primitives::{ use crate::proto::compact_formats::CompactBlock; use crate::wallet::{WalletShieldedOutput, WalletShieldedSpend, WalletTx}; -/// Scans a [`CompactSaplingOutput`] with a set of [`ScanningKey`]s. -/// -/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this -/// output belongs to any of the given [`ScanningKey`]s. -/// -/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented -/// with this output's commitment. -/// -/// [`ScanningKey`]: crate::welding_rig::ScanningKey -#[allow(clippy::too_many_arguments)] -fn scan_output( - params: &P, - height: BlockHeight, - index: usize, - output: CompactOutputDescription, - vks: &[(&AccountId, &K)], - spent_from_accounts: &HashSet, - tree: &mut CommitmentTree, -) -> Option> { - for (account, vk) in vks.iter() { - let (note, to) = match vk.try_decryption(params, height, &output) { - Some(ret) => ret, - None => continue, - }; - - // A note is marked as "change" if the account that received it - // also spent notes in the same transaction. This will catch, - // for instance: - // - Change created by spending fractions of notes. - // - Notes created by consolidation transactions. - // - Notes sent from one account to itself. - let is_change = spent_from_accounts.contains(account); - - let witness = IncrementalWitness::from_tree(tree); - let nf = vk.nf(¬e, &witness); - - return Some(WalletShieldedOutput { - index, - cmu: output.cmu, - ephemeral_key: output.ephemeral_key, - account: **account, - note, - to, - is_change, - witness, - nf, - }); - } - - None -} - /// A key that can be used to perform trial decryption and nullifier /// computation for a Sapling [`CompactSaplingOutput`] /// @@ -87,47 +35,45 @@ fn scan_output( /// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput /// [`scan_block`]: crate::welding_rig::scan_block pub trait ScanningKey { + /// The type of key that is used to decrypt Sapling outputs; + type SaplingNk; + /// The type of nullifier extracted when a note is successfully /// obtained by trial decryption. type Nf; - /// Attempts to decrypt a Sapling note and payment address - /// from the specified ciphertext using this scanning key. - fn try_decryption< - P: consensus::Parameters, - Output: ShieldedOutput, COMPACT_NOTE_SIZE>, - >( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)>; + /// Obtain the underlying Sapling incoming viewing key(s) for this scanning key. + fn to_sapling_keys(&self) -> Vec<(SaplingIvk, Self::SaplingNk)>; /// Produces the nullifier for the specified note and witness, if possible. /// /// IVK-based implementations of this trait cannot successfully derive /// nullifiers, in which case `Self::Nf` should be set to the unit type /// and this function is a no-op. - fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf; + fn sapling_nf( + key: &Self::SaplingNk, + note: &Note, + witness: &IncrementalWitness, + ) -> Self::Nf; } impl ScanningKey for DiversifiableFullViewingKey { + type SaplingNk = NullifierDerivingKey; type Nf = sapling::Nullifier; - fn try_decryption< - P: consensus::Parameters, - Output: ShieldedOutput, COMPACT_NOTE_SIZE>, - >( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)> { - try_sapling_compact_note_decryption(params, height, &self.fvk().vk.ivk(), output) + fn to_sapling_keys(&self) -> Vec<(SaplingIvk, Self::SaplingNk)> { + vec![ + (self.to_ivk(Scope::External), self.to_nk(Scope::External)), + (self.to_ivk(Scope::Internal), self.to_nk(Scope::Internal)), + ] } - fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf { - note.nf(&self.fvk().vk.nk, witness.position() as u64) + fn sapling_nf( + key: &Self::SaplingNk, + note: &Note, + witness: &IncrementalWitness, + ) -> Self::Nf { + note.nf(key, witness.position() as u64) } } @@ -136,22 +82,19 @@ impl ScanningKey for DiversifiableFullViewingKey { /// /// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey impl ScanningKey for ExtendedFullViewingKey { - type Nf = Nullifier; + type SaplingNk = NullifierDerivingKey; + type Nf = sapling::Nullifier; - fn try_decryption< - P: consensus::Parameters, - Output: ShieldedOutput, COMPACT_NOTE_SIZE>, - >( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)> { - try_sapling_compact_note_decryption(params, height, &self.fvk.vk.ivk(), output) + fn to_sapling_keys(&self) -> Vec<(SaplingIvk, Self::SaplingNk)> { + vec![(self.fvk.vk.ivk(), self.fvk.vk.nk)] } - fn nf(&self, note: &Note, witness: &IncrementalWitness) -> Self::Nf { - note.nf(&self.fvk.vk.nk, witness.position() as u64) + fn sapling_nf( + key: &Self::SaplingNk, + note: &Note, + witness: &IncrementalWitness, + ) -> Self::Nf { + note.nf(key, witness.position() as u64) } } @@ -160,21 +103,14 @@ impl ScanningKey for ExtendedFullViewingKey { /// /// [`SaplingIvk`]: zcash_primitives::sapling::SaplingIvk impl ScanningKey for SaplingIvk { + type SaplingNk = (); type Nf = (); - fn try_decryption< - P: consensus::Parameters, - Output: ShieldedOutput, COMPACT_NOTE_SIZE>, - >( - &self, - params: &P, - height: BlockHeight, - output: &Output, - ) -> Option<(Note, PaymentAddress)> { - try_sapling_compact_note_decryption(params, height, self, output) + fn to_sapling_keys(&self) -> Vec<(SaplingIvk, Self::SaplingNk)> { + vec![(self.clone(), ())] } - fn nf(&self, _note: &Note, _witness: &IncrementalWitness) {} + fn sapling_nf(_key: &Self::SaplingNk, _note: &Note, _witness: &IncrementalWitness) {} } /// Scans a [`CompactBlock`] with a set of [`ScanningKey`]s. @@ -265,17 +201,43 @@ pub fn scan_block( }) .collect(); - for (index, output) in tx.outputs.into_iter().enumerate() { + let decoded = &tx + .outputs + .into_iter() + .map(|output| { + ( + SaplingDomain::for_height(params.clone(), block_height), + CompactOutputDescription::try_from(output) + .expect("Invalid output found in compact block decoding."), + ) + }) + .collect::>(); + + let vks = vks + .iter() + .flat_map(|(a, k)| { + k.to_sapling_keys() + .into_iter() + .map(move |(ivk, nk)| (**a, ivk, nk)) + }) + .collect::>(); + + let ivks = vks + .iter() + .map(|(_, ivk, _)| (*ivk).clone()) + .collect::>(); + + let decrypted = batch::try_compact_note_decryption(&ivks, decoded); + + for (index, ((_, output), dec_output)) in decoded.iter().zip(decrypted).enumerate() { // Grab mutable references to new witnesses from previous outputs // in this transaction so that we can update them. Scoped so we // don't hold mutable references to shielded_outputs for too long. let new_witnesses: Vec<_> = shielded_outputs .iter_mut() - .map(|output| &mut output.witness) + .map(|out| &mut out.witness) .collect(); - let output = CompactOutputDescription::try_from(output).ok().unwrap(); - // Increment tree and witnesses let node = Node::new(output.cmu.to_repr()); for witness in &mut *existing_witnesses { @@ -289,16 +251,29 @@ pub fn scan_block( } tree.append(node).unwrap(); - if let Some(output) = scan_output( - params, - block_height, - index, - output, - vks, - &spent_from_accounts, - tree, - ) { - shielded_outputs.push(output); + if let Some(((note, to), ivk_idx)) = dec_output { + // A note is marked as "change" if the account that received it + // also spent notes in the same transaction. This will catch, + // for instance: + // - Change created by spending fractions of notes. + // - Notes created by consolidation transactions. + // - Notes sent from one account to itself. + let (account, _, nk) = &vks[ivk_idx]; + let is_change = spent_from_accounts.contains(account); + let witness = IncrementalWitness::from_tree(tree); + let nf = K::sapling_nf(nk, ¬e, &witness); + + shielded_outputs.push(WalletShieldedOutput { + index, + cmu: output.cmu, + ephemeral_key: output.ephemeral_key.clone(), + account: *account, + note, + to, + is_change, + witness, + nf, + }) } } }