mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 06:44:47 -07:00
mempool: fixes
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
//!
|
||||
//! 1. [`steps::fetcher::Fetcher`] - one mixed batched RPC for
|
||||
//! `getblocktemplate` + `getrawmempool false` + `getmempoolinfo`,
|
||||
//! then a `getmempoolentry` batch and a `getrawtransaction` batch
|
||||
//! on new txids only. The GBT is validated to be a subset of the
|
||||
//! txid listing; on mismatch the cycle is skipped.
|
||||
//! then a single mixed `getmempoolentry`+`getrawtransaction` batch
|
||||
//! on new txids only. GBT-only txs are synthesized inline from the
|
||||
//! GBT payload so block 0 matches Core's selection exactly without
|
||||
//! a follow-up entry fetch that could race the listing.
|
||||
//! 2. [`steps::preparer::Preparer`] - decode and classify into
|
||||
//! `TxsPulled { added, removed }`. Pure CPU.
|
||||
//! 3. [`steps::applier::Applier`] - apply the diff to
|
||||
@@ -345,20 +346,17 @@ impl Mempool {
|
||||
..
|
||||
} = &*self.0;
|
||||
|
||||
let Some(Fetched {
|
||||
let Fetched {
|
||||
live_txids,
|
||||
new_entries,
|
||||
new_txs,
|
||||
gbt,
|
||||
gbt_txids,
|
||||
min_fee,
|
||||
}) = Fetcher::fetch(client, state)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
} = Fetcher::fetch(client, state)?;
|
||||
let pulled = Preparer::prepare(&live_txids, new_entries, new_txs, state);
|
||||
let changed = Applier::apply(state, rebuilder, pulled);
|
||||
Prevouts::fill(state, resolver);
|
||||
rebuilder.tick(state, changed, &gbt, min_fee);
|
||||
rebuilder.tick(state, changed, &gbt_txids, min_fee);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use brk_rpc::BlockTemplateTx;
|
||||
use brk_types::{FeeRate, MempoolEntryInfo, Txid};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -10,6 +9,10 @@ pub struct Fetched {
|
||||
/// keep their first-sight entry on the live store).
|
||||
pub new_entries: Vec<MempoolEntryInfo>,
|
||||
pub new_txs: FxHashMap<Txid, bitcoin::Transaction>,
|
||||
pub gbt: Vec<BlockTemplateTx>,
|
||||
/// Block 0 ordering from `getblocktemplate`. Bodies and stats have
|
||||
/// already been folded into `new_entries`/`new_txs` (or were already
|
||||
/// in the pool); the Rebuilder only needs the txid sequence to
|
||||
/// project Core's exact selection.
|
||||
pub gbt_txids: Vec<Txid>,
|
||||
pub min_fee: FeeRate,
|
||||
}
|
||||
|
||||
@@ -4,64 +4,106 @@ pub use fetched::Fetched;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_rpc::{Client, MempoolState};
|
||||
use brk_types::Txid;
|
||||
use brk_types::{MempoolEntryInfo, Timestamp, Txid, VSize};
|
||||
use parking_lot::RwLock;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{State, stores::TxStore};
|
||||
use crate::State;
|
||||
|
||||
/// Cap before the batch RPC so we never hand bitcoind an unbounded batch.
|
||||
/// GBT-synthesized entries are not subject to this cap; they're bounded
|
||||
/// by the block weight limit Core enforces on its own template.
|
||||
const MAX_TX_FETCHES_PER_CYCLE: usize = 10_000;
|
||||
|
||||
/// Three batched round-trips per cycle, scaling with churn rather than
|
||||
/// Two batched round-trips per cycle, scaling with churn rather than
|
||||
/// mempool size: `getblocktemplate` + `getrawmempool false` +
|
||||
/// `getmempoolinfo` in one mixed batch; then `getmempoolentry` and
|
||||
/// `getrawtransaction` per *new* txid only.
|
||||
/// `getmempoolinfo` in one mixed batch; then `getmempoolentry` +
|
||||
/// `getrawtransaction` for *new* non-GBT txids in a second mixed batch.
|
||||
///
|
||||
/// `getblocktemplate` is validated to be a subset of the txid listing
|
||||
/// inside the RPC layer; mismatches return `Ok(None)` so the cycle is
|
||||
/// skipped without polluting downstream state.
|
||||
/// GBT entries already carry the full tx body and stats, so any GBT tx
|
||||
/// not yet in the local pool is materialized inline from the GBT
|
||||
/// payload instead of being refetched. That removes the GBT/listing
|
||||
/// race that used to skip cycles when a tx vanished from the mempool
|
||||
/// between the GBT and `getrawmempool` calls: block 0 always reflects
|
||||
/// Core's exact selection because we never ask for that data twice.
|
||||
///
|
||||
/// Confirmed prevouts are resolved post-apply by the caller-supplied
|
||||
/// resolver passed to `Mempool::update_with`, so the in-crate path no
|
||||
/// longer issues a fourth batch for parents.
|
||||
/// longer issues a third batch for parents.
|
||||
pub struct Fetcher;
|
||||
|
||||
impl Fetcher {
|
||||
pub fn fetch(client: &Client, lock: &RwLock<State>) -> Result<Option<Fetched>> {
|
||||
let Some(MempoolState {
|
||||
pub fn fetch(client: &Client, lock: &RwLock<State>) -> Result<Fetched> {
|
||||
let MempoolState {
|
||||
live_txids,
|
||||
gbt,
|
||||
min_fee,
|
||||
}) = client.fetch_mempool_state()?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let new_txids = {
|
||||
} = client.fetch_mempool_state()?;
|
||||
|
||||
// One read snapshot decides both the RPC fetch list and the
|
||||
// GBT-synthesis set, so they agree on what's "already known".
|
||||
// Graveyard txs are treated as known so a re-broadcast still
|
||||
// flows through `Preparer::classify_addition` and lands as
|
||||
// [`crate::TxAddition::Revived`].
|
||||
let (new_txids, gbt_synth_set) = {
|
||||
let state = lock.read();
|
||||
Self::new_txids(&live_txids, &state.txs)
|
||||
let mut gbt_txids: FxHashSet<Txid> =
|
||||
FxHashSet::with_capacity_and_hasher(gbt.len(), Default::default());
|
||||
let mut gbt_synth_set: FxHashSet<Txid> = FxHashSet::default();
|
||||
for g in &gbt {
|
||||
gbt_txids.insert(g.txid);
|
||||
if !state.txs.contains(&g.txid) {
|
||||
gbt_synth_set.insert(g.txid);
|
||||
}
|
||||
}
|
||||
let new_txids: Vec<Txid> = live_txids
|
||||
.iter()
|
||||
.filter(|t| !state.txs.contains(t) && !gbt_txids.contains(t))
|
||||
.take(MAX_TX_FETCHES_PER_CYCLE)
|
||||
.copied()
|
||||
.collect();
|
||||
(new_txids, gbt_synth_set)
|
||||
};
|
||||
let new_entries = client.fetch_mempool_entries(&new_txids)?;
|
||||
let new_txs = client.get_raw_transactions(&new_txids)?;
|
||||
Ok(Some(Fetched {
|
||||
|
||||
let (mut new_entries, mut new_txs) = client.fetch_new_pool_data(&new_txids)?;
|
||||
new_entries.reserve(gbt_synth_set.len());
|
||||
new_txs.reserve(gbt_synth_set.len());
|
||||
|
||||
// Consume `gbt` by value: GBT-only txs move their body and
|
||||
// depends into the synthesis path (no clones), and the GBT
|
||||
// ordering is captured as a `Vec<Txid>` for the Rebuilder, which
|
||||
// is the only downstream consumer and only reads txids.
|
||||
//
|
||||
// GBT carries no per-tx arrival timestamp. `now` is correct to
|
||||
// within ~1 cycle for a tx that just entered Core's mempool
|
||||
// (the only kind that triggers synthesis: not in our pool yet
|
||||
// means it just appeared this cycle).
|
||||
let now = Timestamp::now();
|
||||
let gbt_txids: Vec<Txid> = gbt
|
||||
.into_iter()
|
||||
.map(|g| {
|
||||
let txid = g.txid;
|
||||
if gbt_synth_set.contains(&txid) {
|
||||
new_entries.push(MempoolEntryInfo {
|
||||
txid,
|
||||
vsize: VSize::from(g.weight),
|
||||
weight: g.weight,
|
||||
fee: g.fee,
|
||||
first_seen: now,
|
||||
depends: g.depends,
|
||||
});
|
||||
new_txs.insert(txid, g.tx);
|
||||
}
|
||||
txid
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Fetched {
|
||||
live_txids,
|
||||
new_entries,
|
||||
new_txs,
|
||||
gbt,
|
||||
gbt_txids,
|
||||
min_fee,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Live txids the local store hasn't seen yet. Graveyard txs are
|
||||
/// included so a re-broadcast (post-reorg or a peer republishing)
|
||||
/// flows through `Preparer::classify_addition` and lands as
|
||||
/// [`crate::TxAddition::Revived`] instead of sitting orphaned for
|
||||
/// the full graveyard retention.
|
||||
fn new_txids(live_txids: &[Txid], known: &TxStore) -> Vec<Txid> {
|
||||
live_txids
|
||||
.iter()
|
||||
.filter(|txid| !known.contains(txid))
|
||||
.take(MAX_TX_FETCHES_PER_CYCLE)
|
||||
.copied()
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use brk_rpc::BlockTemplateTx;
|
||||
use brk_types::{FeeRate, NextBlockHash, Txid, TxidPrefix};
|
||||
use parking_lot::RwLock;
|
||||
use rustc_hash::FxHashSet;
|
||||
@@ -45,7 +44,7 @@ impl Rebuilder {
|
||||
&self,
|
||||
lock: &RwLock<State>,
|
||||
changed: bool,
|
||||
gbt: &[BlockTemplateTx],
|
||||
gbt_txids: &[Txid],
|
||||
min_fee: FeeRate,
|
||||
) {
|
||||
if changed {
|
||||
@@ -55,7 +54,7 @@ impl Rebuilder {
|
||||
self.skip_clean.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
let snap = Self::build_snapshot(lock, gbt, min_fee);
|
||||
let snap = Self::build_snapshot(lock, gbt_txids, min_fee);
|
||||
let block0_set: FxHashSet<Txid> = snap.block0_txids().collect();
|
||||
let next_hash = snap.next_block_hash;
|
||||
*self.snapshot.write() = Arc::new(snap);
|
||||
@@ -93,7 +92,7 @@ impl Rebuilder {
|
||||
|
||||
fn build_snapshot(
|
||||
lock: &RwLock<State>,
|
||||
gbt: &[BlockTemplateTx],
|
||||
gbt_txids: &[Txid],
|
||||
min_fee: FeeRate,
|
||||
) -> Snapshot {
|
||||
let (txs, prefix_to_idx) = {
|
||||
@@ -102,12 +101,15 @@ impl Rebuilder {
|
||||
};
|
||||
|
||||
// Block 0 from `getblocktemplate`: Core's actual selection.
|
||||
// Fetcher already validated GBT ⊆ live txid listing, so any
|
||||
// drop here is a same-cycle race and the partitioner picks up
|
||||
// the slack so callers always see NUM_BLOCKS blocks.
|
||||
let block0: Vec<TxIndex> = gbt
|
||||
// The Fetcher synthesizes pool entries for GBT txs that aren't
|
||||
// already present (using GBT's inline body + stats), so this
|
||||
// lookup always resolves and block 0 matches Core exactly.
|
||||
// The `filter_map` only drops if a tx was concurrently evicted
|
||||
// from `txs` between `build_txs` and the rebuild, which the
|
||||
// partitioner backfills so callers still see `NUM_BLOCKS`.
|
||||
let block0: Vec<TxIndex> = gbt_txids
|
||||
.iter()
|
||||
.filter_map(|t| prefix_to_idx.get(&TxidPrefix::from(&t.txid)).copied())
|
||||
.filter_map(|txid| prefix_to_idx.get(&TxidPrefix::from(txid)).copied())
|
||||
.collect();
|
||||
let excluded: FxHashSet<TxIndex> = block0.iter().copied().collect();
|
||||
let rest = Partitioner::partition(&txs, &excluded, NUM_BLOCKS.saturating_sub(1));
|
||||
|
||||
Reference in New Issue
Block a user