1use std::{collections::BTreeMap, env, num::NonZeroUsize, path::PathBuf, sync::Arc};
6
7use anyhow::Result;
8use clap::*;
9use futures::{StreamExt, future::join_all};
10use iota_config::{
11 genesis::Genesis,
12 object_storage_config::{ObjectStoreConfig, ObjectStoreType},
13};
14use iota_core::{
15 authority_aggregator::AuthorityAggregatorBuilder,
16 authority_client::{validator::ValidatorAPI, validator_peer::ValidatorPeerAPI},
17};
18use iota_replay::{ReplayToolCommand, execute_replay_command};
19use iota_sdk::{IotaClient, IotaClientBuilder, rpc_types::IotaTransactionBlockResponseOptions};
20use iota_sdk_types::{Address, ObjectId, SenderSignedTransaction, TransactionDigest};
21use iota_snapshot::progress::LOG_TARGET_PROGRESS;
22use iota_types::{
23 base_types::*,
24 crypto::AuthorityPublicKeyBytes,
25 messages_checkpoint::{CheckpointRequest, CheckpointResponse, CheckpointSequenceNumber},
26 messages_grpc::TransactionInfoRequest,
27 transaction::TransactionEnvelope,
28};
29use telemetry_subscribers::TracingHandle;
30
31use crate::{
32 ConciseObjectOutput, GroupedObjectOutput, SnapshotVerifyMode, VerboseObjectOutput,
33 backfill_checkpoint_summaries, check_completed_snapshot,
34 db_tool::{DbToolCommand, execute_db_tool_command, print_db_all_tables},
35 download_formal_snapshot, get_latest_available_epoch, get_object, get_transaction_block,
36 make_clients,
37};
38
39fn progress_only_log_directives() -> String {
43 format!("off,{LOG_TARGET_PROGRESS}=info")
44}
45
46#[derive(Parser, Clone, ValueEnum)]
47pub enum Verbosity {
48 Grouped,
49 Concise,
50 Verbose,
51}
52
53#[derive(Debug, Copy, Clone, ValueEnum)]
55pub enum Network {
56 Mainnet,
57 Testnet,
58 Devnet,
59}
60
61#[derive(Parser)]
62pub enum ToolCommand {
63 LockedObject {
66 #[arg(long, help = "The object ID to fetch")]
69 id: Option<ObjectId>,
70 #[arg(long)]
73 address: Option<Address>,
74 #[arg(long)]
76 fullnode_rpc_url: String,
77 #[arg(long)]
80 rescue: bool,
81 },
82
83 FetchObject {
85 #[arg(long, help = "The object ID to fetch")]
86 id: ObjectId,
87
88 #[arg(long, help = "Fetch object at a specific sequence")]
89 version: Option<u64>,
90
91 #[arg(
92 long,
93 help = "Validator to fetch from - if not specified, all validators are queried"
94 )]
95 validator: Option<AuthorityName>,
96
97 #[arg(long)]
99 fullnode_rpc_url: String,
100
101 #[arg(value_enum, long, default_value = "grouped", ignore_case = true)]
110 verbosity: Verbosity,
111
112 #[arg(long, help = "don't show header in concise output")]
113 concise_no_header: bool,
114 },
115
116 FetchTransaction {
118 #[arg(long)]
120 fullnode_rpc_url: String,
121
122 #[arg(long, help = "The transaction ID to fetch")]
123 digest: TransactionDigest,
124
125 #[arg(long = "show-tx")]
127 show_input_tx: bool,
128 },
129
130 DbTool {
132 #[arg(long)]
134 db_path: String,
135 #[command(subcommand)]
136 cmd: Option<DbToolCommand>,
137 },
138
139 DumpPackages {
147 #[arg(long, short)]
149 rpc_url: String,
150
151 #[arg(long, short)]
154 output_dir: PathBuf,
155
156 #[arg(long)]
159 before_checkpoint: Option<u64>,
160
161 #[arg(short, long)]
164 verbose: bool,
165 },
166
167 DumpValidators {
168 #[arg(long)]
169 genesis: PathBuf,
170
171 #[arg(
172 long,
173 help = "show concise output - name, authority key and network address"
174 )]
175 concise: bool,
176 },
177
178 DumpGenesis {
179 #[arg(long)]
180 genesis: PathBuf,
181 },
182
183 FetchCheckpoint {
187 #[arg(long)]
189 fullnode_rpc_url: String,
190
191 #[arg(long, help = "Fetch checkpoint at a specific sequence number")]
192 sequence_number: Option<CheckpointSequenceNumber>,
193 },
194
195 Anemo {
196 #[command(next_help_heading = "foo", flatten)]
197 args: anemo_cli::Args,
198 },
199
200 #[command(
202 about = "Downloads formal database snapshot via cloud object store, outputs to local disk"
203 )]
204 DownloadFormalSnapshot {
205 #[arg(long, conflicts_with = "latest")]
207 epoch: Option<u64>,
208 #[arg(long)]
210 genesis: PathBuf,
211 #[arg(long)]
214 path: PathBuf,
215 #[arg(long)]
218 num_parallel_downloads: Option<NonZeroUsize>,
219 #[arg(long, default_value = "normal")]
221 verify: Option<SnapshotVerifyMode>,
222 #[arg(long, default_value = "mainnet")]
226 network: Network,
227 #[arg(long, conflicts_with = "no_sign_request")]
230 snapshot_bucket: Option<String>,
231 #[arg(
233 long,
234 conflicts_with = "no_sign_request",
235 help = "Required if --no-sign-request is not set"
236 )]
237 snapshot_bucket_type: Option<ObjectStoreType>,
238 #[arg(long)]
241 snapshot_path: Option<PathBuf>,
242 #[arg(
244 long,
245 conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
246 help = "if set, no authentication is needed for snapshot restore"
247 )]
248 no_sign_request: bool,
249 #[arg(
252 long,
253 conflicts_with = "epoch",
254 help = "defaults to latest available snapshot in chosen bucket"
255 )]
256 latest: bool,
257 #[arg(long)]
260 verbose: bool,
261
262 #[arg(long)]
264 disable_progress_bar: bool,
265
266 #[arg(long)]
271 skip_grpc_indexes: bool,
272 },
273
274 BackfillCheckpointSummaries {
288 #[arg(long)]
291 path: PathBuf,
292 #[arg(long)]
296 ingestion_url: String,
297 #[arg(long)]
300 num_parallel_downloads: Option<NonZeroUsize>,
301 #[arg(long)]
304 verbose: bool,
305 #[arg(long)]
307 disable_progress_bar: bool,
308 },
309
310 Replay {
311 #[arg(long = "rpc")]
312 rpc_url: Option<String>,
313 #[arg(long)]
314 safety_checks: bool,
315 #[arg(long = "authority")]
316 use_authority: bool,
317 #[arg(
318 long,
319 short,
320 help = "Path to the network config file. This should be specified when rpc_url is not present. \
321 If not specified we will use the default network config file at ~/.iota-replay/network-config.yaml"
322 )]
323 cfg_path: Option<PathBuf>,
324 #[arg(
325 long,
326 help = "The name of the chain to replay from, could be one of: mainnet, testnet, devnet.\
327 When rpc_url is not specified, this is used to load the corresponding config from the network config file.\
328 If not specified, mainnet will be used by default"
329 )]
330 chain: Option<String>,
331 #[command(subcommand)]
332 cmd: ReplayToolCommand,
333 },
334
335 SignTransaction {
337 #[arg(long)]
338 genesis: PathBuf,
339
340 #[arg(
341 long,
342 help = "The Base64-encoding of the bcs bytes of SenderSignedTransaction"
343 )]
344 sender_signed_data: String,
345 },
346
347 GenesisCeremony(crate::genesis_ceremony::Ceremony),
349 FireDrill {
351 #[command(subcommand)]
352 fire_drill: crate::fire_drill::FireDrill,
353 },
354
355 #[command(name = "grpc-health-check")]
358 GrpcHealthCheck {
359 #[arg(long, default_value = "http://localhost:50051")]
361 address: String,
362 },
363}
364
365async fn check_locked_object(
366 iota_client: &Arc<IotaClient>,
367 committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
368 id: ObjectId,
369 rescue: bool,
370) -> anyhow::Result<()> {
371 let clients = Arc::new(make_clients(iota_client).await?);
372 let output = get_object(id, None, None, clients.clone()).await?;
373 let output = GroupedObjectOutput::new(output, committee);
374 if output.fully_locked {
375 println!("Object {id} is fully locked.");
376 return Ok(());
377 }
378 let top_record = output.voting_power.first().unwrap();
379 let top_record_stake = top_record.1;
380 let top_record = top_record.0.unwrap();
381 if top_record.4.is_none() {
382 println!(
383 "Object {id} does not seem to be locked by majority of validators (unlocked stake: {top_record_stake})"
384 );
385 return Ok(());
386 }
387
388 let tx_digest = top_record.2;
389 if !rescue {
390 println!("Object {id} is rescueable, top tx: {tx_digest}");
391 return Ok(());
392 }
393 println!("Object {id} is rescueable, trying tx {tx_digest}");
394 let validator = output
395 .grouped_results
396 .get(&Some(top_record))
397 .unwrap()
398 .first()
399 .unwrap();
400 let client = &clients.get(validator).unwrap().1;
401 let tx = client
402 .handle_transaction_info_request(TransactionInfoRequest {
403 transaction_digest: tx_digest,
404 })
405 .await?
406 .transaction;
407 let res = iota_client
408 .quorum_driver_api()
409 .execute_transaction_block(
410 TransactionEnvelope::new(tx),
411 IotaTransactionBlockResponseOptions::full_content(),
412 None,
413 )
414 .await;
415 match res {
416 Ok(_) => {
417 println!("Transaction executed successfully ({tx_digest})");
418 }
419 Err(e) => {
420 println!("Failed to execute transaction ({tx_digest}): {e:?}");
421 }
422 }
423 Ok(())
424}
425
426impl ToolCommand {
427 pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
428 match self {
429 ToolCommand::LockedObject {
430 id,
431 fullnode_rpc_url,
432 rescue,
433 address,
434 } => {
435 let iota_client =
436 Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
437 let committee = Arc::new(
438 iota_client
439 .governance_api()
440 .get_committee_info(None)
441 .await?
442 .validators
443 .into_iter()
444 .collect::<BTreeMap<_, _>>(),
445 );
446 let object_ids = match id {
447 Some(id) => vec![id],
448 None => {
449 let address = address.expect("Either id or address must be provided");
450 iota_client
451 .coin_read_api()
452 .get_coins_stream(address, None)
453 .map(|c| c.coin_object_id)
454 .collect()
455 .await
456 }
457 };
458 for ids in object_ids.chunks(30) {
459 let mut tasks = vec![];
460 for id in ids {
461 tasks.push(check_locked_object(
462 &iota_client,
463 committee.clone(),
464 *id,
465 rescue,
466 ))
467 }
468 join_all(tasks)
469 .await
470 .into_iter()
471 .collect::<Result<Vec<_>, _>>()?;
472 }
473 }
474 ToolCommand::FetchObject {
475 id,
476 validator,
477 version,
478 fullnode_rpc_url,
479 verbosity,
480 concise_no_header,
481 } => {
482 let iota_client =
483 Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
484 let clients = Arc::new(make_clients(&iota_client).await?);
485 let output = get_object(id, version, validator, clients).await?;
486
487 match verbosity {
488 Verbosity::Grouped => {
489 let committee = Arc::new(
490 iota_client
491 .governance_api()
492 .get_committee_info(None)
493 .await?
494 .validators
495 .into_iter()
496 .collect::<BTreeMap<_, _>>(),
497 );
498 println!("{}", GroupedObjectOutput::new(output, committee));
499 }
500 Verbosity::Verbose => {
501 println!("{}", VerboseObjectOutput(output));
502 }
503 Verbosity::Concise => {
504 if !concise_no_header {
505 println!("{}", ConciseObjectOutput::header());
506 }
507 println!("{}", ConciseObjectOutput(output));
508 }
509 }
510 }
511 ToolCommand::FetchTransaction {
512 digest,
513 show_input_tx,
514 fullnode_rpc_url,
515 } => {
516 print!(
517 "{}",
518 get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
519 );
520 }
521 ToolCommand::DbTool { db_path, cmd } => {
522 let path = PathBuf::from(db_path);
523 match cmd {
524 Some(c) => execute_db_tool_command(path, c).await?,
525 None => print_db_all_tables(path)?,
526 }
527 }
528 ToolCommand::DumpPackages {
529 rpc_url,
530 output_dir,
531 before_checkpoint,
532 verbose,
533 } => {
534 if !verbose {
535 tracing_handle
536 .update_log("off")
537 .expect("Failed to update log level");
538 }
539
540 iota_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
541 }
542 ToolCommand::DumpValidators { genesis, concise } => {
543 let genesis = Genesis::load(genesis).unwrap();
544 if !concise {
545 println!("{:#?}", genesis.validator_set_for_tooling());
546 } else {
547 for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
548 let metadata = val_info.verified_metadata();
549 println!(
550 "#{:<2} {:<20} {:?} {:?} {}",
551 i,
552 metadata.name,
553 metadata.iota_pubkey_bytes().concise(),
554 metadata.net_address,
555 anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
556 )
557 }
558 }
559 }
560 ToolCommand::DumpGenesis { genesis } => {
561 let genesis = Genesis::load(genesis)?;
562 println!("{genesis:#?}");
563 }
564 ToolCommand::FetchCheckpoint {
565 sequence_number,
566 fullnode_rpc_url,
567 } => {
568 let iota_client =
569 Arc::new(IotaClientBuilder::default().build(fullnode_rpc_url).await?);
570 let clients = make_clients(&iota_client).await?;
571
572 for (name, (_, client)) in clients {
573 let resp = client
574 .get_checkpoint_v2(CheckpointRequest {
575 sequence_number,
576 request_content: true,
577 certified: true,
578 })
579 .await
580 .unwrap();
581 let CheckpointResponse {
582 checkpoint,
583 contents,
584 } = resp;
585 println!("Validator: {:?}\n", name.concise());
586 println!("Checkpoint: {checkpoint:?}\n");
587 println!("Content: {contents:?}\n");
588 }
589 }
590 ToolCommand::Anemo { args } => {
591 let config = crate::make_anemo_config();
592 anemo_cli::run(config, args).await
593 }
594 ToolCommand::DownloadFormalSnapshot {
595 epoch,
596 genesis,
597 path,
598 num_parallel_downloads,
599 verify,
600 network,
601 snapshot_bucket,
602 snapshot_bucket_type,
603 snapshot_path,
604 no_sign_request,
605 latest,
606 verbose,
607 disable_progress_bar,
608 skip_grpc_indexes,
609 } => {
610 if !verbose {
611 tracing_handle
612 .update_log(progress_only_log_directives())
613 .expect("Failed to update log level");
614 }
615 let num_parallel_downloads = num_parallel_downloads
616 .unwrap_or_else(iota_snapshot::default_download_concurrency);
617 let snapshot_bucket =
618 snapshot_bucket.or_else(|| match (network, no_sign_request) {
619 (Network::Mainnet, false) => Some(
620 env::var("MAINNET_FORMAL_SIGNED_BUCKET")
621 .unwrap_or("iota-mainnet-formal".to_string()),
622 ),
623 (Network::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
624 (Network::Testnet, false) => Some(
625 env::var("TESTNET_FORMAL_SIGNED_BUCKET")
626 .unwrap_or("iota-testnet-formal".to_string()),
627 ),
628 (Network::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
629 (Network::Devnet, false) => Some(
630 env::var("DEVNET_FORMAL_SIGNED_BUCKET")
631 .unwrap_or("iota-devnet-formal".to_string()),
632 ),
633 (Network::Devnet, true) => env::var("DEVNET_FORMAL_UNSIGNED_BUCKET").ok(),
634 });
635
636 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
637 no_sign_request.then(|| {
638 match network {
639 Network::Mainnet => "https://formal-snapshot.mainnet.iota.cafe",
640 Network::Testnet => "https://formal-snapshot.testnet.iota.cafe",
641 Network::Devnet => "https://formal-snapshot.devnet.iota.cafe",
642 }
643 .to_string()
644 })
645 });
646
647 let snapshot_bucket_type = if no_sign_request {
648 ObjectStoreType::S3
649 } else {
650 snapshot_bucket_type
651 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
652 };
653 let snapshot_store_config = match snapshot_bucket_type {
654 ObjectStoreType::S3 => ObjectStoreConfig {
655 object_store: Some(ObjectStoreType::S3),
656 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
657 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
658 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
659 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
660 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
661 aws_virtual_hosted_style_request: env::var(
662 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
663 )
664 .ok()
665 .and_then(|b| b.parse().ok())
666 .unwrap_or(no_sign_request),
667 object_store_connection_limit: 200,
668 no_sign_request,
669 ..Default::default()
670 },
671 ObjectStoreType::GCS => ObjectStoreConfig {
672 object_store: Some(ObjectStoreType::GCS),
673 bucket: snapshot_bucket,
674 google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
675 .ok(),
676 object_store_connection_limit: 200,
677 no_sign_request,
678 ..Default::default()
679 },
680 ObjectStoreType::Azure => ObjectStoreConfig {
681 object_store: Some(ObjectStoreType::Azure),
682 bucket: snapshot_bucket,
683 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
684 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
685 .ok(),
686 object_store_connection_limit: 200,
687 no_sign_request,
688 ..Default::default()
689 },
690 ObjectStoreType::File => {
691 if snapshot_path.is_some() {
692 ObjectStoreConfig {
693 object_store: Some(ObjectStoreType::File),
694 directory: snapshot_path,
695 ..Default::default()
696 }
697 } else {
698 panic!(
699 "--snapshot-path must be specified for --snapshot-bucket-type=file"
700 );
701 }
702 }
703 };
704
705 let latest_available_epoch =
706 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
707 let epoch_to_download = epoch.or(latest_available_epoch).expect(
708 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
709 );
710
711 if let Err(e) =
712 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
713 {
714 panic!("Aborting snapshot restore: {e}, snapshot may not be uploaded yet");
715 }
716
717 let verify = verify.unwrap_or_default();
718 download_formal_snapshot(
719 &path,
720 epoch_to_download,
721 &genesis,
722 snapshot_store_config,
723 num_parallel_downloads,
724 verify,
725 skip_grpc_indexes,
726 disable_progress_bar,
727 )
728 .await?;
729 }
730 ToolCommand::BackfillCheckpointSummaries {
731 path,
732 ingestion_url,
733 num_parallel_downloads,
734 verbose,
735 disable_progress_bar,
736 } => {
737 if !verbose {
738 tracing_handle
739 .update_log(progress_only_log_directives())
740 .expect("Failed to update log level");
741 }
742 let num_parallel_downloads = num_parallel_downloads
743 .unwrap_or_else(iota_snapshot::default_download_concurrency);
744 backfill_checkpoint_summaries(
745 &path,
746 ingestion_url,
747 num_parallel_downloads,
748 disable_progress_bar,
749 )
750 .await?;
751 }
752 ToolCommand::Replay {
753 rpc_url,
754 safety_checks,
755 cmd,
756 use_authority,
757 cfg_path,
758 chain,
759 } => {
760 execute_replay_command(rpc_url, safety_checks, use_authority, cfg_path, chain, cmd)
761 .await?;
762 }
763 ToolCommand::SignTransaction {
764 genesis,
765 sender_signed_data,
766 } => {
767 let genesis = Genesis::load(genesis)?;
768 let sender_signed_tx =
769 SenderSignedTransaction::from_base64(sender_signed_data.as_str()).unwrap();
770 let transaction = TransactionEnvelope::new(sender_signed_tx);
771 let (agg, _) =
772 AuthorityAggregatorBuilder::from_genesis(&genesis).build_network_clients();
773 let result = agg.process_transaction(transaction, None).await;
774 println!("{result:?}");
775 }
776 ToolCommand::GenesisCeremony(cmd) => {
777 crate::genesis_ceremony::run(cmd).await?;
778 }
779 ToolCommand::FireDrill { fire_drill } => {
780 crate::fire_drill::run_fire_drill(fire_drill).await?;
781 }
782 ToolCommand::GrpcHealthCheck { address } => {
783 let client = iota_grpc_client::GrpcClient::new(address)?;
784 client.health(None).await?;
785 println!("OK");
786 }
787 };
788 Ok(())
789 }
790}