1use super::DEFAULT_ENDPOINT;
17use crate::helpers::{args::prepare_endpoint, dev::get_development_key};
18
19use snarkos_node_cdn::CDN_BASE_URL;
20use snarkvm::{
21 console::network::Network,
22 prelude::{Ciphertext, Field, FromBytes, Plaintext, PrivateKey, Record, ViewKey, block::Block},
23};
24
25use anyhow::{Context, Result, bail, ensure};
26use clap::{Parser, builder::NonEmptyStringValueParser};
27#[cfg(feature = "locktick")]
28use locktick::parking_lot::RwLock;
29#[cfg(not(feature = "locktick"))]
30use parking_lot::RwLock;
31use std::{
32 io::{Write, stdout},
33 str::FromStr,
34 sync::Arc,
35};
36use ureq::http::Uri;
37use zeroize::Zeroize;
38
39const MAX_BLOCK_RANGE: u32 = 50;
40
41#[derive(Debug, Parser)]
43#[clap(
44 group(clap::ArgGroup::new("key").required(true).multiple(false))
47)]
48pub struct Scan {
49 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
51 private_key: Option<String>,
52
53 #[clap(short, long, group = "key", value_parser=NonEmptyStringValueParser::default())]
56 view_key: Option<String>,
57
58 #[clap(long, group = "key")]
60 dev_key: Option<u16>,
61
62 #[clap(long, conflicts_with = "last")]
65 start: Option<u32>,
66
67 #[clap(long, conflicts_with = "last")]
70 end: Option<u32>,
71
72 #[clap(long)]
74 last: Option<u32>,
75
76 #[clap(long, default_value = DEFAULT_ENDPOINT)]
78 endpoint: Uri,
79
80 #[clap(long)]
82 verbosity: Option<u8>,
83}
84
85impl Drop for Scan {
86 fn drop(&mut self) {
88 if let Some(mut pk) = self.private_key.take() {
89 pk.zeroize()
90 }
91 }
92}
93
94impl Scan {
95 pub fn parse<N: Network>(self) -> Result<String> {
97 let endpoint = prepare_endpoint(self.endpoint.clone())?;
98
99 let (private_key, view_key) = self.parse_account::<N>()?;
101
102 let (start_height, end_height) = self.parse_block_range::<N>(&endpoint)?;
104
105 let records = Self::fetch_records::<N>(private_key, &view_key, &endpoint, start_height, end_height)?;
107
108 if records.is_empty() {
110 Ok("No records found".to_string())
111 } else {
112 if private_key.is_none() {
113 println!("⚠️ This list may contain records that have already been spent.\n");
114 }
115
116 Ok(serde_json::to_string_pretty(&records)?.replace("\\n", ""))
117 }
118 }
119
120 fn parse_account<N: Network>(&self) -> Result<(Option<PrivateKey<N>>, ViewKey<N>)> {
122 if let Some(private_key) = &self.private_key {
123 let private_key = PrivateKey::<N>::from_str(private_key)?;
124 let view_key = ViewKey::<N>::try_from(private_key)?;
125 Ok((Some(private_key), view_key))
126 } else if let Some(index) = &self.dev_key {
127 let private_key = get_development_key(*index)?;
128 let view_key = ViewKey::<N>::try_from(private_key)?;
129 Ok((Some(private_key), view_key))
130 } else if let Some(view_key) = &self.view_key {
131 Ok((None, ViewKey::<N>::from_str(view_key)?))
132 } else {
133 unreachable!();
135 }
136 }
137
138 fn parse_block_range<N: Network>(&self, endpoint: &Uri) -> Result<(u32, u32)> {
140 match (self.start, self.end, self.last) {
141 (Some(start), Some(end), None) => {
142 ensure!(end > start, "The given scan range is invalid (start = {start}, end = {end})");
143
144 Ok((start, end))
145 }
146 (Some(start), None, None) => {
147 let endpoint = format!("{endpoint}{}/block/height/latest", N::SHORT_NAME);
149 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_body().read_to_string()?)?;
150
151 if start == 0 {
153 println!("⚠️ Attention - Scanning the entire chain. This may take a while...\n");
154 }
155
156 Ok((start, latest_height))
157 }
158 (None, Some(end), None) => Ok((0, end)),
159 (None, None, Some(last)) => {
160 let endpoint = format!("{endpoint}{}/block/height/latest", N::SHORT_NAME);
162 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_body().read_to_string()?)?;
163
164 Ok((latest_height.saturating_sub(last), latest_height))
165 }
166 (None, None, None) => bail!("Missing data about block range."),
167 _ => bail!("`last` flags can't be used with `start` or `end`"),
168 }
169 }
170
171 fn parse_cdn<N: Network>() -> Result<Uri> {
173 Uri::try_from(format!("{CDN_BASE_URL}/{}", N::SHORT_NAME)).with_context(|| "Unexpected error")
175 }
176
177 fn fetch_records<N: Network>(
179 private_key: Option<PrivateKey<N>>,
180 view_key: &ViewKey<N>,
181 endpoint: &Uri,
182 start_height: u32,
183 end_height: u32,
184 ) -> Result<Vec<Record<N, Plaintext<N>>>> {
185 if start_height > end_height {
187 bail!("Invalid block range");
188 }
189
190 let address_x_coordinate = view_key.to_address().to_x_coordinate();
192
193 let records = Arc::new(RwLock::new(Vec::new()));
195
196 let total_blocks = end_height.saturating_sub(start_height);
198
199 print!("\rScanning {total_blocks} blocks for records (0% complete)...");
201 stdout().flush()?;
202
203 let genesis_block: Block<N> =
205 ureq::get(&format!("{endpoint}{}/block/0", N::SHORT_NAME)).call()?.into_body().read_json()?;
206 let is_development_network = genesis_block != Block::from_bytes_le(N::genesis_bytes())?;
208
209 let mut request_start = match is_development_network {
211 true => start_height,
212 false => {
213 let cdn_endpoint = Self::parse_cdn::<N>()?;
215 Self::scan_from_cdn(
217 start_height,
218 end_height,
219 &cdn_endpoint,
220 endpoint,
221 private_key,
222 *view_key,
223 address_x_coordinate,
224 records.clone(),
225 )?;
226
227 end_height.saturating_sub(start_height % MAX_BLOCK_RANGE)
229 }
230 };
231
232 while request_start <= end_height {
234 let percentage_complete = request_start.saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
236 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
237 stdout().flush()?;
238
239 let num_blocks_to_request =
240 std::cmp::min(MAX_BLOCK_RANGE, end_height.saturating_sub(request_start).saturating_add(1));
241 let request_end = request_start.saturating_add(num_blocks_to_request);
242
243 let blocks_endpoint = format!("{endpoint}{}/blocks?start={request_start}&end={request_end}", N::SHORT_NAME);
245 let blocks: Vec<Block<N>> = ureq::get(&blocks_endpoint).call()?.into_body().read_json()?;
247
248 for block in &blocks {
250 Self::scan_block(block, endpoint, private_key, view_key, &address_x_coordinate, records.clone())
251 .with_context(|| format!("Failed to parse block {}", block.hash()))?;
252 }
253
254 request_start = request_start.saturating_add(num_blocks_to_request);
255 }
256
257 println!("\rScanning {total_blocks} blocks for records (100% complete)... \n");
259 stdout().flush()?;
260
261 let result = records.read().clone();
262 Ok(result)
263 }
264
265 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
267 fn scan_from_cdn<N: Network>(
268 start_height: u32,
269 end_height: u32,
270 cdn: &Uri,
271 endpoint: &Uri,
272 private_key: Option<PrivateKey<N>>,
273 view_key: ViewKey<N>,
274 address_x_coordinate: Field<N>,
275 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
276 ) -> Result<()> {
277 let total_blocks = end_height.saturating_sub(start_height);
279
280 let cdn_request_start = start_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
282 let cdn_request_end = end_height.saturating_sub(start_height % MAX_BLOCK_RANGE);
283
284 let rt = tokio::runtime::Runtime::new()?;
286
287 let _shutdown = Default::default();
289
290 let endpoint = endpoint.clone();
292
293 rt.block_on(async move {
295 let result =
296 snarkos_node_cdn::load_blocks(cdn, cdn_request_start, Some(cdn_request_end), _shutdown, move |block| {
297 if block.height() < start_height || block.height() > end_height {
299 return Ok(());
300 }
301
302 let percentage_complete =
304 block.height().saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
305 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
306 stdout().flush()?;
307
308 Self::scan_block(
310 &block,
311 &endpoint,
312 private_key,
313 &view_key,
314 &address_x_coordinate,
315 records.clone(),
316 )?;
317
318 Ok(())
319 })
320 .await;
321 if let Err(error) = result {
322 eprintln!("Error loading blocks from CDN - (height, error):{error:?}");
323 }
324 });
325
326 Ok(())
327 }
328
329 #[allow(clippy::type_complexity)]
331 fn scan_block<N: Network>(
332 block: &Block<N>,
333 endpoint: &Uri,
334 private_key: Option<PrivateKey<N>>,
335 view_key: &ViewKey<N>,
336 address_x_coordinate: &Field<N>,
337 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
338 ) -> Result<()> {
339 for (commitment, ciphertext_record) in block.records() {
340 if ciphertext_record.is_owner_with_address_x_coordinate(view_key, address_x_coordinate) {
342 if let Some(record) =
344 Self::decrypt_record(private_key, view_key, endpoint, *commitment, ciphertext_record)
345 .with_context(|| "Failed to decrypt record")?
346 {
347 records.write().push(record);
348 }
349 }
350 }
351
352 Ok(())
353 }
354
355 fn decrypt_record<N: Network>(
357 private_key: Option<PrivateKey<N>>,
358 view_key: &ViewKey<N>,
359 endpoint: &Uri,
360 commitment: Field<N>,
361 ciphertext_record: &Record<N, Ciphertext<N>>,
362 ) -> Result<Option<Record<N, Plaintext<N>>>> {
363 if let Some(private_key) = private_key {
365 let serial_number = Record::<N, Plaintext<N>>::serial_number(private_key, commitment)?;
367
368 let endpoint = format!("{endpoint}{}/find/transitionID/{serial_number}", N::SHORT_NAME);
370
371 match ureq::get(&endpoint).call() {
373 Ok(_) => Ok(None),
375 Err(_error) => {
377 Ok(Some(ciphertext_record.decrypt(view_key)?))
382 }
383 }
384 } else {
385 Ok(Some(ciphertext_record.decrypt(view_key)?))
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use snarkvm::prelude::{MainnetV0, TestRng};
395
396 type CurrentNetwork = MainnetV0;
397
398 #[test]
399 fn test_parse_account() {
400 let rng = &mut TestRng::default();
401
402 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
404 let view_key = ViewKey::try_from(private_key).unwrap();
405
406 let config = Scan::try_parse_from(
408 ["snarkos", "--private-key", &format!("{private_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
409 )
410 .unwrap();
411 assert!(config.parse_account::<CurrentNetwork>().is_ok());
412
413 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
414 assert_eq!(result_pkey, Some(private_key));
415 assert_eq!(result_vkey, view_key);
416
417 let err = Scan::try_parse_from(["snarkos", "--view-key", "", "--last", "10", "--endpoint", "localhost"].iter())
420 .unwrap_err();
421 assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
422
423 let config = Scan::try_parse_from(
425 ["snarkos", "--view-key", &format!("{view_key}"), "--last", "10", "--endpoint", "localhost"].iter(),
426 )
427 .unwrap();
428
429 let (result_pkey, result_vkey) = config.parse_account::<CurrentNetwork>().unwrap();
430 assert_eq!(result_pkey, None);
431 assert_eq!(result_vkey, view_key);
432
433 let err = Scan::try_parse_from(
435 [
436 "snarkos",
437 "--private-key",
438 &format!("{private_key}"),
439 "--view-key",
440 &format!("{view_key}"),
441 "--last",
442 "10",
443 "--endpoint",
444 "localhost",
445 ]
446 .iter(),
447 )
448 .unwrap_err();
449
450 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
451 }
452
453 #[test]
454 fn test_parse_block_range() -> Result<()> {
455 const TEST_VIEW_KEY: &str = "AViewKey1qQVfici7WarfXgmq9iuH8tzRcrWtb8qYq1pEyRRE4kS7";
457
458 let config = Scan::try_parse_from(
459 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--end", "10", "--endpoint", "localhost"].iter(),
460 )?;
461
462 let endpoint = Uri::default();
463 config.parse_block_range::<CurrentNetwork>(&endpoint).with_context(|| "Failed to parse block range")?;
464
465 let config = Scan::try_parse_from(
467 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "10", "--end", "5", "--endpoint", "localhost"].iter(),
468 )?;
469
470 let endpoint = Uri::default();
471 assert!(config.parse_block_range::<CurrentNetwork>(&endpoint).is_err());
472
473 let err = Scan::try_parse_from(
475 ["snarkos", "--view-key", TEST_VIEW_KEY, "--start", "0", "--last", "10", "--endpoint=localhost"].iter(),
476 )
477 .unwrap_err();
478
479 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
480
481 let err = Scan::try_parse_from(
483 ["snarkos", "--view-key", TEST_VIEW_KEY, "--end", "10", "--last", "10", "--endpoint", "localhost"].iter(),
484 )
485 .unwrap_err();
486
487 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
488
489 let err = Scan::try_parse_from(
491 [
492 "snarkos",
493 "--view-key",
494 TEST_VIEW_KEY,
495 "--start",
496 "0",
497 "--end",
498 "01",
499 "--last",
500 "10",
501 "--endpoint",
502 "localhost",
503 ]
504 .iter(),
505 )
506 .unwrap_err();
507
508 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
509
510 Ok(())
511 }
512}