mempool: fixes

This commit is contained in:
nym21
2026-05-13 18:36:02 +02:00
parent 5cc3fbfa6e
commit 528c134f26
12 changed files with 669 additions and 30 deletions

View File

@@ -9840,7 +9840,7 @@ impl BrkClient {
/// Block template diff since hash
///
/// Delta of the projected next block since `<hash>`. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.
/// Delta of the projected next block since `<hash>`. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at `<hash>`) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `<hash>` on the next call to keep iterating. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.
///
/// Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}`
pub fn get_block_template_diff(&self, hash: NextBlockHash) -> Result<BlockTemplateDiff> {

View File

@@ -35,9 +35,9 @@ use brk_error::Result;
use brk_oracle::Histogram;
use brk_rpc::Client;
use brk_types::{
AddrBytes, AddrMempoolStats, BlockTemplate, BlockTemplateDiff, FeeRate, MempoolBlock,
MempoolInfo, MempoolRecentTx, NextBlockHash, OutpointPrefix, Timestamp, Transaction, TxOut,
Txid, TxidPrefix, Vin, Vout,
AddrBytes, AddrMempoolStats, BlockTemplate, BlockTemplateDiff, BlockTemplateDiffEntry, FeeRate,
MempoolBlock, MempoolInfo, MempoolRecentTx, NextBlockHash, OutpointPrefix, Timestamp,
Transaction, TxOut, Txid, TxidPrefix, Vin, Vout,
};
use parking_lot::{RwLock, RwLockReadGuard};
use rustc_hash::{FxHashMap, FxHashSet};
@@ -138,18 +138,41 @@ impl Mempool {
/// Delta of the projected next block since `since`. `None` when
/// `since` has aged out of the rebuilder's history (server should
/// 404 → client falls back to `block_template`). `removed` is just
/// txids; `added` carries full bodies so clients can patch their
/// local view in one round trip.
/// 404 → client falls back to `block_template`).
///
/// `order` walks the new template in template order; each entry is
/// either a `Retained` index into the prior template (which the
/// client cached when it obtained `since`) or a `New` inline body.
/// `removed` is the convenience list of txids that left.
pub fn block_template_diff(&self, since: NextBlockHash) -> Option<BlockTemplateDiff> {
let past = self.0.rebuilder.historical_block0(since)?;
let prior_index: FxHashMap<Txid, u32> = past
.iter()
.enumerate()
.map(|(idx, txid)| (*txid, idx as u32))
.collect();
let snap = self.snapshot();
let current: FxHashSet<Txid> = snap.block0_txids().collect();
let state = self.read();
let mut order = Vec::with_capacity(snap.blocks.first().map_or(0, Vec::len));
let mut current: FxHashSet<Txid> = FxHashSet::default();
for txid in snap.block0_txids() {
current.insert(txid);
match prior_index.get(&txid) {
Some(&idx) => order.push(BlockTemplateDiffEntry::Retained(idx)),
None => {
let tx = Self::lookup_body(&state, &txid)
.expect("snapshot tx body must be in txs or graveyard");
order.push(BlockTemplateDiffEntry::New(tx));
}
}
}
drop(state);
let removed = past.into_iter().filter(|t| !current.contains(t)).collect();
Some(BlockTemplateDiff {
hash: snap.next_block_hash,
since,
added: self.collect_txs(current.difference(&past).copied()),
removed: past.difference(&current).copied().collect(),
order,
removed,
})
}
@@ -157,10 +180,25 @@ impl Mempool {
let state = self.read();
txids
.into_iter()
.filter_map(|txid| state.txs.get(&txid).cloned())
.map(|txid| {
Self::lookup_body(&state, &txid)
.expect("snapshot tx body must be in txs or graveyard")
})
.collect()
}
/// Body for a txid in a published snapshot. Graveyard fallback
/// covers the eviction race: an Applier may have buried the tx
/// after the snapshot was built. Burial retention (1h) >> snapshot
/// cycle (~1s), so reachability is guaranteed.
fn lookup_body(state: &State, txid: &Txid) -> Option<Transaction> {
state
.txs
.get(txid)
.or_else(|| state.graveyard.get(txid).map(|t| &t.tx))
.cloned()
}
pub fn addr_state_hash(&self, addr: &AddrBytes) -> u64 {
self.read().addrs.stats_hash(addr)
}

View File

@@ -27,8 +27,10 @@ const HISTORY: usize = 10;
#[derive(Default)]
pub struct Rebuilder {
snapshot: RwLock<Arc<Snapshot>>,
/// Past block-0 txid sets keyed by `next_block_hash`, oldest first.
history: RwLock<VecDeque<(NextBlockHash, FxHashSet<Txid>)>>,
/// Past block-0 txid lists keyed by `next_block_hash`, oldest first.
/// Ordered so `block_template_diff` can emit `Retained(prior_index)`
/// entries that line up with the client's cached prior template.
history: RwLock<VecDeque<(NextBlockHash, Vec<Txid>)>>,
rebuild_count: AtomicU64,
}
@@ -40,13 +42,13 @@ impl Rebuilder {
/// is the driver loop's job.
pub fn tick(&self, lock: &RwLock<State>, gbt_txids: &[Txid], min_fee: FeeRate) {
let snap = Self::build_snapshot(lock, gbt_txids, min_fee);
let block0_set: FxHashSet<Txid> = snap.block0_txids().collect();
let block0: Vec<Txid> = snap.block0_txids().collect();
let next_hash = snap.next_block_hash;
*self.snapshot.write() = Arc::new(snap);
let mut hist = self.history.write();
hist.retain(|(h, _)| *h != next_hash);
hist.push_back((next_hash, block0_set));
hist.push_back((next_hash, block0));
while hist.len() > HISTORY {
hist.pop_front();
}
@@ -55,15 +57,15 @@ impl Rebuilder {
self.rebuild_count.fetch_add(1, Ordering::Relaxed);
}
/// Past block-0 txid set for `hash`, or `None` if it has aged out
/// (or was never seen). Used by `block_template_diff` to decide
/// 200 vs 404.
pub fn historical_block0(&self, hash: NextBlockHash) -> Option<FxHashSet<Txid>> {
/// Past block-0 ordered txid list for `hash`, or `None` if it has
/// aged out (or was never seen). Used by `block_template_diff` to
/// decide 200 vs 404 and to resolve `Retained(prior_index)` entries.
pub fn historical_block0(&self, hash: NextBlockHash) -> Option<Vec<Txid>> {
self.history
.read()
.iter()
.find(|(h, _)| *h == hash)
.map(|(_, set)| set.clone())
.map(|(_, block0)| block0.clone())
}
pub fn rebuild_count(&self) -> u64 {

View File

@@ -177,7 +177,7 @@ impl MempoolRoutes for ApiRouter<AppState> {
op.id("get_block_template_diff")
.mempool_tag()
.summary("Block template diff since hash")
.description("Delta of the projected next block since `<hash>`. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.")
.description("Delta of the projected next block since `<hash>`. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at `<hash>`) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `<hash>` on the next call to keep iterating. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.")
.json_response::<BlockTemplateDiff>()
.not_modified()
.not_found()

View File

@@ -3,7 +3,7 @@ use serde::Deserialize;
use brk_types::NextBlockHash;
/// `since` hash for `/api/v1/mining/block-template/diff/{hash}`.
/// `since` hash for `/api/v1/mempool/block-template/diff/{hash}`.
#[derive(Deserialize, JsonSchema)]
pub struct NextBlockHashParam {
pub hash: NextBlockHash,

View File

@@ -5,12 +5,12 @@ use crate::{MempoolBlock, NextBlockHash, Transaction};
/// Projected next-block contents from Bitcoin Core's `getblocktemplate`
/// (block 0 of the snapshot). Returned by
/// `GET /api/v1/mining/block-template`.
/// `GET /api/v1/mempool/block-template`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockTemplate {
/// Pass back as `<hash>` on
/// `/api/v1/mining/block-template/diff/{hash}` to fetch deltas.
/// `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas.
pub hash: NextBlockHash,
/// Aggregate stats for this block (size, vsize, fee range, ...).

View File

@@ -1,11 +1,20 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{NextBlockHash, Transaction, Txid};
use crate::{BlockTemplateDiffEntry, NextBlockHash, Txid};
/// Delta between the current `getblocktemplate` projection and a prior
/// one identified by `since`. Returned by
/// `GET /api/v1/mining/block-template/diff/{hash}`.
/// `GET /api/v1/mempool/block-template/diff/{hash}`.
///
/// `order` carries the full new template in template order: each entry
/// is either a `Retained(idx)` pointing into the prior template (which
/// the client cached at `since`) or a `New(tx)` inline body. Walk it
/// once to rebuild the new template; no separate `added` array to
/// cross-reference.
///
/// `removed` is redundant (computable from `order` by collecting prior
/// indices that don't appear) but shipped for cache-eviction ergonomics.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockTemplateDiff {
@@ -15,9 +24,9 @@ pub struct BlockTemplateDiff {
/// Echoed prior hash the diff was computed against.
pub since: NextBlockHash,
/// Full bodies of transactions that joined the projected next
/// block since `since`.
pub added: Vec<Transaction>,
/// New template in order. Each entry is either an index into the
/// prior template's transactions or a full transaction body.
pub order: Vec<BlockTemplateDiffEntry>,
/// Txids that left the projected next block since `since`
/// (confirmed, evicted, replaced, or pushed past block 0).

View File

@@ -0,0 +1,22 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::Transaction;
/// One slot of the new template in a `BlockTemplateDiff`.
///
/// Untagged on the wire so JSON type disambiguates the variants:
/// - `Retained(idx)` serializes as a bare integer - index into the
/// transactions of the prior template (which the client cached at
/// `since`).
/// - `New(tx)` serializes as a transaction object - a body that was
/// not in the prior template and must be added at this position.
///
/// Reconstruction is a single pass: for each entry, either copy
/// `prior[idx]` or append the inline body.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum BlockTemplateDiffEntry {
Retained(u32),
New(Transaction),
}

View File

@@ -82,6 +82,7 @@ mod index_info;
mod limit;
mod block_template;
mod block_template_diff;
mod block_template_diff_entry;
mod mempool_block;
mod mempool_entry_info;
mod mempool_info;
@@ -230,6 +231,7 @@ pub use block_sizes_weights::*;
pub use block_status::*;
pub use block_template::*;
pub use block_template_diff::*;
pub use block_template_diff_entry::*;
pub use block_timestamp::*;
pub use block_tx_index::*;
pub use block_weight_entry::*;

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
/// Content hash of the projected next block (block 0 of the mempool
/// snapshot). Same value as the mempool ETag. Opaque token: pass back
/// as `since` on `/api/v1/mining/block-template/diff/{hash}` to fetch
/// as `since` on `/api/v1/mempool/block-template/diff/{hash}` to fetch
/// deltas.
#[derive(
Debug,