Skip to main content

sui_tool/db_tool/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use self::db_dump::{StoreName, dump_table, list_tables, table_summary};
5use self::index_search::{SearchRange, search_index};
6use crate::db_tool::db_dump::{compact, print_table_metadata, prune_checkpoints, prune_objects};
7use anyhow::{anyhow, bail};
8use clap::Parser;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use sui_core::authority::authority_per_epoch_store::AuthorityEpochTables;
12use sui_core::authority::authority_store_pruner::PrunerWatermarks;
13use sui_core::authority::authority_store_tables::AuthorityPerpetualTables;
14use sui_core::checkpoints::CheckpointStore;
15use sui_types::base_types::{EpochId, ObjectID};
16use sui_types::digests::{CheckpointContentsDigest, TransactionDigest};
17use sui_types::effects::TransactionEffectsAPI;
18use sui_types::messages_checkpoint::{CheckpointDigest, CheckpointSequenceNumber};
19#[cfg(not(tidehunter))]
20use typed_store::rocks::MetricConf;
21use typed_store::rocks::safe_drop_db;
22pub mod db_dump;
23mod index_search;
24
25#[derive(Parser)]
26#[command(rename_all = "kebab-case")]
27pub enum DbToolCommand {
28    ListTables,
29    Dump(Options),
30    IndexSearchKeyRange(IndexSearchKeyRangeOptions),
31    IndexSearchCount(IndexSearchCountOptions),
32    TableSummary(Options),
33    ListDBMetadata(Options),
34    PrintLastConsensusIndex,
35    PrintConsensusCommit(PrintConsensusCommitOptions),
36    PrintTransaction(PrintTransactionOptions),
37    PrintObject(PrintObjectOptions),
38    PrintCheckpoint(PrintCheckpointOptions),
39    PrintCheckpointContent(PrintCheckpointContentOptions),
40    ResetDB,
41    RewindCheckpointExecution(RewindCheckpointExecutionOptions),
42    Compact,
43    PruneObjects,
44    PruneCheckpoints,
45    SetCheckpointWatermark(SetCheckpointWatermarkOptions),
46}
47
48#[derive(Parser)]
49#[command(rename_all = "kebab-case")]
50pub struct IndexSearchKeyRangeOptions {
51    #[arg(long = "table-name", short = 't')]
52    table_name: String,
53    #[arg(long = "start", short = 's')]
54    start: String,
55    #[arg(long = "end", short = 'e')]
56    end_key: String,
57}
58
59#[derive(Parser)]
60#[command(rename_all = "kebab-case")]
61pub struct IndexSearchCountOptions {
62    #[arg(long = "table-name", short = 't')]
63    table_name: String,
64    #[arg(long = "start", short = 's')]
65    start: String,
66    #[arg(long = "count", short = 'c')]
67    count: u64,
68}
69
70#[derive(Parser)]
71#[command(rename_all = "kebab-case")]
72pub struct Options {
73    /// The type of store to dump
74    #[arg(long = "store", short = 's', value_enum)]
75    store_name: StoreName,
76    /// The name of the table to dump
77    #[arg(long = "table-name", short = 't')]
78    table_name: String,
79    /// The size of page to dump. This is a u16
80    #[arg(long = "page-size", short = 'p')]
81    page_size: u16,
82    /// The page number to dump
83    #[arg(long = "page-num", short = 'n')]
84    page_number: usize,
85
86    // TODO: We should load this automatically from the system object in AuthorityPerpetualTables.
87    // This is very difficult to do right now because you can't share code between
88    // AuthorityPerpetualTables and AuthorityEpochTablesReadonly.
89    /// The epoch to use when loading AuthorityEpochTables.
90    #[arg(long = "epoch", short = 'e')]
91    epoch: Option<EpochId>,
92}
93
94#[derive(Parser)]
95#[command(rename_all = "kebab-case")]
96pub struct PrintConsensusCommitOptions {
97    #[arg(long, help = "Sequence number of the consensus commit")]
98    seqnum: u64,
99}
100
101#[derive(Parser)]
102#[command(rename_all = "kebab-case")]
103pub struct PrintTransactionOptions {
104    #[arg(long, help = "The transaction digest to print")]
105    digest: TransactionDigest,
106}
107
108#[derive(Parser)]
109#[command(rename_all = "kebab-case")]
110pub struct PrintObjectOptions {
111    #[arg(long, help = "The object id to print")]
112    id: ObjectID,
113    #[arg(long, help = "The object version to print")]
114    version: Option<u64>,
115}
116
117#[derive(Parser)]
118#[command(rename_all = "kebab-case")]
119pub struct PrintCheckpointOptions {
120    #[arg(long, help = "The checkpoint digest to print")]
121    digest: CheckpointDigest,
122}
123
124#[derive(Parser)]
125#[command(rename_all = "kebab-case")]
126pub struct PrintCheckpointContentOptions {
127    #[arg(
128        long,
129        help = "The checkpoint content digest (NOT the checkpoint digest)"
130    )]
131    digest: CheckpointContentsDigest,
132}
133
134#[derive(Parser)]
135#[command(rename_all = "kebab-case")]
136pub struct RemoveTransactionOptions {
137    #[arg(long, help = "The transaction digest to remove")]
138    digest: TransactionDigest,
139
140    #[arg(long)]
141    confirm: bool,
142
143    /// The epoch to use when loading AuthorityEpochTables.
144    /// Defaults to the current epoch.
145    #[arg(long = "epoch", short = 'e')]
146    epoch: Option<EpochId>,
147}
148
149#[derive(Parser)]
150#[command(rename_all = "kebab-case")]
151pub struct RemoveObjectLockOptions {
152    #[arg(long, help = "The object ID to remove")]
153    id: ObjectID,
154
155    #[arg(long, help = "The object version to remove")]
156    version: u64,
157
158    #[arg(long)]
159    confirm: bool,
160}
161
162#[derive(Parser)]
163#[command(rename_all = "kebab-case")]
164pub struct RewindCheckpointExecutionOptions {
165    #[arg(long = "epoch")]
166    epoch: EpochId,
167
168    #[arg(long = "checkpoint-sequence-number")]
169    checkpoint_sequence_number: u64,
170}
171
172#[derive(Parser)]
173#[command(rename_all = "kebab-case")]
174pub struct SetCheckpointWatermarkOptions {
175    #[arg(long)]
176    highest_verified: Option<CheckpointSequenceNumber>,
177
178    #[arg(long)]
179    highest_synced: Option<CheckpointSequenceNumber>,
180}
181
182pub async fn execute_db_tool_command(db_path: PathBuf, cmd: DbToolCommand) -> anyhow::Result<()> {
183    match cmd {
184        DbToolCommand::ListTables => print_db_all_tables(db_path),
185        DbToolCommand::Dump(d) => print_all_entries(
186            d.store_name,
187            d.epoch,
188            db_path,
189            &d.table_name,
190            d.page_size,
191            d.page_number,
192        ),
193        DbToolCommand::TableSummary(d) => {
194            print_db_table_summary(d.store_name, d.epoch, db_path, &d.table_name)
195        }
196        DbToolCommand::ListDBMetadata(d) => {
197            print_table_metadata(d.store_name, d.epoch, db_path, &d.table_name)
198        }
199        DbToolCommand::PrintLastConsensusIndex => print_last_consensus_index(&db_path),
200        DbToolCommand::PrintConsensusCommit(d) => print_consensus_commit(&db_path, d),
201        DbToolCommand::PrintTransaction(d) => print_transaction(&db_path, d),
202        DbToolCommand::PrintObject(o) => print_object(&db_path, o),
203        DbToolCommand::PrintCheckpoint(d) => print_checkpoint(&db_path, d),
204        DbToolCommand::PrintCheckpointContent(d) => print_checkpoint_content(&db_path, d),
205        DbToolCommand::ResetDB => reset_db_to_genesis(&db_path).await,
206        DbToolCommand::RewindCheckpointExecution(d) => {
207            rewind_checkpoint_execution(&db_path, d.epoch, d.checkpoint_sequence_number)
208        }
209        DbToolCommand::Compact => compact(db_path),
210        DbToolCommand::PruneObjects => prune_objects(db_path).await,
211        DbToolCommand::PruneCheckpoints => prune_checkpoints(db_path).await,
212        DbToolCommand::IndexSearchKeyRange(rg) => {
213            let res = search_index(
214                db_path,
215                rg.table_name,
216                rg.start,
217                SearchRange::ExclusiveLastKey(rg.end_key),
218            )?;
219            for (k, v) in res {
220                println!("{}: {}", k, v);
221            }
222            Ok(())
223        }
224        DbToolCommand::IndexSearchCount(sc) => {
225            let res = search_index(
226                db_path,
227                sc.table_name,
228                sc.start,
229                SearchRange::Count(sc.count),
230            )?;
231            for (k, v) in res {
232                println!("{}: {}", k, v);
233            }
234            Ok(())
235        }
236        DbToolCommand::SetCheckpointWatermark(d) => set_checkpoint_watermark(&db_path, d),
237    }
238}
239
240pub fn print_db_all_tables(db_path: PathBuf) -> anyhow::Result<()> {
241    list_tables(db_path)?.iter().for_each(|t| println!("{}", t));
242    Ok(())
243}
244
245pub fn print_last_consensus_index(path: &Path) -> anyhow::Result<()> {
246    #[cfg(not(tidehunter))]
247    let epoch_tables = AuthorityEpochTables::open_tables_read_write(
248        path.to_path_buf(),
249        MetricConf::default(),
250        None,
251        None,
252    );
253    #[cfg(tidehunter)]
254    let epoch_tables = AuthorityEpochTables::open_with_path(path);
255    let last_index = epoch_tables.get_last_consensus_index()?;
256    println!("Last consensus index is {:?}", last_index);
257    Ok(())
258}
259
260// TODO: implement for consensus.
261pub fn print_consensus_commit(
262    _path: &Path,
263    _opt: PrintConsensusCommitOptions,
264) -> anyhow::Result<()> {
265    println!("Printing consensus commit is unimplemented");
266    Ok(())
267}
268
269pub fn print_transaction(path: &Path, opt: PrintTransactionOptions) -> anyhow::Result<()> {
270    let perpetual_db = AuthorityPerpetualTables::open(&path.join("store"), None, None);
271    if let Some((epoch, checkpoint_seq_num)) =
272        perpetual_db.get_checkpoint_sequence_number(&opt.digest)?
273    {
274        println!(
275            "Transaction {:?} executed in epoch {} checkpoint {}",
276            opt.digest, epoch, checkpoint_seq_num
277        );
278    };
279    if let Some(effects) = perpetual_db.get_effects(&opt.digest)? {
280        println!(
281            "Transaction {:?} dependencies: {:#?}",
282            opt.digest,
283            effects.dependencies(),
284        );
285    };
286    Ok(())
287}
288
289pub fn print_object(path: &Path, opt: PrintObjectOptions) -> anyhow::Result<()> {
290    let perpetual_db = AuthorityPerpetualTables::open(&path.join("store"), None, None);
291
292    let obj = if let Some(version) = opt.version {
293        perpetual_db.get_object_by_key_fallible(&opt.id, version.into())?
294    } else {
295        perpetual_db.get_object_fallible(&opt.id)?
296    };
297
298    if let Some(obj) = obj {
299        println!("Object {:?}:\n{:#?}", opt.id, obj);
300    } else {
301        println!("Object {:?} not found", opt.id);
302    }
303
304    Ok(())
305}
306
307pub fn print_checkpoint(path: &Path, opt: PrintCheckpointOptions) -> anyhow::Result<()> {
308    let checkpoint_store = CheckpointStore::new(
309        &path.join("checkpoints"),
310        Arc::new(PrunerWatermarks::default()),
311    );
312    let checkpoint = checkpoint_store
313        .get_checkpoint_by_digest(&opt.digest)?
314        .ok_or(anyhow!(
315            "Checkpoint digest {:?} not found in checkpoint store",
316            opt.digest
317        ))?;
318    println!("Checkpoint: {:?}", checkpoint);
319    drop(checkpoint_store);
320    print_checkpoint_content(
321        path,
322        PrintCheckpointContentOptions {
323            digest: checkpoint.content_digest,
324        },
325    )
326}
327
328pub fn print_checkpoint_content(
329    path: &Path,
330    opt: PrintCheckpointContentOptions,
331) -> anyhow::Result<()> {
332    let checkpoint_store = CheckpointStore::new(
333        &path.join("checkpoints"),
334        Arc::new(PrunerWatermarks::default()),
335    );
336    let contents = checkpoint_store
337        .get_checkpoint_contents(&opt.digest)?
338        .ok_or(anyhow!(
339            "Checkpoint content digest {:?} not found in checkpoint store",
340            opt.digest
341        ))?;
342    println!("Checkpoint content: {:?}", contents);
343    Ok(())
344}
345
346pub async fn reset_db_to_genesis(path: &Path) -> anyhow::Result<()> {
347    // Follow the below steps to test:
348    //
349    // Get a db snapshot. Either generate one by running stress locally and enabling db checkpoints or download one from S3 bucket (pretty big in size though).
350    // Download the snapshot for the epoch you want to restore to the local disk. You will find one snapshot per epoch in the S3 bucket. We need to place the snapshot in the dir where config is pointing to. If db-config in fullnode.yaml is /opt/sui/db/authorities_db and we want to restore from epoch 10, we want to copy the snapshot to /opt/sui/db/authorities_dblike this:
351    // aws s3 cp s3://myBucket/dir /opt/sui/db/authorities_db/ --recursive —exclude “*” —include “epoch_10*”
352    // Mark downloaded snapshot as live: mv  /opt/sui/db/authorities_db/epoch_10  /opt/sui/db/authorities_db/live
353    // Reset the downloaded db to execute from genesis with: cargo run --package sui-tool -- db-tool --db-path /opt/sui/db/authorities_db/live reset-db
354    // Start the sui full node: cargo run --release --bin sui-node -- --config-path ~/db_checkpoints/fullnode.yaml
355    // A sample fullnode.yaml config would be:
356    // ---
357    // db-path:  /opt/sui/db/authorities_db
358    // network-address: /ip4/0.0.0.0/tcp/8080/http
359    // json-rpc-address: "0.0.0.0:9000"
360    // websocket-address: "0.0.0.0:9001"
361    // metrics-address: "0.0.0.0:9184"
362    // admin-interface-port: 1337
363    // enable-event-processing: true
364    // grpc-load-shed: ~
365    // grpc-concurrency-limit: ~
366    // p2p-config:
367    //   listen-address: "0.0.0.0:8084"
368    // genesis:
369    //   genesis-file-location:  <path to genesis blob for the network>
370    // authority-store-pruning-config:
371    //   num-latest-epoch-dbs-to-retain: 3
372    //   epoch-db-pruning-period-secs: 3600
373    //   num-epochs-to-retain: 18446744073709551615
374    //   max-checkpoints-in-batch: 10
375    //   max-transactions-in-batch: 1000
376    safe_drop_db(
377        path.join("store").join("perpetual"),
378        std::time::Duration::from_secs(60),
379    )
380    .await?;
381
382    let checkpoint_db = CheckpointStore::new(
383        &path.join("checkpoints"),
384        Arc::new(PrunerWatermarks::default()),
385    );
386    checkpoint_db.reset_db_for_execution_since_genesis()?;
387
388    Ok(())
389}
390
391/// Force sets the highest executed checkpoint.
392/// NOTE: Does not force re-execution of transactions.
393/// Run with: cargo run --package sui-tool -- db-tool --db-path /opt/sui/db/authorities_db/live rewind-checkpoint-execution --epoch 3 --checkpoint-sequence-number 300000
394pub fn rewind_checkpoint_execution(
395    path: &Path,
396    epoch: EpochId,
397    checkpoint_sequence_number: u64,
398) -> anyhow::Result<()> {
399    let checkpoint_db = CheckpointStore::new(
400        &path.join("checkpoints"),
401        Arc::new(PrunerWatermarks::default()),
402    );
403    let Some(checkpoint) =
404        checkpoint_db.get_checkpoint_by_sequence_number(checkpoint_sequence_number)?
405    else {
406        bail!("Checkpoint {checkpoint_sequence_number} not found!");
407    };
408    if epoch != checkpoint.epoch() {
409        bail!(
410            "Checkpoint {checkpoint_sequence_number} is in epoch {} not {epoch}!",
411            checkpoint.epoch()
412        );
413    }
414
415    let highest_executed_sequence_number = checkpoint_db
416        .get_highest_executed_checkpoint_seq_number()?
417        .unwrap_or_default();
418    if checkpoint_sequence_number > highest_executed_sequence_number {
419        bail!(
420            "Must rewind checkpoint execution to be not later than highest executed ({} > {})!",
421            checkpoint_sequence_number,
422            highest_executed_sequence_number
423        );
424    }
425    checkpoint_db.set_highest_executed_checkpoint_subtle(&checkpoint)?;
426    Ok(())
427}
428
429pub fn print_db_table_summary(
430    store: StoreName,
431    epoch: Option<EpochId>,
432    path: PathBuf,
433    table_name: &str,
434) -> anyhow::Result<()> {
435    let summary = table_summary(store, epoch, path, table_name)?;
436    let quantiles = [25, 50, 75, 90, 99];
437    println!(
438        "Total num keys = {}, total key bytes = {}, total value bytes = {}",
439        summary.num_keys, summary.key_bytes_total, summary.value_bytes_total
440    );
441    println!("Key size distribution:\n");
442    quantiles.iter().for_each(|q| {
443        println!(
444            "p{:?} -> {:?} bytes\n",
445            q,
446            summary.key_hist.value_at_quantile(*q as f64 / 100.0)
447        );
448    });
449    println!("Value size distribution:\n");
450    quantiles.iter().for_each(|q| {
451        println!(
452            "p{:?} -> {:?} bytes\n",
453            q,
454            summary.value_hist.value_at_quantile(*q as f64 / 100.0)
455        );
456    });
457    Ok(())
458}
459
460pub fn print_all_entries(
461    store: StoreName,
462    epoch: Option<EpochId>,
463    path: PathBuf,
464    table_name: &str,
465    page_size: u16,
466    page_number: usize,
467) -> anyhow::Result<()> {
468    for (k, v) in dump_table(store, epoch, path, table_name, page_size, page_number)? {
469        println!("{:>100?}: {:?}", k, v);
470    }
471    Ok(())
472}
473
474/// Force sets state sync checkpoint watermarks.
475/// Run with (for example):
476/// cargo run --package sui-tool -- db-tool --db-path /opt/sui/db/authorities_db/live set_checkpoint_watermark --highest-synced 300000
477pub fn set_checkpoint_watermark(
478    path: &Path,
479    options: SetCheckpointWatermarkOptions,
480) -> anyhow::Result<()> {
481    let checkpoint_db = CheckpointStore::new(
482        &path.join("checkpoints"),
483        Arc::new(PrunerWatermarks::default()),
484    );
485
486    if let Some(highest_verified) = options.highest_verified {
487        let Some(checkpoint) = checkpoint_db.get_checkpoint_by_sequence_number(highest_verified)?
488        else {
489            bail!("Checkpoint {highest_verified} not found");
490        };
491        checkpoint_db.update_highest_verified_checkpoint(&checkpoint)?;
492    }
493    if let Some(highest_synced) = options.highest_synced {
494        let Some(checkpoint) = checkpoint_db.get_checkpoint_by_sequence_number(highest_synced)?
495        else {
496            bail!("Checkpoint {highest_synced} not found");
497        };
498        checkpoint_db.update_highest_synced_checkpoint(&checkpoint)?;
499    }
500    Ok(())
501}