Skip to content

Commit 1cd8a9e

Browse files
committed
feat(rpc): getblockstats wires real time/subsidy/block-body fields
Op: extend
1 parent 5ecec6f commit 1cd8a9e

1 file changed

Lines changed: 87 additions & 15 deletions

File tree

crates/rpc/src/handlers/chain.rs

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -230,51 +230,107 @@ pub(crate) fn getblockheader(ctx: &Arc<Context>, params: &Value) -> Result<Value
230230
}
231231

232232
pub(crate) fn getblockstats(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
233+
use bitcoin::consensus::encode::deserialize;
234+
use bitcoin::hex::FromHex as _;
235+
233236
let target = params_array(params)?
234237
.first()
235238
.ok_or(RpcError::InvalidParams("hash_or_height is required"))?;
236239
let height = if let Some(height) = target.as_u64() {
237240
u32::try_from(height).map_err(|_| RpcError::InvalidParams("height exceeds u32"))?
238241
} else if let Some(hash) = target.as_str() {
239-
parse_hash(hash)?;
240-
ctx.height()
242+
let block_hash = parse_hash(hash)?;
243+
ctx.height_for_hash(block_hash)
244+
.unwrap_or_else(|| ctx.height())
241245
} else {
242246
return Err(RpcError::InvalidType(
243247
"hash_or_height must be string or number",
244248
));
245249
};
250+
251+
let block_hash = ctx.block_hash_at_height(height).unwrap_or_default();
252+
let subsidy_sat = subsidy_at_height(height);
253+
let record = ctx.block_by_hash(block_hash);
254+
let time = record.as_ref().map_or(0, |r| r.time);
255+
let mediantime = ctx.median_time_past_for_hash(block_hash).unwrap_or(0);
256+
257+
let mut total_size: u64 = 0;
258+
let mut total_weight: u64 = 0;
259+
let mut total_out: u64 = 0;
260+
let mut ins: u64 = 0;
261+
let mut outs: u64 = 0;
262+
let mut txs: u64 = 0;
263+
let mut swtxs: u64 = 0;
264+
let mut swtotal_size: u64 = 0;
265+
let mut swtotal_weight: u64 = 0;
266+
if let Some(record) = record.as_ref() {
267+
if let Ok(bytes) = Vec::<u8>::from_hex(&record.block_hex) {
268+
total_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
269+
if let Ok(block) = deserialize::<bitcoin::Block>(&bytes) {
270+
total_weight = block.weight().to_wu();
271+
txs = u64::try_from(block.txdata.len()).unwrap_or(u64::MAX);
272+
for tx in &block.txdata {
273+
ins = ins.saturating_add(u64::try_from(tx.input.len()).unwrap_or(u64::MAX));
274+
outs = outs.saturating_add(u64::try_from(tx.output.len()).unwrap_or(u64::MAX));
275+
for output in &tx.output {
276+
total_out = total_out.saturating_add(output.value.to_sat());
277+
}
278+
if tx.input.iter().any(|i| !i.witness.is_empty()) {
279+
swtxs = swtxs.saturating_add(1);
280+
let tx_size = bitcoin::consensus::encode::serialize(tx).len();
281+
swtotal_size =
282+
swtotal_size.saturating_add(u64::try_from(tx_size).unwrap_or(u64::MAX));
283+
swtotal_weight = swtotal_weight.saturating_add(tx.weight().to_wu());
284+
}
285+
}
286+
}
287+
}
288+
}
289+
246290
Ok(json!({
247291
"avgfee": 0,
248292
"avgfeerate": 0,
249293
"avgtxsize": 0,
250-
"blockhash": ctx.block_hash_at_height(height).unwrap_or_default().to_string_be(),
294+
"blockhash": block_hash.to_string_be(),
251295
"feerate_percentiles": [0, 0, 0, 0, 0],
252296
"height": height,
253-
"ins": 0,
297+
"ins": ins,
254298
"maxfee": 0,
255299
"maxfeerate": 0,
256300
"maxtxsize": 0,
257301
"medianfee": 0,
258-
"mediantime": 0,
302+
"mediantime": mediantime,
259303
"mediantxsize": 0,
260304
"minfee": 0,
261305
"minfeerate": 0,
262306
"mintxsize": 0,
263-
"outs": 0,
264-
"subsidy": 0,
265-
"swtotal_size": 0,
266-
"swtotal_weight": 0,
267-
"swtxs": 0,
268-
"time": 0,
269-
"total_out": 0,
270-
"total_size": 0,
271-
"total_weight": 0,
307+
"outs": outs,
308+
"subsidy": subsidy_sat,
309+
"swtotal_size": swtotal_size,
310+
"swtotal_weight": swtotal_weight,
311+
"swtxs": swtxs,
312+
"time": time,
313+
"total_out": total_out,
314+
"total_size": total_size,
315+
"total_weight": total_weight,
272316
"totalfee": 0,
273-
"txs": 0,
317+
"txs": txs,
274318
"utxo_increase": 0,
275319
"utxo_size_inc": 0
276320
}))
277321
}
322+
323+
/// Bitcoin block subsidy at `height` in satoshis. 50 BTC initially, halving
324+
/// every 210,000 blocks, saturating to zero after ~64 halvings.
325+
fn subsidy_at_height(height: u32) -> u64 {
326+
const INITIAL_SUBSIDY_SAT: u64 = 5_000_000_000;
327+
const HALVING_INTERVAL: u32 = 210_000;
328+
let halvings = height / HALVING_INTERVAL;
329+
if halvings >= 64 {
330+
return 0;
331+
}
332+
INITIAL_SUBSIDY_SAT >> halvings
333+
}
278334
pub(crate) fn pruneblockchain(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
279335
let height = required_u64(params, 0, "height is required")?;
280336
let applied = u64::from(ctx.applied_height());
@@ -668,6 +724,22 @@ mod tests {
668724
use super::*;
669725
use bitcoin_rs_chain::{ChainWork, NodeId, TipSnapshot};
670726

727+
#[test]
728+
fn subsidy_at_height_genesis_is_50_btc() {
729+
assert_eq!(subsidy_at_height(0), 5_000_000_000);
730+
}
731+
732+
#[test]
733+
fn subsidy_at_height_first_halving_is_25_btc() {
734+
assert_eq!(subsidy_at_height(210_000), 2_500_000_000);
735+
}
736+
737+
#[test]
738+
fn subsidy_at_height_after_64_halvings_is_zero() {
739+
assert_eq!(subsidy_at_height(64 * 210_000), 0);
740+
assert_eq!(subsidy_at_height(u32::MAX), 0);
741+
}
742+
671743
#[test]
672744
fn getblock_populates_real_header_fields_from_stored_record()
673745
-> Result<(), Box<dyn std::error::Error>> {

0 commit comments

Comments
 (0)