Skip to content

Commit c3494a9

Browse files
prestwichclaude
andauthored
refactor(cold): optimize MDBX backend and remove INT_KEY (#26)
* refactor(cold): optimize MDBX backend and remove INT_KEY Batch append_blocks into a single write transaction instead of one per block. Use MDBX_APPEND/MDBX_APPENDDUP for all block-number-keyed writes, enforcing the append-only contract at the MDBX level. Reuse cursors across block iterations in get_logs_inner, collect_signet_events_in_range, and truncate_above_inner. Fix append_dual buffer overflow for variable-length DUPSORT values. Remove MDBX INTEGERKEY flag from all tables. Big-endian KeySer encoding is incompatible with MDBX's native-endian integer comparison, causing incorrect ordering in lower_bound, range scans, and MDBX_APPEND operations. Bytewise comparison on BE-encoded keys gives correct numeric ordering with no measurable performance difference. Remove out-of-order append conformance test and reorder remaining tests so block numbers are strictly ascending, matching the append-only contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf(hot-mdbx): zero-copy append writes Use `with_reservation(APPEND)` for single-key appends instead of cursor + Vec allocation. For dual-key, encode values directly into the k2||value concatenation buffer and call `Tx::append_dup` directly instead of creating a cursor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf(cold-sql): batch append_blocks into single transaction Extract block write logic into `write_block_to_tx` helper that accepts an open transaction. `append_blocks` now opens one transaction for the entire batch instead of one per block. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump workspace version to 0.4.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 44f144b commit c3494a9

File tree

15 files changed

+327
-410
lines changed

15 files changed

+327
-410
lines changed

Cargo.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["crates/*"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.3.0"
6+
version = "0.4.0"
77
edition = "2024"
88
rust-version = "1.92"
99
authors = ["init4"]
@@ -35,13 +35,13 @@ incremental = false
3535

3636
[workspace.dependencies]
3737
# internal
38-
signet-hot = { version = "0.3.0", path = "./crates/hot" }
39-
signet-hot-mdbx = { version = "0.3.0", path = "./crates/hot-mdbx" }
40-
signet-cold = { version = "0.3.0", path = "./crates/cold" }
41-
signet-cold-mdbx = { version = "0.3.0", path = "./crates/cold-mdbx" }
42-
signet-cold-sql = { version = "0.3.0", path = "./crates/cold-sql" }
43-
signet-storage = { version = "0.3.0", path = "./crates/storage" }
44-
signet-storage-types = { version = "0.3.0", path = "./crates/types" }
38+
signet-hot = { version = "0.4.0", path = "./crates/hot" }
39+
signet-hot-mdbx = { version = "0.4.0", path = "./crates/hot-mdbx" }
40+
signet-cold = { version = "0.4.0", path = "./crates/cold" }
41+
signet-cold-mdbx = { version = "0.4.0", path = "./crates/cold-mdbx" }
42+
signet-cold-sql = { version = "0.4.0", path = "./crates/cold-sql" }
43+
signet-storage = { version = "0.4.0", path = "./crates/storage" }
44+
signet-storage-types = { version = "0.4.0", path = "./crates/types" }
4545

4646
# External, in-house
4747
signet-libmdbx = { version = "0.8.0" }

crates/cold-mdbx/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ alloy.workspace = true
2020
signet-cold.workspace = true
2121
signet-hot.workspace = true
2222
signet-hot-mdbx.workspace = true
23+
signet-libmdbx.workspace = true
2324
signet-storage-types.workspace = true
2425
thiserror.workspace = true
2526

crates/cold-mdbx/src/backend.rs

Lines changed: 100 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,58 @@ use signet_storage_types::{
2525
};
2626
use std::path::Path;
2727

28+
/// Write a single block's data into an open read-write transaction.
29+
///
30+
/// Uses `MDBX_APPEND` / `MDBX_APPENDDUP` for block-number-keyed tables,
31+
/// skipping B-tree traversal. Blocks MUST be appended in ascending order.
32+
fn write_block_to_tx(
33+
tx: &signet_hot_mdbx::Tx<signet_libmdbx::Rw>,
34+
data: BlockData,
35+
) -> Result<(), MdbxColdError> {
36+
let block = data.block_number();
37+
38+
tx.queue_append::<ColdHeaders>(&block, &data.header)?;
39+
// Hash-keyed indices use put (keys are not sequential)
40+
tx.queue_put::<ColdBlockHashIndex>(&data.header.hash(), &block)?;
41+
42+
// Store transactions, senders, and build hash index
43+
let tx_meta: Vec<_> = data
44+
.transactions
45+
.iter()
46+
.enumerate()
47+
.map(|(idx, recovered_tx)| {
48+
let tx_idx = idx as u64;
49+
let sender = recovered_tx.signer();
50+
let tx_signed: &TransactionSigned = recovered_tx;
51+
tx.queue_append_dual::<ColdTransactions>(&block, &tx_idx, tx_signed)?;
52+
tx.queue_append_dual::<ColdTxSenders>(&block, &tx_idx, &sender)?;
53+
tx.queue_put::<ColdTxHashIndex>(tx_signed.hash(), &TxLocation::new(block, tx_idx))?;
54+
Ok((*tx_signed.hash(), sender))
55+
})
56+
.collect::<Result<_, MdbxColdError>>()?;
57+
58+
// Compute and store IndexedReceipts with precomputed metadata
59+
let mut first_log_index = 0u64;
60+
let mut prior_cumulative_gas = 0u64;
61+
for (idx, (receipt, (tx_hash, sender))) in data.receipts.into_iter().zip(tx_meta).enumerate() {
62+
let gas_used = receipt.inner.cumulative_gas_used - prior_cumulative_gas;
63+
prior_cumulative_gas = receipt.inner.cumulative_gas_used;
64+
let ir = IndexedReceipt { receipt, tx_hash, first_log_index, gas_used, sender };
65+
first_log_index += ir.receipt.inner.logs.len() as u64;
66+
tx.queue_append_dual::<ColdReceipts>(&block, &(idx as u64), &ir)?;
67+
}
68+
69+
for (idx, event) in data.signet_events.iter().enumerate() {
70+
tx.queue_append_dual::<ColdSignetEvents>(&block, &(idx as u64), event)?;
71+
}
72+
73+
if let Some(zh) = &data.zenith_header {
74+
tx.queue_append::<ColdZenithHeaders>(&block, zh)?;
75+
}
76+
77+
Ok(())
78+
}
79+
2880
/// MDBX-based cold storage backend.
2981
///
3082
/// This backend stores historical blockchain data in an MDBX database.
@@ -68,57 +120,37 @@ impl MdbxColdBackend {
68120
fn create_tables(&self) -> Result<(), MdbxColdError> {
69121
let tx = self.env.tx_rw()?;
70122

71-
for (name, dual_key_size, fixed_val_size, int_key) in [
72-
(
73-
ColdHeaders::NAME,
74-
ColdHeaders::DUAL_KEY_SIZE,
75-
ColdHeaders::FIXED_VAL_SIZE,
76-
ColdHeaders::INT_KEY,
77-
),
123+
for (name, dual_key_size, fixed_val_size) in [
124+
(ColdHeaders::NAME, ColdHeaders::DUAL_KEY_SIZE, ColdHeaders::FIXED_VAL_SIZE),
78125
(
79126
ColdZenithHeaders::NAME,
80127
ColdZenithHeaders::DUAL_KEY_SIZE,
81128
ColdZenithHeaders::FIXED_VAL_SIZE,
82-
ColdZenithHeaders::INT_KEY,
83129
),
84130
(
85131
ColdBlockHashIndex::NAME,
86132
ColdBlockHashIndex::DUAL_KEY_SIZE,
87133
ColdBlockHashIndex::FIXED_VAL_SIZE,
88-
ColdBlockHashIndex::INT_KEY,
89134
),
90135
(
91136
ColdTxHashIndex::NAME,
92137
ColdTxHashIndex::DUAL_KEY_SIZE,
93138
ColdTxHashIndex::FIXED_VAL_SIZE,
94-
ColdTxHashIndex::INT_KEY,
95139
),
96140
(
97141
ColdTransactions::NAME,
98142
ColdTransactions::DUAL_KEY_SIZE,
99143
ColdTransactions::FIXED_VAL_SIZE,
100-
ColdTransactions::INT_KEY,
101-
),
102-
(
103-
ColdTxSenders::NAME,
104-
ColdTxSenders::DUAL_KEY_SIZE,
105-
ColdTxSenders::FIXED_VAL_SIZE,
106-
ColdTxSenders::INT_KEY,
107-
),
108-
(
109-
ColdReceipts::NAME,
110-
ColdReceipts::DUAL_KEY_SIZE,
111-
ColdReceipts::FIXED_VAL_SIZE,
112-
ColdReceipts::INT_KEY,
113144
),
145+
(ColdTxSenders::NAME, ColdTxSenders::DUAL_KEY_SIZE, ColdTxSenders::FIXED_VAL_SIZE),
146+
(ColdReceipts::NAME, ColdReceipts::DUAL_KEY_SIZE, ColdReceipts::FIXED_VAL_SIZE),
114147
(
115148
ColdSignetEvents::NAME,
116149
ColdSignetEvents::DUAL_KEY_SIZE,
117150
ColdSignetEvents::FIXED_VAL_SIZE,
118-
ColdSignetEvents::INT_KEY,
119151
),
120152
] {
121-
tx.queue_raw_create(name, dual_key_size, fixed_val_size, int_key)?;
153+
tx.queue_raw_create(name, dual_key_size, fixed_val_size)?;
122154
}
123155

124156
tx.raw_commit()?;
@@ -292,9 +324,10 @@ impl MdbxColdBackend {
292324
end: BlockNumber,
293325
) -> Result<Vec<DbSignetEvent>, MdbxColdError> {
294326
let tx = self.env.tx()?;
327+
let mut cursor = tx.traverse_dual::<ColdSignetEvents>()?;
295328
let mut events = Vec::new();
296329
for block in start..=end {
297-
for item in tx.traverse_dual::<ColdSignetEvents>()?.iter_k2(&block)? {
330+
for item in cursor.iter_k2(&block)? {
298331
events.push(item?.1);
299332
}
300333
}
@@ -335,49 +368,16 @@ impl MdbxColdBackend {
335368

336369
fn append_block_inner(&self, data: BlockData) -> Result<(), MdbxColdError> {
337370
let tx = self.env.tx_rw()?;
338-
let block = data.block_number();
339-
340-
// Store the sealed header (hash already cached)
341-
tx.queue_put::<ColdHeaders>(&block, &data.header)?;
342-
tx.queue_put::<ColdBlockHashIndex>(&data.header.hash(), &block)?;
343-
344-
// Store transactions, senders, and build hash index
345-
let tx_meta: Vec<_> = data
346-
.transactions
347-
.iter()
348-
.enumerate()
349-
.map(|(idx, recovered_tx)| {
350-
let tx_idx = idx as u64;
351-
let sender = recovered_tx.signer();
352-
let tx_signed: &TransactionSigned = recovered_tx;
353-
tx.queue_put_dual::<ColdTransactions>(&block, &tx_idx, tx_signed)?;
354-
tx.queue_put_dual::<ColdTxSenders>(&block, &tx_idx, &sender)?;
355-
tx.queue_put::<ColdTxHashIndex>(tx_signed.hash(), &TxLocation::new(block, tx_idx))?;
356-
Ok((*tx_signed.hash(), sender))
357-
})
358-
.collect::<Result<_, MdbxColdError>>()?;
359-
360-
// Compute and store IndexedReceipts with precomputed metadata
361-
let mut first_log_index = 0u64;
362-
let mut prior_cumulative_gas = 0u64;
363-
for (idx, (receipt, (tx_hash, sender))) in
364-
data.receipts.into_iter().zip(tx_meta).enumerate()
365-
{
366-
let gas_used = receipt.inner.cumulative_gas_used - prior_cumulative_gas;
367-
prior_cumulative_gas = receipt.inner.cumulative_gas_used;
368-
let ir = IndexedReceipt { receipt, tx_hash, first_log_index, gas_used, sender };
369-
first_log_index += ir.receipt.inner.logs.len() as u64;
370-
tx.queue_put_dual::<ColdReceipts>(&block, &(idx as u64), &ir)?;
371-
}
372-
373-
for (idx, event) in data.signet_events.iter().enumerate() {
374-
tx.queue_put_dual::<ColdSignetEvents>(&block, &(idx as u64), event)?;
375-
}
371+
write_block_to_tx(&tx, data)?;
372+
tx.raw_commit()?;
373+
Ok(())
374+
}
376375

377-
if let Some(zh) = &data.zenith_header {
378-
tx.queue_put::<ColdZenithHeaders>(&block, zh)?;
376+
fn append_blocks_inner(&self, data: Vec<BlockData>) -> Result<(), MdbxColdError> {
377+
let tx = self.env.tx_rw()?;
378+
for block_data in data {
379+
write_block_to_tx(&tx, block_data)?;
379380
}
380-
381381
tx.raw_commit()?;
382382
Ok(())
383383
}
@@ -412,20 +412,23 @@ impl MdbxColdBackend {
412412
}
413413

414414
// Delete each block's data
415-
for (block_num, sealed) in &headers_to_remove {
416-
// Delete transaction hash indices
417-
for item in tx.traverse_dual::<ColdTransactions>()?.iter_k2(block_num)? {
418-
let (_, tx_signed) = item?;
419-
tx.queue_delete::<ColdTxHashIndex>(tx_signed.hash())?;
420-
}
415+
{
416+
let mut tx_cursor = tx.traverse_dual::<ColdTransactions>()?;
417+
for (block_num, sealed) in &headers_to_remove {
418+
// Delete transaction hash indices
419+
for item in tx_cursor.iter_k2(block_num)? {
420+
let (_, tx_signed) = item?;
421+
tx.queue_delete::<ColdTxHashIndex>(tx_signed.hash())?;
422+
}
421423

422-
tx.queue_delete::<ColdHeaders>(block_num)?;
423-
tx.queue_delete::<ColdBlockHashIndex>(&sealed.hash())?;
424-
tx.clear_k1_for::<ColdTransactions>(block_num)?;
425-
tx.clear_k1_for::<ColdTxSenders>(block_num)?;
426-
tx.clear_k1_for::<ColdReceipts>(block_num)?;
427-
tx.clear_k1_for::<ColdSignetEvents>(block_num)?;
428-
tx.queue_delete::<ColdZenithHeaders>(block_num)?;
424+
tx.queue_delete::<ColdHeaders>(block_num)?;
425+
tx.queue_delete::<ColdBlockHashIndex>(&sealed.hash())?;
426+
tx.clear_k1_for::<ColdTransactions>(block_num)?;
427+
tx.clear_k1_for::<ColdTxSenders>(block_num)?;
428+
tx.clear_k1_for::<ColdReceipts>(block_num)?;
429+
tx.clear_k1_for::<ColdSignetEvents>(block_num)?;
430+
tx.queue_delete::<ColdZenithHeaders>(block_num)?;
431+
}
429432
}
430433

431434
tx.raw_commit()?;
@@ -437,15 +440,28 @@ impl MdbxColdBackend {
437440
let mut results = Vec::new();
438441

439442
let from = filter.get_from_block().unwrap_or(0);
440-
let to = filter.get_to_block().unwrap_or(u64::MAX);
443+
let to = match filter.get_to_block() {
444+
Some(to) => to,
445+
None => {
446+
let mut cursor = tx.new_cursor::<ColdHeaders>()?;
447+
let Some((key, _)) = cursor.last()? else {
448+
return Ok(results);
449+
};
450+
BlockNumber::decode_key(&key)?
451+
}
452+
};
453+
454+
let mut header_cursor = tx.traverse::<ColdHeaders>()?;
455+
let mut receipt_cursor = tx.traverse_dual::<ColdReceipts>()?;
456+
441457
for block_num in from..=to {
442-
let Some(sealed) = tx.traverse::<ColdHeaders>()?.exact(&block_num)? else {
458+
let Some(sealed) = header_cursor.exact(&block_num)? else {
443459
continue;
444460
};
445461
let block_hash = sealed.hash();
446462
let block_timestamp = sealed.timestamp;
447463

448-
for item in tx.traverse_dual::<ColdReceipts>()?.iter_k2(&block_num)? {
464+
for item in receipt_cursor.iter_k2(&block_num)? {
449465
let (tx_idx, ir) = item?;
450466
results.extend(
451467
ir.receipt
@@ -567,10 +583,7 @@ impl ColdStorage for MdbxColdBackend {
567583
}
568584

569585
async fn append_blocks(&self, data: Vec<BlockData>) -> ColdResult<()> {
570-
for block_data in data {
571-
self.append_block_inner(block_data)?;
572-
}
573-
Ok(())
586+
Ok(self.append_blocks_inner(data)?)
574587
}
575588

576589
async fn truncate_above(&self, block: BlockNumber) -> ColdResult<()> {

crates/cold-mdbx/src/tables.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ pub struct ColdHeaders;
2222

2323
impl Table for ColdHeaders {
2424
const NAME: &'static str = "ColdHeaders";
25-
const INT_KEY: bool = true;
2625
type Key = BlockNumber;
2726
type Value = SealedHeader;
2827
}
@@ -41,7 +40,6 @@ pub struct ColdTransactions;
4140

4241
impl Table for ColdTransactions {
4342
const NAME: &'static str = "ColdTransactions";
44-
const INT_KEY: bool = true;
4543
const DUAL_KEY_SIZE: Option<usize> = Some(<u64 as KeySer>::SIZE);
4644
type Key = BlockNumber;
4745
type Value = TransactionSigned;
@@ -63,7 +61,6 @@ pub struct ColdReceipts;
6361

6462
impl Table for ColdReceipts {
6563
const NAME: &'static str = "ColdReceipts";
66-
const INT_KEY: bool = true;
6764
const DUAL_KEY_SIZE: Option<usize> = Some(<u64 as KeySer>::SIZE);
6865
type Key = BlockNumber;
6966
type Value = IndexedReceipt;
@@ -85,7 +82,6 @@ pub struct ColdSignetEvents;
8582

8683
impl Table for ColdSignetEvents {
8784
const NAME: &'static str = "ColdSignetEvents";
88-
const INT_KEY: bool = true;
8985
const DUAL_KEY_SIZE: Option<usize> = Some(<u64 as KeySer>::SIZE);
9086
type Key = BlockNumber;
9187
type Value = DbSignetEvent;
@@ -105,7 +101,6 @@ pub struct ColdZenithHeaders;
105101

106102
impl Table for ColdZenithHeaders {
107103
const NAME: &'static str = "ColdZenithHeaders";
108-
const INT_KEY: bool = true;
109104
type Key = BlockNumber;
110105
type Value = DbZenithHeader;
111106
}
@@ -121,7 +116,6 @@ pub struct ColdTxSenders;
121116

122117
impl Table for ColdTxSenders {
123118
const NAME: &'static str = "ColdTxSenders";
124-
const INT_KEY: bool = true;
125119
const DUAL_KEY_SIZE: Option<usize> = Some(<u64 as KeySer>::SIZE);
126120
const FIXED_VAL_SIZE: Option<usize> = Some(20);
127121
type Key = BlockNumber;
@@ -207,21 +201,6 @@ mod tests {
207201
assert_dual_key::<ColdSignetEvents>();
208202
}
209203

210-
#[test]
211-
fn test_int_key_tables() {
212-
// Tables with int_key should have INT_KEY = true
213-
const { assert!(ColdHeaders::INT_KEY) };
214-
const { assert!(ColdTransactions::INT_KEY) };
215-
const { assert!(ColdTxSenders::INT_KEY) };
216-
const { assert!(ColdReceipts::INT_KEY) };
217-
const { assert!(ColdSignetEvents::INT_KEY) };
218-
const { assert!(ColdZenithHeaders::INT_KEY) };
219-
220-
// Non-int_key tables should have INT_KEY = false
221-
const { assert!(!ColdBlockHashIndex::INT_KEY) };
222-
const { assert!(!ColdTxHashIndex::INT_KEY) };
223-
}
224-
225204
#[test]
226205
fn test_fixed_val_size() {
227206
// ColdTxHashIndex should have fixed value size of 16

0 commit comments

Comments
 (0)