1use orchard::{
29 keys::{FullViewingKey, IncomingViewingKey, PreparedIncomingViewingKey, Scope},
30 note::{ExtractedNoteCommitment, Note, Nullifier},
31 note_encryption::{CompactAction, OrchardDomain},
32 Address,
33};
34use subtle::CtOption;
35use zcash_note_encryption::{try_compact_note_decryption, EphemeralKeyBytes};
36
37#[cfg(feature = "wasm")]
38use wasm_bindgen::prelude::*;
39
40#[derive(Debug, Clone)]
42pub struct ScanAction {
43 pub nullifier: [u8; 32],
45 pub cmx: [u8; 32],
47 pub ephemeral_key: [u8; 32],
49 pub enc_ciphertext: Vec<u8>,
51 pub block_height: u32,
53 pub position_in_block: u32,
55}
56
57#[derive(Debug, Clone)]
59pub struct DecryptedNote {
60 pub note: Note,
62 pub recipient: Address,
64 pub value: u64,
66 pub nullifier: [u8; 32],
68 pub cmx: [u8; 32],
70 pub block_height: u32,
72 pub position_in_block: u32,
74}
75
76pub struct Scanner {
81 prepared_ivk: PreparedIncomingViewingKey,
83}
84
85impl Scanner {
86 pub fn new(ivk: &IncomingViewingKey) -> Self {
88 Self {
89 prepared_ivk: PreparedIncomingViewingKey::new(ivk),
90 }
91 }
92
93 pub fn from_fvk(fvk: &FullViewingKey) -> Self {
95 Self::new(&fvk.to_ivk(Scope::External))
96 }
97
98 pub fn from_fvk_scoped(fvk: &FullViewingKey, scope: Scope) -> Self {
100 Self::new(&fvk.to_ivk(scope))
101 }
102
103 pub fn try_decrypt(&self, action: &ScanAction) -> Option<DecryptedNote> {
105 let nullifier: CtOption<Nullifier> = Nullifier::from_bytes(&action.nullifier);
107 let nullifier = Option::from(nullifier)?;
108 let domain = OrchardDomain::for_nullifier(nullifier);
109
110 let compact = match compact_action_from_scan(action) {
112 Some(c) => c,
113 None => return None,
114 };
115
116 let (note, recipient) = try_compact_note_decryption(&domain, &self.prepared_ivk, &compact)?;
118
119 Some(DecryptedNote {
120 value: note.value().inner(),
121 note,
122 recipient,
123 nullifier: action.nullifier,
124 cmx: action.cmx,
125 block_height: action.block_height,
126 position_in_block: action.position_in_block,
127 })
128 }
129
130 pub fn scan_sequential(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
133 actions.iter().filter_map(|a| self.try_decrypt(a)).collect()
134 }
135
136 #[cfg(feature = "parallel")]
139 pub fn scan_parallel(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
140 use rayon::prelude::*;
141 actions
142 .par_iter()
143 .filter_map(|a| self.try_decrypt(a))
144 .collect()
145 }
146
147 pub fn scan(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
150 #[cfg(feature = "parallel")]
151 {
152 self.scan_parallel(actions)
153 }
154 #[cfg(not(feature = "parallel"))]
155 {
156 self.scan_sequential(actions)
157 }
158 }
159
160 pub fn scan_with_progress<F>(
163 &self,
164 actions: &[ScanAction],
165 chunk_size: usize,
166 mut on_progress: F,
167 ) -> Vec<DecryptedNote>
168 where
169 F: FnMut(usize, usize, &[DecryptedNote]),
170 {
171 let total_chunks = (actions.len() + chunk_size - 1) / chunk_size;
172 let mut all_notes = Vec::new();
173
174 for (i, chunk) in actions.chunks(chunk_size).enumerate() {
175 let notes = self.scan(chunk);
176 on_progress(i, total_chunks, ¬es);
177 all_notes.extend(notes);
178 }
179
180 all_notes
181 }
182}
183
184fn compact_action_from_scan(action: &ScanAction) -> Option<CompactAction> {
186 let nullifier = Option::from(Nullifier::from_bytes(&action.nullifier))?;
188
189 let cmx = Option::from(ExtractedNoteCommitment::from_bytes(&action.cmx))?;
191
192 let epk = EphemeralKeyBytes::from(action.ephemeral_key);
194
195 if action.enc_ciphertext.len() < 52 {
197 return None;
198 }
199
200 Some(CompactAction::from_parts(
201 nullifier,
202 cmx,
203 epk,
204 action.enc_ciphertext[..52].try_into().ok()?,
205 ))
206}
207
208pub struct BatchScanner {
210 scanner: Scanner,
211 pub notes: Vec<DecryptedNote>,
213 pub seen_nullifiers: std::collections::HashSet<[u8; 32]>,
215 pub last_height: u32,
217}
218
219impl BatchScanner {
220 pub fn new(ivk: &IncomingViewingKey) -> Self {
221 Self {
222 scanner: Scanner::new(ivk),
223 notes: Vec::new(),
224 seen_nullifiers: std::collections::HashSet::new(),
225 last_height: 0,
226 }
227 }
228
229 pub fn from_fvk(fvk: &FullViewingKey) -> Self {
230 Self::new(&fvk.to_ivk(Scope::External))
231 }
232
233 pub fn scan_block(&mut self, height: u32, actions: &[ScanAction]) {
235 for action in actions {
237 self.seen_nullifiers.insert(action.nullifier);
238 }
239
240 let found = self.scanner.scan(actions);
242 self.notes.extend(found);
243 self.last_height = height;
244 }
245
246 pub fn unspent_balance(&self) -> u64 {
248 self.notes
249 .iter()
250 .filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
251 .map(|n| n.value)
252 .sum()
253 }
254
255 pub fn spent_balance(&self) -> u64 {
257 self.notes
258 .iter()
259 .filter(|n| self.seen_nullifiers.contains(&n.nullifier))
260 .map(|n| n.value)
261 .sum()
262 }
263
264 pub fn unspent_notes(&self) -> Vec<&DecryptedNote> {
266 self.notes
267 .iter()
268 .filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
269 .collect()
270 }
271}
272
273#[derive(Debug, Clone)]
276pub struct DetectionHint {
277 pub tag: [u8; 4],
279 pub bucket: u8,
281}
282
283impl DetectionHint {
284 pub fn might_match(&self, action_tag: &[u8; 4]) -> bool {
287 self.tag[..2] == action_tag[..2]
290 }
291}
292
293pub struct HintGenerator {
295 _dk: [u8; 32],
297}
298
299impl HintGenerator {
300 pub fn from_fvk(_fvk: &FullViewingKey) -> Self {
302 Self { _dk: [0u8; 32] }
304 }
305
306 pub fn hints_for_range(&self, _start: u32, _count: u32) -> Vec<DetectionHint> {
308 Vec::new()
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_scan_action_size() {
320 let action = ScanAction {
322 nullifier: [0u8; 32],
323 cmx: [0u8; 32],
324 ephemeral_key: [0u8; 32],
325 enc_ciphertext: vec![0u8; 52],
326 block_height: 0,
327 position_in_block: 0,
328 };
329
330 let total_size = action.nullifier.len()
331 + action.cmx.len()
332 + action.ephemeral_key.len()
333 + action.enc_ciphertext.len();
334 assert_eq!(total_size, 148);
335 }
336
337 #[test]
338 fn test_batch_scanner_balance() {
339 let fvk = test_fvk();
342 let scanner = BatchScanner::from_fvk(&fvk);
343 assert_eq!(scanner.unspent_balance(), 0);
344 assert_eq!(scanner.spent_balance(), 0);
345 }
346
347 fn test_fvk() -> FullViewingKey {
348 use orchard::keys::SpendingKey;
349 let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
350 FullViewingKey::from(&sk)
351 }
352}
353
354#[cfg(feature = "wasm-parallel")]
368#[wasm_bindgen]
369pub fn init_thread_pool(num_threads: usize) -> js_sys::Promise {
370 wasm_bindgen_rayon::init_thread_pool(num_threads)
372}
373
374#[cfg(feature = "wasm")]
376#[wasm_bindgen(start)]
377pub fn wasm_init() {
378 console_error_panic_hook::set_once();
379}
380
381#[cfg(feature = "wasm-parallel")]
383pub use wasm_bindgen_rayon::init_thread_pool as _rayon_init;