mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-22 12:23:04 -07:00
408 lines
12 KiB
Rust
408 lines
12 KiB
Rust
use std::cell::OnceCell;
|
|
|
|
use bitcoin::{
|
|
Address, Block, Network, ScriptBuf, Transaction, TxIn, TxOut, consensus::encode::serialize_hex,
|
|
hex::DisplayHex,
|
|
};
|
|
use brk_error::{Error, Result};
|
|
use brk_types::ReadBlock;
|
|
use serde_json::{Map, Value, json};
|
|
|
|
use crate::path::{Path, Step};
|
|
|
|
// `hex` is intentionally absent: matches `bitcoin-cli getblock <hash> 2`
|
|
// and keeps NDJSON dumps tractable. Still reachable explicitly via `blk N hex`.
|
|
const BLOCK_FIELDS: &[&str] = &[
|
|
"height",
|
|
"hash",
|
|
"version",
|
|
"version_hex",
|
|
"merkle",
|
|
"time",
|
|
"nonce",
|
|
"bits",
|
|
"difficulty",
|
|
"prev",
|
|
"txs",
|
|
"n_inputs",
|
|
"n_outputs",
|
|
"witness_txs",
|
|
"size",
|
|
"strippedsize",
|
|
"weight",
|
|
"subsidy",
|
|
"coinbase",
|
|
"coinbase_hex",
|
|
"header_hex",
|
|
"tx",
|
|
];
|
|
|
|
const TX_FIELDS: &[&str] = &[
|
|
"txid",
|
|
"wtxid",
|
|
"version",
|
|
"locktime",
|
|
"size",
|
|
"base_size",
|
|
"vsize",
|
|
"weight",
|
|
"inputs",
|
|
"outputs",
|
|
"is_coinbase",
|
|
"has_witness",
|
|
"is_rbf",
|
|
"total_out",
|
|
"hex",
|
|
"vin",
|
|
"vout",
|
|
];
|
|
|
|
const VIN_FIELDS: &[&str] = &[
|
|
"prev_txid",
|
|
"prev_vout",
|
|
"sequence",
|
|
"script_sig",
|
|
"script_sig_asm",
|
|
"witness",
|
|
"has_witness",
|
|
"is_rbf",
|
|
"coinbase",
|
|
];
|
|
|
|
const VOUT_FIELDS: &[&str] = &[
|
|
"value",
|
|
"script_pubkey",
|
|
"script_pubkey_asm",
|
|
"type",
|
|
"address",
|
|
];
|
|
|
|
pub struct Ctx<'a> {
|
|
block: &'a ReadBlock,
|
|
network: Network,
|
|
size_weight: OnceCell<(usize, usize)>,
|
|
}
|
|
|
|
impl<'a> Ctx<'a> {
|
|
pub fn new(block: &'a ReadBlock, network: Network) -> Self {
|
|
Self {
|
|
block,
|
|
network,
|
|
size_weight: OnceCell::new(),
|
|
}
|
|
}
|
|
|
|
pub fn resolve(&self, path: &Path) -> Result<Value> {
|
|
let (step, rest) = pop(&path.steps)?;
|
|
self.block_field(&step.name, step.index, rest)
|
|
}
|
|
|
|
pub fn resolve_str(&self, path: &Path) -> Result<String> {
|
|
Ok(match self.resolve(path)? {
|
|
Value::String(s) => s,
|
|
other => other.to_string(),
|
|
})
|
|
}
|
|
|
|
pub fn full(&self) -> Value {
|
|
let mut obj = Map::with_capacity(BLOCK_FIELDS.len());
|
|
for &name in BLOCK_FIELDS {
|
|
obj.insert(
|
|
name.into(),
|
|
self.block_field(name, None, &[]).expect("known block field"),
|
|
);
|
|
}
|
|
Value::Object(obj)
|
|
}
|
|
|
|
fn size_and_weight(&self) -> (usize, usize) {
|
|
*self
|
|
.size_weight
|
|
.get_or_init(|| self.block.total_size_and_weight())
|
|
}
|
|
|
|
fn block_field(&self, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
|
|
let b = self.block;
|
|
let raw: &Block = b;
|
|
let scalar = |v| scalar_leaf(v, name, index, rest);
|
|
match name {
|
|
"height" => scalar(json!(*b.height())),
|
|
"hash" => scalar(json!(b.hash().to_string())),
|
|
"time" => scalar(json!(b.header.time)),
|
|
"version" => scalar(json!(b.header.version.to_consensus())),
|
|
"version_hex" => scalar(json!(format!(
|
|
"{:08x}",
|
|
b.header.version.to_consensus() as u32
|
|
))),
|
|
"bits" => scalar(json!(format!("{:08x}", b.header.bits.to_consensus()))),
|
|
"nonce" => scalar(json!(b.header.nonce)),
|
|
"prev" => scalar(json!(b.header.prev_blockhash.to_string())),
|
|
"merkle" => scalar(json!(b.header.merkle_root.to_string())),
|
|
"difficulty" => scalar(json!(b.header.difficulty_float())),
|
|
"txs" => scalar(json!(b.txdata.len())),
|
|
"n_inputs" => scalar(json!(
|
|
b.txdata.iter().map(|tx| tx.input.len()).sum::<usize>()
|
|
)),
|
|
"n_outputs" => scalar(json!(
|
|
b.txdata.iter().map(|tx| tx.output.len()).sum::<usize>()
|
|
)),
|
|
"witness_txs" => scalar(json!(
|
|
b.txdata.iter().filter(|tx| tx_has_witness(tx)).count()
|
|
)),
|
|
"size" => scalar(json!(self.size_and_weight().0)),
|
|
"weight" => scalar(json!(self.size_and_weight().1)),
|
|
"strippedsize" => {
|
|
let (size, weight) = self.size_and_weight();
|
|
scalar(json!((weight - size) / 3))
|
|
}
|
|
"subsidy" => scalar(json!(subsidy_sats(*b.height()))),
|
|
"header_hex" => scalar(json!(serialize_hex(&b.header))),
|
|
"hex" => scalar(json!(serialize_hex(raw))),
|
|
"coinbase" => scalar(json!(b.coinbase_tag().as_str())),
|
|
"coinbase_hex" => {
|
|
debug_assert!(
|
|
!b.txdata.is_empty() && !b.txdata[0].input.is_empty(),
|
|
"consensus-valid block has a coinbase tx with at least one input"
|
|
);
|
|
scalar(json!(b.txdata[0].input[0].script_sig.to_hex_string()))
|
|
}
|
|
"tx" => pick(&b.txdata, name, index, |i, tx| {
|
|
self.resolve_tx(tx, i == 0, rest)
|
|
}),
|
|
other => Err(unknown("block", other)),
|
|
}
|
|
}
|
|
|
|
fn resolve_tx(&self, tx: &Transaction, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
|
if steps.is_empty() {
|
|
let mut obj = Map::with_capacity(TX_FIELDS.len());
|
|
for &name in TX_FIELDS {
|
|
obj.insert(
|
|
name.into(),
|
|
self.tx_field(tx, is_coinbase, name, None, &[])
|
|
.expect("known tx field"),
|
|
);
|
|
}
|
|
return Ok(Value::Object(obj));
|
|
}
|
|
let (step, rest) = pop(steps)?;
|
|
self.tx_field(tx, is_coinbase, &step.name, step.index, rest)
|
|
}
|
|
|
|
fn tx_field(
|
|
&self,
|
|
tx: &Transaction,
|
|
is_coinbase: bool,
|
|
name: &str,
|
|
index: Option<usize>,
|
|
rest: &[Step],
|
|
) -> Result<Value> {
|
|
let scalar = |v| scalar_leaf(v, name, index, rest);
|
|
match name {
|
|
"txid" => scalar(json!(tx.compute_txid().to_string())),
|
|
"wtxid" => scalar(json!(tx.compute_wtxid().to_string())),
|
|
"version" => scalar(json!(tx.version.0)),
|
|
"locktime" => scalar(json!(tx.lock_time.to_consensus_u32())),
|
|
"size" => scalar(json!(tx.total_size())),
|
|
"base_size" => scalar(json!(tx.base_size())),
|
|
"vsize" => scalar(json!(tx.vsize())),
|
|
"weight" => scalar(json!(tx.weight().to_wu())),
|
|
"inputs" => scalar(json!(tx.input.len())),
|
|
"outputs" => scalar(json!(tx.output.len())),
|
|
"is_coinbase" => scalar(json!(is_coinbase)),
|
|
"has_witness" => scalar(json!(tx_has_witness(tx))),
|
|
"is_rbf" => scalar(json!(tx_is_rbf(tx))),
|
|
"total_out" => scalar(json!(tx_total_out(tx))),
|
|
"hex" => scalar(json!(serialize_hex(tx))),
|
|
"vin" => pick(&tx.input, name, index, |j, vin| {
|
|
resolve_vin(vin, is_coinbase && j == 0, rest)
|
|
}),
|
|
"vout" => pick(&tx.output, name, index, |_, vout| {
|
|
self.resolve_vout(vout, rest)
|
|
}),
|
|
other => Err(unknown("tx", other)),
|
|
}
|
|
}
|
|
|
|
fn resolve_vout(&self, vout: &TxOut, steps: &[Step]) -> Result<Value> {
|
|
if steps.is_empty() {
|
|
let mut obj = Map::with_capacity(VOUT_FIELDS.len());
|
|
for &name in VOUT_FIELDS {
|
|
obj.insert(
|
|
name.into(),
|
|
self.vout_field(vout, name, None, &[])
|
|
.expect("known vout field"),
|
|
);
|
|
}
|
|
return Ok(Value::Object(obj));
|
|
}
|
|
let (step, rest) = pop(steps)?;
|
|
self.vout_field(vout, &step.name, step.index, rest)
|
|
}
|
|
|
|
fn vout_field(
|
|
&self,
|
|
vout: &TxOut,
|
|
name: &str,
|
|
index: Option<usize>,
|
|
rest: &[Step],
|
|
) -> Result<Value> {
|
|
let scalar = |v| scalar_leaf(v, name, index, rest);
|
|
match name {
|
|
"value" => scalar(json!(vout.value.to_sat())),
|
|
"script_pubkey" => scalar(json!(vout.script_pubkey.to_hex_string())),
|
|
"script_pubkey_asm" => scalar(json!(vout.script_pubkey.to_asm_string())),
|
|
"type" => scalar(json!(script_type(&vout.script_pubkey))),
|
|
"address" => scalar(self.address_value(&vout.script_pubkey)),
|
|
other => Err(unknown("vout", other)),
|
|
}
|
|
}
|
|
|
|
fn address_value(&self, s: &ScriptBuf) -> Value {
|
|
Address::from_script(s, self.network)
|
|
.map(|a| Value::String(a.to_string()))
|
|
.unwrap_or(Value::Null)
|
|
}
|
|
}
|
|
|
|
fn resolve_vin(vin: &TxIn, is_coinbase: bool, steps: &[Step]) -> Result<Value> {
|
|
if steps.is_empty() {
|
|
let mut obj = Map::with_capacity(VIN_FIELDS.len());
|
|
for &name in VIN_FIELDS {
|
|
obj.insert(
|
|
name.into(),
|
|
vin_field(vin, is_coinbase, name, None, &[]).expect("known vin field"),
|
|
);
|
|
}
|
|
return Ok(Value::Object(obj));
|
|
}
|
|
let (step, rest) = pop(steps)?;
|
|
vin_field(vin, is_coinbase, &step.name, step.index, rest)
|
|
}
|
|
|
|
fn vin_field(
|
|
vin: &TxIn,
|
|
is_coinbase: bool,
|
|
name: &str,
|
|
index: Option<usize>,
|
|
rest: &[Step],
|
|
) -> Result<Value> {
|
|
let scalar = |v| scalar_leaf(v, name, index, rest);
|
|
match name {
|
|
"prev_txid" => scalar(json!(vin.previous_output.txid.to_string())),
|
|
"prev_vout" => scalar(json!(vin.previous_output.vout)),
|
|
"sequence" => scalar(json!(vin.sequence.0)),
|
|
"script_sig" => scalar(json!(vin.script_sig.to_hex_string())),
|
|
"script_sig_asm" => scalar(json!(vin.script_sig.to_asm_string())),
|
|
"witness" => {
|
|
if !rest.is_empty() {
|
|
return Err(Error::Parse(
|
|
"'witness' element has no fields to drill into".into(),
|
|
));
|
|
}
|
|
let items: Vec<String> = vin
|
|
.witness
|
|
.iter()
|
|
.map(|w| w.to_lower_hex_string())
|
|
.collect();
|
|
pick(&items, name, index, |_, hex| Ok(Value::String(hex.clone())))
|
|
}
|
|
"has_witness" => scalar(json!(!vin.witness.is_empty())),
|
|
"is_rbf" => scalar(json!(vin.sequence.is_rbf())),
|
|
"coinbase" => scalar(json!(is_coinbase)),
|
|
other => Err(unknown("vin", other)),
|
|
}
|
|
}
|
|
|
|
fn pick<T>(
|
|
items: &[T],
|
|
name: &str,
|
|
index: Option<usize>,
|
|
mut resolve: impl FnMut(usize, &T) -> Result<Value>,
|
|
) -> Result<Value> {
|
|
match index {
|
|
Some(i) => {
|
|
let item = items
|
|
.get(i)
|
|
.ok_or_else(|| out_of_range(name, i, items.len()))?;
|
|
resolve(i, item)
|
|
}
|
|
None => Ok(Value::Array(
|
|
items
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, item)| resolve(i, item))
|
|
.collect::<Result<_>>()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn pop(steps: &[Step]) -> Result<(&Step, &[Step])> {
|
|
steps
|
|
.split_first()
|
|
.ok_or_else(|| Error::Parse("empty path segment".into()))
|
|
}
|
|
|
|
fn scalar_leaf(v: Value, name: &str, index: Option<usize>, rest: &[Step]) -> Result<Value> {
|
|
if index.is_some() {
|
|
return Err(Error::Parse(format!("'{name}' is not an array")));
|
|
}
|
|
if !rest.is_empty() {
|
|
return Err(Error::Parse(format!(
|
|
"'{name}' has no fields to drill into"
|
|
)));
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
fn out_of_range(name: &str, i: usize, len: usize) -> Error {
|
|
Error::Parse(format!("{name}.{i} out of range (len {len})"))
|
|
}
|
|
|
|
fn unknown(level: &str, name: &str) -> Error {
|
|
Error::Parse(format!(
|
|
"unknown {level} field '{name}' (run `blk --help` for the list)"
|
|
))
|
|
}
|
|
|
|
fn tx_has_witness(tx: &Transaction) -> bool {
|
|
tx.input.iter().any(|i| !i.witness.is_empty())
|
|
}
|
|
|
|
fn tx_is_rbf(tx: &Transaction) -> bool {
|
|
tx.input.iter().any(|i| i.sequence.is_rbf())
|
|
}
|
|
|
|
fn tx_total_out(tx: &Transaction) -> u64 {
|
|
tx.output.iter().map(|o| o.value.to_sat()).sum()
|
|
}
|
|
|
|
fn subsidy_sats(height: u32) -> u64 {
|
|
let halvings = height / 210_000;
|
|
if halvings >= 64 {
|
|
0
|
|
} else {
|
|
(50 * 100_000_000u64) >> halvings
|
|
}
|
|
}
|
|
|
|
fn script_type(s: &ScriptBuf) -> &'static str {
|
|
if s.is_p2pkh() {
|
|
"p2pkh"
|
|
} else if s.is_p2sh() {
|
|
"p2sh"
|
|
} else if s.is_p2wpkh() {
|
|
"p2wpkh"
|
|
} else if s.is_p2wsh() {
|
|
"p2wsh"
|
|
} else if s.is_p2tr() {
|
|
"p2tr"
|
|
} else if s.is_op_return() {
|
|
"op_return"
|
|
} else if s.is_p2pk() {
|
|
"p2pk"
|
|
} else {
|
|
"unknown"
|
|
}
|
|
}
|