Skip to content

Commit 5f49d31

Browse files
committed
feat(l1): persist state-history journal at block commit
Adds a per-block reverse-diff journal in a new STATE_HISTORY column family. Each non-batch trie commit captures pre-images for every overwritten trie node and flat-KV entry, then stages a JournalEntry in the same write batch as the trie writes (atomic on commit). Finality pruning issues a single delete_range when finalized advances. No reorg consumer yet; this PR is the audit-trail foundation for the deep-reorg work in #6685. Skipped entirely during full sync (batch_mode).
1 parent 6e627ac commit 5f49d31

9 files changed

Lines changed: 1309 additions & 25 deletions

File tree

crates/storage/api/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ pub trait StorageWriteBatch: Send {
8888
/// Removes a key-value pair from the specified table.
8989
fn delete(&mut self, table: &'static str, key: &[u8]) -> Result<(), StoreError>;
9090

91+
/// Removes every key in `[start, end)` from the specified table.
92+
///
93+
/// Half-open range; `end` is exclusive. Equivalent to enumerating each key
94+
/// in the range and calling [`delete`], but backends with native range-delete
95+
/// support (e.g. RocksDB's `delete_range_cf`) can implement it more efficiently.
96+
///
97+
/// Lexicographic byte order is used for the range bounds — callers using
98+
/// numeric keys must encode them in a representation whose lex order matches
99+
/// numeric order (e.g. `u64::to_be_bytes()`).
100+
fn delete_range(
101+
&mut self,
102+
table: &'static str,
103+
start: &[u8],
104+
end: &[u8],
105+
) -> Result<(), StoreError>;
106+
91107
/// Commits all changes made in this transaction.
92108
fn commit(&mut self) -> Result<(), StoreError>;
93109
}

crates/storage/api/tables.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ pub const STORAGE_FLATKEYVALUE: &str = "storage_flatkeyvalue";
9292

9393
pub const MISC_VALUES: &str = "misc_values";
9494

95+
/// State-history journal column family: [`u8; 8`] => [`Vec<u8>`]
96+
/// - [`u8; 8`] = `block_number.to_be_bytes()` (big-endian so lex order == numeric order)
97+
/// - [`Vec<u8>`] = `JournalEntry::encode()`
98+
///
99+
/// Stores one reverse-diff entry per committed block, enabling reorgs deeper
100+
/// than the in-memory `TrieLayerCache`. Pruned at finality.
101+
pub const STATE_HISTORY: &str = "state_history";
102+
95103
/// Execution witnesses column family: [`Vec<u8>`] => [`Vec<u8>`]
96104
/// - [`Vec<u8>`] = Composite key
97105
/// ```rust,no_run
@@ -107,7 +115,7 @@ pub const EXECUTION_WITNESSES: &str = "execution_witnesses";
107115
/// - [`Vec<u8>`] = RLP-encoded `BlockAccessList`
108116
pub const BLOCK_ACCESS_LISTS: &str = "block_access_lists";
109117

110-
pub const TABLES: [&str; 20] = [
118+
pub const TABLES: [&str; 21] = [
111119
CHAIN_DATA,
112120
ACCOUNT_CODES,
113121
ACCOUNT_CODE_METADATA,
@@ -128,4 +136,5 @@ pub const TABLES: [&str; 20] = [
128136
MISC_VALUES,
129137
EXECUTION_WITNESSES,
130138
BLOCK_ACCESS_LISTS,
139+
STATE_HISTORY,
131140
];

crates/storage/backend/in_memory.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,31 @@ impl StorageWriteBatch for InMemoryWriteTx {
181181
Ok(())
182182
}
183183

184+
fn delete_range(
185+
&mut self,
186+
table: &'static str,
187+
start: &[u8],
188+
end: &[u8],
189+
) -> Result<(), StoreError> {
190+
let mut db = self
191+
.backend
192+
.write()
193+
.map_err(|_| StoreError::Custom("Failed to acquire write lock".to_string()))?;
194+
195+
let db_mut = Arc::make_mut(&mut *db);
196+
if let Some(table_ref) = db_mut.get_mut(table) {
197+
table_ref.retain(|k, _| !(k.as_slice() >= start && k.as_slice() < end));
198+
}
199+
Ok(())
200+
}
201+
184202
fn commit(&mut self) -> Result<(), StoreError> {
185-
// FIXME: in-memory writes aren't atomic
203+
// FIXME: in-memory writes aren't atomic. Every `put`, `delete`, and
204+
// `delete_range` above mutates the live `Arc<Database>` immediately under
205+
// the write lock; `commit` is a no-op. Anyone using this backend cannot
206+
// rely on multi-op atomicity (e.g. the journal entry + trie writes in
207+
// `apply_trie_updates`, or the `delete_range` + finalized-number update
208+
// in `forkchoice_update_inner`).
186209
Ok(())
187210
}
188211
}

crates/storage/backend/rocksdb.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,20 @@ impl StorageWriteBatch for RocksDBWriteTx {
365365
Ok(())
366366
}
367367

368+
fn delete_range(
369+
&mut self,
370+
table: &'static str,
371+
start: &[u8],
372+
end: &[u8],
373+
) -> Result<(), StoreError> {
374+
let cf = self
375+
.db
376+
.cf_handle(table)
377+
.ok_or_else(|| StoreError::Custom(format!("Table {table:?} not found")))?;
378+
self.batch.delete_range_cf(&cf, start, end);
379+
Ok(())
380+
}
381+
368382
fn commit(&mut self) -> Result<(), StoreError> {
369383
// Take ownership of the batch (replaces it with an empty one) since db.write() consumes it
370384
let batch = std::mem::take(&mut self.batch);

0 commit comments

Comments
 (0)