1use crate::common::CliNodeTypes;
4use alloy_primitives::Address;
5use clap::Parser;
6use reth_db::{
7 mdbx::{self, ffi},
8 models::StorageBeforeTx,
9 DatabaseEnv,
10};
11use reth_db_api::{
12 cursor::DbCursorRO,
13 database::Database,
14 table::Table,
15 tables,
16 transaction::{DbTx, DbTxMut},
17};
18use reth_node_builder::NodeTypesWithDBAdapter;
19use reth_provider::{
20 providers::ProviderNodeTypes, BlockNumReader, DBProvider, DatabaseProviderFactory,
21 MetadataProvider, MetadataWriter, ProviderFactory, PruneCheckpointReader,
22 RocksDBProviderFactory, StageCheckpointWriter, StaticFileProviderFactory, StaticFileWriter,
23 StorageSettings,
24};
25use reth_prune_types::PruneSegment;
26use reth_stages_types::{StageCheckpoint, StageId};
27use reth_static_file_types::StaticFileSegment;
28use reth_storage_api::StageCheckpointReader;
29use tracing::{info, warn};
30
31#[derive(Debug, Parser)]
33pub struct Command;
34
35impl Command {
36 pub async fn execute<N: CliNodeTypes>(
43 self,
44 provider_factory: ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
45 ) -> eyre::Result<()>
46 where
47 N::Primitives: reth_primitives_traits::NodePrimitives<
48 Receipt: reth_db_api::table::Value + reth_codecs::Compact,
49 >,
50 {
51 info!(target: "reth::cli", "Starting v1 → v2 storage migration");
53
54 let provider = provider_factory.provider()?;
55 let current_settings = provider.storage_settings()?;
56
57 if current_settings.is_some_and(|s| s.is_v2()) {
58 info!(target: "reth::cli", "Storage is already v2, nothing to do");
59 return Ok(());
60 }
61
62 let tip =
63 provider.get_stage_checkpoint(StageId::Execution)?.map(|c| c.block_number).unwrap_or(0);
64
65 info!(target: "reth::cli", tip, "Chain tip block number");
66
67 let sf_provider = provider_factory.static_file_provider();
68
69 for segment in [StaticFileSegment::AccountChangeSets, StaticFileSegment::StorageChangeSets]
70 {
71 if sf_provider.get_highest_static_file_block(segment).is_some() {
72 eyre::bail!(
73 "Static file segment {segment:?} already contains data. \
74 Cannot migrate — target must be empty."
75 );
76 }
77 }
78
79 drop(provider);
80
81 Self::migrate_account_changesets(&provider_factory, tip)?;
83 Self::migrate_storage_changesets(&provider_factory, tip)?;
84
85 Self::migrate_receipts::<NodeTypesWithDBAdapter<N, DatabaseEnv>>(&provider_factory, tip)?;
87
88 Self::migrate_to_rocksdb::<_, tables::TransactionHashNumbers>(&provider_factory)?;
90 Self::migrate_to_rocksdb::<_, tables::AccountsHistory>(&provider_factory)?;
91 Self::migrate_to_rocksdb::<_, tables::StoragesHistory>(&provider_factory)?;
92
93 info!(target: "reth::cli", "Writing StorageSettings v2 metadata");
95 {
96 let provider_rw = provider_factory.database_provider_rw()?;
97 provider_rw.write_storage_settings(StorageSettings::v2())?;
98 provider_rw.commit()?;
99 }
100 info!(target: "reth::cli", "Storage settings updated to v2");
101
102 Self::clear_migrated_and_recomputable_tables(&provider_factory)?;
104
105 let db_path = provider_factory.db_ref().path();
107 Self::compact_mdbx(provider_factory.db_ref())?;
108
109 drop(provider_factory);
111
112 let compact_path = db_path.with_file_name("db_compact");
113 Self::swap_compacted_db(&db_path, &compact_path)?;
114
115 info!(target: "reth::cli", "Migration complete. You should now restart the node and let it run the pipeline to rebuild the remaining data.");
120 Ok(())
121 }
122
123 fn migrate_account_changesets<N: ProviderNodeTypes>(
124 factory: &ProviderFactory<N>,
125 tip: u64,
126 ) -> eyre::Result<()> {
127 info!(target: "reth::cli", "Migrating AccountChangeSets → static files");
128 let provider = factory.provider()?.disable_long_read_transaction_safety();
129 let sf_provider = factory.static_file_provider();
130
131 let mut cursor = provider.tx_ref().cursor_read::<tables::AccountChangeSets>()?;
132
133 let first_block = provider
134 .get_prune_checkpoint(PruneSegment::AccountHistory)?
135 .and_then(|cp| cp.block_number)
136 .map_or(0, |b| b + 1);
137
138 let mut writer = sf_provider.latest_writer(StaticFileSegment::AccountChangeSets)?;
141 if first_block > 0 {
142 writer.ensure_at_block(first_block - 1)?;
143 }
144
145 let mut count = 0u64;
146 let mut walker = cursor.walk(Some(first_block))?.peekable();
147
148 for block in first_block..=tip {
149 let mut entries = Vec::new();
150
151 while let Some(Ok((block_number, _))) = walker.peek() {
152 if *block_number != block {
153 break;
154 }
155 let (_, entry) = walker.next().expect("peeked")?;
156 entries.push(entry);
157 }
158
159 if matches!(walker.peek(), Some(Err(_))) {
160 walker.next().expect("peeked")?;
161 }
162
163 count += entries.len() as u64;
164 writer.append_account_changeset(entries, block)?;
165 }
166
167 writer.commit()?;
168
169 info!(target: "reth::cli", count, "AccountChangeSets migrated");
170 Ok(())
171 }
172
173 fn migrate_storage_changesets<N: ProviderNodeTypes>(
174 factory: &ProviderFactory<N>,
175 tip: u64,
176 ) -> eyre::Result<()> {
177 info!(target: "reth::cli", "Migrating StorageChangeSets → static files");
178 let provider = factory.provider()?.disable_long_read_transaction_safety();
179 let sf_provider = factory.static_file_provider();
180
181 let mut cursor = provider.tx_ref().cursor_read::<tables::StorageChangeSets>()?;
182
183 let first_block = provider
184 .get_prune_checkpoint(PruneSegment::StorageHistory)?
185 .and_then(|cp| cp.block_number)
186 .map_or(0, |b| b + 1);
187
188 let mut writer = sf_provider.latest_writer(StaticFileSegment::StorageChangeSets)?;
191 if first_block > 0 {
192 writer.ensure_at_block(first_block - 1)?;
193 }
194
195 let mut count = 0u64;
196 let mut walker = cursor.walk(Some((first_block, Address::ZERO).into()))?.peekable();
197
198 for block in first_block..=tip {
199 let mut entries = Vec::new();
200
201 while let Some(Ok((key, _))) = walker.peek() {
202 if key.block_number() != block {
203 break;
204 }
205 let (key, entry) = walker.next().expect("peeked")?;
206 entries.push(StorageBeforeTx {
207 address: key.address(),
208 key: entry.key,
209 value: entry.value,
210 });
211 }
212
213 if matches!(walker.peek(), Some(Err(_))) {
214 walker.next().expect("peeked")?;
215 }
216
217 count += entries.len() as u64;
218 writer.append_storage_changeset(entries, block)?;
219 }
220
221 writer.commit()?;
222
223 info!(target: "reth::cli", count, "StorageChangeSets migrated");
224 Ok(())
225 }
226
227 fn migrate_receipts<N: ProviderNodeTypes>(
228 factory: &ProviderFactory<N>,
229 tip: u64,
230 ) -> eyre::Result<()>
231 where
232 N::Primitives: reth_primitives_traits::NodePrimitives<
233 Receipt: reth_db_api::table::Value + reth_codecs::Compact,
234 >,
235 {
236 let provider = factory.provider()?;
237 if !provider.prune_modes_ref().receipts_log_filter.is_empty() {
238 info!(target: "reth::cli", "Receipt log filter pruning is enabled, keeping receipts in MDBX");
239 return Ok(());
240 }
241 drop(provider);
242
243 let sf_provider = factory.static_file_provider();
244 let existing = sf_provider.get_highest_static_file_block(StaticFileSegment::Receipts);
245
246 if existing.is_some_and(|b| b >= tip) {
247 info!(target: "reth::cli", "Receipts already in static files, skipping");
248 return Ok(());
249 }
250
251 info!(target: "reth::cli", "Migrating Receipts → static files");
252
253 let provider = factory.provider()?.disable_long_read_transaction_safety();
254 let prune_start = provider
255 .get_prune_checkpoint(PruneSegment::Receipts)?
256 .and_then(|cp| cp.block_number)
257 .map_or(0, |b| b + 1);
258 let first_block = prune_start.max(existing.map_or(0, |b| b + 1));
259
260 if first_block > 0 {
263 let mut writer = sf_provider.latest_writer(StaticFileSegment::Receipts)?;
264 writer.ensure_at_block(first_block - 1)?;
265 writer.commit()?;
266 }
267
268 let before = sf_provider
269 .get_highest_static_file_tx(StaticFileSegment::Receipts)
270 .map_or(0, |tx| tx + 1);
271
272 let block_range = first_block..=tip;
273
274 let segment = reth_static_file::segments::Receipts;
275 reth_static_file::segments::Segment::copy_to_static_files(&segment, provider, block_range)?;
276
277 sf_provider.commit()?;
278
279 let after = sf_provider
280 .get_highest_static_file_tx(StaticFileSegment::Receipts)
281 .map_or(0, |tx| tx + 1);
282 let count = after - before;
283 info!(target: "reth::cli", count, "Receipts migrated");
284 Ok(())
285 }
286
287 fn migrate_to_rocksdb<N: ProviderNodeTypes, T: Table>(
288 factory: &ProviderFactory<N>,
289 ) -> eyre::Result<()> {
290 info!(target: "reth::cli", table = T::NAME, "Migrating MDBX table → RocksDB");
291
292 let provider = factory.provider()?.disable_long_read_transaction_safety();
293 let mut cursor = provider.tx_ref().cursor_read::<T>()?;
294
295 let rocksdb = factory.rocksdb_provider();
296 rocksdb.clear::<T>()?;
297 let mut batch = rocksdb.batch_with_auto_commit();
298
299 let mut count = 0u64;
300 for entry in cursor.walk(None)? {
301 let (key, value) = entry?;
302 batch.put::<T>(key, &value)?;
303 count += 1;
304 }
305
306 batch.commit()?;
307 rocksdb.flush(&[T::NAME])?;
308
309 info!(target: "reth::cli", table = T::NAME, count, "MDBX table migrated to RocksDB");
310 Ok(())
311 }
312
313 fn clear_migrated_and_recomputable_tables<N: ProviderNodeTypes>(
316 factory: &ProviderFactory<N>,
317 ) -> eyre::Result<()> {
318 info!(target: "reth::cli", "Clearing migrated and recomputable MDBX tables");
319 let db = factory.db_ref();
320
321 macro_rules! clear_table {
322 ($table:ty) => {{
323 let tx = db.tx_mut()?;
324 tx.clear::<$table>()?;
325 tx.commit()?;
326 info!(target: "reth::cli", table = <$table as Table>::NAME, "Cleared");
327 }};
328 }
329
330 clear_table!(tables::AccountChangeSets);
332 clear_table!(tables::StorageChangeSets);
333
334 clear_table!(tables::TransactionSenders);
336
337 clear_table!(tables::TransactionHashNumbers);
339 clear_table!(tables::AccountsHistory);
340 clear_table!(tables::StoragesHistory);
341
342 clear_table!(tables::PlainAccountState);
344 clear_table!(tables::PlainStorageState);
345
346 clear_table!(tables::AccountsTrie);
348 clear_table!(tables::StoragesTrie);
349
350 info!(target: "reth::cli", "Resetting stage checkpoints");
352 let provider_rw = factory.database_provider_rw()?;
353 for stage in [StageId::SenderRecovery, StageId::MerkleExecute, StageId::MerkleUnwind] {
354 provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(0))?;
355 info!(target: "reth::cli", %stage, "Checkpoint reset to 0");
356 }
357 provider_rw.save_stage_checkpoint_progress(StageId::MerkleExecute, vec![])?;
358
359 if provider_rw.last_block_number()? > 0 {
360 let first_indices_entry = provider_rw
361 .tx_ref()
362 .cursor_read::<tables::BlockBodyIndices>()?
363 .seek(1)?
364 .map(|(block, _)| block)
365 .ok_or_else(|| eyre::eyre!("no block body indices found"))?;
366
367 if first_indices_entry > 1 {
374 provider_rw.save_stage_checkpoint(
375 StageId::SenderRecovery,
376 StageCheckpoint::new(first_indices_entry - 1),
377 )?;
378
379 let static_file_provider = provider_rw.static_file_provider();
381 let mut senders_writer =
382 static_file_provider.latest_writer(StaticFileSegment::TransactionSenders)?;
383 senders_writer.ensure_at_block(first_indices_entry - 1)?;
384 senders_writer.commit()?;
385
386 warn!(
387 target: "reth::cli",
388 "Missing block body indices data for first {first_indices_entry} blocks, initializing sender recovery with the first block that has a corresponding block body indices entry"
389 );
390 }
391 }
392 provider_rw.commit()?;
393
394 info!(target: "reth::cli", "Recomputable tables cleared");
395 Ok(())
396 }
397
398 fn compact_mdbx(db: &mdbx::DatabaseEnv) -> eyre::Result<()> {
400 let db_path = db.path();
401 let compact_path = db_path.with_file_name("db_compact");
402
403 reth_fs_util::create_dir_all(&compact_path)?;
404
405 info!(target: "reth::cli", ?db_path, ?compact_path, "Compacting MDBX database");
406
407 let compact_dest = compact_path.join("mdbx.dat");
408 let dest_cstr = std::ffi::CString::new(
409 compact_dest.to_str().ok_or_else(|| eyre::eyre!("compact path must be valid UTF-8"))?,
410 )?;
411
412 let flags = ffi::MDBX_CP_COMPACT | ffi::MDBX_CP_FORCE_DYNAMIC_SIZE;
413
414 let rc = db.with_raw_env_ptr(|env_ptr| unsafe {
415 ffi::mdbx_env_copy(env_ptr, dest_cstr.as_ptr(), flags)
416 });
417
418 if rc != 0 {
419 eyre::bail!("mdbx_env_copy failed with error code {rc}: {}", unsafe {
420 std::ffi::CStr::from_ptr(ffi::mdbx_strerror(rc)).to_string_lossy()
421 });
422 }
423
424 info!(target: "reth::cli", "MDBX compaction complete");
425 Ok(())
426 }
427
428 fn swap_compacted_db(
430 db_path: &std::path::Path,
431 compact_path: &std::path::Path,
432 ) -> eyre::Result<()> {
433 let backup_path = db_path.with_file_name("db_pre_compact");
434
435 info!(target: "reth::cli", ?db_path, ?compact_path, "Swapping compacted database");
436
437 reth_fs_util::rename(db_path, &backup_path)?;
438
439 if let Err(e) = reth_fs_util::rename(compact_path, db_path) {
440 if let Err(restore_err) = reth_fs_util::rename(&backup_path, db_path) {
441 eyre::bail!(
442 "failed to move compacted database into place ({e}) and failed to restore \
443 the original ({restore_err}); the original database is now at {} and the \
444 compacted copy is at {}; move the original back to {} manually",
445 backup_path.display(),
446 compact_path.display(),
447 db_path.display(),
448 );
449 }
450 return Err(e.into());
451 }
452
453 std::fs::remove_dir_all(&backup_path)?;
454
455 info!(target: "reth::cli", "Database compaction swap complete");
456 Ok(())
457 }
458}