Skip to content

Commit faa01ee

Browse files
committed
feat(node): open KvStore backend in NodeState
NodeState now owns the open storage handle, dispatched by config.storage_backend over a cfg-gated NodeStorage enum (one variant per backend). The chainstate directory lives at data_dir/chainstate and is created during open. A new integration test exercises every backend the build feature set compiles in. Future commits will add forwarding accessors (get/iter/snapshot/batch) as the chain / utxo / mempool / index subsystems wire themselves to the state handle. Op: extend
1 parent dd49d52 commit faa01ee

2 files changed

Lines changed: 150 additions & 9 deletions

File tree

crates/node/src/state.rs

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,139 @@
11
//! Shared node state aggregating subsystem handles.
22
//!
3-
//! V1 keeps this deliberately minimal: it owns the resolved [`Config`] and a
4-
//! data-directory path, plus the replay log used by [`crate::crash_recovery`].
5-
//! Subsystem wiring (chain / utxo / mempool / index / p2p / rpc / electrum)
6-
//! is constructed by the binary at boot and parks here as the integration
7-
//! point matures.
3+
//! V1 keeps this deliberately minimal: it owns the resolved [`Config`], the
4+
//! data-directory path, the open chainstate storage backend, and the replay log
5+
//! used by [`crate::crash_recovery`]. Subsystem wiring (chain / utxo / mempool
6+
//! / index / p2p / rpc / electrum) parks here as the integration point matures.
87
8+
use core::fmt;
99
use std::path::{Path, PathBuf};
1010

11-
use anyhow::{Context as _, Result};
11+
use anyhow::{Context as _, Result, bail};
1212
use parking_lot::Mutex;
1313

1414
use crate::Config;
1515

16+
enum NodeStorage {
17+
#[cfg(feature = "rocksdb")]
18+
RocksDb(bitcoin_rs_storage::RocksDbStore),
19+
#[cfg(feature = "fjall")]
20+
Fjall(bitcoin_rs_storage::FjallStore),
21+
#[cfg(feature = "redb")]
22+
Redb(bitcoin_rs_storage::RedbStore),
23+
#[cfg(feature = "mdbx")]
24+
Mdbx(bitcoin_rs_storage::MdbxStore),
25+
}
26+
27+
impl NodeStorage {
28+
fn open(config: &Config) -> Result<Self> {
29+
let chainstate_dir = config.data_dir.join("chainstate");
30+
std::fs::create_dir_all(&chainstate_dir)
31+
.with_context(|| format!("create chainstate_dir {}", chainstate_dir.display()))?;
32+
33+
match config.storage_backend.as_str() {
34+
#[cfg(feature = "rocksdb")]
35+
"rocksdb" => Ok(Self::RocksDb(
36+
bitcoin_rs_storage::RocksDbStore::open(&chainstate_dir)
37+
.map_err(anyhow::Error::new)?,
38+
)),
39+
#[cfg(feature = "fjall")]
40+
"fjall" => Ok(Self::Fjall(
41+
bitcoin_rs_storage::FjallStore::open(&chainstate_dir)
42+
.map_err(anyhow::Error::new)?,
43+
)),
44+
#[cfg(feature = "redb")]
45+
"redb" => Ok(Self::Redb(
46+
bitcoin_rs_storage::RedbStore::open(&chainstate_dir).map_err(anyhow::Error::new)?,
47+
)),
48+
#[cfg(feature = "mdbx")]
49+
"mdbx" => Ok(Self::Mdbx(
50+
bitcoin_rs_storage::MdbxStore::open(&chainstate_dir).map_err(anyhow::Error::new)?,
51+
)),
52+
other => bail!(
53+
"unsupported storage backend: {other} (compiled features = {CompiledStorageFeatures})"
54+
),
55+
}
56+
}
57+
58+
const fn kind(&self) -> &'static str {
59+
match self {
60+
#[cfg(feature = "rocksdb")]
61+
Self::RocksDb(store) => {
62+
let _ = store;
63+
"rocksdb"
64+
}
65+
#[cfg(feature = "fjall")]
66+
Self::Fjall(store) => {
67+
let _ = store;
68+
"fjall"
69+
}
70+
#[cfg(feature = "redb")]
71+
Self::Redb(store) => {
72+
let _ = store;
73+
"redb"
74+
}
75+
#[cfg(feature = "mdbx")]
76+
Self::Mdbx(store) => {
77+
let _ = store;
78+
"mdbx"
79+
}
80+
}
81+
}
82+
}
83+
84+
const COMPILED_STORAGE_FEATURES: &[&str] = &[
85+
#[cfg(feature = "rocksdb")]
86+
"rocksdb",
87+
#[cfg(feature = "fjall")]
88+
"fjall",
89+
#[cfg(feature = "redb")]
90+
"redb",
91+
#[cfg(feature = "mdbx")]
92+
"mdbx",
93+
];
94+
95+
struct CompiledStorageFeatures;
96+
97+
impl fmt::Display for CompiledStorageFeatures {
98+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99+
let Some((first, rest)) = COMPILED_STORAGE_FEATURES.split_first() else {
100+
return f.write_str("none");
101+
};
102+
103+
f.write_str(first)?;
104+
for feature in rest {
105+
f.write_str(",")?;
106+
f.write_str(feature)?;
107+
}
108+
Ok(())
109+
}
110+
}
111+
16112
/// Aggregate handle to a running node.
17113
pub struct NodeState {
18114
config: Config,
19115
data_dir: PathBuf,
116+
storage: NodeStorage,
20117
replayed: Mutex<Vec<u32>>,
21118
}
22119

23120
impl NodeState {
24-
/// Opens (or creates) the node's data directory and constructs the
25-
/// in-memory state. The configured storage backend is not yet bound at
26-
/// this layer; see [`crate::run::run`].
121+
/// Opens (or creates) the node's data directory and configured storage
122+
/// backend.
27123
pub fn open(config: Config) -> Result<Self> {
28124
std::fs::create_dir_all(&config.data_dir)
29125
.with_context(|| format!("create data_dir {}", config.data_dir.display()))?;
126+
let storage = NodeStorage::open(&config)?;
127+
tracing::info!(
128+
backend = storage.kind(),
129+
chainstate_dir = %config.data_dir.join("chainstate").display(),
130+
"opened storage backend"
131+
);
30132
let data_dir = config.data_dir.clone();
31133
Ok(Self {
32134
config,
33135
data_dir,
136+
storage,
34137
replayed: Mutex::new(Vec::new()),
35138
})
36139
}
@@ -47,6 +150,12 @@ impl NodeState {
47150
&self.data_dir
48151
}
49152

153+
/// Returns the configured storage backend that was opened.
154+
#[must_use]
155+
pub const fn storage_kind(&self) -> &'static str {
156+
self.storage.kind()
157+
}
158+
50159
/// Heights walked by the most recent crash-recovery replay.
51160
#[must_use]
52161
pub fn replayed_heights(&self) -> Vec<u32> {

crates/node/tests/state_storage.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Integration test: `NodeState` opens the configured storage backend.
2+
3+
use anyhow::Result;
4+
use bitcoin_rs_node::{Config, Network, state::NodeState};
5+
6+
#[test]
7+
fn opens_storage_backend() -> Result<()> {
8+
#[cfg(feature = "rocksdb")]
9+
assert_backend_opens("rocksdb")?;
10+
#[cfg(feature = "fjall")]
11+
assert_backend_opens("fjall")?;
12+
#[cfg(feature = "redb")]
13+
assert_backend_opens("redb")?;
14+
#[cfg(feature = "mdbx")]
15+
assert_backend_opens("mdbx")?;
16+
17+
Ok(())
18+
}
19+
20+
fn assert_backend_opens(backend: &str) -> Result<()> {
21+
let temp = tempfile::tempdir()?;
22+
let mut config = Config::default_for_network(Network::Regtest);
23+
config.data_dir = temp.path().join(backend);
24+
backend.clone_into(&mut config.storage_backend);
25+
config.p2p_listen.clear();
26+
27+
let state = NodeState::open(config)?;
28+
29+
assert_eq!(state.storage_kind(), backend);
30+
assert!(state.data_dir().join("chainstate").is_dir());
31+
Ok(())
32+
}

0 commit comments

Comments
 (0)