Skip to content

Commit 3061f56

Browse files
committed
feat(node,rpc): wire CoinStatsListener into UtxoSet; gettxoutsetinfo surfaces muhash + total_amount + bogosize
Op: extend
1 parent fb4837c commit 3061f56

10 files changed

Lines changed: 90 additions & 7 deletions

File tree

crates/node/src/apply.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub struct ApplyHandles {
3535
pub block_tree: Arc<RwLock<BlockTree>>,
3636
/// Shared UTXO set.
3737
pub utxo: Arc<UtxoSet>,
38+
/// Shared coinstats listener.
39+
pub coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
3840
/// Shared mempool.
3941
pub mempool: Arc<RwLock<Mempool>>,
4042
/// Shared block records exposed to RPC handlers.
@@ -143,6 +145,8 @@ pub fn apply_block(
143145
tracing::debug!(%txid, evicted_count, "apply_block: evicted transaction from mempool");
144146
handles.transactions.write().insert(txid, tx.clone());
145147
}
148+
let tx_count_delta = u64::try_from(block.txdata.len()).unwrap_or(u64::MAX);
149+
handles.coin_stats.finish_block(height, tx_count_delta);
146150
tracing::info!(
147151
height,
148152
%block_hash,

crates/node/src/run.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ pub fn run(mut config: Config) -> Result<()> {
147147
state.blocks(),
148148
state.transactions(),
149149
state.utxo(),
150+
state.coin_stats(),
150151
state.network(),
151152
state.mining_template_id(),
152153
state.peers(),

crates/node/src/state.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ pub struct NodeState {
171171
data_dir: PathBuf,
172172
storage: NodeStorage,
173173
utxo: Arc<UtxoSet>,
174+
coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
174175
mempool: Arc<RwLock<Mempool>>,
175176
chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
176177
applied_tip: Arc<ArcSwapOption<TipSnapshot>>,
@@ -202,7 +203,13 @@ impl NodeState {
202203
std::fs::create_dir_all(&config.data_dir)
203204
.with_context(|| format!("create data_dir {}", config.data_dir.display()))?;
204205
let storage = NodeStorage::open(&config)?;
205-
let utxo = Arc::new(UtxoSet::new());
206+
let mut utxo_set = bitcoin_rs_utxo::UtxoSet::new();
207+
let coin_stats_listener = bitcoin_rs_coinstats::CoinStatsListener::new(
208+
bitcoin_rs_coinstats::CoinStats::default(),
209+
);
210+
utxo_set.set_listener(Box::new(coin_stats_listener.clone()));
211+
let utxo = Arc::new(utxo_set);
212+
let coin_stats = Arc::new(coin_stats_listener);
206213
let mempool = Arc::new(RwLock::new(Mempool::new(MempoolLimits::default())));
207214
let block_tree = Arc::new(RwLock::new(bitcoin_rs_chain::BlockTree::new()));
208215
let chain_tip = block_tree.read().tip_handle();
@@ -226,6 +233,7 @@ impl NodeState {
226233
applied_tip: Arc::clone(&applied_tip),
227234
block_tree: Arc::clone(&block_tree),
228235
utxo: Arc::clone(&utxo),
236+
coin_stats: Arc::clone(&coin_stats),
229237
mempool: Arc::clone(&mempool),
230238
blocks: Arc::clone(&blocks),
231239
transactions: Arc::clone(&transactions),
@@ -246,6 +254,7 @@ impl NodeState {
246254
data_dir,
247255
storage,
248256
utxo,
257+
coin_stats,
249258
mempool,
250259
chain_tip,
251260
applied_tip,
@@ -289,6 +298,12 @@ impl NodeState {
289298
Arc::clone(&self.utxo)
290299
}
291300

301+
/// Returns the shared coinstats listener handle.
302+
#[must_use]
303+
pub fn coin_stats(&self) -> Arc<bitcoin_rs_coinstats::CoinStatsListener> {
304+
Arc::clone(&self.coin_stats)
305+
}
306+
292307
/// Returns the shared mempool handle.
293308
#[must_use]
294309
pub fn mempool(&self) -> Arc<RwLock<Mempool>> {
@@ -435,6 +450,7 @@ impl NodeState {
435450
applied_tip: Arc::clone(&self.applied_tip),
436451
block_tree: Arc::clone(&self.block_tree),
437452
utxo: Arc::clone(&self.utxo),
453+
coin_stats: Arc::clone(&self.coin_stats),
438454
mempool: Arc::clone(&self.mempool),
439455
blocks: Arc::clone(&self.blocks),
440456
transactions: Arc::clone(&self.transactions),
@@ -507,6 +523,21 @@ mod tests {
507523
Ok(())
508524
}
509525

526+
#[test]
527+
fn open_constructs_coin_stats_listener() -> anyhow::Result<()> {
528+
let dir = tempfile::tempdir()?;
529+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
530+
config.data_dir = dir.path().join("node");
531+
config.p2p_listen.clear();
532+
let state = NodeState::open(config)?;
533+
let snapshot = state.coin_stats().snapshot();
534+
assert_eq!(
535+
snapshot.tx_count, 0,
536+
"freshly opened coin_stats has zero txs"
537+
);
538+
Ok(())
539+
}
540+
510541
#[test]
511542
fn open_constructs_block_sync_orchestrator() -> anyhow::Result<()> {
512543
let dir = tempfile::tempdir()?;

crates/node/src/sync.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ mod tests {
477477
applied_tip,
478478
block_tree,
479479
utxo: Arc::new(UtxoSet::new()),
480+
coin_stats: Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
481+
bitcoin_rs_coinstats::CoinStats::default(),
482+
)),
480483
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
481484
blocks: Arc::new(RwLock::new(Vec::new())),
482485
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),

crates/node/tests/rpc_wiring.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ fn rpc_context_shares_arc_identity_with_node_state() -> Result<()> {
2828
let blocks = state.blocks();
2929
let transactions = state.transactions();
3030
let utxo = Arc::new(UtxoSet::new());
31+
let coin_stats = state.coin_stats();
3132
let network = state.network();
3233
let mining_template_id = state.mining_template_id();
3334
let peers = state.peers();
@@ -39,6 +40,7 @@ fn rpc_context_shares_arc_identity_with_node_state() -> Result<()> {
3940
Arc::clone(&blocks),
4041
Arc::clone(&transactions),
4142
Arc::clone(&utxo),
43+
Arc::clone(&coin_stats),
4244
Arc::clone(&network),
4345
Arc::clone(&mining_template_id),
4446
Arc::clone(&peers),
@@ -65,6 +67,10 @@ fn rpc_context_shares_arc_identity_with_node_state() -> Result<()> {
6567
"transactions must share identity"
6668
);
6769
assert!(Arc::ptr_eq(&ctx.utxo, &utxo), "utxo must share identity");
70+
assert!(
71+
Arc::ptr_eq(&ctx.coin_stats, &coin_stats),
72+
"coin_stats must share identity"
73+
);
6874
assert!(
6975
Arc::ptr_eq(&ctx.network, &network),
7076
"network must share identity"

crates/node/tests/sync_smoke.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ fn apply_handles(
157157
applied_tip,
158158
block_tree,
159159
utxo: Arc::new(UtxoSet::new()),
160+
coin_stats: Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
161+
bitcoin_rs_coinstats::CoinStats::default(),
162+
)),
160163
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
161164
blocks: Arc::new(RwLock::new(Vec::new())),
162165
transactions: Arc::new(RwLock::new(HashMap::<Txid, Transaction>::new())),

crates/rpc/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ workspace = true
1313

1414
[features]
1515
default = []
16-
rocksdb = ["bitcoin-rs-storage/rocksdb", "bitcoin-rs-p2p/rocksdb"]
17-
fjall = ["bitcoin-rs-storage/fjall", "bitcoin-rs-p2p/fjall"]
18-
redb = ["bitcoin-rs-storage/redb", "bitcoin-rs-p2p/redb"]
16+
rocksdb = ["bitcoin-rs-coinstats/rocksdb", "bitcoin-rs-storage/rocksdb", "bitcoin-rs-p2p/rocksdb"]
17+
fjall = ["bitcoin-rs-coinstats/fjall", "bitcoin-rs-storage/fjall", "bitcoin-rs-p2p/fjall"]
18+
redb = ["bitcoin-rs-coinstats/redb", "bitcoin-rs-storage/redb", "bitcoin-rs-p2p/redb"]
1919
mdbx = ["bitcoin-rs-storage/mdbx"]
2020

2121
[dependencies]

crates/rpc/src/context.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub struct Context {
8686
pub transactions: Arc<RwLock<HashMap<Txid, Transaction>>>,
8787
/// UTXO set snapshot handle used by chain metadata RPCs.
8888
pub utxo: Arc<bitcoin_rs_utxo::UtxoSet>,
89+
/// Incremental UTXO-set statistics.
90+
pub coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
8991
/// Network counters and peers.
9092
pub network: Arc<RwLock<NetworkState>>,
9193
/// Shared registry of currently-handshook peers.
@@ -132,6 +134,9 @@ impl Context {
132134
blocks: Arc::new(RwLock::new(Vec::new())),
133135
transactions: Arc::new(RwLock::new(HashMap::new())),
134136
utxo: Arc::new(bitcoin_rs_utxo::UtxoSet::new()),
137+
coin_stats: Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
138+
bitcoin_rs_coinstats::CoinStats::default(),
139+
)),
135140
network: Arc::new(RwLock::new(NetworkState::default())),
136141
peers: Arc::new(RwLock::new(Vec::new())),
137142
mining_template_id: Arc::new(ArcSwap::from_pointee(CompactString::new("0"))),
@@ -154,6 +159,7 @@ impl Context {
154159
blocks: Arc<RwLock<Vec<BlockRecord>>>,
155160
transactions: Arc<RwLock<HashMap<Txid, Transaction>>>,
156161
utxo: Arc<bitcoin_rs_utxo::UtxoSet>,
162+
coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
157163
network: Arc<RwLock<NetworkState>>,
158164
mining_template_id: Arc<ArcSwap<CompactString>>,
159165
peers: Arc<RwLock<Vec<bitcoin_rs_p2p::PeerInfo>>>,
@@ -166,6 +172,7 @@ impl Context {
166172
blocks,
167173
transactions,
168174
utxo,
175+
coin_stats,
169176
network,
170177
peers,
171178
mining_template_id,
@@ -288,13 +295,17 @@ mod tests {
288295
let chain_tip = Arc::new(ArcSwapOption::empty());
289296
let applied_tip = Arc::new(ArcSwapOption::empty());
290297
let utxo = Arc::new(bitcoin_rs_utxo::UtxoSet::new());
298+
let coin_stats = Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
299+
bitcoin_rs_coinstats::CoinStats::default(),
300+
));
291301
let ctx = Context::from_handles(
292302
Arc::clone(&chain_tip),
293303
Arc::clone(&applied_tip),
294304
Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
295305
Arc::new(RwLock::new(Vec::new())),
296306
Arc::new(RwLock::new(HashMap::new())),
297307
Arc::clone(&utxo),
308+
Arc::clone(&coin_stats),
298309
Arc::new(RwLock::new(NetworkState::default())),
299310
Arc::new(ArcSwap::from_pointee(CompactString::new("0"))),
300311
Arc::new(RwLock::new(Vec::new())),
@@ -311,5 +322,9 @@ mod tests {
311322
Arc::ptr_eq(&ctx.utxo, &utxo),
312323
"utxo must be shared with caller"
313324
);
325+
assert!(
326+
Arc::ptr_eq(&ctx.coin_stats, &coin_stats),
327+
"coin_stats must be shared with caller"
328+
);
314329
}
315330
}

crates/rpc/src/handlers/chain.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,22 @@ pub(crate) fn getblockstats(ctx: &Arc<Context>, params: &Value) -> Result<Value,
160160

161161
pub(crate) fn gettxoutsetinfo(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
162162
ensure_no_params(params)?;
163+
let snapshot = ctx.coin_stats.snapshot();
164+
let muhash_bytes = snapshot.muhash.finalize();
165+
let mut muhash_hex = String::with_capacity(muhash_bytes.len() * 2);
166+
for byte in muhash_bytes {
167+
use core::fmt::Write as _;
168+
169+
let _: core::fmt::Result = write!(&mut muhash_hex, "{byte:02x}");
170+
}
171+
let total_amount_btc = bitcoin::Amount::from_sat(snapshot.total_amount).to_btc();
163172
Ok(json!({
164173
"height": ctx.applied_height(),
165174
"bestblock": ctx.applied_hash().to_string_be(),
166175
"txouts": ctx.utxo.len(),
167-
"bogosize": 0,
168-
"hash_serialized_2": Hash256::default().to_string_be(),
169-
"total_amount": 0.0,
176+
"bogosize": snapshot.bogo_size,
177+
"hash_serialized_2": muhash_hex,
178+
"total_amount": total_amount_btc,
170179
"transactions": ctx.utxo.record_count(),
171180
"disk_size": 0
172181
}))

crates/rpc/tests/handler_smoke.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ fn gettxoutsetinfo_returns_real_utxo_counts() -> Result<(), Box<dyn std::error::
151151

152152
assert_eq!(result.get("txouts").as_u64(), Some(0));
153153
assert_eq!(result.get("transactions").as_u64(), Some(0));
154+
assert_eq!(result.get("bogosize").as_u64(), Some(0));
155+
assert_eq!(result.get("total_amount").as_f64(), Some(0.0));
156+
let hash_serialized_value = result.get("hash_serialized_2");
157+
let hash_serialized = hash_serialized_value
158+
.as_str()
159+
.ok_or("hash_serialized_2 must be a string")?;
160+
assert_eq!(hash_serialized.len(), 768);
161+
assert!(hash_serialized.ends_with("01"));
154162
Ok(())
155163
}
156164

@@ -294,6 +302,9 @@ fn context_with_peers(peers: Arc<RwLock<Vec<PeerInfo>>>) -> Arc<Context> {
294302
Arc::new(RwLock::new(Vec::new())),
295303
Arc::new(RwLock::new(HashMap::new())),
296304
Arc::new(UtxoSet::new()),
305+
Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
306+
bitcoin_rs_coinstats::CoinStats::default(),
307+
)),
297308
Arc::new(RwLock::new(NetworkState::default())),
298309
Arc::new(ArcSwap::from_pointee(CompactString::new("0"))),
299310
peers,

0 commit comments

Comments
 (0)