1use super::DEFAULT_ENDPOINT;
17use crate::{
18 commands::Developer,
19 helpers::{args::prepare_endpoint, dev::get_development_key},
20};
21
22use snarkos_node_cdn::CDN_BASE_URL;
23use snarkvm::{
24 console::network::Network,
25 prelude::{Ciphertext, Field, Plaintext, PrivateKey, Record, ViewKey, block::Block},
26};
27
28#[cfg(not(feature = "test_targets"))]
29use snarkvm::prelude::FromBytes;
30
31use anyhow::{Context, Result, bail, ensure};
32use clap::{Parser, builder::NonEmptyStringValueParser};
33#[cfg(feature = "locktick")]
34use locktick::parking_lot::RwLock;
35#[cfg(not(feature = "locktick"))]
36use parking_lot::RwLock;
37use std::{
38 io::{Write, stdout},
39 str::FromStr,
40 sync::Arc,
41};
42use ureq::http::Uri;
43use zeroize::Zeroize;
44
45const MAX_BLOCK_RANGE: u32 = 50;
46
47#[derive(Debug, Parser)]
49#[clap(
50 group(clap::ArgGroup::new("key").required(true).multiple(false))
53)]
54pub struct Scan {
55 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
57 private_key: Option<String>,
58
59 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
62 view_key: Option<String>,
63
64 #[clap(long, group = "key")]
66 dev_key: Option<u16>,
67
68 #[clap(long, conflicts_with = "last")]
71 start: Option<u32>,
72
73 #[clap(long, conflicts_with = "last")]
76 end: Option<u32>,
77
78 #[clap(long)]
80 last: Option<u32>,
81
82 #[clap(long, default_value = DEFAULT_ENDPOINT)]
84 endpoint: Uri,
85
86 #[clap(long)]
88 verbosity: Option<u8>,
89}
90
91impl Drop for Scan {
92 fn drop(&mut self) {
94 if let Some(mut pk) = self.private_key.take() {
95 pk.zeroize()
96 }
97 }
98}
99
100impl Scan {
101 pub fn parse<N: Network>(self) -> Result<String> {
103 let endpoint = prepare_endpoint(self.endpoint.clone())?;
104
105 let (private_key, view_key) = self.parse_account::<N>()?;
107
108 let (start_height, end_height) = self.parse_block_range::<N>(&endpoint)?;
110
111 let records = Self::fetch_records::<N>(private_key, &view_key, &endpoint, start_height, end_height)
113 .with_context(|| "Failed to fetch records")?;
114
115 if records.is_empty() {
117 Ok("No records found".to_string())
118 } else {
119 if private_key.is_none() {
120 println!("⚠️ This list may contain records that have already been spent.\n");
121 }
122
123 Ok(serde_json::to_string_pretty(&records)?.replace("\\n", ""))
124 }
125 }
126
127 fn parse_account<N: Network>(&self) -> Result<(Option<PrivateKey<N>>, ViewKey<N>)> {
129 if let Some(private_key) = &self.private_key {
130 let private_key = PrivateKey::<N>::from_str(private_key)?;
131 let view_key = ViewKey::<N>::try_from(private_key)?;
132 Ok((Some(private_key), view_key))
133 } else if let Some(index) = &self.dev_key {
134 let private_key = get_development_key(*index)?;
135 let view_key = ViewKey::<N>::try_from(private_key)?;
136 Ok((Some(private_key), view_key))
137 } else if let Some(view_key) = &self.view_key {
138 Ok((None, ViewKey::<N>::from_str(view_key)?))
139 } else {
140 unreachable!();
142 }
143 }
144
145 fn parse_block_range<N: Network>(&self, endpoint: &Uri) -> Result<(u32, u32)> {
147 match (self.start, self.end, self.last) {
148 (Some(start), Some(end), None) => {
149 ensure!(end > start, "The given scan range is invalid (start = {start}, end = {end})");
150
151 Ok((start, end))
152 }
153 (Some(start), None, None) => {
154 let endpoint = Developer::build_endpoint::<N>(endpoint, "block/height/latest")?;
156 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_body().read_to_string()?)?;
157
158 if start == 0 {
160 println!("⚠️ Attention - Scanning the entire chain. This may take a while...\n");
161 }
162
163 Ok((start, latest_height))
164 }
165 (None, Some(end), None) => Ok((0, end)),
166 (None, None, Some(last)) => {
167 let endpoint = Developer::build_endpoint::<N>(endpoint, "block/height/latest")?;
169 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_body().read_to_string()?)?;
170
171 Ok((latest_height.saturating_sub(last), latest_height))
172 }
173 (None, None, None) => bail!("Missing data about block range."),
174 _ => bail!("`last` flags can't be used with `start` or `end`"),
175 }
176 }
177
178 fn parse_cdn<N: Network>() -> Result<Uri> {
180 Uri::try_from(format!("{CDN_BASE_URL}/{}", N::SHORT_NAME)).with_context(|| "Unexpected error")
182 }
183
184 fn fetch_records<N: Network>(
186 private_key: Option<PrivateKey<N>>,
187 view_key: &ViewKey<N>,
188 endpoint: &Uri,
189 start_height: u32,
190 end_height: u32,
191 ) -> Result<Vec<Record<N, Plaintext<N>>>> {
192 if start_height > end_height {
194 bail!("Invalid block range");
195 }
196
197 let address_x_coordinate = view_key.to_address().to_x_coordinate();
199
200 let records = Arc::new(RwLock::new(Vec::new()));
202
203 let total_blocks = end_height.saturating_sub(start_height);
205
206 print!("\rScanning {total_blocks} blocks for records (0% complete)...");
208 stdout().flush()?;
209
210 #[cfg(feature = "test_targets")]
212 let is_development_network = true;
213
214 #[cfg(not(feature = "test_targets"))]
216 let is_development_network = {
217 let endpoint = Developer::build_endpoint::<N>(endpoint, "block/0")?;
219 let endpoint_genesis_block: Block<N> = ureq::get(endpoint).call()?.into_body().read_json()?;
220 endpoint_genesis_block != Block::from_bytes_le(N::genesis_bytes())?
222 };
223
224 let mut request_start = match is_development_network {
226 true => start_height,
227 false => {
228 let cdn_endpoint = Self::parse_cdn::<N>()?;
230 let new_start_height = Self::scan_from_cdn(
232 start_height,
233 end_height,
234 &cdn_endpoint,
235 endpoint,
236 private_key,
237 *view_key,
238 address_x_coordinate,
239 records.clone(),
240 )?;
241
242 new_start_height.max(start_height)
244 }
245 };
246
247 while request_start <= end_height {
249 let percentage_complete = request_start.saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
251 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
252 stdout().flush()?;
253
254 let num_blocks_to_request =
255 std::cmp::min(MAX_BLOCK_RANGE, end_height.saturating_sub(request_start).saturating_add(1));
256 let request_end = request_start.saturating_add(num_blocks_to_request);
257
258 let blocks_endpoint =
260 Developer::build_endpoint::<N>(endpoint, &format!("blocks?start={request_start}&end={request_end}"))?;
261 let blocks: Vec<Block<N>> = ureq::get(&blocks_endpoint).call()?.into_body().read_json()?;
263
264 for block in &blocks {
266 Self::scan_block(block, endpoint, private_key, view_key, &address_x_coordinate, records.clone())
267 .with_context(|| format!("Failed to parse block {}", block.hash()))?;
268 }
269
270 request_start = request_start.saturating_add(num_blocks_to_request);
271 }
272
273 println!("\rScanning {total_blocks} blocks for records (100% complete)... \n");
275 stdout().flush()?;
276
277 let result = records.read().clone();
278 Ok(result)
279 }
280
281 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
283 fn scan_from_cdn<N: Network>(
284 start_height: u32,
285 end_height: u32,
286 cdn: &Uri,
287 endpoint: &Uri,
288 private_key: Option<PrivateKey<N>>,
289 view_key: ViewKey<N>,
290 address_x_coordinate: Field<N>,
291 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
292 ) -> Result<u32> {
293 let total_blocks = end_height.saturating_sub(start_height);
295
296 let cdn_request_start = start_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
298 let cdn_request_end = end_height.saturating_sub(end_height % MAX_BLOCK_RANGE).saturating_add(MAX_BLOCK_RANGE);
299
300 let rt = tokio::runtime::Runtime::new()?;
302
303 let _shutdown = Default::default();
305
306 let endpoint = endpoint.clone();
308
309 rt.block_on(async move {
311 let result =
312 snarkos_node_cdn::load_blocks(cdn, cdn_request_start, Some(cdn_request_end), _shutdown, move |block| {
313 if block.height() < start_height || block.height() > end_height {
315 return Ok(());
316 }
317
318 let percentage_complete =
320 block.height().saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
321 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
322 stdout().flush()?;
323
324 Self::scan_block(
326 &block,
327 &endpoint,
328 private_key,
329 &view_key,
330 &address_x_coordinate,
331 records.clone(),
332 )?;
333
334 Ok(())
335 })
336 .await;
337 match result {
338 Ok(height) => Ok(height),
339 Err(error) => {
340 eprintln!("Error loading blocks from CDN - (height, error):{error:?}");
341 Ok(error.0)
342 }
343 }
344 })
345 }
346
347 #[allow(clippy::type_complexity)]
349 fn scan_block<N: Network>(
350 block: &Block<N>,
351 endpoint: &Uri,
352 private_key: Option<PrivateKey<N>>,
353 view_key: &ViewKey<N>,
354 address_x_coordinate: &Field<N>,
355 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
356 ) -> Result<()> {
357 for (commitment, ciphertext_record) in block.records() {
358 if ciphertext_record.is_owner_with_address_x_coordinate(view_key, address_x_coordinate) {
360 if let Some(record) =
362 Self::decrypt_record(private_key, view_key, endpoint, *commitment, ciphertext_record)
363 .with_context(|| "Failed to decrypt record")?
364 {
365 records.write().push(record);
366 }
367 }
368 }
369
370 Ok(())
371 }
372
373 fn decrypt_record<N: Network>(
375 private_key: Option<PrivateKey<N>>,
376 view_key: &ViewKey<N>,
377 endpoint: &Uri,
378 commitment: Field<N>,
379 ciphertext_record: &Record<N, Ciphertext<N>>,
380 ) -> Result<Option<Record<N, Plaintext<N>>>> {
381 if let Some(private_key) = private_key {
383 let serial_number = Record::<N, Plaintext<N>>::serial_number(private_key, commitment)?;
385
386 let endpoint = Developer::build_endpoint::<N>(endpoint, &format!("find/transitionID/{serial_number}"))?;
388
389 match ureq::get(&endpoint).call() {
391 Ok(_) => Ok(None),
393 Err(_error) => {
395 Ok(Some(ciphertext_record.decrypt(view_key)?))
400 }
401 }
402 } else {
403 Ok(Some(ciphertext_record.decrypt(view_key)?))
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use snarkvm::prelude::{MainnetV0, TestRng};
413
414 type CurrentNetwork = MainnetV0;
415
416 #[test]
417 fn test_parse_account() {
418 let rng = &mut TestRng::default();
419
420 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
422 let view_key = ViewKey::try_from(private_key).unwrap();
423
424 let config = Scan::try_parse_from(
426 ["snarkos", "--private-key", &format!("{private_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
427 )
428 .unwrap();
429 assert!(config.parse_account::<CurrentNetwork>().is_ok());
430
431 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
432 assert_eq!(result_pkey, Some(private_key));
433 assert_eq!(result_vkey, view_key);
434
435 let err = Scan::try_parse_from(["snarkos", "--view-key", "", "--last", "10", "--endpoint", "localhost"].iter())
438 .unwrap_err();
439 assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
440
441 let config = Scan::try_parse_from(
443 ["snarkos", "--view-key", &format!("{view_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
444 )
445 .unwrap();
446
447 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
448 assert_eq!(result_pkey, None);
449 assert_eq!(result_vkey, view_key);
450
451 let err = Scan::try_parse_from(
453 [
454 "snarkos",
455 "--private-key",
456 &format!("{private_key}"),
457 "--view-key",
458 &format!("{view_key}"),
459 "--last",
460 "10",
461 "--endpoint",
462 "localhost",
463 ]
464 .iter(),
465 )
466 .unwrap_err();
467
468 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
469 }
470
471 #[test]
472 fn test_parse_block_range() -> Result<()> {
473 const TEST_VIEW_KEY: &str = "AViewKey1qQVfici7WarfXgmq9iuH8tzRcrWtb8qYq1pEyRRE4kS7";
475
476 let config = Scan::try_parse_from(
477 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--end", "10", "--endpoint", "localhost"].iter(),
478 )?;
479
480 let endpoint = Uri::default();
481 config.parse_block_range::<CurrentNetwork>(&endpoint).with_context(|| "Failed to parse block range")?;
482
483 let config = Scan::try_parse_from(
485 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "10", "--end", "5", "--endpoint", "localhost"].iter(),
486 )?;
487
488 let endpoint = Uri::default();
489 assert!(config.parse_block_range::<CurrentNetwork>(&endpoint).is_err());
490
491 let err = Scan::try_parse_from(
493 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--last", "10", "--endpoint=localhost"].iter(),
494 )
495 .unwrap_err();
496
497 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
498
499 let err = Scan::try_parse_from(
501 ["snarkos", "--view-key", TEST_VIEW_KEY, "--end", "10", "--last", "10", "--endpoint", "localhost"].iter(),
502 )
503 .unwrap_err();
504
505 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
506
507 let err = Scan::try_parse_from(
509 [
510 "snarkos",
511 "--view-key",
512 TEST_VIEW_KEY,
513 "--start",
514 "0",
515 "--end",
516 "01",
517 "--last",
518 "10",
519 "--endpoint",
520 "localhost",
521 ]
522 .iter(),
523 )
524 .unwrap_err();
525
526 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
527
528 Ok(())
529 }
530}