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