mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 06:14:47 -07:00
mempool: use bitcoin projected block, rest is a very simple prediction
This commit is contained in:
@@ -194,4 +194,37 @@ impl ClientInner {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Mixed-method batch: each `(method, args)` pair becomes one request
|
||||
/// in a single round-trip. Each result is independently parsed by the
|
||||
/// caller using its own `T`. Outer `Result` fails on transport errors;
|
||||
/// inner `Result`s fail on per-item RPC errors.
|
||||
pub(crate) fn call_mixed_batch(
|
||||
&self,
|
||||
requests: &[(&str, Vec<Value>)],
|
||||
) -> Result<Vec<Result<Box<RawValue>>>> {
|
||||
let params: Vec<Box<RawValue>> = requests
|
||||
.iter()
|
||||
.map(|(_, args)| serde_json::value::to_raw_value(args).map_err(Error::from))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let client = self.client.read();
|
||||
let built: Vec<Request> = requests
|
||||
.iter()
|
||||
.zip(¶ms)
|
||||
.map(|((method, _), p)| client.build_request(method, Some(p)))
|
||||
.collect();
|
||||
|
||||
let responses = client
|
||||
.send_batch(&built)
|
||||
.map_err(|e| Error::Parse(format!("mixed batch failed: {e}")))?;
|
||||
|
||||
Ok(responses
|
||||
.into_iter()
|
||||
.map(|resp| {
|
||||
let resp = resp.ok_or(Error::Internal("Missing response in JSON-RPC batch"))?;
|
||||
resp.result::<Box<RawValue>>().map_err(Error::from)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ mod client;
|
||||
mod methods;
|
||||
|
||||
use client::ClientInner;
|
||||
pub use methods::MempoolState;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockchainInfo {
|
||||
|
||||
@@ -9,8 +9,7 @@ use brk_types::{
|
||||
use corepc_jsonrpc::error::Error as JsonRpcError;
|
||||
use corepc_types::v30::{
|
||||
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
|
||||
GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose,
|
||||
GetTxOut,
|
||||
GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetTxOut,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
@@ -31,6 +30,94 @@ use crate::{
|
||||
/// spend too long on a single batch before yielding results.
|
||||
const BATCH_CHUNK: usize = 2000;
|
||||
|
||||
/// Live mempool state fetched in one batched bitcoind round-trip:
|
||||
/// `getrawmempool verbose` + `getblocktemplate` + `getmempoolinfo`.
|
||||
/// `gbt` is validated to be a subset of `entries` before construction;
|
||||
/// callers that want strict consistency should rely on this fact.
|
||||
pub struct MempoolState {
|
||||
pub entries: Vec<MempoolEntryInfo>,
|
||||
pub gbt: Vec<BlockTemplateTx>,
|
||||
pub min_fee: FeeRate,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VerboseEntryRaw {
|
||||
vsize: VSize,
|
||||
weight: Weight,
|
||||
time: Timestamp,
|
||||
#[serde(rename = "ancestorcount")]
|
||||
ancestor_count: u64,
|
||||
#[serde(rename = "ancestorsize")]
|
||||
ancestor_size: VSize,
|
||||
#[serde(rename = "descendantsize")]
|
||||
descendant_size: VSize,
|
||||
fees: VerboseFeesRaw,
|
||||
depends: Vec<String>,
|
||||
#[serde(rename = "chunkweight", default)]
|
||||
chunk_weight: Option<Weight>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VerboseFeesRaw {
|
||||
base: Bitcoin,
|
||||
ancestor: Bitcoin,
|
||||
descendant: Bitcoin,
|
||||
#[serde(default)]
|
||||
chunk: Option<Bitcoin>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GbtResponseRaw {
|
||||
transactions: Vec<GbtTxRaw>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GbtTxRaw {
|
||||
txid: bitcoin::Txid,
|
||||
fee: u64,
|
||||
}
|
||||
|
||||
fn build_verbose(raw: FxHashMap<String, VerboseEntryRaw>) -> Result<Vec<MempoolEntryInfo>> {
|
||||
raw.into_iter()
|
||||
.map(|(txid_str, e)| {
|
||||
let depends = e
|
||||
.depends
|
||||
.iter()
|
||||
.map(|s| Client::parse_txid(s, "depends txid"))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(MempoolEntryInfo {
|
||||
txid: Client::parse_txid(&txid_str, "mempool txid")?,
|
||||
vsize: e.vsize,
|
||||
weight: e.weight,
|
||||
fee: Sats::from(e.fees.base),
|
||||
first_seen: e.time,
|
||||
ancestor_count: e.ancestor_count,
|
||||
ancestor_size: e.ancestor_size,
|
||||
ancestor_fee: Sats::from(e.fees.ancestor),
|
||||
descendant_size: e.descendant_size,
|
||||
descendant_fee: Sats::from(e.fees.descendant),
|
||||
chunk_fee: e.fees.chunk.map(Sats::from),
|
||||
chunk_weight: e.chunk_weight,
|
||||
depends,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_gbt(raw: GbtResponseRaw) -> Vec<BlockTemplateTx> {
|
||||
raw.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_min_fee(raw: GetMempoolInfo) -> FeeRate {
|
||||
FeeRate::from(raw.mempool_min_fee * 100_000.0)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
||||
let r: GetBlockchainInfo = self.0.call_with_retry("getblockchaininfo", &[])?;
|
||||
@@ -181,15 +268,6 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Live `mempoolminfee` in sat/vB, already maxed against `minrelaytxfee`
|
||||
/// per Core's contract. Wallets must pay at least this rate or bitcoind
|
||||
/// will reject the broadcast; rises above the relay floor when the
|
||||
/// mempool is purging by fee.
|
||||
pub fn get_mempool_min_fee(&self) -> Result<FeeRate> {
|
||||
let r: GetMempoolInfo = self.0.call_with_retry("getmempoolinfo", &[])?;
|
||||
Ok(FeeRate::from(r.mempool_min_fee * 100_000.0))
|
||||
}
|
||||
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
|
||||
let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?;
|
||||
r.0.iter()
|
||||
@@ -197,33 +275,6 @@ impl Client {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all mempool entries with their fee data in a single RPC call
|
||||
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<MempoolEntryInfo>> {
|
||||
let r: GetRawMempoolVerbose = self
|
||||
.0
|
||||
.call_with_retry("getrawmempool", &[Value::Bool(true)])?;
|
||||
r.0.into_iter()
|
||||
.map(|(txid_str, entry)| {
|
||||
let depends = entry
|
||||
.depends
|
||||
.iter()
|
||||
.map(|s| Self::parse_txid(s, "depends txid"))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(MempoolEntryInfo {
|
||||
txid: Self::parse_txid(&txid_str, "mempool txid")?,
|
||||
vsize: VSize::from(entry.vsize as u64),
|
||||
weight: Weight::from(entry.weight as u64),
|
||||
fee: Sats::from(Bitcoin::from(entry.fees.base)),
|
||||
first_seen: Timestamp::from(entry.time),
|
||||
ancestor_count: entry.ancestor_count as u64,
|
||||
ancestor_size: entry.ancestor_size as u64,
|
||||
ancestor_fee: Sats::from(Bitcoin::from(entry.fees.ancestor)),
|
||||
depends,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
@@ -327,29 +378,50 @@ impl Client {
|
||||
Ok(Txid::from(txid))
|
||||
}
|
||||
|
||||
/// Transactions (txid + fee) Bitcoin Core would include in the next
|
||||
/// block it would mine, via `getblocktemplate`. Core requires the
|
||||
/// `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
#[derive(Deserialize)]
|
||||
struct Response {
|
||||
transactions: Vec<Tx>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Tx {
|
||||
txid: bitcoin::Txid,
|
||||
fee: u64,
|
||||
/// Verbose mempool listing + Core's projected next block + live
|
||||
/// `mempoolminfee`, fetched in a single bitcoind round-trip.
|
||||
/// Validates that every GBT txid is present in the verbose listing
|
||||
/// and returns `Ok(None)` on mismatch so the caller can skip the
|
||||
/// cycle (within-batch races inside bitcoind are rare; persistent
|
||||
/// drift is bug-shaped). Other failures bubble up as `Err`.
|
||||
pub fn fetch_mempool_state(&self) -> Result<Option<MempoolState>> {
|
||||
let requests: [(&str, Vec<Value>); 3] = [
|
||||
("getrawmempool", vec![Value::Bool(true)]),
|
||||
(
|
||||
"getblocktemplate",
|
||||
vec![serde_json::json!({ "rules": ["segwit"] })],
|
||||
),
|
||||
("getmempoolinfo", vec![]),
|
||||
];
|
||||
let mut out = self.0.call_mixed_batch(&requests)?.into_iter();
|
||||
let verbose_raw = out.next().ok_or(Error::Internal("missing verbose"))??;
|
||||
let gbt_raw = out.next().ok_or(Error::Internal("missing gbt"))??;
|
||||
let info_raw = out.next().ok_or(Error::Internal("missing mempoolinfo"))??;
|
||||
|
||||
let verbose: FxHashMap<String, VerboseEntryRaw> = serde_json::from_str(verbose_raw.get())?;
|
||||
let entries = build_verbose(verbose)?;
|
||||
let gbt = build_gbt(serde_json::from_str(gbt_raw.get())?);
|
||||
let min_fee = build_min_fee(serde_json::from_str(info_raw.get())?);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let entry_set: rustc_hash::FxHashSet<Txid> = entries.iter().map(|e| e.txid).collect();
|
||||
let missing = gbt.iter().filter(|t| !entry_set.contains(&t.txid)).count();
|
||||
if missing > 0 {
|
||||
tracing::warn!(
|
||||
missing,
|
||||
gbt_total = gbt.len(),
|
||||
"getblocktemplate has {missing} txids not in verbose mempool; skipping cycle"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let args = [serde_json::json!({ "rules": ["segwit"] })];
|
||||
let r: Response = self.0.call_with_retry("getblocktemplate", &args)?;
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee),
|
||||
})
|
||||
.collect())
|
||||
Ok(Some(MempoolState {
|
||||
entries,
|
||||
gbt,
|
||||
min_fee,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get_closest_valid_height(&self, hash: BlockHash) -> Result<(Height, BlockHash)> {
|
||||
|
||||
Reference in New Issue
Block a user