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 = compact_action_from_scan(action)?;
112
113 let (note, recipient) = try_compact_note_decryption(&domain, &self.prepared_ivk, &compact)?;
115
116 let recomputed = ExtractedNoteCommitment::from(note.commitment());
120 if recomputed.to_bytes() != action.cmx {
121 return None;
122 }
123
124 Some(DecryptedNote {
125 value: note.value().inner(),
126 note,
127 recipient,
128 nullifier: action.nullifier,
129 cmx: action.cmx,
130 block_height: action.block_height,
131 position_in_block: action.position_in_block,
132 })
133 }
134
135 pub fn scan_sequential(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
138 actions.iter().filter_map(|a| self.try_decrypt(a)).collect()
139 }
140
141 #[cfg(feature = "parallel")]
144 pub fn scan_parallel(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
145 use rayon::prelude::*;
146 actions
147 .par_iter()
148 .filter_map(|a| self.try_decrypt(a))
149 .collect()
150 }
151
152 pub fn scan(&self, actions: &[ScanAction]) -> Vec<DecryptedNote> {
155 #[cfg(feature = "parallel")]
156 {
157 self.scan_parallel(actions)
158 }
159 #[cfg(not(feature = "parallel"))]
160 {
161 self.scan_sequential(actions)
162 }
163 }
164
165 pub fn scan_with_progress<F>(
168 &self,
169 actions: &[ScanAction],
170 chunk_size: usize,
171 mut on_progress: F,
172 ) -> Vec<DecryptedNote>
173 where
174 F: FnMut(usize, usize, &[DecryptedNote]),
175 {
176 let total_chunks = actions.len().div_ceil(chunk_size);
177 let mut all_notes = Vec::new();
178
179 for (i, chunk) in actions.chunks(chunk_size).enumerate() {
180 let notes = self.scan(chunk);
181 on_progress(i, total_chunks, ¬es);
182 all_notes.extend(notes);
183 }
184
185 all_notes
186 }
187}
188
189fn compact_action_from_scan(action: &ScanAction) -> Option<CompactAction> {
191 let nullifier = Option::from(Nullifier::from_bytes(&action.nullifier))?;
193
194 let cmx = Option::from(ExtractedNoteCommitment::from_bytes(&action.cmx))?;
196
197 let epk = EphemeralKeyBytes::from(action.ephemeral_key);
199
200 if action.enc_ciphertext.len() < 52 {
202 return None;
203 }
204
205 Some(CompactAction::from_parts(
206 nullifier,
207 cmx,
208 epk,
209 action.enc_ciphertext[..52].try_into().ok()?,
210 ))
211}
212
213pub struct BatchScanner {
215 scanner: Scanner,
216 pub notes: Vec<DecryptedNote>,
218 pub seen_nullifiers: std::collections::HashSet<[u8; 32]>,
220 pub last_height: u32,
222}
223
224impl BatchScanner {
225 pub fn new(ivk: &IncomingViewingKey) -> Self {
226 Self {
227 scanner: Scanner::new(ivk),
228 notes: Vec::new(),
229 seen_nullifiers: std::collections::HashSet::new(),
230 last_height: 0,
231 }
232 }
233
234 pub fn from_fvk(fvk: &FullViewingKey) -> Self {
235 Self::new(&fvk.to_ivk(Scope::External))
236 }
237
238 pub fn scan_block(&mut self, height: u32, actions: &[ScanAction]) {
240 for action in actions {
242 self.seen_nullifiers.insert(action.nullifier);
243 }
244
245 let found = self.scanner.scan(actions);
247 self.notes.extend(found);
248 self.last_height = height;
249 }
250
251 pub fn unspent_balance(&self) -> u64 {
253 self.notes
254 .iter()
255 .filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
256 .map(|n| n.value)
257 .sum()
258 }
259
260 pub fn spent_balance(&self) -> u64 {
262 self.notes
263 .iter()
264 .filter(|n| self.seen_nullifiers.contains(&n.nullifier))
265 .map(|n| n.value)
266 .sum()
267 }
268
269 pub fn unspent_notes(&self) -> Vec<&DecryptedNote> {
271 self.notes
272 .iter()
273 .filter(|n| !self.seen_nullifiers.contains(&n.nullifier))
274 .collect()
275 }
276}
277
278#[derive(Debug, Clone)]
281pub struct DetectionHint {
282 pub tag: [u8; 4],
284 pub bucket: u8,
286}
287
288impl DetectionHint {
289 pub fn might_match(&self, action_tag: &[u8; 4]) -> bool {
292 self.tag[..2] == action_tag[..2]
295 }
296}
297
298pub struct HintGenerator {
300 _dk: [u8; 32],
302}
303
304impl HintGenerator {
305 pub fn from_fvk(_fvk: &FullViewingKey) -> Self {
307 Self { _dk: [0u8; 32] }
309 }
310
311 pub fn hints_for_range(&self, _start: u32, _count: u32) -> Vec<DetectionHint> {
313 Vec::new()
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_scan_action_size() {
325 let action = ScanAction {
327 nullifier: [0u8; 32],
328 cmx: [0u8; 32],
329 ephemeral_key: [0u8; 32],
330 enc_ciphertext: vec![0u8; 52],
331 block_height: 0,
332 position_in_block: 0,
333 };
334
335 let total_size = action.nullifier.len()
336 + action.cmx.len()
337 + action.ephemeral_key.len()
338 + action.enc_ciphertext.len();
339 assert_eq!(total_size, 148);
340 }
341
342 #[test]
343 fn test_batch_scanner_balance() {
344 let fvk = test_fvk();
347 let scanner = BatchScanner::from_fvk(&fvk);
348 assert_eq!(scanner.unspent_balance(), 0);
349 assert_eq!(scanner.spent_balance(), 0);
350 }
351
352 fn test_fvk() -> FullViewingKey {
353 use orchard::keys::SpendingKey;
354 let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
355 FullViewingKey::from(&sk)
356 }
357}
358
359#[cfg(feature = "wasm-parallel")]
373#[wasm_bindgen]
374pub fn init_thread_pool(num_threads: usize) -> js_sys::Promise {
375 wasm_bindgen_rayon::init_thread_pool(num_threads)
377}
378
379#[cfg(feature = "wasm")]
381#[wasm_bindgen(start)]
382pub fn wasm_init() {
383 console_error_panic_hook::set_once();
384}
385
386#[cfg(feature = "wasm-parallel")]
388pub use wasm_bindgen_rayon::init_thread_pool as _rayon_init;