use brk_types::{
BlockTemplate, BlockTemplateDiff, BlockTemplateDiffEntry, MempoolBlock, NextBlockHash,
Transaction, Txid,
};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::warn;
use crate::{Mempool, state::State};
impl Mempool {
pub fn next_block_hash(&self) -> NextBlockHash {
self.snapshot().next_block_hash
}
#[must_use]
pub fn block_template(&self) -> BlockTemplate {
let snap = self.snapshot();
BlockTemplate {
hash: snap.next_block_hash,
stats: snap
.block_stats
.first()
.map(MempoolBlock::from)
.unwrap_or_default(),
transactions: self.collect_txs(snap.block0_txids()),
}
}
#[must_use]
pub fn block_template_diff(&self, since: NextBlockHash) -> Option<BlockTemplateDiff> {
let past = self.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 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 => match Self::lookup_body(&state, &txid) {
Some(tx) => order.push(BlockTemplateDiffEntry::New(tx)),
None => warn!(?txid, "block_template_diff: snapshot tx body missing"),
},
}
}
drop(state);
let removed = past.into_iter().filter(|t| !current.contains(t)).collect();
Some(BlockTemplateDiff {
hash: snap.next_block_hash,
since,
order,
removed,
})
}
fn collect_txs(&self, txids: impl IntoIterator<Item = Txid>) -> Vec<Transaction> {
let state = self.read();
txids
.into_iter()
.filter_map(|txid| {
let body = Self::lookup_body(&state, &txid);
if body.is_none() {
warn!(?txid, "block_template: snapshot tx body missing");
}
body
})
.collect()
}
fn lookup_body(state: &State, txid: &Txid) -> Option<Transaction> {
state
.txs
.get(txid)
.or_else(|| state.graveyard.get(txid).map(|t| &t.tx))
.cloned()
}
}
#[cfg(test)]
mod tests {
use brk_types::{BlockTemplateDiffEntry, FeeRate};
use super::*;
use crate::{
state::TxEntry,
test_support::{fake_entry_info, fake_tx, p2wpkh_script},
};
fn insert_tx(mempool: &Mempool, seed: u8, fee: u64, vsize: u64) -> Txid {
let tx = fake_tx(seed, &[None], &[(p2wpkh_script(seed + 1), 1_234)]);
let txid = tx.txid;
let info = fake_entry_info(txid, fee, vsize);
let entry = TxEntry::new(&info, vsize, false);
let mut state = mempool.test_state_lock().write();
state.txs.insert(tx, entry);
txid
}
#[test]
fn block_template_hash_matches_next_block_hash() {
let mempool = Mempool::for_test();
let txid = insert_tx(&mempool, 0xA0, 1_234, 100);
mempool.test_tick(&[txid], FeeRate::new(1.0));
let template = mempool.block_template();
assert_eq!(template.hash, mempool.next_block_hash());
assert_eq!(template.transactions.len(), 1);
assert_eq!(template.transactions[0].txid, txid);
}
#[test]
fn block_template_diff_round_trip_reconstructs_t1_from_t0() {
let mempool = Mempool::for_test();
let txid_a = insert_tx(&mempool, 0xA1, 1_111, 100);
let txid_b = insert_tx(&mempool, 0xA2, 2_222, 100);
mempool.test_tick(&[txid_a, txid_b], FeeRate::new(1.0));
let t0 = mempool.block_template();
let txid_c = insert_tx(&mempool, 0xA3, 3_333, 100);
mempool.test_tick(&[txid_a, txid_b, txid_c], FeeRate::new(1.0));
let t1 = mempool.block_template();
let diff = mempool
.block_template_diff(t0.hash)
.expect("t0 is still in history");
assert_eq!(diff.since, t0.hash);
assert_eq!(diff.hash, t1.hash);
let mut reconstructed = Vec::with_capacity(diff.order.len());
for entry in &diff.order {
match entry {
BlockTemplateDiffEntry::Retained(idx) => {
reconstructed.push(t0.transactions[*idx as usize].clone());
}
BlockTemplateDiffEntry::New(tx) => reconstructed.push(tx.clone()),
}
}
let expected: Vec<_> = t1.transactions.iter().map(|tx| tx.txid).collect();
let got: Vec<_> = reconstructed.iter().map(|tx| tx.txid).collect();
assert_eq!(got, expected, "diff round-trips back into T1 ordering");
assert!(diff.removed.is_empty());
}
#[test]
fn block_template_diff_removed_lists_evicted_txs() {
let mempool = Mempool::for_test();
let txid_a = insert_tx(&mempool, 0xA4, 1_111, 100);
let txid_b = insert_tx(&mempool, 0xA5, 2_222, 100);
mempool.test_tick(&[txid_a, txid_b], FeeRate::new(1.0));
let t0 = mempool.block_template();
mempool.test_tick(&[txid_b], FeeRate::new(1.0));
let diff = mempool.block_template_diff(t0.hash).unwrap();
assert_eq!(diff.removed, vec![txid_a]);
}
#[test]
fn block_template_diff_unknown_since_returns_none() {
let mempool = Mempool::for_test();
mempool.test_tick(&[], FeeRate::new(1.0));
let bogus = NextBlockHash::new(0xDEAD_BEEF);
assert!(mempool.block_template_diff(bogus).is_none());
}
#[test]
fn block_template_empty_pool_has_no_transactions() {
let mempool = Mempool::for_test();
mempool.test_tick(&[], FeeRate::new(2.0));
let template = mempool.block_template();
assert!(template.transactions.is_empty());
}
}