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 snarkos_utilities::SimpleStoppable;
24
25use snarkvm::{
26 console::network::Network,
27 prelude::{Ciphertext, Field, Plaintext, PrivateKey, Record, ViewKey, block::Block},
28};
29
30#[cfg(not(feature = "test_targets"))]
31use snarkvm::prelude::FromBytes;
32
33use anyhow::{Context, Result, anyhow, bail, ensure};
34use clap::{Parser, builder::NonEmptyStringValueParser};
35#[cfg(feature = "locktick")]
36use locktick::parking_lot::RwLock;
37#[cfg(not(feature = "locktick"))]
38use parking_lot::RwLock;
39use std::{
40 io::{Write, stdout},
41 str::FromStr,
42 sync::Arc,
43};
44use tracing::debug;
45use ureq::http::Uri;
46use zeroize::Zeroize;
47
48const MAX_BLOCK_RANGE: u32 = 50;
49
50#[derive(Debug, Parser)]
52#[clap(
53 group(clap::ArgGroup::new("key").required(true).multiple(false))
56)]
57pub struct Scan {
58 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
60 private_key: Option<String>,
61
62 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
65 view_key: Option<String>,
66
67 #[clap(long, group = "key")]
69 dev_key: Option<u16>,
70
71 #[clap(long, conflicts_with = "last")]
74 start: Option<u32>,
75
76 #[clap(long, conflicts_with = "last")]
79 end: Option<u32>,
80
81 #[clap(long)]
83 last: Option<u32>,
84
85 #[clap(long, default_value = DEFAULT_ENDPOINT)]
87 endpoint: Uri,
88
89 #[clap(long)]
91 verbosity: Option<u8>,
92}
93
94impl Drop for Scan {
95 fn drop(&mut self) {
97 if let Some(mut pk) = self.private_key.take() {
98 pk.zeroize()
99 }
100 }
101}
102
103impl Scan {
104 pub fn parse<N: Network>(self) -> Result<String> {
106 let endpoint = prepare_endpoint(self.endpoint.clone())?;
107
108 let (private_key, view_key) = self.parse_account::<N>()?;
110
111 let (start_height, end_height) = self.parse_block_range::<N>(&endpoint)?;
113
114 let records = Self::fetch_records::<N>(private_key, &view_key, &endpoint, start_height, end_height)
116 .with_context(|| "Failed to fetch records")?;
117
118 if records.is_empty() {
120 Ok("No records found".to_string())
121 } else {
122 if private_key.is_none() {
123 println!("⚠️ This list may contain records that have already been spent.\n");
124 }
125
126 Ok(serde_json::to_string_pretty(&records)?.replace("\\n", ""))
127 }
128 }
129
130 fn parse_account<N: Network>(&self) -> Result<(Option<PrivateKey<N>>, ViewKey<N>)> {
132 if let Some(private_key) = &self.private_key {
133 let private_key = PrivateKey::<N>::from_str(private_key)?;
134 let view_key = ViewKey::<N>::try_from(private_key)?;
135 Ok((Some(private_key), view_key))
136 } else if let Some(index) = &self.dev_key {
137 let private_key = get_development_key(*index)?;
138 let view_key = ViewKey::<N>::try_from(private_key)?;
139 Ok((Some(private_key), view_key))
140 } else if let Some(view_key) = &self.view_key {
141 Ok((None, ViewKey::<N>::from_str(view_key)?))
142 } else {
143 unreachable!();
145 }
146 }
147
148 fn parse_block_range<N: Network>(&self, endpoint: &Uri) -> Result<(u32, u32)> {
150 let end = if let Some(end) = self.end {
152 end
153 } else {
154 let (endpoint, _api_version) = Developer::build_endpoint::<N>(endpoint, "block/height/latest")?;
156 let result = ureq::get(&endpoint).call().map_err(|e| e.into());
157 let end: u32 = Developer::handle_ureq_result(result)
158 .and_then(|body| body.ok_or(anyhow!("Endpoint returned 404 for latest block height")))?
159 .read_to_string()?
160 .parse()?;
161
162 debug!("Set end height to {end} based on latest block height of the endpoint");
163 end
164 };
165
166 let start = if let Some(start) = self.start {
168 start
169 } else if let Some(last) = self.last {
170 let start = end.saturating_sub(last);
171 debug!("Setting start height to {start} (based on last={last} and end={end})");
172 start
173 } else {
174 debug!("Picking default value (0) for start height");
175 0
176 };
177
178 ensure!(end > start, "The given scan range is invalid (start = {start}, end = {end})");
179
180 if start == 0 && self.end.is_none() {
182 println!("⚠️ Attention - Scanning the entire chain. This may take a while...\n");
183 }
184
185 Ok((start, end))
186 }
187
188 fn parse_cdn<N: Network>() -> Result<Uri> {
190 Uri::try_from(format!("{CDN_BASE_URL}/{}", N::SHORT_NAME)).with_context(|| "Unexpected error")
192 }
193
194 fn fetch_records<N: Network>(
196 private_key: Option<PrivateKey<N>>,
197 view_key: &ViewKey<N>,
198 endpoint: &Uri,
199 start_height: u32,
200 end_height: u32,
201 ) -> Result<Vec<Record<N, Plaintext<N>>>> {
202 if start_height > end_height {
204 bail!("Invalid block range. Start height ({start_height}) is not smaller than end height ({end_height}).");
205 }
206
207 let address_x_coordinate = view_key.to_address().to_x_coordinate();
209
210 let records = Arc::new(RwLock::new(Vec::new()));
212
213 let total_blocks = end_height.saturating_sub(start_height);
215
216 print!("\rScanning {total_blocks} blocks for records (0% complete)...");
218 stdout().flush()?;
219
220 #[cfg(feature = "test_targets")]
222 let is_development_network = true;
223
224 #[cfg(not(feature = "test_targets"))]
226 let is_development_network = {
227 let endpoint_genesis_block: Block<N> = match Developer::http_get_json::<N, _>(endpoint, "block/0")? {
229 Some(block) => block,
230 None => bail!("Enpoint returend 404 for genesis block"),
231 };
232
233 endpoint_genesis_block != Block::from_bytes_le(N::genesis_bytes())?
235 };
236
237 let mut request_start = match is_development_network {
239 true => start_height,
240 false => {
241 let cdn_endpoint = Self::parse_cdn::<N>()?;
243 let new_start_height = Self::scan_from_cdn(
245 start_height,
246 end_height,
247 &cdn_endpoint,
248 endpoint,
249 private_key,
250 *view_key,
251 address_x_coordinate,
252 records.clone(),
253 )?;
254
255 new_start_height.max(start_height)
257 }
258 };
259
260 while request_start <= end_height {
262 let percentage_complete = request_start.saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
264 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
265 stdout().flush()?;
266
267 let num_blocks_to_request =
268 std::cmp::min(MAX_BLOCK_RANGE, end_height.saturating_sub(request_start).saturating_add(1));
269 let request_end = request_start.saturating_add(num_blocks_to_request);
270
271 let blocks: Vec<Block<N>> =
273 Developer::http_get_json::<N, _>(endpoint, &format!("blocks?start={request_start}&end={request_end}"))
274 .and_then(|blocks| blocks.ok_or(anyhow!("Enpoint returend 404 for the specified block range")))
275 .with_context(|| format!("Failed to fetch blocks range {request_start}..{request_end}"))?;
276
277 for block in &blocks {
279 Self::scan_block(block, endpoint, private_key, view_key, &address_x_coordinate, records.clone())
280 .with_context(|| format!("Failed to parse block {}", block.hash()))?;
281 }
282
283 request_start = request_start.saturating_add(num_blocks_to_request);
284 }
285
286 println!("\rScanning {total_blocks} blocks for records (100% complete)... \n");
288 stdout().flush()?;
289
290 let result = records.read().clone();
291 Ok(result)
292 }
293
294 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
296 fn scan_from_cdn<N: Network>(
297 start_height: u32,
298 end_height: u32,
299 cdn: &Uri,
300 endpoint: &Uri,
301 private_key: Option<PrivateKey<N>>,
302 view_key: ViewKey<N>,
303 address_x_coordinate: Field<N>,
304 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
305 ) -> Result<u32> {
306 let total_blocks = end_height.saturating_sub(start_height);
308
309 let cdn_request_start = start_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
311 let cdn_request_end = end_height.saturating_sub(end_height % MAX_BLOCK_RANGE).saturating_add(MAX_BLOCK_RANGE);
312
313 let rt = tokio::runtime::Runtime::new()?;
315
316 let _shutdown = SimpleStoppable::new();
318
319 let endpoint = endpoint.clone();
321
322 rt.block_on(async move {
324 let result =
325 snarkos_node_cdn::load_blocks(cdn, cdn_request_start, Some(cdn_request_end), _shutdown, move |block| {
326 if block.height() < start_height || block.height() > end_height {
328 return Ok(());
329 }
330
331 let percentage_complete =
333 block.height().saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
334 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
335 stdout().flush()?;
336
337 Self::scan_block(
339 &block,
340 &endpoint,
341 private_key,
342 &view_key,
343 &address_x_coordinate,
344 records.clone(),
345 )?;
346
347 Ok(())
348 })
349 .await;
350 match result {
351 Ok(height) => Ok(height),
352 Err(error) => {
353 eprintln!("Error loading blocks from CDN - (height, error):{error:?}");
354 Ok(error.0)
355 }
356 }
357 })
358 }
359
360 #[allow(clippy::type_complexity)]
362 fn scan_block<N: Network>(
363 block: &Block<N>,
364 endpoint: &Uri,
365 private_key: Option<PrivateKey<N>>,
366 view_key: &ViewKey<N>,
367 address_x_coordinate: &Field<N>,
368 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
369 ) -> Result<()> {
370 for (commitment, ciphertext_record) in block.records() {
371 if ciphertext_record.is_owner_with_address_x_coordinate(view_key, address_x_coordinate) {
373 if let Some(record) =
375 Self::decrypt_record(private_key, view_key, endpoint, *commitment, ciphertext_record)
376 .with_context(|| "Failed to decrypt record")?
377 {
378 records.write().push(record);
379 }
380 }
381 }
382
383 Ok(())
384 }
385
386 fn decrypt_record<N: Network>(
388 private_key: Option<PrivateKey<N>>,
389 view_key: &ViewKey<N>,
390 endpoint: &Uri,
391 commitment: Field<N>,
392 ciphertext_record: &Record<N, Ciphertext<N>>,
393 ) -> Result<Option<Record<N, Plaintext<N>>>> {
394 if let Some(private_key) = private_key {
396 let serial_number = Record::<N, Plaintext<N>>::serial_number(private_key, commitment)?;
398
399 let (endpoint, _api_version) =
401 Developer::build_endpoint::<N>(endpoint, &format!("find/transitionID/{serial_number}"))?;
402
403 match ureq::get(&endpoint).call() {
405 Ok(_) => Ok(None),
407 Err(_error) => {
409 Ok(Some(ciphertext_record.decrypt(view_key)?))
414 }
415 }
416 } else {
417 Ok(Some(ciphertext_record.decrypt(view_key)?))
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use snarkvm::prelude::{MainnetV0, TestRng};
427
428 type CurrentNetwork = MainnetV0;
429
430 #[test]
431 fn test_parse_account() {
432 let rng = &mut TestRng::default();
433
434 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
436 let view_key = ViewKey::try_from(private_key).unwrap();
437
438 let config = Scan::try_parse_from(
440 ["snarkos", "--private-key", &format!("{private_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
441 )
442 .unwrap();
443 assert!(config.parse_account::<CurrentNetwork>().is_ok());
444
445 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
446 assert_eq!(result_pkey, Some(private_key));
447 assert_eq!(result_vkey, view_key);
448
449 let err = Scan::try_parse_from(["snarkos", "--view-key", "", "--last", "10", "--endpoint", "localhost"].iter())
452 .unwrap_err();
453 assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
454
455 let config = Scan::try_parse_from(
457 ["snarkos", "--view-key", &format!("{view_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
458 )
459 .unwrap();
460
461 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
462 assert_eq!(result_pkey, None);
463 assert_eq!(result_vkey, view_key);
464
465 let err = Scan::try_parse_from(
467 [
468 "snarkos",
469 "--private-key",
470 &format!("{private_key}"),
471 "--view-key",
472 &format!("{view_key}"),
473 "--last",
474 "10",
475 "--endpoint",
476 "localhost",
477 ]
478 .iter(),
479 )
480 .unwrap_err();
481
482 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
483 }
484
485 #[test]
486 fn test_parse_block_range() -> Result<()> {
487 const TEST_VIEW_KEY: &str = "AViewKey1qQVfici7WarfXgmq9iuH8tzRcrWtb8qYq1pEyRRE4kS7";
489
490 let config = Scan::try_parse_from(
491 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--end", "10", "--endpoint", "localhost"].iter(),
492 )?;
493
494 let endpoint = Uri::default();
495 config.parse_block_range::<CurrentNetwork>(&endpoint).with_context(|| "Failed to parse block range")?;
496
497 let config = Scan::try_parse_from(
499 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "10", "--end", "5", "--endpoint", "localhost"].iter(),
500 )?;
501
502 let endpoint = Uri::default();
503 assert!(config.parse_block_range::<CurrentNetwork>(&endpoint).is_err());
504
505 let err = Scan::try_parse_from(
507 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--last", "10", "--endpoint=localhost"].iter(),
508 )
509 .unwrap_err();
510
511 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
512
513 let err = Scan::try_parse_from(
515 ["snarkos", "--view-key", TEST_VIEW_KEY, "--end", "10", "--last", "10", "--endpoint", "localhost"].iter(),
516 )
517 .unwrap_err();
518
519 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
520
521 let err = Scan::try_parse_from(
523 [
524 "snarkos",
525 "--view-key",
526 TEST_VIEW_KEY,
527 "--start",
528 "0",
529 "--end",
530 "01",
531 "--last",
532 "10",
533 "--endpoint",
534 "localhost",
535 ]
536 .iter(),
537 )
538 .unwrap_err();
539
540 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
541
542 Ok(())
543 }
544}