1use crate::db_tool::{DbToolCommand, execute_db_tool_command, print_db_all_tables};
5use crate::{
6 ConciseObjectOutput, GroupedObjectOutput, SnapshotVerifyMode, VerboseObjectOutput,
7 check_completed_snapshot, download_formal_snapshot, get_latest_available_epoch, get_object,
8 get_transaction_block, make_clients, restore_from_db_checkpoint,
9};
10use anyhow::Result;
11use consensus_core::storage::{Store, rocksdb_store::RocksDBStore};
12use consensus_core::{BlockAPI, CommitAPI, CommitRange};
13use futures::TryStreamExt;
14use futures::future::join_all;
15use std::path::PathBuf;
16use std::{collections::BTreeMap, env, sync::Arc};
17use sui_config::genesis::Genesis;
18use sui_core::authority_client::AuthorityAPI;
19use sui_protocol_config::Chain;
20use sui_rpc_api::Client;
21use sui_types::gas_coin::GasCoin;
22use sui_types::messages_consensus::ConsensusTransaction;
23use sui_types::transaction::Transaction;
24use telemetry_subscribers::TracingHandle;
25
26use sui_types::{
27 base_types::*, crypto::AuthorityPublicKeyBytes, messages_grpc::TransactionInfoRequest,
28};
29
30use clap::*;
31use sui_config::Config;
32use sui_config::object_storage_config::{ObjectStoreConfig, ObjectStoreType};
33use sui_types::messages_checkpoint::{
34 CheckpointRequest, CheckpointResponse, CheckpointSequenceNumber,
35};
36
37#[derive(Parser, Clone, ValueEnum)]
38pub enum Verbosity {
39 Grouped,
40 Concise,
41 Verbose,
42}
43
44#[derive(Parser)]
45pub enum ToolCommand {
46 #[command(name = "scan-consensus-commits")]
47 ScanConsensusCommits {
48 #[arg(long = "db-path")]
49 db_path: String,
50 #[arg(long = "start-commit")]
51 start_commit: Option<u32>,
52 #[arg(long = "end-commit")]
53 end_commit: Option<u32>,
54 },
55
56 #[command(name = "locked-object")]
58 LockedObject {
59 #[arg(long, help = "The object ID to fetch")]
62 id: Option<ObjectID>,
63 #[arg(long = "address")]
66 address: Option<SuiAddress>,
67 #[arg(long = "fullnode-rpc-url")]
69 fullnode_rpc_url: String,
70 #[arg(long = "rescue")]
72 rescue: bool,
73 },
74
75 #[command(name = "fetch-object")]
77 FetchObject {
78 #[arg(long, help = "The object ID to fetch")]
79 id: ObjectID,
80
81 #[arg(long, help = "Fetch object at a specific sequence")]
82 version: Option<u64>,
83
84 #[arg(
85 long,
86 help = "Validator to fetch from - if not specified, all validators are queried"
87 )]
88 validator: Option<AuthorityName>,
89
90 #[arg(long = "fullnode-rpc-url")]
92 fullnode_rpc_url: String,
93
94 #[arg(
103 value_enum,
104 long = "verbosity",
105 default_value = "grouped",
106 ignore_case = true
107 )]
108 verbosity: Verbosity,
109
110 #[arg(
111 long = "concise-no-header",
112 help = "don't show header in concise output"
113 )]
114 concise_no_header: bool,
115 },
116
117 #[command(name = "fetch-transaction")]
119 FetchTransaction {
120 #[arg(long = "fullnode-rpc-url")]
122 fullnode_rpc_url: String,
123
124 #[arg(long, help = "The transaction ID to fetch")]
125 digest: TransactionDigest,
126
127 #[arg(long = "show-tx")]
129 show_input_tx: bool,
130 },
131
132 #[command(name = "db-tool")]
134 DbTool {
135 #[arg(long = "db-path")]
137 db_path: String,
138 #[command(subcommand)]
139 cmd: Option<DbToolCommand>,
140 },
141 #[command(name = "dump-packages")]
147 DumpPackages {
148 #[clap(long, short)]
150 rpc_url: String,
151
152 #[clap(long, short)]
154 output_dir: PathBuf,
155
156 #[clap(long)]
159 before_checkpoint: Option<u64>,
160
161 #[clap(short, long = "verbose")]
164 verbose: bool,
165 },
166
167 #[command(name = "dump-validators")]
168 DumpValidators {
169 #[arg(long = "genesis")]
170 genesis: PathBuf,
171
172 #[arg(
173 long = "concise",
174 help = "show concise output - name, protocol key and network address"
175 )]
176 concise: bool,
177 },
178
179 #[command(name = "dump-genesis")]
180 DumpGenesis {
181 #[arg(long = "genesis")]
182 genesis: PathBuf,
183 },
184
185 #[command(name = "fetch-checkpoint")]
188 FetchCheckpoint {
189 #[arg(long = "fullnode-rpc-url")]
191 fullnode_rpc_url: String,
192
193 #[arg(long, help = "Fetch checkpoint at a specific sequence number")]
194 sequence_number: Option<CheckpointSequenceNumber>,
195 },
196
197 #[command(name = "anemo")]
198 Anemo {
199 #[command(next_help_heading = "foo", flatten)]
200 args: anemo_cli::Args,
201 },
202
203 #[command(name = "restore-db")]
204 RestoreFromDBCheckpoint {
205 #[arg(long = "config-path")]
206 config_path: PathBuf,
207 #[arg(long = "db-checkpoint-path")]
208 db_checkpoint_path: PathBuf,
209 },
210
211 #[clap(
215 name = "download-formal-snapshot",
216 about = "Downloads formal database snapshot via cloud object store, outputs to local disk"
217 )]
218 DownloadFormalSnapshot {
219 #[clap(long = "epoch", conflicts_with = "latest")]
220 epoch: Option<u64>,
221 #[clap(long = "genesis")]
222 genesis: PathBuf,
223 #[clap(long = "path")]
224 path: PathBuf,
225 #[clap(long = "num-parallel-downloads")]
227 num_parallel_downloads: Option<usize>,
228 #[clap(long = "num-parallel-chunks", default_value = "8")]
230 num_parallel_chunks: usize,
231 #[clap(long = "verify", default_value = "normal")]
233 verify: Option<SnapshotVerifyMode>,
234 #[clap(long = "network", default_value = "mainnet")]
238 network: Chain,
239 #[clap(long = "snapshot-bucket", conflicts_with = "no_sign_request")]
242 snapshot_bucket: Option<String>,
243 #[clap(
245 long = "snapshot-bucket-type",
246 conflicts_with = "no_sign_request",
247 help = "Required if --no-sign-request is not set"
248 )]
249 snapshot_bucket_type: Option<ObjectStoreType>,
250 #[clap(long = "snapshot-path")]
253 snapshot_path: Option<PathBuf>,
254 #[clap(
256 long = "no-sign-request",
257 conflicts_with_all = &["snapshot_bucket", "snapshot_bucket_type"],
258 help = "if set, no authentication is needed for snapshot restore"
259 )]
260 no_sign_request: bool,
261 #[clap(
264 long = "latest",
265 conflicts_with = "epoch",
266 help = "defaults to latest available snapshot in chosen bucket"
267 )]
268 latest: bool,
269 #[clap(long = "verbose")]
272 verbose: bool,
273
274 #[clap(long = "max-retries", default_value = "10")]
277 max_retries: usize,
278 #[clap(long = "metrics-port", default_value = "9185")]
280 metrics_port: u16,
281 },
282
283 #[command(name = "db-shell")]
285 DbShell(crate::db_shell::DbShellArgs),
286
287 #[cfg(all(feature = "tideconsole", not(windows)))]
289 #[command(name = "tideconsole")]
290 TideConsole {
291 #[arg(short, long)]
293 db: Option<PathBuf>,
294 #[arg(short, long)]
296 exec: Option<String>,
297 #[arg(short, long)]
299 script: Option<PathBuf>,
300 },
301}
302
303async fn check_locked_object(
304 sui_client: &Client,
305 committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
306 id: ObjectID,
307 rescue: bool,
308) -> anyhow::Result<()> {
309 let clients = Arc::new(make_clients(sui_client).await?);
310 let output = get_object(id, None, None, clients.clone()).await?;
311 let output = GroupedObjectOutput::new(output, committee);
312 if output.fully_locked {
313 println!("Object {} is fully locked.", id);
314 return Ok(());
315 }
316 let top_record = output.voting_power.first().unwrap();
317 let top_record_stake = top_record.1;
318 let top_record = top_record.0.clone().unwrap();
319 if top_record.4.is_none() {
320 println!(
321 "Object {} does not seem to be locked by majority of validators (unlocked stake: {})",
322 id, top_record_stake
323 );
324 return Ok(());
325 }
326
327 let tx_digest = top_record.2;
328 if !rescue {
329 println!("Object {} is rescueable, top tx: {:?}", id, tx_digest);
330 return Ok(());
331 }
332 println!("Object {} is rescueable, trying tx {}", id, tx_digest);
333 let validator = output
334 .grouped_results
335 .get(&Some(top_record))
336 .unwrap()
337 .first()
338 .unwrap();
339 let client = &clients.get(validator).unwrap().1;
340 let tx = client
341 .handle_transaction_info_request(TransactionInfoRequest {
342 transaction_digest: tx_digest,
343 })
344 .await?
345 .transaction;
346 let tx = Transaction::new(tx);
347 let res = sui_client.clone().execute_transaction(&tx).await;
348 match res {
349 Ok(_) => {
350 println!("Transaction executed successfully ({:?})", tx_digest);
351 }
352 Err(e) => {
353 println!("Failed to execute transaction ({:?}): {:?}", tx_digest, e);
354 }
355 }
356 Ok(())
357}
358
359impl ToolCommand {
360 #[allow(clippy::format_in_format_args)]
361 pub async fn execute(self, tracing_handle: TracingHandle) -> Result<(), anyhow::Error> {
362 match self {
363 ToolCommand::ScanConsensusCommits {
364 db_path,
365 start_commit,
366 end_commit,
367 } => {
368 let rocks_db_store = RocksDBStore::new(&db_path);
369
370 let start_commit = start_commit.unwrap_or(0);
371 let end_commit = end_commit.unwrap_or(u32::MAX);
372
373 let commits = rocks_db_store
374 .scan_commits(CommitRange::new(start_commit..=end_commit))
375 .unwrap();
376 println!("found {} consensus commits", commits.len());
377
378 for commit in commits {
379 let inner = &*commit;
380 let block_refs = inner.blocks();
381 let blocks = rocks_db_store.read_blocks(block_refs).unwrap();
382
383 for block in blocks.iter().flatten() {
384 let data = block.transactions_data();
385 println!(
386 "\"index\": \"{}\", \"leader\": \"{}\", \"blocks\": \"{:#?}\", {} txs",
387 inner.index(),
388 inner.leader(),
389 inner.blocks(),
390 data.len()
391 );
392 for txns in &data {
393 let tx: ConsensusTransaction = bcs::from_bytes(txns).unwrap();
394 println!("\t{:?}", tx.key());
395 }
396 }
397 }
398 }
399 ToolCommand::LockedObject {
400 id,
401 fullnode_rpc_url,
402 rescue,
403 address,
404 } => {
405 let sui_client = Client::new(fullnode_rpc_url)?;
406 let committee = Arc::new(
407 sui_client
408 .get_committee(None)
409 .await?
410 .voting_rights
411 .into_iter()
412 .collect::<BTreeMap<_, _>>(),
413 );
414 let object_ids = match id {
415 Some(id) => vec![id],
416 None => {
417 let address = address.expect("Either id or address must be provided");
418 sui_client
419 .list_owned_objects(address, Some(GasCoin::type_()))
420 .map_ok(|o| o.id())
421 .try_collect()
422 .await?
423 }
424 };
425 for ids in object_ids.chunks(30) {
426 let mut tasks = vec![];
427 for id in ids {
428 tasks.push(check_locked_object(
429 &sui_client,
430 committee.clone(),
431 *id,
432 rescue,
433 ))
434 }
435 join_all(tasks)
436 .await
437 .into_iter()
438 .collect::<Result<Vec<_>, _>>()?;
439 }
440 }
441 ToolCommand::FetchObject {
442 id,
443 validator,
444 version,
445 fullnode_rpc_url,
446 verbosity,
447 concise_no_header,
448 } => {
449 let sui_client = Client::new(fullnode_rpc_url)?;
450 let clients = Arc::new(make_clients(&sui_client).await?);
451 let output = get_object(id, version, validator, clients).await?;
452
453 match verbosity {
454 Verbosity::Grouped => {
455 let committee = Arc::new(
456 sui_client
457 .get_committee(None)
458 .await?
459 .voting_rights
460 .into_iter()
461 .collect::<BTreeMap<_, _>>(),
462 );
463 println!("{}", GroupedObjectOutput::new(output, committee));
464 }
465 Verbosity::Verbose => {
466 println!("{}", VerboseObjectOutput(output));
467 }
468 Verbosity::Concise => {
469 if !concise_no_header {
470 println!("{}", ConciseObjectOutput::header());
471 }
472 println!("{}", ConciseObjectOutput(output));
473 }
474 }
475 }
476 ToolCommand::FetchTransaction {
477 digest,
478 show_input_tx,
479 fullnode_rpc_url,
480 } => {
481 print!(
482 "{}",
483 get_transaction_block(digest, show_input_tx, fullnode_rpc_url).await?
484 );
485 }
486 ToolCommand::DbTool { db_path, cmd } => {
487 let path = PathBuf::from(db_path);
488 match cmd {
489 Some(c) => execute_db_tool_command(path, c).await?,
490 None => print_db_all_tables(path)?,
491 }
492 }
493 ToolCommand::DumpPackages {
494 rpc_url,
495 output_dir,
496 before_checkpoint,
497 verbose,
498 } => {
499 if !verbose {
500 tracing_handle
501 .update_log("off")
502 .expect("Failed to update log level");
503 }
504
505 sui_package_dump::dump(rpc_url, output_dir, before_checkpoint).await?;
506 }
507 ToolCommand::DumpValidators { genesis, concise } => {
508 let genesis = Genesis::load(genesis).unwrap();
509 if !concise {
510 println!("{:#?}", genesis.validator_set_for_tooling());
511 } else {
512 for (i, val_info) in genesis.validator_set_for_tooling().iter().enumerate() {
513 let metadata = val_info.verified_metadata();
514 println!(
515 "#{:<2} {:<20} {:?} {:?} {}",
516 i,
517 metadata.name,
518 metadata.sui_pubkey_bytes().concise(),
519 metadata.net_address,
520 anemo::PeerId(metadata.network_pubkey.0.to_bytes()),
521 )
522 }
523 }
524 }
525 ToolCommand::DumpGenesis { genesis } => {
526 let genesis = Genesis::load(genesis)?;
527 println!("{:#?}", genesis);
528 }
529 ToolCommand::FetchCheckpoint {
530 sequence_number,
531 fullnode_rpc_url,
532 } => {
533 let sui_client = Client::new(fullnode_rpc_url)?;
534 let clients = make_clients(&sui_client).await?;
535
536 for (name, (_, client)) in clients {
537 let resp = client
538 .handle_checkpoint(CheckpointRequest {
539 sequence_number,
540 request_content: true,
541 })
542 .await
543 .unwrap();
544 let CheckpointResponse {
545 checkpoint,
546 contents,
547 } = resp;
548
549 let summary = checkpoint.clone().unwrap().data().clone();
550 let mut file = std::fs::File::create("/tmp/ckpt_summary")
552 .expect("Failed to create /tmp/summary");
553 let bytes =
554 bcs::to_bytes(&summary).expect("Failed to serialize summary to BCS");
555 use std::io::Write;
556 file.write_all(&bytes)
557 .expect("Failed to write summary to /tmp/ckpt_summary");
558
559 println!("Validator: {:?}\n", name.concise());
560 println!("Checkpoint: {:?}\n", checkpoint);
561 println!("Content: {:?}\n", contents);
562 }
563 }
564 ToolCommand::Anemo { args } => {
565 let config = crate::make_anemo_config();
566 anemo_cli::run(config, args).await
567 }
568 ToolCommand::RestoreFromDBCheckpoint {
569 config_path,
570 db_checkpoint_path,
571 } => {
572 let config = sui_config::NodeConfig::load(config_path)?;
573 restore_from_db_checkpoint(&config, &db_checkpoint_path).await?;
574 }
575 ToolCommand::DownloadFormalSnapshot {
576 epoch,
577 genesis,
578 path,
579 num_parallel_downloads,
580 num_parallel_chunks,
581 verify,
582 network,
583 snapshot_bucket,
584 snapshot_bucket_type,
585 snapshot_path,
586 no_sign_request,
587 latest,
588 verbose,
589 max_retries,
590 metrics_port,
591 } => {
592 if !verbose {
593 tracing_handle
594 .update_log("off")
595 .expect("Failed to update log level");
596 }
597 let num_parallel_downloads = num_parallel_downloads.unwrap_or(50).min(200);
598 let snapshot_bucket =
599 snapshot_bucket.or_else(|| match (network, no_sign_request) {
600 (Chain::Mainnet, false) => Some(
601 env::var("MAINNET_FORMAL_SIGNED_BUCKET")
602 .unwrap_or("mysten-mainnet-formal".to_string()),
603 ),
604 (Chain::Mainnet, true) => env::var("MAINNET_FORMAL_UNSIGNED_BUCKET").ok(),
605 (Chain::Testnet, true) => env::var("TESTNET_FORMAL_UNSIGNED_BUCKET").ok(),
606 (Chain::Testnet, _) => Some(
607 env::var("TESTNET_FORMAL_SIGNED_BUCKET")
608 .unwrap_or("mysten-testnet-formal".to_string()),
609 ),
610 (Chain::Unknown, _) => {
611 panic!("Cannot generate default snapshot bucket for unknown network");
612 }
613 });
614
615 let aws_endpoint = env::var("AWS_SNAPSHOT_ENDPOINT").ok().or_else(|| {
616 if no_sign_request {
617 if network == Chain::Mainnet {
618 Some("https://formal-snapshot.mainnet.sui.io".to_string())
619 } else if network == Chain::Testnet {
620 Some("https://formal-snapshot.testnet.sui.io".to_string())
621 } else {
622 None
623 }
624 } else {
625 None
626 }
627 });
628
629 let snapshot_bucket_type = if no_sign_request {
630 ObjectStoreType::S3
631 } else {
632 snapshot_bucket_type
633 .expect("You must set either --snapshot-bucket-type or --no-sign-request")
634 };
635 let snapshot_store_config = match snapshot_bucket_type {
636 ObjectStoreType::S3 => ObjectStoreConfig {
637 object_store: Some(ObjectStoreType::S3),
638 bucket: snapshot_bucket.filter(|s| !s.is_empty()),
639 aws_access_key_id: env::var("AWS_SNAPSHOT_ACCESS_KEY_ID").ok(),
640 aws_secret_access_key: env::var("AWS_SNAPSHOT_SECRET_ACCESS_KEY").ok(),
641 aws_region: env::var("AWS_SNAPSHOT_REGION").ok(),
642 aws_endpoint: aws_endpoint.filter(|s| !s.is_empty()),
643 aws_virtual_hosted_style_request: env::var(
644 "AWS_SNAPSHOT_VIRTUAL_HOSTED_REQUESTS",
645 )
646 .ok()
647 .and_then(|b| b.parse().ok())
648 .unwrap_or(no_sign_request),
649 object_store_connection_limit: 200,
650 no_sign_request,
651 ..Default::default()
652 },
653 ObjectStoreType::GCS => ObjectStoreConfig {
654 object_store: Some(ObjectStoreType::GCS),
655 bucket: snapshot_bucket,
656 google_service_account: env::var("GCS_SNAPSHOT_SERVICE_ACCOUNT_FILE_PATH")
657 .ok(),
658 object_store_connection_limit: 200,
659 no_sign_request,
660 ..Default::default()
661 },
662 ObjectStoreType::Azure => ObjectStoreConfig {
663 object_store: Some(ObjectStoreType::Azure),
664 bucket: snapshot_bucket,
665 azure_storage_account: env::var("AZURE_SNAPSHOT_STORAGE_ACCOUNT").ok(),
666 azure_storage_access_key: env::var("AZURE_SNAPSHOT_STORAGE_ACCESS_KEY")
667 .ok(),
668 object_store_connection_limit: 200,
669 no_sign_request,
670 ..Default::default()
671 },
672 ObjectStoreType::File => {
673 if snapshot_path.is_some() {
674 ObjectStoreConfig {
675 object_store: Some(ObjectStoreType::File),
676 directory: snapshot_path,
677 ..Default::default()
678 }
679 } else {
680 panic!(
681 "--snapshot-path must be specified for --snapshot-bucket-type=file"
682 );
683 }
684 }
685 };
686
687 let ingestion_url = match network {
688 Chain::Mainnet => "https://checkpoints.mainnet.sui.io",
689 Chain::Testnet => "https://checkpoints.testnet.sui.io",
690 _ => panic!("Cannot generate default ingestion url for unknown network"),
691 };
692
693 let latest_available_epoch =
694 latest.then_some(get_latest_available_epoch(&snapshot_store_config).await?);
695 let epoch_to_download = epoch.or(latest_available_epoch).expect(
696 "Either pass epoch with --epoch <epoch_num> or use latest with --latest",
697 );
698
699 if let Err(e) =
700 check_completed_snapshot(&snapshot_store_config, epoch_to_download).await
701 {
702 panic!(
703 "Aborting snapshot restore: {}, snapshot may not be uploaded yet",
704 e
705 );
706 }
707
708 let verify = verify.unwrap_or_default();
709 download_formal_snapshot(
710 &path,
711 epoch_to_download,
712 &genesis,
713 snapshot_store_config,
714 ingestion_url,
715 num_parallel_downloads,
716 num_parallel_chunks,
717 network,
718 verify,
719 max_retries,
720 metrics_port,
721 )
722 .await?;
723 }
724 ToolCommand::DbShell(args) => {
725 tokio::task::spawn_blocking(move || crate::db_shell::run(args)).await??;
726 }
727 #[cfg(all(feature = "tideconsole", not(windows)))]
728 ToolCommand::TideConsole { db, exec, script } => {
729 tokio::task::spawn_blocking(move || crate::tideconsole_cmd::run(db, exec, script))
730 .await??;
731 }
732 };
733 Ok(())
734 }
735}