use orchard::{
keys::{FullViewingKey, IncomingViewingKey, PreparedIncomingViewingKey, Scope},
note::{ExtractedNoteCommitment, Note, Nullifier},
note_encryption::{CompactAction, OrchardDomain},
Address,
};
use subtle::CtOption;
use zcash_note_encryption::{try_compact_note_decryption, EphemeralKeyBytes};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone)]
pub struct ScanAction {
pub nullifier: [u8; 32],
pub cmx: [u8; 32],
pub ephemeral_key: [u8; 32],
pub enc_ciphertext: Vec<u8>,
pub block_height: u32,
pub position_in_block: u32,
}
#[derive(Debug, Clone)]
pub struct DecryptedNote {
pub note: Note,
pub recipient: Address,
pub value: u64,
pub nullifier: [u8; 32],
pub cmx: [u8; 32],
pub block_height: u32,
pub position_in_block: u32,
}
pub struct Scanner {
prepared_ivk: PreparedIncomingViewingKey,
}
impl Scanner {
pub fn new(ivk: &IncomingViewingKey) -> Self {
Self {
prepared_ivk: PreparedIncomingViewingKey::new(ivk),
}
}
pub fn from_fvk(fvk: &FullViewingKey) -> Self {
Self::new(&fvk.to_ivk(Scope::External))
}
pub fn from_fvk_scoped(fvk: &FullViewingKey, scope: Scope) -> Self {
Self::new(&fvk.to_ivk(scope))
}
pub fn try_decrypt(&self, action: &ScanAction) -> Option<DecryptedNote> {
let nullifier: CtOption<Nullifier> = Nullifier::from_bytes(&action.nullifier);
let nullifier = Option::from(nullifier)?;
let domain = OrchardDomain::for_nullifier(nullifier);
let compact = compact_action_from_scan(action)?;
let (note, recipient) = try_compact_note_decryption(&domain, &self.prepared_ivk, &compact)?;
let recomputed = ExtractedNoteCommitment::from(note.commitment());
if recomputed.to_bytes() != action.cmx {
return None;
}
Some(DecryptedNote {
value: note.value().inner(),
note,
recipient,
nullifier: action.nullifier,
cmx: action.cmx,
block_height: action.block_height,
position_in_block: action.position_in_block,
})
}
pub fn scan_sequential(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
actions.iter().filter_map(|a| self.try_decrypt(a)).collect()
}
#[cfg(feature = "parallel")]
pub fn scan_parallel(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
use rayon::prelude::*;
actions
.par_iter()
.filter_map(|a| self.try_decrypt(a))
.collect()
}
pub fn scan(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
#[cfg(feature = "parallel")]
{
self.scan_parallel(actions)
}
#[cfg(not(feature = "parallel"))]
{
self.scan_sequential(actions)
}
}
pub fn scan_with_progress<F>(
&self,
actions: &[ScanAction],
chunk_size: usize,
mut on_progress: F,
) -> Vec<DecryptedNote>
where
F: FnMut(usize, usize, &[DecryptedNote]),
{
let total_chunks = actions.len().div_ceil(chunk_size);
let mut all_notes = Vec::new();
for (i, chunk) in actions.chunks(chunk_size).enumerate() {
let notes = self.scan(chunk);
on_progress(i, total_chunks, ¬es);
all_notes.extend(notes);
}
all_notes
}
}
fn compact_action_from_scan(action: &ScanAction) -> Option<CompactAction> {
let nullifier = Option::from(Nullifier::from_bytes(&action.nullifier))?;
let cmx = Option::from(ExtractedNoteCommitment::from_bytes(&action.cmx))?;
let epk = EphemeralKeyBytes::from(action.ephemeral_key);
if action.enc_ciphertext.len() < 52 {
return None;
}
Some(CompactAction::from_parts(
nullifier,
cmx,
epk,
action.enc_ciphertext[..52].try_into().ok()?,
))
}
pub struct BatchScanner {
scanner: Scanner,
pub notes: Vec<DecryptedNote>,
pub seen_nullifiers: std::collections::HashSet<[u8; 32]>,
pub last_height: u32,
}
impl BatchScanner {
pub fn new(ivk: &IncomingViewingKey) -> Self {
Self {
scanner: Scanner::new(ivk),
notes: Vec::new(),
seen_nullifiers: std::collections::HashSet::new(),
last_height: 0,
}
}
pub fn from_fvk(fvk: &FullViewingKey) -> Self {
Self::new(&fvk.to_ivk(Scope::External))
}
pub fn scan_block(&mut self, height: u32, actions: &[ScanAction]) {
for action in actions {
self.seen_nullifiers.insert(action.nullifier);
}
let found = self.scanner.scan(actions);
self.notes.extend(found);
self.last_height = height;
}
pub fn unspent_balance(&self) -> u64 {
self.notes
.iter()
.filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
.map(|n| n.value)
.sum()
}
pub fn spent_balance(&self) -> u64 {
self.notes
.iter()
.filter(|n| self.seen_nullifiers.contains(&n.nullifier))
.map(|n| n.value)
.sum()
}
pub fn unspent_notes(&self) -> Vec<&DecryptedNote> {
self.notes
.iter()
.filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DetectionHint {
pub tag: [u8; 4],
pub bucket: u8,
}
impl DetectionHint {
pub fn might_match(&self, action_tag: &[u8; 4]) -> bool {
self.tag[..2] == action_tag[..2]
}
}
pub struct HintGenerator {
_dk: [u8; 32],
}
impl HintGenerator {
pub fn from_fvk(_fvk: &FullViewingKey) -> Self {
Self { _dk: [0u8; 32] }
}
pub fn hints_for_range(&self, _start: u32, _count: u32) -> Vec<DetectionHint> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_action_size() {
let action = ScanAction {
nullifier: [0u8; 32],
cmx: [0u8; 32],
ephemeral_key: [0u8; 32],
enc_ciphertext: vec![0u8; 52],
block_height: 0,
position_in_block: 0,
};
let total_size = action.nullifier.len()
+ action.cmx.len()
+ action.ephemeral_key.len()
+ action.enc_ciphertext.len();
assert_eq!(total_size, 148);
}
#[test]
fn test_batch_scanner_balance() {
let fvk = test_fvk();
let scanner = BatchScanner::from_fvk(&fvk);
assert_eq!(scanner.unspent_balance(), 0);
assert_eq!(scanner.spent_balance(), 0);
}
fn test_fvk() -> FullViewingKey {
use orchard::keys::SpendingKey;
let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
FullViewingKey::from(&sk)
}
}
#[cfg(feature = "wasm-parallel")]
#[wasm_bindgen]
pub fn init_thread_pool(num_threads: usize) -> js_sys::Promise {
wasm_bindgen_rayon::init_thread_pool(num_threads)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen(start)]
pub fn wasm_init() {
console_error_panic_hook::set_once();
}
#[cfg(feature = "wasm-parallel")]
pub use wasm_bindgen_rayon::init_thread_pool as _rayon_init;