mempool: use bitcoin projected block, rest is a very simple prediction

This commit is contained in:
nym21
2026-05-07 18:30:26 +02:00
parent 1b39d21bbe
commit f4910efd7d
69 changed files with 4340 additions and 5906 deletions

View File

@@ -46,9 +46,8 @@ impl Args {
Some((k, v)) => (k.to_string(), v.to_string()),
None => (
rest.to_string(),
iter.next().ok_or_else(|| {
Error::Parse(format!("--{rest} requires a value"))
})?,
iter.next()
.ok_or_else(|| Error::Parse(format!("--{rest} requires a value")))?,
),
};
match key.as_str() {
@@ -75,11 +74,6 @@ impl Args {
.next()
.ok_or_else(|| Error::Parse("missing selector".into()))?;
let paths: Vec<Path> = iter.map(|f| Path::parse(&f)).collect::<Result<_>>()?;
if paths.is_empty() {
return Err(Error::Parse(
"missing field. ask for at least one (e.g. `blk 0 hash`)".into(),
));
}
Ok(Self {
selector,
paths,
@@ -117,9 +111,7 @@ impl Args {
.unwrap_or_else(|| self.bitcoin_dir().join(".cookie"));
let auth = if cookie.is_file() {
Auth::CookieFile(cookie)
} else if let (Some(u), Some(p)) =
(self.rpcuser.as_deref(), self.rpcpassword.as_deref())
{
} else if let (Some(u), Some(p)) = (self.rpcuser.as_deref(), self.rpcpassword.as_deref()) {
Auth::UserPass(u.to_string(), p.to_string())
} else {
return Err(Error::Parse(

View File

@@ -74,6 +74,40 @@ impl<'a> Ctx<'a> {
})
}
pub fn full(&self) -> Value {
let b = self.block;
let (size, weight) = self.size_and_weight();
let tx: Vec<Value> = b
.txdata
.iter()
.enumerate()
.map(|(i, tx)| tx_to_value(tx, i == 0))
.collect();
json!({
"height": *b.height(),
"hash": b.hash().to_string(),
"version": b.header.version.to_consensus(),
"version_hex": format!("{:08x}", b.header.version.to_consensus() as u32),
"merkle": b.header.merkle_root.to_string(),
"time": b.header.time,
"nonce": b.header.nonce,
"bits": b.header.bits.to_consensus(),
"difficulty": b.header.difficulty_float(),
"prev": b.header.prev_blockhash.to_string(),
"txs": b.txdata.len(),
"n_inputs": b.txdata.iter().map(|t| t.input.len()).sum::<usize>(),
"n_outputs": b.txdata.iter().map(|t| t.output.len()).sum::<usize>(),
"witness_txs": b.txdata.iter().filter(|t| tx_has_witness(t)).count(),
"size": size,
"strippedsize": (weight - size) / 3,
"weight": weight,
"subsidy": subsidy_sats(*b.height()),
"coinbase": b.coinbase_tag().as_str(),
"header_hex": serialize_hex(&b.header),
"tx": tx,
})
}
fn size_and_weight(&self) -> (usize, usize) {
*self
.size_weight

View File

@@ -36,13 +36,20 @@ impl Formatter {
row.push('\t');
}
for c in ctx.resolve_str(path)?.chars() {
row.push(if matches!(c, '\t' | '\n' | '\r') { ' ' } else { c });
row.push(if matches!(c, '\t' | '\n' | '\r') {
' '
} else {
c
});
}
}
Ok(row)
}
fn object(&self, ctx: &Ctx) -> Result<Value> {
if self.fields.is_empty() {
return Ok(ctx.full());
}
let mut obj = Map::with_capacity(self.fields.len());
for path in &self.fields {
obj.insert(path.raw.clone(), ctx.resolve(path)?);
@@ -50,4 +57,3 @@ impl Formatter {
Ok(Value::Object(obj))
}
}

View File

@@ -41,7 +41,11 @@ fn run() -> Result<()> {
let mode = Mode::pick(args.pretty, args.compact, args.paths.len());
let reader = Reader::new(args.blocks_dir(), &client);
let formatter = Formatter::new(mode, args.paths);
for block in reader.range(start, end)?.iter() {
let parser_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
/ 2;
for block in reader.range_with(start, end, parser_threads)?.iter() {
let block = block?;
let line = formatter.format(&Ctx::new(&block))?;
if !line.is_empty() {

View File

@@ -10,6 +10,8 @@ impl Mode {
pub fn pick(pretty: bool, compact: bool, n_fields: usize) -> Self {
if pretty {
Self::Pretty
} else if n_fields == 0 {
Self::Json
} else if n_fields == 1 {
Self::Bare
} else if compact {

View File

@@ -14,7 +14,9 @@ impl Selector {
}
};
if end < start {
return Err(Error::Parse(format!("range end {end} before start {start}")));
return Err(Error::Parse(format!(
"range end {end} before start {start}"
)));
}
Ok((start, end))
}

View File

@@ -12,10 +12,14 @@ pub fn print() {
section("USAGE");
println!(
" blk {} {} [field ...] [OPTIONS]",
" blk {} [{} ...] [OPTIONS]",
"<selector>".bright_black(),
"<field>".bright_black()
);
println!(
" {}",
"no fields = full block as JSON (analog of `bitcoin-cli getblock <hash> 2`)".bright_black()
);
println!();
section("SELECTOR");
@@ -28,8 +32,7 @@ pub fn print() {
section("FIELDS");
println!(
" {}",
"dotted paths drill into nested data; omit an index for arrays"
.bright_black()
"dotted paths drill into nested data; omit an index for arrays".bright_black()
);
println!();
group("block");
@@ -58,29 +61,48 @@ pub fn print() {
println!();
println!(
" {}",
"Naked tx / tx.i / vin / vout returns the whole sub-object as JSON."
.bright_black()
"Naked tx / tx.i / vin / vout returns the whole sub-object as JSON.".bright_black()
);
println!();
section("OUTPUT");
out("no fields", "full block JSON object, one per line (NDJSON)");
out("1 field", "bare value, one per line");
out("2+ fields", "compact JSON object, one per line (NDJSON)");
out("-p, --pretty", "pretty JSON object instead");
out("-c, --compact", "tab-separated values, no field names (TSV)");
out(
"-c, --compact",
"tab-separated values, no field names (TSV)",
);
println!();
section("OPTIONS");
opt("--bitcoindir", "<PATH>", "Bitcoin directory", Some("[OS default]"));
opt("--blocksdir", "<PATH>", "Blocks directory", Some("[<bitcoindir>/blocks]"));
opt(
"--bitcoindir",
"<PATH>",
"Bitcoin directory",
Some("[OS default]"),
);
opt(
"--blocksdir",
"<PATH>",
"Blocks directory",
Some("[<bitcoindir>/blocks]"),
);
opt("--rpcconnect", "<IP>", "RPC host", Some("[localhost]"));
opt("--rpcport", "<PORT>", "RPC port", Some("[8332]"));
opt("--rpccookiefile", "<PATH>", "RPC cookie file", Some("[<bitcoindir>/.cookie]"));
opt(
"--rpccookiefile",
"<PATH>",
"RPC cookie file",
Some("[<bitcoindir>/.cookie]"),
);
opt("--rpcuser", "<USERNAME>", "RPC username", None);
opt("--rpcpassword", "<PASSWORD>", "RPC password", None);
println!();
section("EXAMPLES");
ex("blk 800000", "full block as JSON");
ex("blk 800000 hash", "bare hash");
ex("blk 800000 height hash time", "one compact JSON line");
ex("blk 800000 tx.0.txid", "coinbase txid");
@@ -128,7 +150,11 @@ fn sel(token: &str, desc: &str) {
}
fn out(label: &str, desc: &str) {
println!(" {label}{}{}{desc}", pad(label, LABEL_W), " ".repeat(GAP));
println!(
" {label}{}{}{desc}",
pad(label, LABEL_W),
" ".repeat(GAP)
);
}
fn opt(flag: &str, ph: &str, desc: &str, default: Option<&str>) {