librustzcash/zcash_client_backend/src/scanning.rs

1031 lines
38 KiB
Rust

//! Tools for scanning a compact representation of the Zcash block chain.
use std::collections::{HashMap, HashSet};
use std::convert::TryFrom;
use std::fmt::{self, Debug};
use incrementalmerkletree::{Position, Retention};
use sapling::{
note_encryption::{CompactOutputDescription, PreparedIncomingViewingKey, SaplingDomain},
zip32::DiversifiableFullViewingKey,
SaplingIvk,
};
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
use zcash_note_encryption::batch;
use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade};
use zcash_primitives::{
consensus,
zip32::{AccountId, Scope},
};
use crate::data_api::{BlockMetadata, ScannedBlock, ScannedBundles};
use crate::{
proto::compact_formats::CompactBlock,
scan::{Batch, BatchRunner, Tasks},
wallet::{WalletSaplingOutput, WalletSaplingSpend, WalletTx},
ShieldedProtocol,
};
/// A key that can be used to perform trial decryption and nullifier
/// computation for a Sapling [`CompactSaplingOutput`]
///
/// The purpose of this trait is to enable [`scan_block`]
/// and related methods to be used with either incoming viewing keys
/// or full viewing keys, with the data returned from trial decryption
/// being dependent upon the type of key used. In the case that an
/// incoming viewing key is used, only the note and payment address
/// will be returned; in the case of a full viewing key, the
/// nullifier for the note can also be obtained.
///
/// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput
/// [`scan_block`]: crate::scanning::scan_block
pub trait ScanningKey {
/// The type representing the scope of the scanning key.
type Scope: Clone + Eq + std::hash::Hash + Send + 'static;
/// The type of key that is used to decrypt Sapling outputs;
type SaplingNk: Clone;
type SaplingKeys: IntoIterator<Item = (Self::Scope, SaplingIvk, Self::SaplingNk)>;
/// The type of nullifier extracted when a note is successfully
/// obtained by trial decryption.
type Nf;
/// Obtain the underlying Sapling incoming viewing key(s) for this scanning key.
fn to_sapling_keys(&self) -> Self::SaplingKeys;
/// 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 sapling_nf(key: &Self::SaplingNk, note: &sapling::Note, note_position: Position)
-> Self::Nf;
}
impl<K: ScanningKey> ScanningKey for &K {
type Scope = K::Scope;
type SaplingNk = K::SaplingNk;
type SaplingKeys = K::SaplingKeys;
type Nf = K::Nf;
fn to_sapling_keys(&self) -> Self::SaplingKeys {
(*self).to_sapling_keys()
}
fn sapling_nf(key: &Self::SaplingNk, note: &sapling::Note, position: Position) -> Self::Nf {
K::sapling_nf(key, note, position)
}
}
impl ScanningKey for DiversifiableFullViewingKey {
type Scope = Scope;
type SaplingNk = sapling::NullifierDerivingKey;
type SaplingKeys = [(Self::Scope, SaplingIvk, Self::SaplingNk); 2];
type Nf = sapling::Nullifier;
fn to_sapling_keys(&self) -> Self::SaplingKeys {
[
(
Scope::External,
self.to_ivk(Scope::External),
self.to_nk(Scope::External),
),
(
Scope::Internal,
self.to_ivk(Scope::Internal),
self.to_nk(Scope::Internal),
),
]
}
fn sapling_nf(key: &Self::SaplingNk, note: &sapling::Note, position: Position) -> Self::Nf {
note.nf(key, position.into())
}
}
impl ScanningKey for (Scope, SaplingIvk, sapling::NullifierDerivingKey) {
type Scope = Scope;
type SaplingNk = sapling::NullifierDerivingKey;
type SaplingKeys = [(Self::Scope, SaplingIvk, Self::SaplingNk); 1];
type Nf = sapling::Nullifier;
fn to_sapling_keys(&self) -> Self::SaplingKeys {
[self.clone()]
}
fn sapling_nf(key: &Self::SaplingNk, note: &sapling::Note, position: Position) -> Self::Nf {
note.nf(key, position.into())
}
}
/// The [`ScanningKey`] implementation for [`SaplingIvk`]s.
/// Nullifiers cannot be derived when scanning with these keys.
///
/// [`SaplingIvk`]: sapling::SaplingIvk
impl ScanningKey for SaplingIvk {
type Scope = ();
type SaplingNk = ();
type SaplingKeys = [(Self::Scope, SaplingIvk, Self::SaplingNk); 1];
type Nf = ();
fn to_sapling_keys(&self) -> Self::SaplingKeys {
[((), self.clone(), ())]
}
fn sapling_nf(_key: &Self::SaplingNk, _note: &sapling::Note, _position: Position) {}
}
/// Errors that may occur in chain scanning
#[derive(Clone, Debug)]
pub enum ScanError {
/// The hash of the parent block given by a proposed new chain tip does not match the hash of
/// the current chain tip.
PrevHashMismatch { at_height: BlockHeight },
/// The block height field of the proposed new block is not equal to the height of the previous
/// block + 1.
BlockHeightDiscontinuity {
prev_height: BlockHeight,
new_height: BlockHeight,
},
/// The note commitment tree size for the given protocol at the proposed new block is not equal
/// to the size at the previous block plus the count of this block's outputs.
TreeSizeMismatch {
protocol: ShieldedProtocol,
at_height: BlockHeight,
given: u32,
computed: u32,
},
/// The size of the note commitment tree for the given protocol was not provided as part of a
/// [`CompactBlock`] being scanned, making it impossible to construct the nullifier for a
/// detected note.
TreeSizeUnknown {
protocol: ShieldedProtocol,
at_height: BlockHeight,
},
/// We were provided chain metadata for a block containing note commitment tree metadata
/// that is invalidated by the data in the block itself. This may be caused by the presence
/// of default values in the chain metadata.
TreeSizeInvalid {
protocol: ShieldedProtocol,
at_height: BlockHeight,
},
}
impl ScanError {
/// Returns whether this error is the result of a failed continuity check
pub fn is_continuity_error(&self) -> bool {
use ScanError::*;
match self {
PrevHashMismatch { .. } => true,
BlockHeightDiscontinuity { .. } => true,
TreeSizeMismatch { .. } => true,
TreeSizeUnknown { .. } => false,
TreeSizeInvalid { .. } => false,
}
}
/// Returns the block height at which the scan error occurred
pub fn at_height(&self) -> BlockHeight {
use ScanError::*;
match self {
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,
}
}
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ScanError::*;
match &self {
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 {}; previous height was: {}", new_height, prev_height)
}
TreeSizeMismatch { protocol, at_height, given, computed } => {
write!(f, "The {:?} note commitment tree size provided by a compact block did not match the expected size at height {}; given {}, expected {}", protocol, at_height, given, computed)
}
TreeSizeUnknown { protocol, at_height } => {
write!(f, "Unable to determine {:?} note commitment tree size at height {}", protocol, at_height)
}
TreeSizeInvalid { protocol, at_height } => {
write!(f, "Received invalid (potentially default) {:?} note commitment tree size metadata at height {}", protocol, at_height)
}
}
}
}
/// Scans a [`CompactBlock`] with a set of [`ScanningKey`]s.
///
/// Returns a vector of [`WalletTx`]s belonging to any of the given
/// [`ScanningKey`]s. If scanning with a full viewing key, the nullifiers
/// of the resulting [`WalletSaplingOutput`]s will also be computed.
///
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
/// incremented appropriately.
///
/// The implementation of [`ScanningKey`] may either support or omit the computation of
/// the nullifiers for received notes; the implementation for [`ExtendedFullViewingKey`]
/// will derive the nullifiers for received notes and return them as part of the resulting
/// [`WalletSaplingOutput`]s, whereas the implementation for [`SaplingIvk`] cannot
/// do so and will return the unit value in those outputs instead.
///
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey
/// [`SaplingIvk`]: sapling::SaplingIvk
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
/// [`ScanningKey`]: crate::scanning::ScanningKey
/// [`CommitmentTree`]: sapling::CommitmentTree
/// [`IncrementalWitness`]: sapling::IncrementalWitness
/// [`WalletSaplingOutput`]: crate::wallet::WalletSaplingOutput
/// [`WalletTx`]: crate::wallet::WalletTx
pub fn scan_block<P: consensus::Parameters + Send + 'static, K: ScanningKey>(
params: &P,
block: CompactBlock,
vks: &[(&AccountId, &K)],
sapling_nullifiers: &[(AccountId, sapling::Nullifier)],
prior_block_metadata: Option<&BlockMetadata>,
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
scan_block_with_runner::<_, _, ()>(
params,
block,
vks,
sapling_nullifiers,
prior_block_metadata,
None,
)
}
type TaggedBatch<S> = Batch<(AccountId, S), SaplingDomain, CompactOutputDescription>;
type TaggedBatchRunner<S, T> =
BatchRunner<(AccountId, S), SaplingDomain, CompactOutputDescription, T>;
#[tracing::instrument(skip_all, fields(height = block.height))]
pub(crate) fn add_block_to_runner<P, S, T>(
params: &P,
block: CompactBlock,
batch_runner: &mut TaggedBatchRunner<S, T>,
) where
P: consensus::Parameters + Send + 'static,
S: Clone + Send + 'static,
T: Tasks<TaggedBatch<S>>,
{
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
.into_iter()
.map(|output| {
CompactOutputDescription::try_from(output)
.expect("Invalid output found in compact block decoding.")
})
.collect::<Vec<_>>();
batch_runner.add_outputs(
block_hash,
txid,
|| SaplingDomain::new(zip212_enforcement),
&outputs,
)
}
}
fn check_hash_continuity(
block: &CompactBlock,
prior_block_metadata: Option<&BlockMetadata>,
) -> Option<ScanError> {
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
}
#[tracing::instrument(skip_all, fields(height = block.height))]
pub(crate) fn scan_block_with_runner<
P: consensus::Parameters + Send + 'static,
K: ScanningKey,
T: Tasks<TaggedBatch<K::Scope>> + Sync,
>(
params: &P,
block: CompactBlock,
vks: &[(&AccountId, K)],
nullifiers: &[(AccountId, sapling::Nullifier)],
prior_block_metadata: Option<&BlockMetadata>,
mut batch_runner: Option<&mut TaggedBatchRunner<K::Scope, T>>,
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) {
return Err(scan_error);
}
let cur_height = block.height();
let cur_hash = block.hash();
let zip212_enforcement = consensus::sapling_zip212_enforcement(params, cur_height);
let mut sapling_commitment_tree_size = prior_block_metadata
.and_then(|m| m.sapling_tree_size())
.map_or_else(
|| {
block.chain_metadata.as_ref().map_or_else(
|| {
// If we're below Sapling activation, or Sapling activation is not set, the tree size is zero
params
.activation_height(NetworkUpgrade::Sapling)
.map_or_else(
|| Ok(0),
|sapling_activation| {
if cur_height < sapling_activation {
Ok(0)
} else {
Err(ScanError::TreeSizeUnknown {
protocol: ShieldedProtocol::Sapling,
at_height: cur_height,
})
}
},
)
},
|m| {
let sapling_output_count: u32 = block
.vtx
.iter()
.map(|tx| tx.outputs.len())
.sum::<usize>()
.try_into()
.expect("Sapling output count cannot exceed a u32");
// The default for m.sapling_commitment_tree_size is zero, so we need to check
// that the subtraction will not underflow; if it would do so, we were given
// invalid chain metadata for a block with Sapling outputs.
m.sapling_commitment_tree_size
.checked_sub(sapling_output_count)
.ok_or(ScanError::TreeSizeInvalid {
protocol: ShieldedProtocol::Sapling,
at_height: cur_height,
})
},
)
},
Ok,
)?;
#[cfg(feature = "orchard")]
let mut orchard_commitment_tree_size = prior_block_metadata
.and_then(|m| m.orchard_tree_size())
.map_or_else(
|| {
block.chain_metadata.as_ref().map_or_else(
|| {
// If we're below Orchard activation, or Orchard activation is not set, the tree size is zero
params.activation_height(NetworkUpgrade::Nu5).map_or_else(
|| Ok(0),
|orchard_activation| {
if cur_height < orchard_activation {
Ok(0)
} else {
Err(ScanError::TreeSizeUnknown {
protocol: ShieldedProtocol::Orchard,
at_height: cur_height,
})
}
},
)
},
|m| {
let orchard_action_count: u32 = block
.vtx
.iter()
.map(|tx| tx.actions.len())
.sum::<usize>()
.try_into()
.expect("Orchard action count cannot exceed a u32");
// The default for m.orchard_commitment_tree_size is zero, so we need to check
// that the subtraction will not underflow; if it would do so, we were given
// invalid chain metadata for a block with Orchard actions.
m.orchard_commitment_tree_size
.checked_sub(orchard_action_count)
.ok_or(ScanError::TreeSizeInvalid {
protocol: ShieldedProtocol::Orchard,
at_height: cur_height,
})
},
)
},
Ok,
)?;
let compact_block_tx_count = block.vtx.len();
let mut wtxs: Vec<WalletTx<K::Nf, K::Scope>> = vec![];
let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len());
let mut sapling_note_commitments: Vec<(sapling::Node, Retention<BlockHeight>)> = vec![];
for (tx_idx, tx) in block.vtx.into_iter().enumerate() {
let txid = tx.txid();
let tx_index =
u16::try_from(tx.index).expect("Cannot fit more than 2^16 transactions in a block");
// Check for spent notes. The comparison against known-unspent nullifiers is done
// in constant time.
// TODO: However, this is O(|nullifiers| * |notes|); does using
// constant-time operations here really make sense?
let mut shielded_spends = vec![];
let mut sapling_unlinked_nullifiers = Vec::with_capacity(tx.spends.len());
for (index, spend) in tx.spends.into_iter().enumerate() {
let spend_nf = spend
.nf()
.expect("Could not deserialize nullifier for spend from protobuf representation.");
// Find the first tracked nullifier that matches this spend, and produce
// a WalletShieldedSpend if there is a match, in constant time.
let spend = nullifiers
.iter()
.map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
.fold(CtOption::new(AccountId::ZERO, 0.into()), |first, next| {
CtOption::conditional_select(&next, &first, first.is_some())
})
.map(|account| WalletSaplingSpend::from_parts(index, spend_nf, account));
if spend.is_some().into() {
shielded_spends.push(spend.unwrap());
} else {
// This nullifier didn't match any we are currently tracking; save it in
// case it matches an earlier block range we haven't scanned yet.
sapling_unlinked_nullifiers.push(spend_nf);
}
}
sapling_nullifier_map.push((txid, tx_index, sapling_unlinked_nullifiers));
// Collect the set of accounts that were spent from in this transaction
let spent_from_accounts: HashSet<_> = shielded_spends
.iter()
.map(|spend| spend.account())
.collect();
// We keep track of the number of outputs and actions here because tx.outputs
// and tx.actions end up being moved.
let tx_outputs_len =
u32::try_from(tx.outputs.len()).expect("Sapling output count cannot exceed a u32");
#[cfg(feature = "orchard")]
let tx_actions_len =
u32::try_from(tx.actions.len()).expect("Orchard action count cannot exceed a u32");
// Check for incoming notes while incrementing tree and witnesses
let mut shielded_outputs: Vec<WalletSaplingOutput<K::Nf, K::Scope>> = vec![];
{
let decoded = &tx
.outputs
.into_iter()
.map(|output| {
(
SaplingDomain::new(zip212_enforcement),
CompactOutputDescription::try_from(output)
.expect("Invalid output found in compact block decoding."),
)
})
.collect::<Vec<_>>();
let decrypted: Vec<_> = if let Some(runner) = batch_runner.as_mut() {
let vks = vks
.iter()
.flat_map(|(a, k)| {
k.to_sapling_keys()
.into_iter()
.map(move |(scope, _, nk)| ((**a, scope), nk))
})
.collect::<HashMap<_, _>>();
let mut decrypted = runner.collect_results(cur_hash, txid);
(0..decoded.len())
.map(|i| {
decrypted.remove(&(txid, i)).map(|d_note| {
let a = d_note.ivk_tag.0;
let nk = vks.get(&d_note.ivk_tag).expect(
"The batch runner and scan_block must use the same set of IVKs.",
);
(d_note.note, a, d_note.ivk_tag.1, (*nk).clone())
})
})
.collect()
} else {
let vks = vks
.iter()
.flat_map(|(a, k)| {
k.to_sapling_keys()
.into_iter()
.map(move |(scope, ivk, nk)| (**a, scope, ivk, nk))
})
.collect::<Vec<_>>();
let ivks = vks
.iter()
.map(|(_, _, ivk, _)| ivk)
.map(PreparedIncomingViewingKey::new)
.collect::<Vec<_>>();
batch::try_compact_note_decryption(&ivks, &decoded[..])
.into_iter()
.map(|v| {
v.map(|((note, _), ivk_idx)| {
let (account, scope, _, nk) = &vks[ivk_idx];
(note, *account, scope.clone(), (*nk).clone())
})
})
.collect()
};
for (output_idx, ((_, output), dec_output)) in decoded.iter().zip(decrypted).enumerate()
{
// Collect block note commitments
let node = sapling::Node::from_cmu(&output.cmu);
let is_checkpoint =
output_idx + 1 == decoded.len() && tx_idx + 1 == compact_block_tx_count;
let retention = match (dec_output.is_some(), is_checkpoint) {
(is_marked, true) => Retention::Checkpoint {
id: cur_height,
is_marked,
},
(true, false) => Retention::Marked,
(false, false) => Retention::Ephemeral,
};
if let Some((note, account, scope, nk)) = 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 is_change = spent_from_accounts.contains(&account);
let note_commitment_tree_position = Position::from(u64::from(
sapling_commitment_tree_size + u32::try_from(output_idx).unwrap(),
));
let nf = K::sapling_nf(&nk, &note, note_commitment_tree_position);
shielded_outputs.push(WalletSaplingOutput::from_parts(
output_idx,
output.cmu,
output.ephemeral_key.clone(),
account,
note,
is_change,
note_commitment_tree_position,
nf,
scope,
));
}
sapling_note_commitments.push((node, retention));
}
}
if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) {
wtxs.push(WalletTx {
txid,
index: tx_index as usize,
sapling_spends: shielded_spends,
sapling_outputs: shielded_outputs,
});
}
sapling_commitment_tree_size += tx_outputs_len;
#[cfg(feature = "orchard")]
{
orchard_commitment_tree_size += tx_actions_len;
}
}
if let Some(chain_meta) = block.chain_metadata {
if chain_meta.sapling_commitment_tree_size != sapling_commitment_tree_size {
return Err(ScanError::TreeSizeMismatch {
protocol: ShieldedProtocol::Sapling,
at_height: cur_height,
given: chain_meta.sapling_commitment_tree_size,
computed: sapling_commitment_tree_size,
});
}
#[cfg(feature = "orchard")]
if chain_meta.orchard_commitment_tree_size != orchard_commitment_tree_size {
return Err(ScanError::TreeSizeMismatch {
protocol: ShieldedProtocol::Orchard,
at_height: cur_height,
given: chain_meta.orchard_commitment_tree_size,
computed: orchard_commitment_tree_size,
});
}
}
Ok(ScannedBlock::from_parts(
cur_height,
cur_hash,
block.time,
wtxs,
ScannedBundles::new(
sapling_commitment_tree_size,
sapling_note_commitments,
sapling_nullifier_map,
),
#[cfg(feature = "orchard")]
ScannedBundles::new(
orchard_commitment_tree_size,
vec![], // FIXME: collect the Orchard nullifiers
vec![], // FIXME: collect the Orchard note commitments
),
))
}
#[cfg(test)]
mod tests {
use group::{
ff::{Field, PrimeField},
GroupEncoding,
};
use incrementalmerkletree::{Position, Retention};
use rand_core::{OsRng, RngCore};
use sapling::{
constants::SPENDING_KEY_GENERATOR,
note_encryption::{sapling_note_encryption, PreparedIncomingViewingKey, SaplingDomain},
util::generate_random_rseed,
value::NoteValue,
zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
Nullifier, SaplingIvk,
};
use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
consensus::{sapling_zip212_enforcement, BlockHeight, Network},
memo::MemoBytes,
transaction::components::amount::NonNegativeAmount,
zip32::AccountId,
};
use crate::{
data_api::BlockMetadata,
proto::compact_formats::{
self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
scan::BatchRunner,
};
use super::{add_block_to_runner, scan_block, scan_block_with_runner, ScanningKey};
fn random_compact_tx(mut rng: impl RngCore) -> 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().as_ref().to_owned()
};
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; 52],
};
let mut ctx = CompactTx::default();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.hash = txid;
ctx.spends.push(cspend);
ctx.outputs.push(cout);
ctx
}
/// Create a fake CompactBlock at the given height, with a transaction containing a
/// single spend of the given nullifier and a single output paying the given address.
/// Returns the CompactBlock.
///
/// Set `initial_tree_sizes` to `None` to simulate a `CompactBlock` retrieved
/// from a `lightwalletd` that is not currently tracking note commitment tree sizes.
fn fake_compact_block(
height: BlockHeight,
prev_hash: BlockHash,
nf: Nullifier,
dfvk: &DiversifiableFullViewingKey,
value: NonNegativeAmount,
tx_after: bool,
initial_tree_sizes: Option<(u32, u32)>,
) -> CompactBlock {
let zip212_enforcement = sapling_zip212_enforcement(&Network::TestNetwork, height);
let to = dfvk.default_address().1;
// Create a fake Note for the account
let mut rng = OsRng;
let rseed = generate_random_rseed(zip212_enforcement, &mut rng);
let note = sapling::Note::from_parts(to, NoteValue::from(value), rseed);
let encryptor = sapling_note_encryption(
Some(dfvk.fvk().ovk),
note.clone(),
*MemoBytes::empty().as_array(),
&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();
// Create a fake CompactBlock containing the note
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()
};
// Add a random Sapling tx before ours
{
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.as_ref()[..52].to_vec(),
};
let mut ctx = CompactTx::default();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.hash = txid;
ctx.spends.push(cspend);
ctx.outputs.push(cout);
ctx.index = cb.vtx.len() as u64;
cb.vtx.push(ctx);
// Optionally add another random Sapling tx after ours
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>(),
}
});
cb
}
#[test]
fn scan_block_with_my_tx() {
fn go(scan_multithreaded: bool) {
let account = AccountId::ZERO;
let extsk = ExtendedSpendingKey::master(&[]);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let cb = fake_compact_block(
1u32.into(),
BlockHash([0; 32]),
Nullifier([0; 32]),
&dfvk,
NonNegativeAmount::const_from_u64(5),
false,
None,
);
assert_eq!(cb.vtx.len(), 2);
let mut batch_runner = if scan_multithreaded {
let mut runner = BatchRunner::<_, _, _, ()>::new(
10,
dfvk.to_sapling_keys()
.iter()
.map(|(scope, ivk, _)| ((account, *scope), ivk))
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))),
);
add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner);
runner.flush();
Some(runner)
} else {
None
};
let scanned_block = scan_block_with_runner(
&Network::TestNetwork,
cb,
&[(&account, &dfvk)],
&[],
Some(&BlockMetadata::from_parts(
BlockHeight::from(0),
BlockHash([0u8; 32]),
Some(0),
#[cfg(feature = "orchard")]
Some(0),
)),
batch_runner.as_mut(),
)
.unwrap();
let txs = scanned_block.transactions();
assert_eq!(txs.len(), 1);
let tx = &txs[0];
assert_eq!(tx.index, 1);
assert_eq!(tx.sapling_spends.len(), 0);
assert_eq!(tx.sapling_outputs.len(), 1);
assert_eq!(tx.sapling_outputs[0].index(), 0);
assert_eq!(tx.sapling_outputs[0].account(), account);
assert_eq!(tx.sapling_outputs[0].note().value().inner(), 5);
assert_eq!(
tx.sapling_outputs[0].note_commitment_tree_position(),
Position::from(1)
);
assert_eq!(scanned_block.sapling().final_tree_size(), 2);
assert_eq!(
scanned_block
.sapling()
.commitments()
.iter()
.map(|(_, retention)| *retention)
.collect::<Vec<_>>(),
vec![
Retention::Ephemeral,
Retention::Checkpoint {
id: scanned_block.height(),
is_marked: true
}
]
);
}
go(false);
go(true);
}
#[test]
fn scan_block_with_txs_after_my_tx() {
fn go(scan_multithreaded: bool) {
let account = AccountId::ZERO;
let extsk = ExtendedSpendingKey::master(&[]);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let cb = fake_compact_block(
1u32.into(),
BlockHash([0; 32]),
Nullifier([0; 32]),
&dfvk,
NonNegativeAmount::const_from_u64(5),
true,
Some((0, 0)),
);
assert_eq!(cb.vtx.len(), 3);
let mut batch_runner = if scan_multithreaded {
let mut runner = BatchRunner::<_, _, _, ()>::new(
10,
dfvk.to_sapling_keys()
.iter()
.map(|(scope, ivk, _)| ((account, *scope), ivk))
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))),
);
add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner);
runner.flush();
Some(runner)
} else {
None
};
let scanned_block = scan_block_with_runner(
&Network::TestNetwork,
cb,
&[(&AccountId::ZERO, &dfvk)],
&[],
None,
batch_runner.as_mut(),
)
.unwrap();
let txs = scanned_block.transactions();
assert_eq!(txs.len(), 1);
let tx = &txs[0];
assert_eq!(tx.index, 1);
assert_eq!(tx.sapling_spends.len(), 0);
assert_eq!(tx.sapling_outputs.len(), 1);
assert_eq!(tx.sapling_outputs[0].index(), 0);
assert_eq!(tx.sapling_outputs[0].account(), AccountId::ZERO);
assert_eq!(tx.sapling_outputs[0].note().value().inner(), 5);
assert_eq!(
scanned_block
.sapling()
.commitments()
.iter()
.map(|(_, retention)| *retention)
.collect::<Vec<_>>(),
vec![
Retention::Ephemeral,
Retention::Marked,
Retention::Checkpoint {
id: scanned_block.height(),
is_marked: false
}
]
);
}
go(false);
go(true);
}
#[test]
fn scan_block_with_my_spend() {
let extsk = ExtendedSpendingKey::master(&[]);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let nf = Nullifier([7; 32]);
let account = AccountId::try_from(12).unwrap();
let cb = fake_compact_block(
1u32.into(),
BlockHash([0; 32]),
nf,
&dfvk,
NonNegativeAmount::const_from_u64(5),
false,
Some((0, 0)),
);
assert_eq!(cb.vtx.len(), 2);
let vks: Vec<(&AccountId, &SaplingIvk)> = vec![];
let scanned_block =
scan_block(&Network::TestNetwork, cb, &vks[..], &[(account, nf)], None).unwrap();
let txs = scanned_block.transactions();
assert_eq!(txs.len(), 1);
let tx = &txs[0];
assert_eq!(tx.index, 1);
assert_eq!(tx.sapling_spends.len(), 1);
assert_eq!(tx.sapling_outputs.len(), 0);
assert_eq!(tx.sapling_spends[0].index(), 0);
assert_eq!(tx.sapling_spends[0].nf(), &nf);
assert_eq!(tx.sapling_spends[0].account(), account);
assert_eq!(
scanned_block
.sapling()
.commitments()
.iter()
.map(|(_, retention)| *retention)
.collect::<Vec<_>>(),
vec![
Retention::Ephemeral,
Retention::Checkpoint {
id: scanned_block.height(),
is_marked: false
}
]
);
}
}