Skip to content

Commit 69230ae

Browse files
author
bitcoin-rs
committed
feat(storage): KvStore trait + rocksdb/fjall/redb (default-buildable) + mdbx (gated on rustc 1.92+)
Op: extend
1 parent ed91f50 commit 69230ae

13 files changed

Lines changed: 2323 additions & 0 deletions

Cargo.lock

Lines changed: 725 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ compact_str = { version = ">=0.9, <1", default-features = false, features = ["st
171171

172172
# --- Zero-copy / compression --------------------------------------------------
173173
bytemuck = { version = ">=1.25, <2", default-features = false, features = ["derive"] }
174+
bytes = { version = ">=1.11, <2", default-features = false }
174175
zerocopy = { version = ">=0.8, <0.9", default-features = false, features = ["derive", "std"] }
175176
lz4_flex = { version = ">=0.13, <0.14", default-features = false, features = ["std", "frame"] }
176177

crates/storage/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,33 @@ description = "bitcoin-rs :: storage"
1111
[lints]
1212
workspace = true
1313

14+
[features]
15+
default = ["rocksdb"]
16+
rocksdb = ["dep:rust-rocksdb"]
17+
fjall = ["dep:fjall"]
18+
redb = ["dep:redb"]
19+
mdbx = ["dep:signet-libmdbx"]
20+
1421
[dependencies]
22+
bytemuck.workspace = true
23+
bytes.workspace = true
24+
crossbeam-channel.workspace = true
25+
parking_lot.workspace = true
26+
serde.workspace = true
27+
thiserror.workspace = true
28+
tracing.workspace = true
29+
30+
rust-rocksdb = { workspace = true, optional = true }
31+
fjall = { workspace = true, optional = true }
32+
redb = { workspace = true, optional = true }
33+
signet-libmdbx = { workspace = true, optional = true }
34+
35+
[dev-dependencies]
36+
criterion.workspace = true
37+
rayon.workspace = true
38+
sha2.workspace = true
39+
tempfile = "3"
40+
41+
[[bench]]
42+
name = "kvstore_backends"
43+
harness = false
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
//! Criterion benchmarks for the storage backends.
2+
// SPEC: Criterion generates undocumented harness functions in bench targets.
3+
#![allow(missing_docs)]
4+
5+
use std::{hint::black_box, path::Path};
6+
7+
use bitcoin_rs_storage::{ColumnFamily, KvStore, StorageError, WriteBatch};
8+
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
9+
10+
const WRITE_ROWS: u32 = 1_000_000;
11+
const READ_ROWS: u32 = 1_000_000;
12+
const PREFIX_ROWS: u32 = 100_000;
13+
const MIXED_THREADS: usize = 16;
14+
const MIXED_ROWS_PER_THREAD: u32 = 62_500;
15+
16+
fn bench_sequential_writes_1m<S, Open>(c: &mut Criterion, backend: &str, open: Open)
17+
where
18+
S: KvStore,
19+
Open: Fn(&Path) -> Result<S, StorageError> + Copy,
20+
{
21+
c.bench_function(&format!("{backend}/bench_sequential_writes_1m"), |b| {
22+
b.iter_batched(
23+
|| {
24+
let temp = must(tempfile::TempDir::new());
25+
let store = must(open(temp.path()));
26+
(temp, store)
27+
},
28+
|(_temp, store)| {
29+
let mut batch = store.new_batch();
30+
for counter in 0_u32..WRITE_ROWS {
31+
let key = counter.to_le_bytes();
32+
batch.put(ColumnFamily::TxConfirmed, &key, &key);
33+
}
34+
must(store.write(batch));
35+
must(store.flush());
36+
},
37+
BatchSize::SmallInput,
38+
);
39+
});
40+
}
41+
42+
fn bench_random_writes_1m<S, Open>(c: &mut Criterion, backend: &str, open: Open)
43+
where
44+
S: KvStore,
45+
Open: Fn(&Path) -> Result<S, StorageError> + Copy,
46+
{
47+
c.bench_function(&format!("{backend}/bench_random_writes_1m"), |b| {
48+
b.iter_batched(
49+
|| {
50+
let temp = must(tempfile::TempDir::new());
51+
let store = must(open(temp.path()));
52+
(temp, store)
53+
},
54+
|(_temp, store)| {
55+
let mut batch = store.new_batch();
56+
let mut state = 0x9e37_79b9_u32;
57+
for _ in 0_u32..WRITE_ROWS {
58+
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
59+
let key = state.to_le_bytes();
60+
batch.put(ColumnFamily::TxConfirmed, &key, &key);
61+
}
62+
must(store.write(batch));
63+
must(store.flush());
64+
},
65+
BatchSize::SmallInput,
66+
);
67+
});
68+
}
69+
70+
fn bench_point_get_1m<S, Open>(c: &mut Criterion, backend: &str, open: Open)
71+
where
72+
S: KvStore,
73+
Open: Fn(&Path) -> Result<S, StorageError> + Copy,
74+
{
75+
let temp = must(tempfile::TempDir::new());
76+
let store = must(open(temp.path()));
77+
write_rows(&store, READ_ROWS);
78+
79+
c.bench_function(&format!("{backend}/bench_point_get_1m"), |b| {
80+
b.iter(|| {
81+
for counter in 0_u32..READ_ROWS {
82+
let key = counter.to_le_bytes();
83+
black_box(must(store.get(ColumnFamily::TxConfirmed, &key)));
84+
}
85+
});
86+
});
87+
}
88+
89+
fn bench_prefix_iter_100k<S, Open>(c: &mut Criterion, backend: &str, open: Open)
90+
where
91+
S: KvStore,
92+
Open: Fn(&Path) -> Result<S, StorageError> + Copy,
93+
{
94+
let temp = must(tempfile::TempDir::new());
95+
let store = must(open(temp.path()));
96+
let mut batch = store.new_batch();
97+
for counter in 0_u32..PREFIX_ROWS {
98+
let key = prefixed_key(counter);
99+
batch.put(ColumnFamily::TxConfirmed, &key, &key);
100+
}
101+
must(store.write(batch));
102+
103+
c.bench_function(&format!("{backend}/bench_prefix_iter_100k"), |b| {
104+
b.iter(|| {
105+
let iterator = must(store.iter_prefix(ColumnFamily::TxConfirmed, &[0x7f]));
106+
let rows = must(iterator.collect::<Result<Vec<_>, _>>());
107+
black_box(rows);
108+
});
109+
});
110+
}
111+
112+
fn bench_mixed_16thread_workload<S, Open>(c: &mut Criterion, backend: &str, open: Open)
113+
where
114+
S: KvStore,
115+
Open: Fn(&Path) -> Result<S, StorageError> + Copy,
116+
{
117+
let temp = must(tempfile::TempDir::new());
118+
let store = must(open(temp.path()));
119+
write_rows(&store, MIXED_ROWS_PER_THREAD);
120+
121+
c.bench_function(&format!("{backend}/bench_mixed_16thread_workload"), |b| {
122+
b.iter(|| {
123+
rayon::scope(|scope| {
124+
for thread in 0_usize..MIXED_THREADS {
125+
let store_ref = &store;
126+
scope.spawn(move |_| {
127+
let thread_u32 = match u32::try_from(thread) {
128+
Ok(value) => value,
129+
Err(error) => panic!("thread index conversion failed: {error}"),
130+
};
131+
let mut batch = store_ref.new_batch();
132+
for counter in 0_u32..MIXED_ROWS_PER_THREAD {
133+
let read_key = counter.to_le_bytes();
134+
black_box(must(store_ref.get(ColumnFamily::TxConfirmed, &read_key)));
135+
let write_key = counter
136+
.wrapping_add(thread_u32.wrapping_mul(MIXED_ROWS_PER_THREAD))
137+
.to_le_bytes();
138+
batch.put(ColumnFamily::TxMempool, &write_key, &read_key);
139+
}
140+
must(store_ref.write(batch));
141+
});
142+
}
143+
});
144+
must(store.flush());
145+
});
146+
});
147+
}
148+
149+
fn write_rows(store: &impl KvStore, rows: u32) {
150+
let mut batch = store.new_batch();
151+
for counter in 0_u32..rows {
152+
let key = counter.to_le_bytes();
153+
batch.put(ColumnFamily::TxConfirmed, &key, &key);
154+
}
155+
must(store.write(batch));
156+
must(store.flush());
157+
}
158+
159+
fn prefixed_key(counter: u32) -> Vec<u8> {
160+
let mut key = Vec::with_capacity(5);
161+
key.push(0x7f);
162+
key.extend_from_slice(&counter.to_le_bytes());
163+
key
164+
}
165+
166+
fn must<T, E: std::fmt::Display>(result: Result<T, E>) -> T {
167+
match result {
168+
Ok(value) => value,
169+
Err(error) => panic!("benchmark setup failed: {error}"),
170+
}
171+
}
172+
173+
fn benches(c: &mut Criterion) {
174+
#[cfg(feature = "rocksdb")]
175+
{
176+
bench_sequential_writes_1m::<bitcoin_rs_storage::RocksDbStore, _>(c, "rocksdb", |path| {
177+
bitcoin_rs_storage::RocksDbStore::open(path)
178+
});
179+
bench_random_writes_1m::<bitcoin_rs_storage::RocksDbStore, _>(c, "rocksdb", |path| {
180+
bitcoin_rs_storage::RocksDbStore::open(path)
181+
});
182+
bench_point_get_1m::<bitcoin_rs_storage::RocksDbStore, _>(c, "rocksdb", |path| {
183+
bitcoin_rs_storage::RocksDbStore::open(path)
184+
});
185+
bench_prefix_iter_100k::<bitcoin_rs_storage::RocksDbStore, _>(c, "rocksdb", |path| {
186+
bitcoin_rs_storage::RocksDbStore::open(path)
187+
});
188+
bench_mixed_16thread_workload::<bitcoin_rs_storage::RocksDbStore, _>(
189+
c,
190+
"rocksdb",
191+
|path| bitcoin_rs_storage::RocksDbStore::open(path),
192+
);
193+
}
194+
195+
#[cfg(feature = "fjall")]
196+
{
197+
bench_sequential_writes_1m::<bitcoin_rs_storage::FjallStore, _>(c, "fjall", |path| {
198+
bitcoin_rs_storage::FjallStore::open(path)
199+
});
200+
bench_random_writes_1m::<bitcoin_rs_storage::FjallStore, _>(c, "fjall", |path| {
201+
bitcoin_rs_storage::FjallStore::open(path)
202+
});
203+
bench_point_get_1m::<bitcoin_rs_storage::FjallStore, _>(c, "fjall", |path| {
204+
bitcoin_rs_storage::FjallStore::open(path)
205+
});
206+
bench_prefix_iter_100k::<bitcoin_rs_storage::FjallStore, _>(c, "fjall", |path| {
207+
bitcoin_rs_storage::FjallStore::open(path)
208+
});
209+
bench_mixed_16thread_workload::<bitcoin_rs_storage::FjallStore, _>(c, "fjall", |path| {
210+
bitcoin_rs_storage::FjallStore::open(path)
211+
});
212+
}
213+
214+
#[cfg(feature = "redb")]
215+
{
216+
bench_sequential_writes_1m::<bitcoin_rs_storage::RedbStore, _>(c, "redb", |path| {
217+
bitcoin_rs_storage::RedbStore::open(path)
218+
});
219+
bench_random_writes_1m::<bitcoin_rs_storage::RedbStore, _>(c, "redb", |path| {
220+
bitcoin_rs_storage::RedbStore::open(path)
221+
});
222+
bench_point_get_1m::<bitcoin_rs_storage::RedbStore, _>(c, "redb", |path| {
223+
bitcoin_rs_storage::RedbStore::open(path)
224+
});
225+
bench_prefix_iter_100k::<bitcoin_rs_storage::RedbStore, _>(c, "redb", |path| {
226+
bitcoin_rs_storage::RedbStore::open(path)
227+
});
228+
bench_mixed_16thread_workload::<bitcoin_rs_storage::RedbStore, _>(c, "redb", |path| {
229+
bitcoin_rs_storage::RedbStore::open(path)
230+
});
231+
}
232+
233+
#[cfg(feature = "mdbx")]
234+
{
235+
bench_sequential_writes_1m::<bitcoin_rs_storage::MdbxStore, _>(c, "mdbx", |path| {
236+
bitcoin_rs_storage::MdbxStore::open(path)
237+
});
238+
bench_random_writes_1m::<bitcoin_rs_storage::MdbxStore, _>(c, "mdbx", |path| {
239+
bitcoin_rs_storage::MdbxStore::open(path)
240+
});
241+
bench_point_get_1m::<bitcoin_rs_storage::MdbxStore, _>(c, "mdbx", |path| {
242+
bitcoin_rs_storage::MdbxStore::open(path)
243+
});
244+
bench_prefix_iter_100k::<bitcoin_rs_storage::MdbxStore, _>(c, "mdbx", |path| {
245+
bitcoin_rs_storage::MdbxStore::open(path)
246+
});
247+
bench_mixed_16thread_workload::<bitcoin_rs_storage::MdbxStore, _>(c, "mdbx", |path| {
248+
bitcoin_rs_storage::MdbxStore::open(path)
249+
});
250+
}
251+
}
252+
253+
criterion_group!(kvstore_backends, benches);
254+
criterion_main!(kvstore_backends);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/// Logical storage column families.
2+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
3+
#[repr(u8)]
4+
pub enum ColumnFamily {
5+
/// Confirmed transaction index rows.
6+
TxConfirmed = 0,
7+
/// Mempool transaction index rows.
8+
TxMempool = 1,
9+
/// Block header rows.
10+
BlockHeaders = 2,
11+
/// Transaction funding rows.
12+
Funding = 3,
13+
/// Transaction spending rows.
14+
Spending = 4,
15+
/// BIP157/158 compact filter rows.
16+
Filters = 5,
17+
/// BIP157/158 filter-header rows.
18+
FilterHeaders = 6,
19+
/// Coinstats index rows.
20+
Coinstats = 7,
21+
/// Block-tree node rows.
22+
BlockTree = 8,
23+
/// UTXO snapshot metadata rows.
24+
UtxoMeta = 9,
25+
}
26+
27+
impl ColumnFamily {
28+
/// All supported column families, in stable on-disk order.
29+
pub const ALL: &'static [Self] = &[
30+
Self::TxConfirmed,
31+
Self::TxMempool,
32+
Self::BlockHeaders,
33+
Self::Funding,
34+
Self::Spending,
35+
Self::Filters,
36+
Self::FilterHeaders,
37+
Self::Coinstats,
38+
Self::BlockTree,
39+
Self::UtxoMeta,
40+
];
41+
42+
/// Stable backend column-family/table name.
43+
pub const fn name(self) -> &'static str {
44+
match self {
45+
Self::TxConfirmed => "tx_confirmed",
46+
Self::TxMempool => "tx_mempool",
47+
Self::BlockHeaders => "block_headers",
48+
Self::Funding => "funding",
49+
Self::Spending => "spending",
50+
Self::Filters => "filters",
51+
Self::FilterHeaders => "filter_headers",
52+
Self::Coinstats => "coinstats",
53+
Self::BlockTree => "block_tree",
54+
Self::UtxoMeta => "utxo_meta",
55+
}
56+
}
57+
58+
/// Alias for [`Self::name`].
59+
pub const fn as_str(self) -> &'static str {
60+
self.name()
61+
}
62+
63+
/// Converts the stable one-byte representation to a column family.
64+
pub const fn from_u8(byte: u8) -> Option<Self> {
65+
match byte {
66+
0 => Some(Self::TxConfirmed),
67+
1 => Some(Self::TxMempool),
68+
2 => Some(Self::BlockHeaders),
69+
3 => Some(Self::Funding),
70+
4 => Some(Self::Spending),
71+
5 => Some(Self::Filters),
72+
6 => Some(Self::FilterHeaders),
73+
7 => Some(Self::Coinstats),
74+
8 => Some(Self::BlockTree),
75+
9 => Some(Self::UtxoMeta),
76+
_ => None,
77+
}
78+
}
79+
80+
/// Stable zero-based index for arrays keyed by column family.
81+
pub const fn index(self) -> usize {
82+
match self {
83+
Self::TxConfirmed => 0,
84+
Self::TxMempool => 1,
85+
Self::BlockHeaders => 2,
86+
Self::Funding => 3,
87+
Self::Spending => 4,
88+
Self::Filters => 5,
89+
Self::FilterHeaders => 6,
90+
Self::Coinstats => 7,
91+
Self::BlockTree => 8,
92+
Self::UtxoMeta => 9,
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)