1use 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 #[arg(long = "store", short = 's', value_enum)]
75 store_name: StoreName,
76 #[arg(long = "table-name", short = 't')]
78 table_name: String,
79 #[arg(long = "page-size", short = 'p')]
81 page_size: u16,
82 #[arg(long = "page-num", short = 'n')]
84 page_number: usize,
85
86 #[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 #[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
260pub 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 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
391pub 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
474pub 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}