1#![allow(clippy::type_complexity)]
17
18use snarkos_node_cdn::CDN_BASE_URL;
19use snarkvm::{
20 console::network::{CanaryV0, MainnetV0, Network, TestnetV0},
21 prelude::{Ciphertext, Field, FromBytes, Plaintext, PrivateKey, Record, ViewKey, block::Block},
22};
23
24use anyhow::{Result, bail, ensure};
25use clap::Parser;
26#[cfg(feature = "locktick")]
27use locktick::parking_lot::RwLock;
28#[cfg(not(feature = "locktick"))]
29use parking_lot::RwLock;
30use std::{
31 io::{Write, stdout},
32 str::FromStr,
33 sync::Arc,
34};
35use zeroize::Zeroize;
36
37const MAX_BLOCK_RANGE: u32 = 50;
38
39#[derive(Debug, Parser, Zeroize)]
41pub struct Scan {
42 #[clap(default_value = "0", long = "network")]
44 pub network: u16,
45
46 #[clap(short, long)]
48 private_key: Option<String>,
49
50 #[clap(short, long)]
52 view_key: Option<String>,
53
54 #[clap(long, conflicts_with = "last")]
56 start: Option<u32>,
57
58 #[clap(long, conflicts_with = "last")]
60 end: Option<u32>,
61
62 #[clap(long)]
64 last: Option<u32>,
65
66 #[clap(long, default_value = "https://api.explorer.provable.com/v1")]
68 endpoint: String,
69}
70
71impl Scan {
72 pub fn parse(self) -> Result<String> {
73 match self.network {
75 MainnetV0::ID => self.scan_records::<MainnetV0>(),
76 TestnetV0::ID => self.scan_records::<TestnetV0>(),
77 CanaryV0::ID => self.scan_records::<CanaryV0>(),
78 unknown_id => bail!("Unknown network ID ({unknown_id})"),
79 }
80 }
81
82 fn scan_records<N: Network>(&self) -> Result<String> {
84 let (private_key, view_key) = self.parse_account::<N>()?;
86
87 let (start_height, end_height) = self.parse_block_range()?;
89
90 let records = Self::fetch_records::<N>(private_key, &view_key, &self.endpoint, start_height, end_height)?;
92
93 if records.is_empty() {
95 Ok("No records found".to_string())
96 } else {
97 if private_key.is_none() {
98 println!("⚠️ This list may contain records that have already been spent.\n");
99 }
100
101 Ok(serde_json::to_string_pretty(&records)?.replace("\\n", ""))
102 }
103 }
104
105 fn parse_account<N: Network>(&self) -> Result<(Option<PrivateKey<N>>, ViewKey<N>)> {
107 match (&self.private_key, &self.view_key) {
108 (Some(private_key), Some(view_key)) => {
109 let private_key = PrivateKey::<N>::from_str(private_key)?;
111 let expected_view_key = ViewKey::<N>::try_from(private_key)?;
113 let view_key = ViewKey::<N>::from_str(view_key)?;
115
116 ensure!(
117 expected_view_key == view_key,
118 "The provided private key does not correspond to the provided view key."
119 );
120
121 Ok((Some(private_key), view_key))
122 }
123 (Some(private_key), _) => {
124 let private_key = PrivateKey::<N>::from_str(private_key)?;
126 let view_key = ViewKey::<N>::try_from(private_key)?;
128
129 Ok((Some(private_key), view_key))
130 }
131 (None, Some(view_key)) => Ok((None, ViewKey::<N>::from_str(view_key)?)),
132 (None, None) => bail!("Missing private key or view key."),
133 }
134 }
135
136 fn parse_block_range(&self) -> Result<(u32, u32)> {
138 let network = match self.network {
140 MainnetV0::ID => "mainnet",
141 TestnetV0::ID => "testnet",
142 CanaryV0::ID => "canary",
143 unknown_id => bail!("Unknown network ID ({unknown_id})"),
144 };
145
146 match (self.start, self.end, self.last) {
147 (Some(start), Some(end), None) => {
148 ensure!(end > start, "The given scan range is invalid (start = {start}, end = {end})");
149
150 Ok((start, end))
151 }
152 (Some(start), None, None) => {
153 let endpoint = format!("{}/{network}/block/height/latest", self.endpoint);
155 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_string()?)?;
156
157 if start == 0 {
159 println!("⚠️ Attention - Scanning the entire chain. This may take a while...\n");
160 }
161
162 Ok((start, latest_height))
163 }
164 (None, Some(end), None) => Ok((0, end)),
165 (None, None, Some(last)) => {
166 let endpoint = format!("{}/{network}/block/height/latest", self.endpoint);
168 let latest_height = u32::from_str(&ureq::get(&endpoint).call()?.into_string()?)?;
169
170 Ok((latest_height.saturating_sub(last), latest_height))
171 }
172 (None, None, None) => bail!("Missing data about block range."),
173 _ => bail!("`last` flags can't be used with `start` or `end`"),
174 }
175 }
176
177 fn parse_cdn<N: Network>() -> Result<String> {
179 match N::ID {
180 MainnetV0::ID => Ok(format!("{CDN_BASE_URL}/mainnet")),
181 TestnetV0::ID => Ok(format!("{CDN_BASE_URL}/testnet")),
182 CanaryV0::ID => Ok(format!("{CDN_BASE_URL}/canary")),
183 _ => bail!("Unknown network ID ({})", N::ID),
184 }
185 }
186
187 fn fetch_records<N: Network>(
189 private_key: Option<PrivateKey<N>>,
190 view_key: &ViewKey<N>,
191 endpoint: &str,
192 start_height: u32,
193 end_height: u32,
194 ) -> Result<Vec<Record<N, Plaintext<N>>>> {
195 if start_height > end_height {
197 bail!("Invalid block range");
198 }
199
200 let network = match N::ID {
202 MainnetV0::ID => "mainnet",
203 TestnetV0::ID => "testnet",
204 CanaryV0::ID => "canary",
205 unknown_id => bail!("Unknown network ID ({unknown_id})"),
206 };
207
208 let address_x_coordinate = view_key.to_address().to_x_coordinate();
210
211 let records = Arc::new(RwLock::new(Vec::new()));
213
214 let total_blocks = end_height.saturating_sub(start_height);
216
217 print!("\rScanning {total_blocks} blocks for records (0% complete)...");
219 stdout().flush()?;
220
221 let genesis_block: Block<N> = ureq::get(&format!("{endpoint}/{network}/block/0")).call()?.into_json()?;
223 let is_development_network = genesis_block != Block::from_bytes_le(N::genesis_bytes())?;
225
226 let mut request_start = match is_development_network {
228 true => start_height,
229 false => {
230 let cdn_endpoint = Self::parse_cdn::<N>()?;
232 Self::scan_from_cdn(
234 start_height,
235 end_height,
236 cdn_endpoint,
237 endpoint.to_string(),
238 private_key,
239 *view_key,
240 address_x_coordinate,
241 records.clone(),
242 )?;
243
244 end_height.saturating_sub(start_height % MAX_BLOCK_RANGE)
246 }
247 };
248
249 while request_start <= end_height {
251 let percentage_complete = request_start.saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
253 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
254 stdout().flush()?;
255
256 let num_blocks_to_request =
257 std::cmp::min(MAX_BLOCK_RANGE, end_height.saturating_sub(request_start).saturating_add(1));
258 let request_end = request_start.saturating_add(num_blocks_to_request);
259
260 let blocks_endpoint = format!("{endpoint}/{network}/blocks?start={request_start}&end={request_end}");
262 let blocks: Vec<Block<N>> = ureq::get(&blocks_endpoint).call()?.into_json()?;
264
265 for block in &blocks {
267 Self::scan_block(block, endpoint, private_key, view_key, &address_x_coordinate, records.clone())?;
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)]
283 fn scan_from_cdn<N: Network>(
284 start_height: u32,
285 end_height: u32,
286 cdn: String,
287 endpoint: String,
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<()> {
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(start_height % MAX_BLOCK_RANGE);
299
300 let rt = tokio::runtime::Runtime::new()?;
302
303 let _shutdown = Default::default();
305
306 rt.block_on(async move {
308 let result = snarkos_node_cdn::load_blocks(
309 &cdn,
310 cdn_request_start,
311 Some(cdn_request_end),
312 _shutdown,
313 move |block| {
314 if block.height() < start_height || block.height() > end_height {
316 return Ok(());
317 }
318
319 let percentage_complete =
321 block.height().saturating_sub(start_height) as f64 * 100.0 / total_blocks as f64;
322 print!("\rScanning {total_blocks} blocks for records ({percentage_complete:.2}% complete)...");
323 stdout().flush()?;
324
325 Self::scan_block(
327 &block,
328 &endpoint,
329 private_key,
330 &view_key,
331 &address_x_coordinate,
332 records.clone(),
333 )?;
334
335 Ok(())
336 },
337 )
338 .await;
339 if let Err(error) = result {
340 eprintln!("Error loading blocks from CDN - (height, error):{error:?}");
341 }
342 });
343
344 Ok(())
345 }
346
347 fn scan_block<N: Network>(
349 block: &Block<N>,
350 endpoint: &str,
351 private_key: Option<PrivateKey<N>>,
352 view_key: &ViewKey<N>,
353 address_x_coordinate: &Field<N>,
354 records: Arc<RwLock<Vec<Record<N, Plaintext<N>>>>>,
355 ) -> Result<()> {
356 for (commitment, ciphertext_record) in block.records() {
357 if ciphertext_record.is_owner_with_address_x_coordinate(view_key, address_x_coordinate) {
359 if let Some(record) =
361 Self::decrypt_record(private_key, view_key, endpoint, *commitment, ciphertext_record)?
362 {
363 records.write().push(record);
364 }
365 }
366 }
367
368 Ok(())
369 }
370
371 fn decrypt_record<N: Network>(
373 private_key: Option<PrivateKey<N>>,
374 view_key: &ViewKey<N>,
375 endpoint: &str,
376 commitment: Field<N>,
377 ciphertext_record: &Record<N, Ciphertext<N>>,
378 ) -> Result<Option<Record<N, Plaintext<N>>>> {
379 if let Some(private_key) = private_key {
381 let serial_number = Record::<N, Plaintext<N>>::serial_number(private_key, commitment)?;
383
384 let network = match N::ID {
386 MainnetV0::ID => "mainnet",
387 TestnetV0::ID => "testnet",
388 CanaryV0::ID => "canary",
389 unknown_id => bail!("Unknown network ID ({unknown_id})"),
390 };
391
392 let endpoint = format!("{endpoint}/{network}/find/transitionID/{serial_number}");
394
395 match ureq::get(&endpoint).call() {
397 Ok(_) => Ok(None),
399 Err(_error) => {
401 Ok(Some(ciphertext_record.decrypt(view_key)?))
406 }
407 }
408 } else {
409 Ok(Some(ciphertext_record.decrypt(view_key)?))
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use snarkvm::prelude::{MainnetV0, TestRng};
419
420 type CurrentNetwork = MainnetV0;
421
422 #[test]
423 fn test_parse_account() {
424 let rng = &mut TestRng::default();
425
426 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
428 let view_key = ViewKey::try_from(private_key).unwrap();
429
430 let unassociated_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
432 let unassociated_view_key = ViewKey::try_from(unassociated_private_key).unwrap();
433
434 let config = 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 "",
445 ]
446 .iter(),
447 )
448 .unwrap();
449 assert!(config.parse_account::<CurrentNetwork>().is_ok());
450
451 let config = Scan::try_parse_from(
452 [
453 "snarkos",
454 "--private-key",
455 &format!("{private_key}"),
456 "--view-key",
457 &format!("{unassociated_view_key}"),
458 "--last",
459 "10",
460 "--endpoint",
461 "",
462 ]
463 .iter(),
464 )
465 .unwrap();
466 assert!(config.parse_account::<CurrentNetwork>().is_err());
467 }
468
469 #[test]
470 fn test_parse_block_range() {
471 let config =
472 Scan::try_parse_from(["snarkos", "--view-key", "", "--start", "0", "--end", "10", "--endpoint", ""].iter())
473 .unwrap();
474 assert!(config.parse_block_range().is_ok());
475
476 let config =
478 Scan::try_parse_from(["snarkos", "--view-key", "", "--start", "10", "--end", "5", "--endpoint", ""].iter())
479 .unwrap();
480 assert!(config.parse_block_range().is_err());
481
482 assert!(
484 Scan::try_parse_from(
485 ["snarkos", "--view-key", "", "--start", "0", "--last", "10", "--endpoint", ""].iter(),
486 )
487 .is_err()
488 );
489
490 assert!(
492 Scan::try_parse_from(["snarkos", "--view-key", "", "--end", "10", "--last", "10", "--endpoint", ""].iter())
493 .is_err()
494 );
495
496 assert!(
498 Scan::try_parse_from(
499 ["snarkos", "--view-key", "", "--start", "0", "--end", "01", "--last", "10", "--endpoint", ""].iter(),
500 )
501 .is_err()
502 );
503 }
504}