1use std::path::{Path, PathBuf};
6
7use anyhow::{anyhow, bail};
8use clap::Parser;
9use iota_core::{
10 authority::{
11 authority_per_epoch_store::AuthorityEpochTables,
12 authority_store_tables::AuthorityPerpetualTables,
13 },
14 checkpoints::CheckpointStore,
15};
16use iota_sdk_types::{CheckpointContentsDigest, CheckpointDigest, ObjectId, TransactionDigest};
17use iota_types::{
18 base_types::EpochId, effects::TransactionEffectsAPI,
19 messages_checkpoint::CheckpointSequenceNumber, storage::ObjectStore,
20};
21use typed_store::rocks::MetricConf;
22
23use self::{
24 db_dump::{StoreName, dump_table, duplicate_objects_summary, list_tables, table_summary},
25 index_search::{SearchRange, search_index},
26};
27use crate::db_tool::db_dump::{compact, print_table_metadata, prune_checkpoints, prune_objects};
28pub mod db_dump;
29mod index_search;
30
31#[derive(Parser)]
32pub enum DbToolCommand {
33 ListTables,
34 Dump(Options),
35 IndexSearchKeyRange(IndexSearchKeyRangeOptions),
36 IndexSearchCount(IndexSearchCountOptions),
37 TableSummary(Options),
38 DuplicatesSummary,
39 ListDBMetadata(Options),
40 PrintLastConsensusIndex,
41 PrintTransaction(PrintTransactionOptions),
42 PrintObject(PrintObjectOptions),
43 PrintCheckpoint(PrintCheckpointOptions),
44 PrintCheckpointContent(PrintCheckpointContentOptions),
45 RewindCheckpointExecution(RewindCheckpointExecutionOptions),
46 Compact,
47 PruneObjects,
48 PruneCheckpoints,
49 SetCheckpointWatermark(SetCheckpointWatermarkOptions),
50}
51
52#[derive(Parser)]
53pub struct IndexSearchKeyRangeOptions {
54 #[arg(long, short = 't')]
55 table_name: String,
56 #[arg(long, short = 's')]
57 start: String,
58 #[arg(long = "end", short = 'e')]
59 end_key: String,
60}
61
62#[derive(Parser)]
63pub struct IndexSearchCountOptions {
64 #[arg(long, short = 't')]
65 table_name: String,
66 #[arg(long, short = 's')]
67 start: String,
68 #[arg(long, short = 'c')]
69 count: u64,
70}
71
72#[derive(Parser)]
73pub struct Options {
74 #[arg(long = "store", short = 's', value_enum)]
76 store_name: StoreName,
77 #[arg(long, short = 't')]
79 table_name: String,
80 #[arg(long, short = 'p')]
82 page_size: u16,
83 #[arg(long = "page-num", short = 'n')]
85 page_number: usize,
86
87 #[arg(long, short = 'e')]
92 epoch: Option<EpochId>,
93}
94
95#[derive(Parser)]
96pub struct PrintTransactionOptions {
97 #[arg(long, help = "The transaction digest to print")]
98 digest: TransactionDigest,
99}
100
101#[derive(Parser)]
102pub struct PrintObjectOptions {
103 #[arg(long, help = "The object id to print")]
104 id: ObjectId,
105 #[arg(long, help = "The object version to print")]
106 version: Option<u64>,
107}
108
109#[derive(Parser)]
110pub struct PrintCheckpointOptions {
111 #[arg(long, help = "The checkpoint digest to print")]
112 digest: CheckpointDigest,
113}
114
115#[derive(Parser)]
116pub struct PrintCheckpointContentOptions {
117 #[arg(
118 long,
119 help = "The checkpoint content digest (NOT the checkpoint digest)"
120 )]
121 digest: CheckpointContentsDigest,
122}
123
124#[derive(Parser)]
125pub struct RemoveTransactionOptions {
126 #[arg(long, help = "The transaction digest to remove")]
127 digest: TransactionDigest,
128
129 #[arg(long)]
130 confirm: bool,
131
132 #[arg(long, short = 'e')]
135 epoch: Option<EpochId>,
136}
137
138#[derive(Parser)]
139pub struct RemoveObjectLockOptions {
140 #[arg(long, help = "The object ID to remove")]
141 id: ObjectId,
142
143 #[arg(long, help = "The object version to remove")]
144 version: u64,
145
146 #[arg(long)]
147 confirm: bool,
148}
149
150#[derive(Parser)]
151pub struct RewindCheckpointExecutionOptions {
152 #[arg(long)]
153 epoch: EpochId,
154
155 #[arg(long)]
156 checkpoint_sequence_number: u64,
157}
158
159#[derive(Parser)]
160pub struct SetCheckpointWatermarkOptions {
161 #[arg(long)]
162 highest_verified: Option<CheckpointSequenceNumber>,
163
164 #[arg(long)]
165 highest_synced: Option<CheckpointSequenceNumber>,
166}
167
168pub async fn execute_db_tool_command(db_path: PathBuf, cmd: DbToolCommand) -> anyhow::Result<()> {
169 match cmd {
170 DbToolCommand::ListTables => print_db_all_tables(db_path),
171 DbToolCommand::Dump(d) => print_all_entries(
172 d.store_name,
173 d.epoch,
174 db_path,
175 &d.table_name,
176 d.page_size,
177 d.page_number,
178 ),
179 DbToolCommand::TableSummary(d) => {
180 print_db_table_summary(d.store_name, d.epoch, db_path, &d.table_name)
181 }
182 DbToolCommand::DuplicatesSummary => print_db_duplicates_summary(db_path),
183 DbToolCommand::ListDBMetadata(d) => {
184 print_table_metadata(d.store_name, d.epoch, db_path, &d.table_name)
185 }
186 DbToolCommand::PrintLastConsensusIndex => print_last_consensus_index(&db_path),
187 DbToolCommand::PrintTransaction(d) => print_transaction(&db_path, d),
188 DbToolCommand::PrintObject(o) => print_object(&db_path, o),
189 DbToolCommand::PrintCheckpoint(d) => print_checkpoint(&db_path, d),
190 DbToolCommand::PrintCheckpointContent(d) => print_checkpoint_content(&db_path, d),
191 DbToolCommand::RewindCheckpointExecution(d) => {
192 rewind_checkpoint_execution(&db_path, d.epoch, d.checkpoint_sequence_number)
193 }
194 DbToolCommand::Compact => compact(db_path),
195 DbToolCommand::PruneObjects => prune_objects(db_path).await,
196 DbToolCommand::PruneCheckpoints => prune_checkpoints(db_path).await,
197 DbToolCommand::IndexSearchKeyRange(rg) => {
198 let res = search_index(
199 db_path,
200 rg.table_name,
201 rg.start,
202 SearchRange::ExclusiveLastKey(rg.end_key),
203 )?;
204 for (k, v) in res {
205 println!("{k}: {v}");
206 }
207 Ok(())
208 }
209 DbToolCommand::IndexSearchCount(sc) => {
210 let res = search_index(
211 db_path,
212 sc.table_name,
213 sc.start,
214 SearchRange::Count(sc.count),
215 )?;
216 for (k, v) in res {
217 println!("{k}: {v}");
218 }
219 Ok(())
220 }
221 DbToolCommand::SetCheckpointWatermark(d) => set_checkpoint_watermark(&db_path, d),
222 }
223}
224
225pub fn print_db_all_tables(db_path: PathBuf) -> anyhow::Result<()> {
226 list_tables(db_path)?.iter().for_each(|t| println!("{t}"));
227 Ok(())
228}
229
230pub fn print_db_duplicates_summary(db_path: PathBuf) -> anyhow::Result<()> {
231 let (total_count, duplicate_count, total_bytes, duplicated_bytes) =
232 duplicate_objects_summary(db_path)?;
233 println!(
234 "Total objects = {total_count}, duplicated objects = {duplicate_count}, total bytes = {total_bytes}, duplicated bytes = {duplicated_bytes}"
235 );
236 Ok(())
237}
238
239pub fn print_last_consensus_index(path: &Path) -> anyhow::Result<()> {
240 let epoch_tables = AuthorityEpochTables::open_tables_read_write(
241 path.to_path_buf(),
242 MetricConf::default(),
243 None,
244 None,
245 );
246 let last_index = epoch_tables.get_last_consensus_index()?;
247 println!("Last consensus index is {last_index:?}");
248 Ok(())
249}
250
251pub fn print_transaction(path: &Path, opt: PrintTransactionOptions) -> anyhow::Result<()> {
252 let perpetual_db = AuthorityPerpetualTables::open(&path.join("store"), None);
253 if let Some((epoch, checkpoint_seq_num)) =
254 perpetual_db.get_checkpoint_sequence_number(&opt.digest)?
255 {
256 println!(
257 "Transaction {:?} executed in epoch {} checkpoint {}",
258 opt.digest, epoch, checkpoint_seq_num
259 );
260 };
261 if let Some(effects) = perpetual_db.get_effects(&opt.digest)? {
262 println!(
263 "Transaction {:?} dependencies: {:#?}",
264 opt.digest,
265 effects.dependencies(),
266 );
267 };
268 Ok(())
269}
270
271pub fn print_object(path: &Path, opt: PrintObjectOptions) -> anyhow::Result<()> {
272 let perpetual_db = AuthorityPerpetualTables::open(&path.join("store"), None);
273
274 let obj = if let Some(version) = opt.version {
275 perpetual_db.try_get_object_by_key(&opt.id, version.into())?
276 } else {
277 perpetual_db.try_get_object(&opt.id)?
278 };
279
280 if let Some(obj) = obj {
281 println!("Object {:?}:\n{:#?}", opt.id, obj);
282 } else {
283 println!("Object {:?} not found", opt.id);
284 }
285
286 Ok(())
287}
288
289pub fn print_checkpoint(path: &Path, opt: PrintCheckpointOptions) -> anyhow::Result<()> {
290 let checkpoint_store = CheckpointStore::new(&path.join("checkpoints"));
291 let checkpoint = checkpoint_store
292 .get_checkpoint_by_digest(&opt.digest)?
293 .ok_or(anyhow!(
294 "Checkpoint digest {:?} not found in checkpoint store",
295 opt.digest
296 ))?;
297 println!("Checkpoint: {checkpoint:?}");
298 drop(checkpoint_store);
299 print_checkpoint_content(
300 path,
301 PrintCheckpointContentOptions {
302 digest: checkpoint.contents_digest,
303 },
304 )
305}
306
307pub fn print_checkpoint_content(
308 path: &Path,
309 opt: PrintCheckpointContentOptions,
310) -> anyhow::Result<()> {
311 let checkpoint_store = CheckpointStore::new(&path.join("checkpoints"));
312 let contents = checkpoint_store
313 .get_checkpoint_contents(&opt.digest)?
314 .ok_or(anyhow!(
315 "Checkpoint content digest {:?} not found in checkpoint store",
316 opt.digest
317 ))?;
318 println!("Checkpoint content: {contents:?}");
319 Ok(())
320}
321
322pub fn rewind_checkpoint_execution(
328 path: &Path,
329 epoch: EpochId,
330 checkpoint_sequence_number: u64,
331) -> anyhow::Result<()> {
332 let checkpoint_db = CheckpointStore::new(&path.join("checkpoints"));
333 let Some(checkpoint) =
334 checkpoint_db.get_checkpoint_by_sequence_number(checkpoint_sequence_number)?
335 else {
336 bail!("Checkpoint {checkpoint_sequence_number} not found!");
337 };
338 if epoch != checkpoint.epoch() {
339 bail!(
340 "Checkpoint {checkpoint_sequence_number} is in epoch {} not {epoch}!",
341 checkpoint.epoch()
342 );
343 }
344
345 let highest_executed_sequence_number = checkpoint_db
346 .get_highest_executed_checkpoint_seq_number()?
347 .unwrap_or_default();
348 if checkpoint_sequence_number > highest_executed_sequence_number {
349 bail!(
350 "Must rewind checkpoint execution to be not later than highest executed ({checkpoint_sequence_number} > {highest_executed_sequence_number})!"
351 );
352 }
353 checkpoint_db.set_highest_executed_checkpoint_subtle(&checkpoint)?;
354 Ok(())
355}
356
357pub fn print_db_table_summary(
358 store: StoreName,
359 epoch: Option<EpochId>,
360 path: PathBuf,
361 table_name: &str,
362) -> anyhow::Result<()> {
363 let summary = table_summary(store, epoch, path, table_name)?;
364 let quantiles = [25, 50, 75, 90, 99];
365 println!(
366 "Total num keys = {}, total key bytes = {}, total value bytes = {}",
367 summary.num_keys, summary.key_bytes_total, summary.value_bytes_total
368 );
369 println!("Key size distribution:\n");
370 quantiles.iter().for_each(|q| {
371 println!(
372 "p{:?} -> {:?} bytes\n",
373 q,
374 summary.key_hist.value_at_quantile(*q as f64 / 100.0)
375 );
376 });
377 println!("Value size distribution:\n");
378 quantiles.iter().for_each(|q| {
379 println!(
380 "p{:?} -> {:?} bytes\n",
381 q,
382 summary.value_hist.value_at_quantile(*q as f64 / 100.0)
383 );
384 });
385 Ok(())
386}
387
388pub fn print_all_entries(
389 store: StoreName,
390 epoch: Option<EpochId>,
391 path: PathBuf,
392 table_name: &str,
393 page_size: u16,
394 page_number: usize,
395) -> anyhow::Result<()> {
396 for (k, v) in dump_table(store, epoch, path, table_name, page_size, page_number)? {
397 println!("{k:>100?}: {v:?}");
398 }
399 Ok(())
400}
401
402pub fn set_checkpoint_watermark(
408 path: &Path,
409 options: SetCheckpointWatermarkOptions,
410) -> anyhow::Result<()> {
411 let checkpoint_db = CheckpointStore::new(&path.join("checkpoints"));
412
413 if let Some(highest_verified) = options.highest_verified {
414 let Some(checkpoint) = checkpoint_db.get_checkpoint_by_sequence_number(highest_verified)?
415 else {
416 bail!("Checkpoint {highest_verified} not found");
417 };
418 checkpoint_db.set_highest_verified_checkpoint_subtle(&checkpoint)?;
419 }
420 if let Some(highest_synced) = options.highest_synced {
421 let Some(checkpoint) = checkpoint_db.get_checkpoint_by_sequence_number(highest_synced)?
422 else {
423 bail!("Checkpoint {highest_synced} not found");
424 };
425 if checkpoint_db
429 .get_checkpoint_contents(&checkpoint.contents_digest)?
430 .is_none()
431 {
432 bail!(
433 "Checkpoint contents not found for checkpoint {highest_synced}. \
434 Setting highest_synced without contents would cause the executor to panic."
435 );
436 }
437 checkpoint_db.set_highest_synced_checkpoint_subtle(&checkpoint)?;
438 }
439 Ok(())
440}