Skip to content

Commit 33333f9

Browse files
committed
feat(node): event loop + signals + tracing + metrics + crash recovery + graceful shutdown
Central integration crate. Layered Config (TOML + bitcoin.conf compat + env + clap CLI) with serde-deserialize backing. Crossbeam-channel Select loop over mempool/defrag/metrics tick channels plus a SIGINT/SIGTERM-bridged shutdown receiver (signal_hook on a dedicated thread). Tracing v1: env-filter + JSON stderr. Metrics v1: in-memory recorder installable via metrics::set_global_recorder (Prometheus HTTP listener follows when metrics-exporter-prometheus's tokio-free path lands). Crash recovery reads a JSON sidecar in data_dir; partial commits (last_committed_height < height) trigger an in-memory replay log advanced to the tip. Graceful shutdown via parking_lot::Condvar + 5s drain deadline. NodeState aggregates Config + data_dir + replay log; subsystem wiring (chain / utxo / mempool / index / p2p / rpc / electrum) is the binary's job. Test suite: 4 tests covering config-layer precedence, bitcoin.conf compat, crash replay walk, and the shutdown signal round-trip. Op: extend
1 parent b04b138 commit 33333f9

18 files changed

Lines changed: 2340 additions & 2 deletions

Cargo.lock

Lines changed: 705 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ serde = { version = ">=1, <2", default-features = false, features = ["d
197197
serde_json = { version = ">=1, <2", default-features = false, features = ["std", "raw_value"] }
198198
sonic-rs = { version = ">=0.5, <0.6", default-features = false }
199199
# toml: `+default = [parse, display]`. Latest 1.1.2 — bump floor past 0.8.
200-
toml = { version = ">=0.8.23, <2", default-features = false, features = ["parse", "display"] }
200+
toml = { version = ">=0.8.23, <2", default-features = false, features = ["parse", "display", "serde"] }
201201
clap = { version = ">=4.6, <5", default-features = false, features = [
202202
"std", "color", "error-context", "help", "suggestions", "usage",
203203
"derive", "env", "wrap_help",

crates/node/Cargo.toml

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

14+
[features]
15+
default = []
16+
rocksdb = [
17+
"bitcoin-rs-chain/rocksdb",
18+
"bitcoin-rs-coinstats/rocksdb",
19+
"bitcoin-rs-consensus/rocksdb",
20+
"bitcoin-rs-electrum/rocksdb",
21+
"bitcoin-rs-filters/rocksdb",
22+
"bitcoin-rs-index/rocksdb",
23+
"bitcoin-rs-mempool/rocksdb",
24+
"bitcoin-rs-mining/rocksdb",
25+
"bitcoin-rs-p2p/rocksdb",
26+
"bitcoin-rs-pruning/rocksdb",
27+
"bitcoin-rs-rpc/rocksdb",
28+
"bitcoin-rs-script/rocksdb",
29+
"bitcoin-rs-storage/rocksdb",
30+
"bitcoin-rs-utreexo/rocksdb",
31+
"bitcoin-rs-utxo/rocksdb",
32+
"bitcoin-rs-wallet/rocksdb",
33+
]
34+
fjall = [
35+
"bitcoin-rs-chain/fjall",
36+
"bitcoin-rs-coinstats/fjall",
37+
"bitcoin-rs-consensus/fjall",
38+
"bitcoin-rs-electrum/fjall",
39+
"bitcoin-rs-filters/fjall",
40+
"bitcoin-rs-index/fjall",
41+
"bitcoin-rs-mempool/fjall",
42+
"bitcoin-rs-mining/fjall",
43+
"bitcoin-rs-p2p/fjall",
44+
"bitcoin-rs-pruning/fjall",
45+
"bitcoin-rs-rpc/fjall",
46+
"bitcoin-rs-script/fjall",
47+
"bitcoin-rs-storage/fjall",
48+
"bitcoin-rs-utreexo/fjall",
49+
"bitcoin-rs-utxo/fjall",
50+
"bitcoin-rs-wallet/fjall",
51+
]
52+
redb = [
53+
"bitcoin-rs-chain/redb",
54+
"bitcoin-rs-coinstats/redb",
55+
"bitcoin-rs-consensus/redb",
56+
"bitcoin-rs-electrum/redb",
57+
"bitcoin-rs-filters/redb",
58+
"bitcoin-rs-index/redb",
59+
"bitcoin-rs-mempool/redb",
60+
"bitcoin-rs-mining/redb",
61+
"bitcoin-rs-p2p/redb",
62+
"bitcoin-rs-pruning/redb",
63+
"bitcoin-rs-rpc/redb",
64+
"bitcoin-rs-script/redb",
65+
"bitcoin-rs-storage/redb",
66+
"bitcoin-rs-utreexo/redb",
67+
"bitcoin-rs-utxo/redb",
68+
"bitcoin-rs-wallet/redb",
69+
]
70+
mdbx = [
71+
"bitcoin-rs-coinstats/mdbx",
72+
"bitcoin-rs-electrum/mdbx",
73+
"bitcoin-rs-mining/mdbx",
74+
"bitcoin-rs-pruning/mdbx",
75+
"bitcoin-rs-rpc/mdbx",
76+
"bitcoin-rs-storage/mdbx",
77+
"bitcoin-rs-utxo/mdbx",
78+
"bitcoin-rs-wallet/mdbx",
79+
]
80+
kernel = ["bitcoin-rs-consensus/kernel"]
81+
prometheus-http = ["metrics-exporter-prometheus/http-listener"]
82+
1483
[dependencies]
84+
bitcoin-rs-primitives.workspace = true
85+
bitcoin-rs-script.workspace = true
86+
bitcoin-rs-consensus.workspace = true
87+
bitcoin-rs-storage.workspace = true
88+
bitcoin-rs-utxo.workspace = true
89+
bitcoin-rs-utreexo.workspace = true
90+
bitcoin-rs-chain.workspace = true
91+
bitcoin-rs-index.workspace = true
92+
bitcoin-rs-filters.workspace = true
93+
bitcoin-rs-coinstats.workspace = true
94+
bitcoin-rs-pruning.workspace = true
95+
bitcoin-rs-mempool.workspace = true
96+
bitcoin-rs-p2p.workspace = true
97+
bitcoin-rs-wallet.workspace = true
98+
bitcoin-rs-mining.workspace = true
99+
bitcoin-rs-rpc.workspace = true
100+
bitcoin-rs-electrum.workspace = true
101+
102+
bitcoin.workspace = true
103+
clap.workspace = true
104+
toml.workspace = true
105+
serde.workspace = true
106+
serde_json.workspace = true
107+
crossbeam-channel.workspace = true
108+
crossbeam-utils.workspace = true
109+
parking_lot.workspace = true
110+
arc-swap.workspace = true
111+
signal-hook = { workspace = true, features = ["iterator"] }
112+
tracing.workspace = true
113+
tracing-subscriber.workspace = true
114+
metrics.workspace = true
115+
metrics-exporter-prometheus.workspace = true
116+
quanta.workspace = true
117+
thiserror.workspace = true
118+
anyhow.workspace = true
119+
120+
[dev-dependencies]
121+
tempfile = "3"
122+
proptest.workspace = true
123+
anyhow.workspace = true
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use std::path::Path;
2+
3+
use anyhow::{Context as _, Result};
4+
5+
use crate::config::{Auth, Config, ConfigLayer};
6+
use bitcoin_rs_primitives::Network;
7+
8+
/// Applies a Bitcoin Core `bitcoin.conf` file to `config`.
9+
pub fn apply_file(config: &mut Config, path: &Path) -> Result<()> {
10+
let text = std::fs::read_to_string(path)
11+
.with_context(|| format!("failed to read bitcoin.conf {}", path.display()))?;
12+
let layer = parse_for_network(&text, config.network);
13+
layer.apply_to(config);
14+
Ok(())
15+
}
16+
17+
fn parse_for_network(text: &str, network: Network) -> ConfigLayer {
18+
let mut global = ConfigLayer::default();
19+
let mut selected = ConfigLayer::default();
20+
let mut applies_to_selected = true;
21+
22+
for raw_line in text.lines() {
23+
let line = strip_inline_comment(raw_line).trim();
24+
if line.is_empty() {
25+
continue;
26+
}
27+
if let Some(section) = parse_section(line) {
28+
applies_to_selected = section_matches_network(section, network);
29+
continue;
30+
}
31+
let Some((raw_key, raw_value)) = line.split_once('=') else {
32+
continue;
33+
};
34+
let key = raw_key.trim().trim_start_matches('-');
35+
let value = raw_value.trim();
36+
if applies_to_selected {
37+
apply_key(&mut selected, key, value);
38+
} else {
39+
apply_key(&mut global, key, value);
40+
}
41+
}
42+
43+
global.apply_from(&selected);
44+
global
45+
}
46+
47+
fn apply_key(layer: &mut ConfigLayer, key: &str, value: &str) {
48+
match key {
49+
"prune" => {
50+
if let Ok(prune_target_mb) = value.parse() {
51+
layer.prune_target_mb = Some(prune_target_mb);
52+
}
53+
}
54+
"rpcuser" => layer.rpc_user = Some(value.to_owned()),
55+
"rpcpassword" => layer.rpc_password = Some(value.to_owned()),
56+
"rpccookiefile" => layer.rpc_cookie = Some(value.into()),
57+
"server" => {}
58+
"listen" => {
59+
if parse_core_bool(value).is_some_and(|listen| !listen) {
60+
layer.p2p_listen = Some(Vec::new());
61+
}
62+
}
63+
"txindex" => layer.txindex = parse_core_bool(value),
64+
"blockfilterindex" => layer.blockfilterindex = parse_core_bool(value),
65+
"dbcache" => {
66+
if let Ok(dbcache_mb) = value.parse() {
67+
layer.dbcache_mb = Some(dbcache_mb);
68+
}
69+
}
70+
_ => {}
71+
}
72+
if layer.rpc_user.is_some() || layer.rpc_password.is_some() {
73+
let user = layer
74+
.rpc_user
75+
.clone()
76+
.unwrap_or_else(|| "bitcoin-rs".to_owned());
77+
let password = layer.rpc_password.clone().unwrap_or_default();
78+
layer.rpc_auth = Some(Auth::basic(user, password));
79+
}
80+
}
81+
82+
fn parse_core_bool(value: &str) -> Option<bool> {
83+
match value.trim().to_ascii_lowercase().as_str() {
84+
"1" | "true" | "yes" | "on" => Some(true),
85+
"0" | "false" | "no" | "off" => Some(false),
86+
_ => None,
87+
}
88+
}
89+
90+
fn parse_section(line: &str) -> Option<&str> {
91+
line.strip_prefix('[')?.strip_suffix(']').map(str::trim)
92+
}
93+
94+
fn section_matches_network(section: &str, network: Network) -> bool {
95+
match section.trim().to_ascii_lowercase().as_str() {
96+
"main" | "mainnet" => network == Network::Mainnet,
97+
"test" | "testnet" | "testnet3" => network == Network::Testnet3,
98+
"testnet4" => network == Network::Testnet4,
99+
"signet" => network == Network::Signet,
100+
"regtest" => network == Network::Regtest,
101+
_ => false,
102+
}
103+
}
104+
105+
fn strip_inline_comment(line: &str) -> &str {
106+
let hash = line.find('#');
107+
let semicolon = line.find(';');
108+
match (hash, semicolon) {
109+
(Some(left), Some(right)) => &line[..left.min(right)],
110+
(Some(index), None) | (None, Some(index)) => &line[..index],
111+
(None, None) => line,
112+
}
113+
}
114+
115+
trait ConfigLayerMerge {
116+
fn apply_from(&mut self, other: &Self);
117+
}
118+
119+
impl ConfigLayerMerge for ConfigLayer {
120+
fn apply_from(&mut self, other: &Self) {
121+
if other.network.is_some() {
122+
self.network = other.network;
123+
}
124+
if other.data_dir.is_some() {
125+
self.data_dir.clone_from(&other.data_dir);
126+
}
127+
if other.storage_backend.is_some() {
128+
self.storage_backend.clone_from(&other.storage_backend);
129+
}
130+
if other.rpc_bind.is_some() {
131+
self.rpc_bind = other.rpc_bind;
132+
}
133+
if other.rpc_auth.is_some() {
134+
self.rpc_auth.clone_from(&other.rpc_auth);
135+
}
136+
if other.rpc_user.is_some() {
137+
self.rpc_user.clone_from(&other.rpc_user);
138+
}
139+
if other.rpc_password.is_some() {
140+
self.rpc_password.clone_from(&other.rpc_password);
141+
}
142+
if other.rpc_cookie.is_some() {
143+
self.rpc_cookie.clone_from(&other.rpc_cookie);
144+
}
145+
if other.electrum_bind.is_some() {
146+
self.electrum_bind = other.electrum_bind;
147+
}
148+
if other.electrum_tls_cert.is_some() {
149+
self.electrum_tls_cert.clone_from(&other.electrum_tls_cert);
150+
}
151+
if other.p2p_listen.is_some() {
152+
self.p2p_listen.clone_from(&other.p2p_listen);
153+
}
154+
if other.dns_seeds_enabled.is_some() {
155+
self.dns_seeds_enabled = other.dns_seeds_enabled;
156+
}
157+
if other.prune_target_mb.is_some() {
158+
self.prune_target_mb = other.prune_target_mb;
159+
}
160+
if other.utreexo_mode.is_some() {
161+
self.utreexo_mode = other.utreexo_mode;
162+
}
163+
if other.txindex.is_some() {
164+
self.txindex = other.txindex;
165+
}
166+
if other.blockfilterindex.is_some() {
167+
self.blockfilterindex = other.blockfilterindex;
168+
}
169+
if other.dbcache_mb.is_some() {
170+
self.dbcache_mb = other.dbcache_mb;
171+
}
172+
if other.log_level.is_some() {
173+
self.log_level.clone_from(&other.log_level);
174+
}
175+
if other.metrics_bind.is_some() {
176+
self.metrics_bind = other.metrics_bind;
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)