global: next block template (+ diff)

This commit is contained in:
nym21
2026-05-09 12:56:11 +02:00
parent 3f2b5d3084
commit e62b0ac2a5
20 changed files with 637 additions and 203 deletions

View File

@@ -30,9 +30,11 @@ use std::{
use brk_error::Result;
use brk_rpc::Client;
use brk_types::{
AddrBytes, AddrMempoolStats, FeeRate, MempoolInfo, MempoolRecentTx, OutpointPrefix, OutputType,
Sats, Timestamp, Transaction, TxOut, Txid, TxidPrefix, Vin, Vout,
AddrBytes, AddrMempoolStats, BlockTemplate, BlockTemplateDiff, FeeRate, MempoolBlock,
MempoolInfo, MempoolRecentTx, NextBlockHash, OutpointPrefix, OutputType, Sats, Timestamp,
Transaction, TxOut, Txid, TxidPrefix, Vin, Vout,
};
use rustc_hash::FxHashSet;
use parking_lot::{RwLock, RwLockReadGuard};
use tracing::error;
@@ -102,10 +104,58 @@ impl Mempool {
self.snapshot().block_stats.clone()
}
pub fn next_block_hash(&self) -> u64 {
pub fn next_block_hash(&self) -> NextBlockHash {
self.snapshot().next_block_hash
}
/// Full projected next block: Core's `getblocktemplate` selection
/// (block 0) with aggregate stats and full tx bodies in GBT order.
pub fn block_template(&self) -> BlockTemplate {
let snap = self.snapshot();
let stats = MempoolBlock::from(&snap.block_stats[0]);
let txids: Vec<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let transactions = self.collect_txs(&txids);
BlockTemplate {
hash: snap.next_block_hash,
stats,
transactions,
}
}
/// 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.
pub fn block_template_diff(&self, since: NextBlockHash) -> Option<BlockTemplateDiff> {
let past = self.0.rebuilder.historical_block0(since)?;
let snap = self.snapshot();
let current: FxHashSet<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let added_txids: Vec<Txid> = current.difference(&past).copied().collect();
let removed: Vec<Txid> = past.difference(&current).copied().collect();
let added = self.collect_txs(&added_txids);
Some(BlockTemplateDiff {
hash: snap.next_block_hash,
since,
added,
removed,
})
}
fn collect_txs(&self, txids: &[Txid]) -> Vec<Transaction> {
let state = self.read();
txids
.iter()
.filter_map(|txid| state.txs.get(txid).cloned())
.collect()
}
pub fn addr_state_hash(&self, addr: &AddrBytes) -> u64 {
self.read().addrs.stats_hash(addr)
}

View File

@@ -1,10 +1,13 @@
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
use std::{
collections::VecDeque,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use brk_rpc::BlockTemplateTx;
use brk_types::{FeeRate, TxidPrefix};
use brk_types::{FeeRate, NextBlockHash, Txid, TxidPrefix};
use parking_lot::RwLock;
use rustc_hash::FxHashSet;
@@ -20,10 +23,13 @@ pub use brk_types::RecommendedFees;
pub use snapshot::{BlockStats, SnapTx, Snapshot, TxIndex};
const NUM_BLOCKS: usize = 8;
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>)>>,
dirty: AtomicBool,
rebuild_count: AtomicU64,
skip_clean: AtomicU64,
@@ -49,11 +55,38 @@ impl Rebuilder {
self.skip_clean.fetch_add(1, Ordering::Relaxed);
return;
}
*self.snapshot.write() = Arc::new(Self::build_snapshot(lock, gbt, min_fee));
let snap = Self::build_snapshot(lock, gbt, min_fee);
let block0_set: FxHashSet<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let next_hash = snap.next_block_hash;
*self.snapshot.write() = Arc::new(snap);
self.push_history(next_hash, block0_set);
self.dirty.store(false, Ordering::Release);
self.rebuild_count.fetch_add(1, Ordering::Relaxed);
}
fn push_history(&self, hash: NextBlockHash, set: FxHashSet<Txid>) {
let mut hist = self.history.write();
hist.retain(|(h, _)| *h != hash);
hist.push_back((hash, set));
while hist.len() > HISTORY {
hist.pop_front();
}
}
/// 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>> {
self.history
.read()
.iter()
.find(|(h, _)| *h == hash)
.map(|(_, set)| set.clone())
}
pub fn rebuild_count(&self) -> u64 {
self.rebuild_count.load(Ordering::Relaxed)
}

View File

@@ -11,7 +11,7 @@ pub use tx_index::TxIndex;
use std::hash::{DefaultHasher, Hash, Hasher};
use brk_types::{FeeRate, RecommendedFees, TxidPrefix};
use brk_types::{FeeRate, NextBlockHash, RecommendedFees, TxidPrefix};
use fees::Fees;
@@ -30,7 +30,7 @@ pub struct Snapshot {
pub fees: RecommendedFees,
/// Content hash of the projected next block. Same value as the
/// mempool ETag.
pub next_block_hash: u64,
pub next_block_hash: NextBlockHash,
/// Per-snapshot `TxidPrefix -> TxIndex` index, so live queries can
/// resolve a prefix to the snapshot's compact index without
/// re-walking `txs`. Built once by `builder::build_txs` and reused
@@ -70,13 +70,13 @@ impl Snapshot {
}
}
fn hash_next_block(blocks: &[Vec<TxIndex>]) -> u64 {
fn hash_next_block(blocks: &[Vec<TxIndex>]) -> NextBlockHash {
let Some(block) = blocks.first() else {
return 0;
return NextBlockHash::ZERO;
};
let mut hasher = DefaultHasher::new();
block.hash(&mut hasher);
hasher.finish()
NextBlockHash::new(hasher.finish())
}
pub fn tx(&self, idx: TxIndex) -> Option<&SnapTx> {

View File

@@ -1,4 +1,4 @@
use brk_types::{FeeRate, Sats, VSize, get_weighted_percentile};
use brk_types::{FeeRate, MempoolBlock, Sats, VSize, get_weighted_percentile};
use super::{SnapTx, TxIndex};
@@ -83,3 +83,9 @@ impl BlockStats {
self.fee_range[3]
}
}
impl From<&BlockStats> for MempoolBlock {
fn from(s: &BlockStats) -> Self {
Self::new(s.tx_count, s.total_size, s.total_vsize, s.total_fee, s.fee_range)
}
}