use ScanError::*;
use core::{
convert::TryFrom,
fmt::{self, Debug},
};
use std::{
collections::{HashMap, HashSet},
hash::Hash,
};
use incrementalmerkletree::{Marking, Position, Retention};
use sapling::{SaplingIvk, note_encryption::SaplingDomain};
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
use zcash_keys::keys::UnifiedFullViewingKey;
use zcash_note_encryption::{BatchDomain, Domain, ShieldedOutput};
use zcash_primitives::transaction::TxId;
use zcash_protocol::{
ShieldedPool,
consensus::{self, BlockHeight},
};
use zip32::Scope;
use crate::{
data_api::{BlockMetadata, NullifierQuery, ScannedBlock, WalletRead},
proto::compact_formats::CompactBlock,
scan::DecryptedOutput,
wallet::WalletOutput,
};
#[cfg(feature = "orchard")]
use orchard::note_encryption::OrchardDomain;
pub(crate) mod compact;
pub mod full;
pub trait ScanningKeyOps<D: Domain, AccountId, Nf> {
fn prepare(&self) -> D::IncomingViewingKey;
fn account_id(&self) -> &AccountId;
fn key_scope(&self) -> Option<Scope>;
fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf>;
}
impl<D: Domain, AccountId, Nf, K: ScanningKeyOps<D, AccountId, Nf>> ScanningKeyOps<D, AccountId, Nf>
for &K
{
fn prepare(&self) -> D::IncomingViewingKey {
(*self).prepare()
}
fn account_id(&self) -> &AccountId {
(*self).account_id()
}
fn key_scope(&self) -> Option<Scope> {
(*self).key_scope()
}
fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
(*self).nf(note, note_position)
}
}
impl<D: Domain, AccountId, Nf> ScanningKeyOps<D, AccountId, Nf>
for Box<dyn ScanningKeyOps<D, AccountId, Nf>>
{
fn prepare(&self) -> D::IncomingViewingKey {
self.as_ref().prepare()
}
fn account_id(&self) -> &AccountId {
self.as_ref().account_id()
}
fn key_scope(&self) -> Option<Scope> {
self.as_ref().key_scope()
}
fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
self.as_ref().nf(note, note_position)
}
}
impl<D: Domain, AccountId: Send + Sync, Nf> ScanningKeyOps<D, AccountId, Nf>
for Box<dyn ScanningKeyOps<D, AccountId, Nf> + Send + Sync>
{
fn prepare(&self) -> D::IncomingViewingKey {
self.as_ref().prepare()
}
fn account_id(&self) -> &AccountId {
self.as_ref().account_id()
}
fn key_scope(&self) -> Option<Scope> {
self.as_ref().key_scope()
}
fn nf(&self, note: &D::Note, note_position: Position) -> Option<Nf> {
self.as_ref().nf(note, note_position)
}
}
pub struct ScanningKey<Ivk, Nk, AccountId> {
ivk: Ivk,
nk: Option<Nk>,
account_id: AccountId,
key_scope: Option<Scope>,
}
impl<Ivk, Nk, AccountId> ScanningKey<Ivk, Nk, AccountId> {
pub fn new(ivk: Ivk, nk: Option<Nk>, account_id: AccountId, key_scope: Option<Scope>) -> Self {
Self {
ivk,
nk,
account_id,
key_scope,
}
}
}
impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
for ScanningKey<sapling::SaplingIvk, sapling::NullifierDerivingKey, AccountId>
{
fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
sapling::note_encryption::PreparedIncomingViewingKey::new(&self.ivk)
}
fn nf(&self, note: &sapling::Note, position: Position) -> Option<sapling::Nullifier> {
self.nk.as_ref().map(|key| note.nf(key, position.into()))
}
fn account_id(&self) -> &AccountId {
&self.account_id
}
fn key_scope(&self) -> Option<Scope> {
self.key_scope
}
}
impl<AccountId> ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier>
for (AccountId, SaplingIvk)
{
fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey {
sapling::note_encryption::PreparedIncomingViewingKey::new(&self.1)
}
fn nf(&self, _note: &sapling::Note, _position: Position) -> Option<sapling::Nullifier> {
None
}
fn account_id(&self) -> &AccountId {
&self.0
}
fn key_scope(&self) -> Option<Scope> {
None
}
}
#[cfg(feature = "orchard")]
impl<AccountId> ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
{
fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
}
fn nf(
&self,
note: &orchard::note::Note,
_position: Position,
) -> Option<orchard::note::Nullifier> {
self.nk.as_ref().map(|key| note.nullifier(key))
}
fn account_id(&self) -> &AccountId {
&self.account_id
}
fn key_scope(&self) -> Option<Scope> {
self.key_scope
}
}
#[cfg(feature = "orchard")]
pub(crate) type IronwoodDomain = orchard::note_encryption::IronwoodDomain;
#[cfg(feature = "orchard")]
pub(crate) type IronwoodNullifier = orchard::note::Nullifier;
#[cfg(feature = "orchard")]
impl<AccountId> ScanningKeyOps<IronwoodDomain, AccountId, orchard::note::Nullifier>
for ScanningKey<orchard::keys::IncomingViewingKey, orchard::keys::FullViewingKey, AccountId>
{
fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey {
orchard::keys::PreparedIncomingViewingKey::new(&self.ivk)
}
fn nf(
&self,
note: &orchard::note::Note,
_position: Position,
) -> Option<orchard::note::Nullifier> {
self.nk.as_ref().map(|key| note.nullifier(key))
}
fn account_id(&self) -> &AccountId {
&self.account_id
}
fn key_scope(&self) -> Option<Scope> {
self.key_scope
}
}
pub struct ScanningKeys<AccountId, IvkTag> {
sapling: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
>,
#[cfg(feature = "orchard")]
orchard: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
>,
#[cfg(feature = "orchard")]
ironwood: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
>,
}
impl<AccountId, IvkTag> ScanningKeys<AccountId, IvkTag> {
pub fn new(
sapling: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
>,
#[cfg(feature = "orchard")] orchard: HashMap<
IvkTag,
Box<
dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
+ Send
+ Sync,
>,
>,
#[cfg(feature = "orchard")] ironwood: HashMap<
IvkTag,
Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
>,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
pub fn empty() -> Self {
Self {
sapling: HashMap::new(),
#[cfg(feature = "orchard")]
orchard: HashMap::new(),
#[cfg(feature = "orchard")]
ironwood: HashMap::new(),
}
}
pub fn sapling(
&self,
) -> &HashMap<
IvkTag,
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
> {
&self.sapling
}
#[cfg(feature = "orchard")]
pub fn orchard(
&self,
) -> &HashMap<
IvkTag,
Box<dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier> + Send + Sync>,
> {
&self.orchard
}
#[cfg(feature = "orchard")]
pub fn ironwood(
&self,
) -> &HashMap<
IvkTag,
Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
> {
&self.ironwood
}
}
impl<AccountId: Copy + Eq + Hash + Send + Sync + 'static>
ScanningKeys<AccountId, (AccountId, Scope)>
{
pub fn from_account_ufvks(
ufvks: impl IntoIterator<Item = (AccountId, UnifiedFullViewingKey)>,
) -> Self {
#![allow(clippy::type_complexity)]
let mut sapling: HashMap<
(AccountId, Scope),
Box<dyn ScanningKeyOps<SaplingDomain, AccountId, sapling::Nullifier> + Send + Sync>,
> = HashMap::new();
#[cfg(feature = "orchard")]
let mut orchard: HashMap<
(AccountId, Scope),
Box<
dyn ScanningKeyOps<OrchardDomain, AccountId, orchard::note::Nullifier>
+ Send
+ Sync,
>,
> = HashMap::new();
#[cfg(feature = "orchard")]
let mut ironwood: HashMap<
(AccountId, Scope),
Box<dyn ScanningKeyOps<IronwoodDomain, AccountId, IronwoodNullifier> + Send + Sync>,
> = HashMap::new();
for (account_id, ufvk) in ufvks {
if let Some(dfvk) = ufvk.sapling() {
for scope in [Scope::External, Scope::Internal] {
sapling.insert(
(account_id, scope),
Box::new(ScanningKey {
ivk: dfvk.to_ivk(scope),
nk: Some(dfvk.to_nk(scope)),
account_id,
key_scope: Some(scope),
}),
);
}
}
#[cfg(feature = "orchard")]
if let Some(fvk) = ufvk.orchard() {
for scope in [Scope::External, Scope::Internal] {
orchard.insert(
(account_id, scope),
Box::new(ScanningKey {
ivk: fvk.to_ivk(scope),
nk: Some(fvk.clone()),
account_id,
key_scope: Some(scope),
}),
);
}
for scope in [Scope::External, Scope::Internal] {
ironwood.insert(
(account_id, scope),
Box::new(ScanningKey {
ivk: fvk.to_ivk(scope),
nk: Some(fvk.clone()),
account_id,
key_scope: Some(scope),
}),
);
}
}
}
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
}
pub struct Nullifiers<AccountId> {
sapling: Vec<(AccountId, sapling::Nullifier)>,
#[cfg(feature = "orchard")]
orchard: Vec<(AccountId, orchard::note::Nullifier)>,
#[cfg(feature = "orchard")]
ironwood: Vec<(AccountId, IronwoodNullifier)>,
}
impl<AccountId> Nullifiers<AccountId> {
pub fn empty() -> Self {
Self {
sapling: vec![],
#[cfg(feature = "orchard")]
orchard: vec![],
#[cfg(feature = "orchard")]
ironwood: vec![],
}
}
pub fn unspent<DbT: WalletRead<AccountId = AccountId>>(
db_data: &DbT,
) -> Result<Self, DbT::Error> {
Ok(Self::new(
db_data.get_sapling_nullifiers(NullifierQuery::Unspent)?,
#[cfg(feature = "orchard")]
db_data.get_orchard_nullifiers(NullifierQuery::Unspent)?,
#[cfg(feature = "orchard")]
db_data.get_ironwood_nullifiers(NullifierQuery::Unspent)?,
))
}
pub(crate) fn new(
sapling: Vec<(AccountId, sapling::Nullifier)>,
#[cfg(feature = "orchard")] orchard: Vec<(AccountId, orchard::note::Nullifier)>,
#[cfg(feature = "orchard")] ironwood: Vec<(AccountId, IronwoodNullifier)>,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
pub fn sapling(&self) -> &[(AccountId, sapling::Nullifier)] {
self.sapling.as_ref()
}
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> &[(AccountId, orchard::note::Nullifier)] {
self.orchard.as_ref()
}
#[cfg(feature = "orchard")]
pub fn ironwood(&self) -> &[(AccountId, IronwoodNullifier)] {
self.ironwood.as_ref()
}
pub(crate) fn retain_sapling(&mut self, f: impl Fn(&(AccountId, sapling::Nullifier)) -> bool) {
self.sapling.retain(f);
}
pub(crate) fn extend_sapling(
&mut self,
nfs: impl IntoIterator<Item = (AccountId, sapling::Nullifier)>,
) {
self.sapling.extend(nfs);
}
#[cfg(feature = "orchard")]
pub(crate) fn retain_orchard(
&mut self,
f: impl Fn(&(AccountId, orchard::note::Nullifier)) -> bool,
) {
self.orchard.retain(f);
}
#[cfg(feature = "orchard")]
pub(crate) fn extend_orchard(
&mut self,
nfs: impl IntoIterator<Item = (AccountId, orchard::note::Nullifier)>,
) {
self.orchard.extend(nfs);
}
#[cfg(feature = "orchard")]
pub(crate) fn retain_ironwood(&mut self, f: impl Fn(&(AccountId, IronwoodNullifier)) -> bool) {
self.ironwood.retain(f);
}
#[cfg(feature = "orchard")]
pub(crate) fn extend_ironwood(
&mut self,
nfs: impl IntoIterator<Item = (AccountId, IronwoodNullifier)>,
) {
self.ironwood.extend(nfs);
}
}
impl<AccountId: Copy> Nullifiers<AccountId> {
pub fn update_with(&mut self, scanned_block: &ScannedBlock<AccountId>) {
let sapling_spent_nf: Vec<&sapling::Nullifier> = scanned_block
.transactions()
.iter()
.flat_map(|tx| tx.sapling_spends().iter().map(|spend| spend.nf()))
.collect();
self.retain_sapling(|(_, nf)| !sapling_spent_nf.contains(&nf));
self.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")]
{
let orchard_spent_nf: Vec<&orchard::note::Nullifier> = scanned_block
.transactions()
.iter()
.flat_map(|tx| tx.orchard_spends().iter().map(|spend| spend.nf()))
.collect();
self.retain_orchard(|(_, nf)| !orchard_spent_nf.contains(&nf));
self.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)))
}));
let ironwood_spent_nf: Vec<&IronwoodNullifier> = scanned_block
.transactions()
.iter()
.flat_map(|tx| tx.ironwood_spends().iter().map(|spend| spend.nf()))
.collect();
self.retain_ironwood(|(_, nf)| !ironwood_spent_nf.contains(&nf));
self.extend_ironwood(scanned_block.transactions().iter().flat_map(|tx| {
tx.ironwood_outputs()
.iter()
.flat_map(|out| out.nf().into_iter().map(|nf| (*out.account_id(), *nf)))
}));
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ScanError {
EncodingInvalid {
at_height: BlockHeight,
txid: TxId,
pool_type: ShieldedPool,
index: usize,
},
PrevHashMismatch { at_height: BlockHeight },
BlockHeightDiscontinuity {
prev_height: BlockHeight,
new_height: BlockHeight,
},
TreeSizeMismatch {
protocol: ShieldedPool,
at_height: BlockHeight,
given: u32,
computed: u32,
},
TreeSizeUnknown {
protocol: ShieldedPool,
at_height: BlockHeight,
},
TreeSizeInvalid {
protocol: ShieldedPool,
at_height: BlockHeight,
},
TreeSizeOverflow {
protocol: ShieldedPool,
at_height: BlockHeight,
},
}
impl ScanError {
pub fn is_continuity_error(&self) -> bool {
match self {
EncodingInvalid { .. } => false,
PrevHashMismatch { .. } => true,
BlockHeightDiscontinuity { .. } => true,
TreeSizeMismatch { .. } => true,
TreeSizeUnknown { .. } => false,
TreeSizeInvalid { .. } => false,
TreeSizeOverflow { .. } => false,
}
}
pub fn at_height(&self) -> BlockHeight {
match self {
EncodingInvalid { at_height, .. } => *at_height,
PrevHashMismatch { at_height } => *at_height,
BlockHeightDiscontinuity { new_height, .. } => *new_height,
TreeSizeMismatch { at_height, .. } => *at_height,
TreeSizeUnknown { at_height, .. } => *at_height,
TreeSizeInvalid { at_height, .. } => *at_height,
TreeSizeOverflow { at_height, .. } => *at_height,
}
}
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
EncodingInvalid {
txid,
pool_type,
index,
..
} => write!(
f,
"{pool_type:?} output {index} of transaction {txid} was improperly encoded."
),
PrevHashMismatch { at_height } => write!(
f,
"The parent hash of proposed block does not correspond to the block hash at height {at_height}."
),
BlockHeightDiscontinuity {
prev_height,
new_height,
} => {
write!(
f,
"Block height discontinuity at height {new_height}; previous height was: {prev_height}"
)
}
TreeSizeMismatch {
protocol,
at_height,
given,
computed,
} => {
write!(
f,
"The {protocol:?} note commitment tree size provided by a compact block did not match the expected size at height {at_height}; given {given}, expected {computed}"
)
}
TreeSizeUnknown {
protocol,
at_height,
} => {
write!(
f,
"Unable to determine {protocol:?} note commitment tree size at height {at_height}"
)
}
TreeSizeInvalid {
protocol,
at_height,
} => {
write!(
f,
"Received invalid (potentially default) {protocol:?} note commitment tree size metadata at height {at_height}"
)
}
TreeSizeOverflow {
protocol,
at_height,
} => {
write!(
f,
"The {protocol:?} note commitment tree size at height {at_height} would exceed the `u32` range."
)
}
}
}
}
impl core::error::Error for ScanError {}
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
P: consensus::Parameters + Send + 'static,
AccountId: Default + Eq + Hash + ConditionallySelectable + Send + Sync + 'static,
IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
{
compact::scan_block_with_runners::<_, _, _, (), (), ()>(
params,
block,
scanning_keys,
nullifiers,
prior_block_metadata,
None,
)
}
struct PositionTracker {
sapling_tree_position: u32,
sapling_final_tree_size: u32,
#[cfg(feature = "orchard")]
orchard_tree_position: u32,
#[cfg(feature = "orchard")]
orchard_final_tree_size: u32,
#[cfg(feature = "orchard")]
ironwood_tree_position: u32,
#[cfg(feature = "orchard")]
ironwood_final_tree_size: u32,
}
impl PositionTracker {
fn sapling_note_position(&self, output_idx: usize) -> Position {
Position::from(u64::from(
self.sapling_tree_position + u32::try_from(output_idx).unwrap(),
))
}
#[cfg(feature = "orchard")]
fn orchard_note_position(&self, output_idx: usize) -> Position {
Position::from(u64::from(
self.orchard_tree_position + u32::try_from(output_idx).unwrap(),
))
}
#[cfg(feature = "orchard")]
fn ironwood_note_position(&self, output_idx: usize) -> Position {
Position::from(u64::from(
self.ironwood_tree_position + u32::try_from(output_idx).unwrap(),
))
}
}
fn find_spent<'a, I, AccountId, Spend, Nf, WS>(
spends: I,
nullifiers: &[(AccountId, Nf)],
extract_nf: impl Fn(&Spend) -> Nf,
construct_wallet_spend: impl Fn(usize, Nf, AccountId) -> WS,
) -> (Vec<WS>, Vec<Nf>)
where
I: IntoIterator<Item = &'a Spend>,
I::IntoIter: ExactSizeIterator,
Spend: 'a,
AccountId: ConditionallySelectable + Default,
Nf: ConstantTimeEq + Copy,
{
let spends = spends.into_iter();
let mut found_spent = vec![];
let mut unlinked_nullifiers = Vec::with_capacity(spends.len());
for (index, spend) in spends.enumerate() {
let spend_nf = extract_nf(spend);
let ct_spend = nullifiers
.iter()
.map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
.fold(
CtOption::new(AccountId::default(), 0.into()),
|first, next| CtOption::conditional_select(&next, &first, first.is_some()),
)
.map(|account| construct_wallet_spend(index, spend_nf, account));
if let Some(spend) = ct_spend.into() {
found_spent.push(spend);
} else {
unlinked_nullifiers.push(spend_nf);
}
}
(found_spent, unlinked_nullifiers)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn find_received<
AccountId: Copy + Eq + Hash,
D: BatchDomain,
M,
Nf,
IvkTag: Copy + std::hash::Hash + Eq + Send + 'static,
SK: ScanningKeyOps<D, AccountId, Nf>,
Output: ShieldedOutput<D, CIPHERTEXT_SIZE>,
NoteCommitment,
Note,
const CIPHERTEXT_SIZE: usize,
>(
block_height: BlockHeight,
last_commitments_in_block: bool,
txid: TxId,
note_position: impl Fn(usize) -> Position,
keys: &HashMap<IvkTag, SK>,
spent_from_accounts: &HashSet<AccountId>,
decoded: &[(D, Output)],
batch_results: Option<impl FnOnce(TxId) -> HashMap<usize, DecryptedOutput<IvkTag, D, M>>>,
decrypt_inline: impl FnOnce(
&[D::IncomingViewingKey],
&[(D, Output)],
) -> Vec<Option<((D::Note, D::Recipient, M), usize)>>,
extract_note_commitment: impl Fn(&Output) -> NoteCommitment,
enrich_note: impl Fn(D::Note) -> Note,
) -> (
Vec<WalletOutput<Note, Nf, AccountId>>,
Vec<(NoteCommitment, Retention<BlockHeight>)>,
) {
let (decrypted_opts, decrypted_len) = if let Some(collect_results) = batch_results {
let mut decrypted = collect_results(txid);
let decrypted_len = decrypted.len();
(
(0..decoded.len())
.map(|i| {
decrypted
.remove(&i)
.map(|d_out| (d_out.ivk_tag, d_out.note))
})
.collect::<Vec<_>>(),
decrypted_len,
)
} else {
let mut ivks = Vec::with_capacity(keys.len());
let mut ivk_lookup = Vec::with_capacity(keys.len());
for (key_id, key) in keys.iter() {
ivks.push(key.prepare());
ivk_lookup.push(key_id);
}
let mut decrypted_len = 0;
(
decrypt_inline(&ivks, decoded)
.into_iter()
.map(|v| {
v.map(|((note, _, _), ivk_idx)| {
decrypted_len += 1;
(*ivk_lookup[ivk_idx], note)
})
})
.collect::<Vec<_>>(),
decrypted_len,
)
};
let mut shielded_outputs = Vec::with_capacity(decrypted_len);
let mut note_commitments = Vec::with_capacity(decoded.len());
for (output_idx, ((_, output), decrypted_note)) in
decoded.iter().zip(decrypted_opts).enumerate()
{
let node = extract_note_commitment(output);
let is_checkpoint = output_idx + 1 == decoded.len() && last_commitments_in_block;
let retention = match (decrypted_note.is_some(), is_checkpoint) {
(is_marked, true) => Retention::Checkpoint {
id: block_height,
marking: if is_marked {
Marking::Marked
} else {
Marking::None
},
},
(true, false) => Retention::Marked,
(false, false) => Retention::Ephemeral,
};
if let Some((key_id, note)) = decrypted_note {
let key = keys
.get(&key_id)
.expect("Key is available for decrypted output");
let is_change = spent_from_accounts.contains(key.account_id());
let note_commitment_tree_position = note_position(output_idx);
let nf = key.nf(¬e, note_commitment_tree_position);
shielded_outputs.push(WalletOutput::from_parts(
output_idx,
output.ephemeral_key(),
enrich_note(note),
is_change,
note_commitment_tree_position,
nf,
*key.account_id(),
key.key_scope(),
));
}
note_commitments.push((node, retention))
}
(shielded_outputs, note_commitments)
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use group::{
GroupEncoding,
ff::{Field, PrimeField},
};
use rand::{Rng, rand_core::UnwrapErr, rngs::SysRng};
use sapling::{
Nullifier,
constants::SPENDING_KEY_GENERATOR,
note_encryption::{SaplingDomain, sapling_note_encryption},
util::generate_random_rseed,
value::NoteValue,
zip32::DiversifiableFullViewingKey,
};
#[allow(non_upper_case_globals)]
const OsRng: UnwrapErr<SysRng> = UnwrapErr(SysRng);
use zcash_note_encryption::{COMPACT_NOTE_SIZE, Domain};
use zcash_primitives::{
block::BlockHash, transaction::components::sapling::zip212_enforcement,
};
use zcash_protocol::{
consensus::{BlockHeight, Network},
memo::MemoBytes,
value::Zatoshis,
};
use crate::proto::compact_formats::{
self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
};
fn random_compact_tx(mut rng: impl Rng) -> CompactTx {
let fake_nf = {
let mut nf = vec![0; 32];
rng.fill_bytes(&mut nf);
nf
};
let fake_cmu = {
let fake_cmu = bls12_381::Scalar::random(&mut rng);
fake_cmu.to_repr().to_vec()
};
let fake_epk = {
let mut buffer = [0; 64];
rng.fill_bytes(&mut buffer);
let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
fake_epk.to_bytes().to_vec()
};
let cspend = CompactSaplingSpend { nf: fake_nf };
let cout = CompactSaplingOutput {
cmu: fake_cmu,
ephemeral_key: fake_epk,
ciphertext: vec![0; COMPACT_NOTE_SIZE],
};
let mut ctx = CompactTx::default();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.txid = txid;
ctx.spends.push(cspend);
ctx.outputs.push(cout);
ctx
}
pub fn fake_compact_block(
height: BlockHeight,
prev_hash: BlockHash,
nf: Nullifier,
dfvk: &DiversifiableFullViewingKey,
value: Zatoshis,
tx_after: bool,
initial_tree_sizes: Option<(u32, u32)>,
) -> CompactBlock {
let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height);
let to = dfvk.default_address().1;
let mut rng = OsRng;
let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
let encryptor = sapling_note_encryption(
Some(dfvk.fvk().ovk),
note.clone(),
MemoBytes::empty().into_bytes(),
&mut rng,
);
let cmu = note.cmu().to_bytes().to_vec();
let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let mut cb = CompactBlock {
hash: {
let mut hash = vec![0; 32];
rng.fill_bytes(&mut hash);
hash
},
prev_hash: prev_hash.0.to_vec(),
height: height.into(),
..Default::default()
};
{
let mut tx = random_compact_tx(&mut rng);
tx.index = cb.vtx.len() as u64;
cb.vtx.push(tx);
}
let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
let cout = CompactSaplingOutput {
cmu,
ephemeral_key,
ciphertext: enc_ciphertext[..52].to_vec(),
};
let mut ctx = CompactTx::default();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.txid = txid;
ctx.spends.push(cspend);
ctx.outputs.push(cout);
ctx.index = cb.vtx.len() as u64;
cb.vtx.push(ctx);
if tx_after {
let mut tx = random_compact_tx(&mut rng);
tx.index = cb.vtx.len() as u64;
cb.vtx.push(tx);
}
cb.chain_metadata =
initial_tree_sizes.map(|(initial_sapling_tree_size, initial_orchard_tree_size)| {
compact::ChainMetadata {
sapling_commitment_tree_size: initial_sapling_tree_size
+ cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
orchard_commitment_tree_size: initial_orchard_tree_size
+ cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
ironwood_commitment_tree_size: 0,
}
});
cb
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, thread::spawn};
use super::ScanningKeys;
#[test]
fn arc_scanning_keys() {
let keys = Arc::new(ScanningKeys::<(), ()>::empty());
spawn(move || {
let _ = keys.sapling().get(&());
});
}
}