fedimint_dbtool/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::return_self_not_must_use)]
6
7pub mod envs;
8
9use std::path::PathBuf;
10
11use anyhow::Result;
12use bytes::Bytes;
13use clap::{Parser, Subcommand};
14use fedimint_client::module_init::ClientModuleInitRegistry;
15use fedimint_client_module::module::init::ClientModuleInit;
16use fedimint_core::db::{IDatabaseTransactionOpsCore, IRawDatabaseExt};
17use fedimint_core::util::handle_version_hash_command;
18use fedimint_ln_client::LightningClientInit;
19use fedimint_ln_server::LightningInit;
20use fedimint_logging::TracingSetup;
21use fedimint_meta_client::MetaClientInit;
22use fedimint_meta_server::MetaInit;
23use fedimint_mint_client::MintClientInit;
24use fedimint_mint_server::MintInit;
25use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
26use fedimint_wallet_client::WalletClientInit;
27use fedimint_wallet_server::WalletInit;
28use futures::StreamExt;
29use hex::ToHex;
30
31use crate::dump::DatabaseDump;
32use crate::envs::{FM_DBTOOL_CONFIG_DIR_ENV, FM_DBTOOL_DATABASE_ENV, FM_PASSWORD_ENV};
33
34mod dump;
35
36#[derive(Debug, Clone, Parser)]
37#[command(version)]
38struct Options {
39    #[clap(long, env = FM_DBTOOL_DATABASE_ENV)]
40    database_dir: String,
41
42    #[clap(long, hide = true)]
43    /// Run dbtool like it doesn't know about any module kind. This is a
44    /// internal option for testing.
45    no_modules: bool,
46
47    #[command(subcommand)]
48    command: DbCommand,
49}
50
51/// Tool to inspect and manipulate rocksdb databases. All binary arguments
52/// (keys, values) have to be hex encoded.
53#[derive(Debug, Clone, Subcommand)]
54enum DbCommand {
55    /// List all key-value pairs where the key begins with `prefix`
56    List {
57        #[arg(long, value_parser = hex_parser)]
58        prefix: Bytes,
59    },
60    /// Write a key-value pair to the database, overwriting the previous value
61    /// if present
62    Write {
63        #[arg(long, value_parser = hex_parser)]
64        key: Bytes,
65        #[arg(long, value_parser = hex_parser)]
66        value: Bytes,
67    },
68    /// Delete a single entry from the database identified by `key`
69    Delete {
70        #[arg(long, value_parser = hex_parser)]
71        key: Bytes,
72    },
73    /// Deletes all keys starting
74    DeletePrefix {
75        #[arg(long, value_parser = hex_parser)]
76        prefix: Bytes,
77    },
78    /// Dump a subset of the specified database and serialize the retrieved data
79    /// to JSON. Module and prefix are used to specify which subset of the
80    /// database to dump. Password is used to decrypt the server's
81    /// configuration file. If dumping the client database, the password can
82    /// be an arbitrary string.
83    Dump {
84        #[clap(long, env = FM_DBTOOL_CONFIG_DIR_ENV)]
85        cfg_dir: PathBuf,
86        #[arg(long, env = FM_PASSWORD_ENV)]
87        password: String,
88        #[arg(long, required = false)]
89        modules: Option<String>,
90        #[arg(long, required = false)]
91        prefixes: Option<String>,
92    },
93}
94
95fn hex_parser(hex: &str) -> Result<Bytes> {
96    let bytes: Vec<u8> = hex::FromHex::from_hex(hex)?;
97    Ok(bytes.into())
98}
99
100fn print_kv(key: &[u8], value: &[u8]) {
101    println!(
102        "{} {}",
103        key.encode_hex::<String>(),
104        value.encode_hex::<String>()
105    );
106}
107
108pub struct FedimintDBTool {
109    server_module_inits: ServerModuleInitRegistry,
110    client_module_inits: ClientModuleInitRegistry,
111    cli_args: Options,
112}
113
114impl FedimintDBTool {
115    /// Build a new `fedimintdb-tool` with a custom version hash
116    pub fn new(version_hash: &str) -> anyhow::Result<Self> {
117        handle_version_hash_command(version_hash);
118        TracingSetup::default().init()?;
119
120        Ok(Self {
121            server_module_inits: ServerModuleInitRegistry::new(),
122            client_module_inits: ClientModuleInitRegistry::new(),
123            cli_args: Options::parse(),
124        })
125    }
126
127    pub fn with_server_module_init<T>(mut self, r#gen: T) -> Self
128    where
129        T: ServerModuleInit + 'static + Send + Sync,
130    {
131        self.server_module_inits.attach(r#gen);
132        self
133    }
134
135    pub fn with_client_module_init<T>(mut self, r#gen: T) -> Self
136    where
137        T: ClientModuleInit + 'static + Send + Sync,
138    {
139        self.client_module_inits.attach(r#gen);
140        self
141    }
142
143    pub fn with_default_modules_inits(self) -> Self {
144        self.with_server_module_init(WalletInit)
145            .with_server_module_init(MintInit)
146            .with_server_module_init(LightningInit)
147            .with_server_module_init(fedimint_lnv2_server::LightningInit)
148            .with_server_module_init(MetaInit)
149            .with_client_module_init(WalletClientInit::default())
150            .with_client_module_init(MintClientInit)
151            .with_client_module_init(LightningClientInit::default())
152            .with_client_module_init(fedimint_lnv2_client::LightningClientInit::default())
153            .with_client_module_init(MetaClientInit)
154    }
155
156    pub async fn run(&self) -> anyhow::Result<()> {
157        let options = &self.cli_args;
158        match &options.command {
159            DbCommand::List { prefix } => {
160                let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
161                    .unwrap()
162                    .into_database();
163                let mut dbtx = rocksdb.begin_transaction().await;
164                let prefix_iter = dbtx
165                    .raw_find_by_prefix(prefix)
166                    .await?
167                    .collect::<Vec<_>>()
168                    .await;
169                for (key, value) in prefix_iter {
170                    print_kv(&key, &value);
171                }
172                dbtx.commit_tx().await;
173            }
174            DbCommand::Write { key, value } => {
175                let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
176                    .unwrap()
177                    .into_database();
178                let mut dbtx = rocksdb.begin_transaction().await;
179                dbtx.raw_insert_bytes(key, value)
180                    .await
181                    .expect("Error inserting entry into RocksDb");
182                dbtx.commit_tx().await;
183            }
184            DbCommand::Delete { key } => {
185                let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
186                    .unwrap()
187                    .into_database();
188                let mut dbtx = rocksdb.begin_transaction().await;
189                dbtx.raw_remove_entry(key)
190                    .await
191                    .expect("Error removing entry from RocksDb");
192                dbtx.commit_tx().await;
193            }
194            DbCommand::Dump {
195                cfg_dir,
196                modules,
197                prefixes,
198                password,
199            } => {
200                let modules = match modules {
201                    Some(mods) => mods
202                        .split(',')
203                        .map(|s| s.to_string().to_lowercase())
204                        .collect::<Vec<String>>(),
205                    None => Vec::new(),
206                };
207
208                let prefix_names = match prefixes {
209                    Some(db_prefixes) => db_prefixes
210                        .split(',')
211                        .map(|s| s.to_string().to_lowercase())
212                        .collect::<Vec<String>>(),
213                    None => Vec::new(),
214                };
215
216                let (module_inits, client_module_inits) = if options.no_modules {
217                    (
218                        ServerModuleInitRegistry::new(),
219                        ClientModuleInitRegistry::new(),
220                    )
221                } else {
222                    (
223                        self.server_module_inits.clone(),
224                        self.client_module_inits.clone(),
225                    )
226                };
227
228                let mut dbdump = DatabaseDump::new(
229                    cfg_dir.clone(),
230                    options.database_dir.clone(),
231                    password.to_string(),
232                    module_inits,
233                    client_module_inits,
234                    modules,
235                    prefix_names,
236                )
237                .await?;
238                dbdump.dump_database().await?;
239            }
240            DbCommand::DeletePrefix { prefix } => {
241                let rocksdb = fedimint_rocksdb::RocksDb::open(&options.database_dir)
242                    .unwrap()
243                    .into_database();
244                let mut dbtx = rocksdb.begin_transaction().await;
245                dbtx.raw_remove_by_prefix(prefix).await?;
246                dbtx.commit_tx().await;
247            }
248        }
249
250        Ok(())
251    }
252}