zcash_client_backend: Pass nullifiers separately from scanning keys.

This commit is contained in:
Kris Nuttycombe 2024-02-26 18:09:12 -07:00
parent a63d5e51d1
commit 27f6207a7c
4 changed files with 141 additions and 95 deletions

View File

@ -30,6 +30,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::scanning`:
- `impl ScanningKeyOps<OrchardDomain, ..> for ScanningKey<..>` for Orchard key types.
- `ScanningKeys::orchard`
- `Nullifiers::{orchard, extend_orchard, retain_orchard}`
- `TaggedOrchardBatch`
- `TaggedOrchardBatchRunner`
- `zcash_client_backend::wallet`:
@ -103,6 +104,7 @@ and this library adheres to Rust's notion of
- `ScanningKey` is now a concrete type that bundles an incoming viewing key
with an optional nullifier key and key source metadata.
- `ScanningKeys`
- `Nullifiers`
- `impl Clone for zcash_client_backend::{
zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
wallet::WalletTransparentOutput,

View File

@ -151,7 +151,7 @@ use zcash_primitives::consensus::{self, BlockHeight};
use crate::{
data_api::{NullifierQuery, WalletWrite},
proto::compact_formats::CompactBlock,
scanning::{scan_block_with_runners, BatchRunners, ScanningKeys},
scanning::{scan_block_with_runners, BatchRunners, Nullifiers, ScanningKeys},
};
pub mod error;
@ -303,27 +303,12 @@ where
let account_ufvks = data_db
.get_unified_full_viewing_keys()
.map_err(Error::Wallet)?;
let mut scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks);
// Get the nullifiers for the unspent notes we are tracking
scanning_keys.extend_sapling_nullifiers(
data_db
.get_sapling_nullifiers(NullifierQuery::Unspent)
.map_err(Error::Wallet)?,
);
#[cfg(feature = "orchard")]
scanning_keys.extend_orchard_nullifiers(
data_db
.get_orchard_nullifiers(NullifierQuery::Unspent)
.map_err(Error::Wallet)?,
);
let 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| {
runners.add_block(params, block).map_err(|e| e.into())
})?;
runners.flush();
let mut prior_block_metadata = if from_height > BlockHeight::from(0) {
@ -334,6 +319,17 @@ where
None
};
// Get the nullifiers for the unspent notes we are tracking
let mut nullifiers = Nullifiers::new(
data_db
.get_sapling_nullifiers(NullifierQuery::Unspent)
.map_err(Error::Wallet)?,
#[cfg(feature = "orchard")]
data_db
.get_orchard_nullifiers(NullifierQuery::Unspent)
.map_err(Error::Wallet)?,
);
let mut scanned_blocks = vec![];
let mut scan_summary = ScanSummary::for_range(from_height..from_height);
block_source.with_blocks::<_, DbT::Error>(
@ -345,6 +341,7 @@ where
params,
block,
&scanning_keys,
&nullifiers,
prior_block_metadata.as_ref(),
Some(&mut runners),
)
@ -365,14 +362,12 @@ where
.iter()
.flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf()))
.collect();
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| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
},
));
nullifiers.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf));
nullifiers.extend_sapling(scanned_block.transactions.iter().flat_map(|tx| {
tx.sapling_outputs()
.iter()
.flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
}));
#[cfg(feature = "orchard")]
{
@ -382,14 +377,12 @@ where
.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.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
}),
);
nullifiers.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf));
nullifiers.extend_orchard(scanned_block.transactions.iter().flat_map(|tx| {
tx.orchard_outputs()
.iter()
.flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
}));
}
prior_block_metadata = Some(scanned_block.to_block_metadata());

View File

@ -183,60 +183,59 @@ impl<AccountId> ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifie
}
}
/// A set of scanning keys, along with the vector of `(Account, Nullifier)` pairs that
/// notes the wallet is tracking.
/// A set of keys to be used in scanning for decryptable transaction outputs.
pub struct ScanningKeys<AccountId, IvkTag> {
sapling_keys:
HashMap<IvkTag, Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>>,
sapling_nullifiers: Vec<(AccountId, sapling::Nullifier)>,
sapling: HashMap<IvkTag, Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>>,
#[cfg(feature = "orchard")]
orchard_keys: HashMap<
orchard: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
>,
#[cfg(feature = "orchard")]
orchard_nullifiers: Vec<(AccountId, orchard::note::Nullifier)>,
}
impl<AccountId, IvkTag> ScanningKeys<AccountId, IvkTag> {
/// Constructs a new set of scanning keys.
pub fn new(
sapling: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>,
>,
#[cfg(feature = "orchard")] orchard: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
>,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
}
}
/// Constructs a new empty set of scanning keys.
pub fn empty() -> Self {
Self {
sapling_keys: HashMap::new(),
sapling_nullifiers: vec![],
sapling: HashMap::new(),
#[cfg(feature = "orchard")]
orchard_keys: HashMap::new(),
#[cfg(feature = "orchard")]
orchard_nullifiers: vec![],
orchard: HashMap::new(),
}
}
/// Returns the Sapling keys to be used for incoming note detection.
pub fn sapling_keys(
pub fn sapling(
&self,
) -> &HashMap<IvkTag, Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>>
{
&self.sapling_keys
}
/// Returns the Sapling nullifiers for notes that the wallet is tracking.
pub fn sapling_nullifiers(&self) -> &[(AccountId, sapling::Nullifier)] {
self.sapling_nullifiers.as_ref()
&self.sapling
}
/// Returns the Orchard keys to be used for incoming note detection.
#[cfg(feature = "orchard")]
pub fn orchard_keys(
pub fn orchard(
&self,
) -> &HashMap<IvkTag, Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>>
{
&self.orchard_keys
}
/// Returns the Orchard nullifiers for notes that the wallet is tracking.
#[cfg(feature = "orchard")]
pub fn orchard_nullifiers(&self) -> &[(AccountId, orchard::note::Nullifier)] {
self.orchard_nullifiers.as_ref()
&self.orchard
}
}
@ -248,12 +247,12 @@ impl<AccountId: Copy + Eq + Hash + 'static> ScanningKeys<AccountId, (AccountId,
) -> Self {
#![allow(clippy::type_complexity)]
let mut sapling_keys: HashMap<
let mut sapling: HashMap<
(AccountId, Scope),
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>>,
> = HashMap::new();
#[cfg(feature = "orchard")]
let mut orchard_keys: HashMap<
let mut orchard: HashMap<
(AccountId, Scope),
Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>>,
> = HashMap::new();
@ -261,7 +260,7 @@ impl<AccountId: Copy + Eq + Hash + 'static> ScanningKeys<AccountId, (AccountId,
for (account_id, ufvk) in ufvks {
if let Some(dfvk) = ufvk.sapling() {
for scope in [Scope::External, Scope::Internal] {
sapling_keys.insert(
sapling.insert(
(account_id, scope),
Box::new(ScanningKey {
ivk: dfvk.to_ivk(scope),
@ -276,7 +275,7 @@ impl<AccountId: Copy + Eq + Hash + 'static> ScanningKeys<AccountId, (AccountId,
#[cfg(feature = "orchard")]
if let Some(fvk) = ufvk.orchard() {
for scope in [Scope::External, Scope::Internal] {
orchard_keys.insert(
orchard.insert(
(account_id, scope),
Box::new(ScanningKey {
ivk: fvk.to_ivk(scope),
@ -290,46 +289,81 @@ impl<AccountId: Copy + Eq + Hash + 'static> ScanningKeys<AccountId, (AccountId,
}
Self {
sapling_keys,
sapling_nullifiers: vec![],
sapling,
#[cfg(feature = "orchard")]
orchard_keys,
#[cfg(feature = "orchard")]
orchard_nullifiers: vec![],
orchard,
}
}
}
/// The set of nullifiers being tracked by a wallet.
pub struct Nullifiers<AccountId> {
sapling: Vec<(AccountId, sapling::Nullifier)>,
#[cfg(feature = "orchard")]
orchard: Vec<(AccountId, orchard::note::Nullifier)>,
}
impl<AccountId> Nullifiers<AccountId> {
/// Constructs a new empty set of nullifiers
pub fn empty() -> Self {
Self {
sapling: vec![],
#[cfg(feature = "orchard")]
orchard: vec![],
}
}
/// Construct a nullifier set from its constituent parts.
pub(crate) fn new(
sapling: Vec<(AccountId, sapling::Nullifier)>,
#[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
}
}
/// Returns the Sapling nullifiers for notes that the wallet is tracking.
pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] {
self.sapling.as_ref()
}
/// Returns the Orchard nullifiers for notes that the wallet is tracking.
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] {
self.orchard.as_ref()
}
/// Discards Sapling nullifiers from the tracked nullifier set, retaining only those that
/// satisfy the given predicate.
pub(crate) fn retain_sapling_nullifiers(
&mut self,
f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool,
) {
self.sapling_nullifiers.retain(f);
pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) {
self.sapling.retain(f);
}
/// Adds the given nullifiers to the tracked nullifier set.
pub(crate) fn extend_sapling_nullifiers(
pub(crate) fn extend_sapling(
&mut self,
nfs: impl IntoIterator<Item = (AccountId, sapling::Nullifier)>,
) {
self.sapling_nullifiers.extend(nfs);
self.sapling.extend(nfs);
}
#[cfg(feature = "orchard")]
pub(crate) fn retain_orchard_nullifiers(
pub(crate) fn retain_orchard(
&mut self,
f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool,
) {
self.orchard_nullifiers.retain(f);
self.orchard.retain(f);
}
#[cfg(feature = "orchard")]
pub(crate) fn extend_orchard_nullifiers(
pub(crate) fn extend_orchard(
&mut self,
nfs: impl IntoIterator<Item = (AccountId, orchard::note::Nullifier)>,
) {
self.orchard_nullifiers.extend(nfs);
self.orchard.extend(nfs);
}
}
@ -450,6 +484,7 @@ pub fn scan_block<P, AccountId, IvkTag>(
params: &P,
block: CompactBlock,
scanning_keys: &ScanningKeys<AccountId, IvkTag>,
nullifiers: &Nullifiers<AccountId>,
prior_block_metadata: Option<&BlockMetadata>,
) -> Result<ScannedBlock<AccountId>, ScanError>
where
@ -461,6 +496,7 @@ where
params,
block,
scanning_keys,
nullifiers,
prior_block_metadata,
None,
)
@ -527,7 +563,7 @@ where
sapling: BatchRunner::new(
batch_size_threshold,
scanning_keys
.sapling_keys()
.sapling()
.iter()
.map(|(id, key)| (id.clone(), key.prepare())),
),
@ -535,7 +571,7 @@ where
orchard: BatchRunner::new(
batch_size_threshold,
scanning_keys
.orchard_keys()
.orchard()
.iter()
.map(|(id, key)| (id.clone(), key.prepare())),
),
@ -612,6 +648,7 @@ pub(crate) fn scan_block_with_runners<P, AccountId, IvkTag, TS, TO>(
params: &P,
block: CompactBlock,
scanning_keys: &ScanningKeys<AccountId, IvkTag>,
nullifiers: &Nullifiers<AccountId>,
prior_block_metadata: Option<&BlockMetadata>,
mut batch_runners: Option<&mut BatchRunners<IvkTag, TS, TO>>,
) -> Result<ScannedBlock<AccountId>, ScanError>
@ -762,7 +799,7 @@ where
let (sapling_spends, sapling_unlinked_nullifiers) = find_spent(
&tx.spends,
&scanning_keys.sapling_nullifiers,
&nullifiers.sapling,
|spend| {
spend.nf().expect(
"Could not deserialize nullifier for spend from protobuf representation.",
@ -777,7 +814,7 @@ where
let orchard_spends = {
let (orchard_spends, orchard_unlinked_nullifiers) = find_spent(
&tx.actions,
&scanning_keys.orchard_nullifiers,
&nullifiers.orchard,
|spend| {
spend.nf().expect(
"Could not deserialize nullifier for spend from protobuf representation.",
@ -802,7 +839,7 @@ where
txid,
tx_idx,
sapling_commitment_tree_size,
&scanning_keys.sapling_keys,
&scanning_keys.sapling,
&spent_from_accounts,
&tx.outputs
.iter()
@ -836,7 +873,7 @@ where
txid,
tx_idx,
orchard_commitment_tree_size,
&scanning_keys.orchard_keys,
&scanning_keys.orchard,
&spent_from_accounts,
&tx.actions
.iter()
@ -1090,6 +1127,8 @@ fn find_received<
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use group::{
ff::{Field, PrimeField},
GroupEncoding,
@ -1122,7 +1161,7 @@ mod tests {
scanning::{BatchRunners, ScanningKeys},
};
use super::{scan_block, scan_block_with_runners};
use super::{scan_block, scan_block_with_runners, Nullifiers};
fn random_compact_tx(mut rng: impl RngCore) -> CompactTx {
let fake_nf = {
@ -1280,6 +1319,7 @@ mod tests {
&network,
cb,
&scanning_keys,
&Nullifiers::empty(),
Some(&BlockMetadata::from_parts(
BlockHeight::from(0),
BlockHash([0u8; 32]),
@ -1361,9 +1401,15 @@ mod tests {
None
};
let scanned_block =
scan_block_with_runners(&network, cb, &scanning_keys, None, batch_runners.as_mut())
.unwrap();
let scanned_block = scan_block_with_runners(
&network,
cb,
&scanning_keys,
&Nullifiers::empty(),
None,
batch_runners.as_mut(),
)
.unwrap();
let txs = scanned_block.transactions();
assert_eq!(txs.len(), 1);
@ -1403,10 +1449,14 @@ mod tests {
let account = AccountId::try_from(12).unwrap();
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32], account).expect("Valid USK");
let ufvk = usk.to_unified_full_viewing_key();
let mut scanning_keys = ScanningKeys::empty();
let scanning_keys = ScanningKeys::<AccountId, Infallible>::empty();
let nf = Nullifier([7; 32]);
scanning_keys.extend_sapling_nullifiers([(account, nf)]);
let nullifiers = Nullifiers::new(
vec![(account, nf)],
#[cfg(feature = "orchard")]
vec![],
);
let cb = fake_compact_block(
1u32.into(),
@ -1419,7 +1469,7 @@ mod tests {
);
assert_eq!(cb.vtx.len(), 2);
let scanned_block = scan_block(&network, cb, &scanning_keys, None).unwrap();
let scanned_block = scan_block(&network, cb, &scanning_keys, &nullifiers, None).unwrap();
let txs = scanned_block.transactions();
assert_eq!(txs.len(), 1);

View File

@ -284,7 +284,7 @@ mod tests {
},
decrypt_transaction,
proto::compact_formats::{CompactBlock, CompactTx},
scanning::{scan_block, ScanningKeys},
scanning::{scan_block, Nullifiers, ScanningKeys},
wallet::Recipient,
PoolType, ShieldedProtocol, TransferType,
};
@ -606,6 +606,7 @@ mod tests {
&params,
block,
&scanning_keys,
&Nullifiers::empty(),
Some(&BlockMetadata::from_parts(
height - 1,
prev_hash,