Allow block scanning using either IVKs or FVKs.

This commit is contained in:
Kris Nuttycombe 2021-03-09 20:55:44 -07:00
parent 16289750e8
commit 0e022f2283
6 changed files with 157 additions and 105 deletions

View File

@ -9,7 +9,7 @@ use zcash_primitives::{
consensus::BlockHeight,
merkle_tree::{CommitmentTree, IncrementalWitness},
note_encryption::Memo,
primitives::{Note, Nullifier, PaymentAddress},
primitives::{Nullifier, PaymentAddress},
sapling::Node,
transaction::{components::Amount, Transaction, TxId},
zip32::ExtendedFullViewingKey,
@ -20,7 +20,7 @@ use crate::{
data_api::wallet::ANCHOR_OFFSET,
decrypt::DecryptedOutput,
proto::compact_formats::CompactBlock,
wallet::{AccountId, SpendableNote, WalletShieldedOutput, WalletTx},
wallet::{AccountId, SpendableNote, WalletTx},
};
pub mod chain;
@ -184,7 +184,7 @@ pub struct PrunedBlock<'a> {
pub block_hash: BlockHash,
pub block_time: u32,
pub commitment_tree: &'a CommitmentTree<Node>,
pub transactions: &'a Vec<WalletTx>,
pub transactions: &'a Vec<WalletTx<Nullifier>>,
}
pub struct ReceivedTransaction<'a> {
@ -205,6 +205,7 @@ pub struct SentTransaction<'a> {
/// This trait encapsulates the write capabilities required to update stored
/// wallet data.
pub trait WalletWrite: WalletRead {
#[allow(clippy::type_complexity)]
fn insert_pruned_block(
&mut self,
block: &PrunedBlock,
@ -251,70 +252,6 @@ pub trait BlockSource {
F: FnMut(CompactBlock) -> Result<(), Self::Error>;
}
/// This trait provides a generalization over shielded output representations
/// that allows a wallet to avoid coupling to a specific one.
// TODO: it'd probably be better not to unify the definitions of
// `WalletShieldedOutput` and `DecryptedOutput` via a compositional
// approach, if possible.
pub trait ShieldedOutput {
fn index(&self) -> usize;
fn account(&self) -> AccountId;
fn to(&self) -> &PaymentAddress;
fn note(&self) -> &Note;
fn memo(&self) -> Option<&Memo>;
fn is_change(&self) -> Option<bool>;
fn nullifier(&self) -> Option<Nullifier>;
}
impl ShieldedOutput for WalletShieldedOutput {
fn index(&self) -> usize {
self.index
}
fn account(&self) -> AccountId {
self.account
}
fn to(&self) -> &PaymentAddress {
&self.to
}
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
None
}
fn is_change(&self) -> Option<bool> {
Some(self.is_change)
}
fn nullifier(&self) -> Option<Nullifier> {
self.nf.clone()
}
}
impl ShieldedOutput for DecryptedOutput {
fn index(&self) -> usize {
self.index
}
fn account(&self) -> AccountId {
self.account
}
fn to(&self) -> &PaymentAddress {
&self.to
}
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
Some(&self.memo)
}
fn is_change(&self) -> Option<bool> {
None
}
fn nullifier(&self) -> Option<Nullifier> {
None
}
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use std::collections::HashMap;
@ -447,6 +384,7 @@ pub mod testing {
}
impl WalletWrite for MockWalletDB {
#[allow(clippy::type_complexity)]
fn insert_pruned_block(
&mut self,
_block: &PrunedBlock,

View File

@ -291,7 +291,7 @@ where
let block_hash = BlockHash::from_slice(&block.hash);
let block_time = block.time;
let txs: Vec<WalletTx> = {
let txs: Vec<WalletTx<Nullifier>> = {
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
scan_block(
@ -344,11 +344,10 @@ where
.flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf))
.collect();
nullifiers.retain(|(_, nf)| !spent_nf.contains(nf));
nullifiers.extend(txs.iter().flat_map(|tx| {
tx.shielded_outputs
.iter()
.flat_map(|out| out.nf.map(|nf| (out.account, nf)))
}));
nullifiers.extend(
txs.iter()
.flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))),
);
witnesses.extend(new_witnesses);

View File

@ -30,13 +30,13 @@ impl ConditionallySelectable for AccountId {
/// A subset of a [`Transaction`] relevant to wallets and light clients.
///
/// [`Transaction`]: zcash_primitives::transaction::Transaction
pub struct WalletTx {
pub struct WalletTx<N> {
pub txid: TxId,
pub index: usize,
pub num_spends: usize,
pub num_outputs: usize,
pub shielded_spends: Vec<WalletShieldedSpend>,
pub shielded_outputs: Vec<WalletShieldedOutput>,
pub shielded_outputs: Vec<WalletShieldedOutput<N>>,
}
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
@ -51,7 +51,7 @@ pub struct WalletShieldedSpend {
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
///
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
pub struct WalletShieldedOutput {
pub struct WalletShieldedOutput<N> {
pub index: usize,
pub cmu: bls12_381::Scalar,
pub epk: jubjub::ExtendedPoint,
@ -60,7 +60,7 @@ pub struct WalletShieldedOutput {
pub to: PaymentAddress,
pub is_change: bool,
pub witness: IncrementalWitness<Node>,
pub nf: Option<Nullifier>,
pub nf: N,
}
pub struct SpendableNote {

View File

@ -7,7 +7,7 @@ use zcash_primitives::{
consensus::{self, BlockHeight},
merkle_tree::{CommitmentTree, IncrementalWitness},
note_encryption::try_sapling_compact_note_decryption,
primitives::{Nullifier, ViewingKey},
primitives::{Note, Nullifier, PaymentAddress, SaplingIvk, ViewingKey},
sapling::Node,
transaction::TxId,
};
@ -25,17 +25,17 @@ use crate::wallet::{AccountId, WalletShieldedOutput, WalletShieldedSpend, Wallet
///
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
#[allow(clippy::too_many_arguments)]
fn scan_output<P: consensus::Parameters>(
fn scan_output<P: consensus::Parameters, K: ScanningKey>(
params: &P,
height: BlockHeight,
(index, output): (usize, CompactOutput),
vks: &[(AccountId, &ViewingKey)],
vks: &[(AccountId, &K)],
spent_from_accounts: &HashSet<AccountId>,
tree: &mut CommitmentTree<Node>,
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
block_witnesses: &mut [&mut IncrementalWitness<Node>],
new_witnesses: &mut [&mut IncrementalWitness<Node>],
) -> Option<WalletShieldedOutput> {
) -> Option<WalletShieldedOutput<K::Nf>> {
let cmu = output.cmu().ok()?;
let epk = output.epk().ok()?;
let ct = output.ciphertext;
@ -54,12 +54,10 @@ fn scan_output<P: consensus::Parameters>(
tree.append(node).unwrap();
for (account, vk) in vks.iter() {
let ivk = vk.ivk();
let (note, to) =
match try_sapling_compact_note_decryption(params, height, &ivk, &epk, &cmu, &ct) {
Some(ret) => ret,
None => continue,
};
let (note, to) = match vk.try_decryption(params, height, &epk, &cmu, &ct) {
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,
@ -70,7 +68,7 @@ fn scan_output<P: consensus::Parameters>(
let is_change = spent_from_accounts.contains(&account);
let witness = IncrementalWitness::from_tree(tree);
let nf = note.nf(&vk, witness.position() as u64);
let nf = vk.nf(&note, &witness);
return Some(WalletShieldedOutput {
index,
@ -81,30 +79,81 @@ fn scan_output<P: consensus::Parameters>(
to,
is_change,
witness,
nf: Some(nf),
nf,
});
}
None
}
/// Scans a [`CompactBlock`] with a set of [`ViewingKey`]s.
pub trait ScanningKey {
type Nf;
fn try_decryption<P: consensus::Parameters>(
&self,
params: &P,
height: BlockHeight,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
ct: &[u8],
) -> Option<(Note, PaymentAddress)>;
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf;
}
impl ScanningKey for ViewingKey {
type Nf = Nullifier;
fn try_decryption<P: consensus::Parameters>(
&self,
params: &P,
height: BlockHeight,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
ct: &[u8],
) -> Option<(Note, PaymentAddress)> {
try_sapling_compact_note_decryption(params, height, &self.ivk(), &epk, &cmu, &ct)
}
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf {
note.nf(self, witness.position() as u64)
}
}
impl ScanningKey for SaplingIvk {
type Nf = ();
fn try_decryption<P: consensus::Parameters>(
&self,
params: &P,
height: BlockHeight,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
ct: &[u8],
) -> Option<(Note, PaymentAddress)> {
try_sapling_compact_note_decryption(params, height, self, &epk, &cmu, &ct)
}
fn nf(&self, _note: &Note, _witness: &IncrementalWitness<Node>) {}
}
/// Scans a [`CompactBlock`] with a set of [`ScanningKeys`]s.
///
/// Returns a vector of [`WalletTx`]s belonging to any of the given
/// [`ViewingKey`]s.
/// [`ScanningKey`]s. If scanning with a full viewing key, the nullifiers
/// of the resulting [`WalletShieldedOutput`]s will also be computed.
///
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
/// incremented appropriately.
///
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
pub fn scan_block<P: consensus::Parameters>(
pub fn scan_block<P: consensus::Parameters, K: ScanningKey>(
params: &P,
block: CompactBlock,
vks: &[(AccountId, &ViewingKey)],
vks: &[(AccountId, &K)],
nullifiers: &[(AccountId, Nullifier)],
tree: &mut CommitmentTree<Node>,
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
) -> Vec<WalletTx> {
let mut wtxs: Vec<WalletTx> = vec![];
) -> Vec<WalletTx<K::Nf>> {
let mut wtxs: Vec<WalletTx<K::Nf>> = vec![];
let block_height = block.height();
for tx in block.vtx.into_iter() {
@ -145,7 +194,7 @@ pub fn scan_block<P: consensus::Parameters>(
shielded_spends.iter().map(|spend| spend.account).collect();
// Check for incoming notes while incrementing tree and witnesses
let mut shielded_outputs: Vec<WalletShieldedOutput> = vec![];
let mut shielded_outputs: Vec<WalletShieldedOutput<K::Nf>> = vec![];
{
// Grab mutable references to new witnesses from previous transactions
// in this block so that we can update them. Scoped so we don't hold
@ -211,7 +260,7 @@ mod tests {
constants::SPENDING_KEY_GENERATOR,
merkle_tree::CommitmentTree,
note_encryption::{Memo, SaplingNoteEncryption},
primitives::{Note, Nullifier},
primitives::{Note, Nullifier, SaplingIvk},
transaction::components::Amount,
util::generate_random_rseed,
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
@ -408,12 +457,13 @@ mod tests {
let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false);
assert_eq!(cb.vtx.len(), 2);
let vks: Vec<(AccountId, &SaplingIvk)> = vec![];
let mut tree = CommitmentTree::empty();
let txs = scan_block(
&Network::TestNetwork,
cb,
&[],
&vks[..],
&[(account, nf)],
&mut tree,
&mut [],

View File

@ -388,6 +388,7 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
}
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
#[allow(clippy::type_complexity)]
fn insert_pruned_block(
&mut self,
block: &PrunedBlock,

View File

@ -9,7 +9,7 @@ use zcash_primitives::{
consensus::{self, BlockHeight, NetworkUpgrade},
merkle_tree::{CommitmentTree, IncrementalWitness},
note_encryption::Memo,
primitives::{Nullifier, PaymentAddress},
primitives::{Note, Nullifier, PaymentAddress},
sapling::Node,
transaction::{components::Amount, Transaction, TxId},
zip32::ExtendedFullViewingKey,
@ -17,12 +17,12 @@ use zcash_primitives::{
use zcash_client_backend::{
address::RecipientAddress,
data_api::{error::Error, ShieldedOutput},
data_api::error::Error,
encoding::{
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
encode_payment_address,
},
wallet::{AccountId, WalletTx},
wallet::{AccountId, WalletShieldedOutput, WalletTx},
DecryptedOutput,
};
@ -31,6 +31,70 @@ use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDB};
pub mod init;
pub mod transact;
/// This trait provides a generalization over shielded output representations
/// that allows a wallet to avoid coupling to a specific one.
// TODO: it'd probably be better not to unify the definitions of
// `WalletShieldedOutput` and `DecryptedOutput` via a compositional
// approach, if possible.
pub trait ShieldedOutput {
fn index(&self) -> usize;
fn account(&self) -> AccountId;
fn to(&self) -> &PaymentAddress;
fn note(&self) -> &Note;
fn memo(&self) -> Option<&Memo>;
fn is_change(&self) -> Option<bool>;
fn nullifier(&self) -> Option<Nullifier>;
}
impl ShieldedOutput for WalletShieldedOutput<Nullifier> {
fn index(&self) -> usize {
self.index
}
fn account(&self) -> AccountId {
self.account
}
fn to(&self) -> &PaymentAddress {
&self.to
}
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
None
}
fn is_change(&self) -> Option<bool> {
Some(self.is_change)
}
fn nullifier(&self) -> Option<Nullifier> {
Some(self.nf)
}
}
impl ShieldedOutput for DecryptedOutput {
fn index(&self) -> usize {
self.index
}
fn account(&self) -> AccountId {
self.account
}
fn to(&self) -> &PaymentAddress {
&self.to
}
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
Some(&self.memo)
}
fn is_change(&self) -> Option<bool> {
None
}
fn nullifier(&self) -> Option<Nullifier> {
None
}
}
/// Returns the address for the account.
///
/// # Examples
@ -458,9 +522,9 @@ pub fn insert_block<'a, P>(
Ok(())
}
pub fn put_tx_meta<'a, P>(
pub fn put_tx_meta<'a, P, N>(
stmts: &mut DataConnStmtCache<'a, P>,
tx: &WalletTx,
tx: &WalletTx<N>,
height: BlockHeight,
) -> Result<i64, SqliteClientError> {
let txid = tx.txid.0.to_vec();