mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
global: reused + mempool + favicon
This commit is contained in:
@@ -45,13 +45,14 @@ pub fn linearize_clusters(graph: &Graph) -> Vec<Package> {
|
||||
let clusters = find_components(graph);
|
||||
let mut packages: Vec<Package> = Vec::with_capacity(clusters.len());
|
||||
|
||||
for cluster in clusters {
|
||||
for (cluster_id, cluster) in clusters.into_iter().enumerate() {
|
||||
let cluster_id = cluster_id as u32;
|
||||
if cluster.nodes.len() == 1 {
|
||||
packages.push(singleton_package(&cluster));
|
||||
packages.push(singleton_package(&cluster, cluster_id));
|
||||
continue;
|
||||
}
|
||||
for chunk in sfl::linearize(&cluster) {
|
||||
packages.push(chunk_to_package(&cluster, &chunk));
|
||||
for (chunk_order, chunk) in sfl::linearize(&cluster).iter().enumerate() {
|
||||
packages.push(chunk_to_package(&cluster, chunk, cluster_id, chunk_order as u32));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,19 +169,24 @@ fn kahn_topo_rank(nodes: &[ClusterNode]) -> Vec<u32> {
|
||||
}
|
||||
|
||||
/// Build a one-tx `Package` for a cluster of size 1.
|
||||
fn singleton_package(cluster: &Cluster) -> Package {
|
||||
fn singleton_package(cluster: &Cluster, cluster_id: u32) -> Package {
|
||||
let node = &cluster.nodes[0];
|
||||
let fee_rate = FeeRate::from((node.fee, node.vsize));
|
||||
let mut package = Package::new(fee_rate);
|
||||
let mut package = Package::new(fee_rate, cluster_id, 0);
|
||||
package.add_tx(node.tx_index, u64::from(node.vsize));
|
||||
package
|
||||
}
|
||||
|
||||
/// Convert an SFL-emitted chunk (set of local indices) into a `Package`.
|
||||
/// Txs inside the package are ordered parents-first by `topo_rank`.
|
||||
fn chunk_to_package(cluster: &Cluster, chunk: &sfl::Chunk) -> Package {
|
||||
fn chunk_to_package(
|
||||
cluster: &Cluster,
|
||||
chunk: &sfl::Chunk,
|
||||
cluster_id: u32,
|
||||
chunk_order: u32,
|
||||
) -> Package {
|
||||
let fee_rate = FeeRate::from((Sats::from(chunk.fee), VSize::from(chunk.vsize)));
|
||||
let mut package = Package::new(fee_rate);
|
||||
let mut package = Package::new(fee_rate, cluster_id, chunk_order);
|
||||
|
||||
let mut ordered: SmallVec<[LocalIdx; 8]> = chunk.nodes.iter().copied().collect();
|
||||
ordered.sort_by_key(|&local| cluster.topo_rank[local as usize]);
|
||||
|
||||
@@ -35,7 +35,11 @@ pub fn linearize(cluster: &Cluster) -> Vec<Chunk> {
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
assert!(n <= BITMASK_LIMIT, "cluster size {} exceeds u128 capacity", n);
|
||||
assert!(
|
||||
n <= BITMASK_LIMIT,
|
||||
"cluster size {} exceeds u128 capacity",
|
||||
n
|
||||
);
|
||||
|
||||
let mut parents_mask: Vec<u128> = vec![0; n];
|
||||
let mut ancestor_incl: Vec<u128> = vec![0; n];
|
||||
@@ -97,6 +101,7 @@ fn best_subset(
|
||||
best
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn recurse(
|
||||
idx: usize,
|
||||
topo_order: &[LocalIdx],
|
||||
@@ -120,18 +125,34 @@ fn recurse(
|
||||
|
||||
// Not in remaining, or a parent (within remaining) is excluded:
|
||||
// this node is forced-excluded, no branching.
|
||||
if (bit & remaining) == 0
|
||||
|| (parents_mask[node as usize] & remaining & !included) != 0
|
||||
{
|
||||
if (bit & remaining) == 0 || (parents_mask[node as usize] & remaining & !included) != 0 {
|
||||
recurse(
|
||||
idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best,
|
||||
idx + 1,
|
||||
topo_order,
|
||||
parents_mask,
|
||||
remaining,
|
||||
included,
|
||||
f,
|
||||
v,
|
||||
fee_of,
|
||||
vsize_of,
|
||||
best,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude
|
||||
recurse(
|
||||
idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best,
|
||||
idx + 1,
|
||||
topo_order,
|
||||
parents_mask,
|
||||
remaining,
|
||||
included,
|
||||
f,
|
||||
v,
|
||||
fee_of,
|
||||
vsize_of,
|
||||
best,
|
||||
);
|
||||
// Include
|
||||
recurse(
|
||||
|
||||
@@ -9,7 +9,7 @@ pub use package::Package;
|
||||
use crate::entry::Entry;
|
||||
|
||||
/// Target vsize per block (~1MB, derived from 4MW weight limit).
|
||||
const BLOCK_VSIZE: u64 = 1_000_000;
|
||||
pub(crate) const BLOCK_VSIZE: u64 = 1_000_000;
|
||||
|
||||
/// Number of projected blocks to build (last one is a catch-all overflow).
|
||||
const NUM_BLOCKS: usize = 8;
|
||||
|
||||
@@ -9,19 +9,27 @@ use crate::types::TxIndex;
|
||||
/// i.e. what a miner collects per vsize when the package is mined.
|
||||
/// Packages are produced by SFL in descending-`fee_rate` order within a
|
||||
/// cluster and are atomic (all-or-nothing) at mining time.
|
||||
///
|
||||
/// `cluster_id` + `chunk_order` let the partitioner enforce intra-cluster
|
||||
/// ordering when its look-ahead would otherwise pull a child chunk into
|
||||
/// an earlier block than its parent chunk.
|
||||
pub struct Package {
|
||||
/// Transactions in topological order (parents before children).
|
||||
pub txs: Vec<TxIndex>,
|
||||
pub vsize: u64,
|
||||
pub fee_rate: FeeRate,
|
||||
pub cluster_id: u32,
|
||||
pub chunk_order: u32,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn new(fee_rate: FeeRate) -> Self {
|
||||
pub fn new(fee_rate: FeeRate, cluster_id: u32, chunk_order: u32) -> Self {
|
||||
Self {
|
||||
txs: Vec::new(),
|
||||
vsize: 0,
|
||||
fee_rate,
|
||||
cluster_id,
|
||||
chunk_order,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,30 @@ const LOOK_AHEAD_COUNT: usize = 100;
|
||||
/// chunks. The final block is a catch-all containing every remaining
|
||||
/// package, so no low-rate tx is silently dropped from the projection
|
||||
/// (matches mempool.space's last-block behavior).
|
||||
///
|
||||
/// Look-ahead respects intra-cluster order: a chunk is only taken once
|
||||
/// every earlier-rate chunk of the same cluster has been placed, so a
|
||||
/// child chunk never lands in an earlier block than its parent chunk.
|
||||
pub fn partition_into_blocks(
|
||||
mut packages: Vec<Package>,
|
||||
num_blocks: usize,
|
||||
) -> Vec<Vec<Package>> {
|
||||
// Stable sort for deterministic output across equal fee rates. SFL
|
||||
// guarantees chunks within a cluster come in non-increasing rate
|
||||
// order, so stable sorting by fee_rate preserves intra-cluster
|
||||
// topology automatically.
|
||||
// Stable sort preserves SFL's per-cluster non-increasing-rate emission
|
||||
// order in the global list, which is what `cluster_next` relies on.
|
||||
packages.sort_by_key(|p| Reverse(p.fee_rate));
|
||||
|
||||
let num_clusters = packages
|
||||
.iter()
|
||||
.map(|p| p.cluster_id as usize + 1)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let mut cluster_next: Vec<u32> = vec![0; num_clusters];
|
||||
|
||||
let mut slots: Vec<Option<Package>> = packages.into_iter().map(Some).collect();
|
||||
let mut blocks: Vec<Vec<Package>> = Vec::with_capacity(num_blocks);
|
||||
let normal_blocks = num_blocks.saturating_sub(1);
|
||||
|
||||
let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks);
|
||||
let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks, &mut cluster_next);
|
||||
|
||||
if blocks.len() < num_blocks {
|
||||
let mut overflow: Vec<Package> = Vec::new();
|
||||
@@ -49,6 +58,7 @@ fn fill_normal_blocks(
|
||||
slots: &mut [Option<Package>],
|
||||
blocks: &mut Vec<Vec<Package>>,
|
||||
target_blocks: usize,
|
||||
cluster_next: &mut [u32],
|
||||
) -> usize {
|
||||
let mut current_block: Vec<Package> = Vec::new();
|
||||
let mut current_vsize: u64 = 0;
|
||||
@@ -63,9 +73,7 @@ fn fill_normal_blocks(
|
||||
let remaining_space = BLOCK_VSIZE.saturating_sub(current_vsize);
|
||||
|
||||
if pkg.vsize <= remaining_space {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
current_vsize += pkg.vsize;
|
||||
current_block.push(pkg);
|
||||
take(slots, idx, &mut current_block, &mut current_vsize, cluster_next);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -73,9 +81,7 @@ fn fill_normal_blocks(
|
||||
if current_block.is_empty() {
|
||||
// Oversized package with no partial block to preserve; take it
|
||||
// anyway so we don't stall on a package larger than BLOCK_VSIZE.
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
current_vsize += pkg.vsize;
|
||||
current_block.push(pkg);
|
||||
take(slots, idx, &mut current_block, &mut current_vsize, cluster_next);
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -86,6 +92,7 @@ fn fill_normal_blocks(
|
||||
remaining_space,
|
||||
&mut current_block,
|
||||
&mut current_vsize,
|
||||
cluster_next,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -102,23 +109,44 @@ fn fill_normal_blocks(
|
||||
}
|
||||
|
||||
/// Scan the look-ahead window for a package small enough to fit in the
|
||||
/// remaining space and move it into the current block.
|
||||
/// remaining space, skipping any candidate whose cluster has an earlier
|
||||
/// unplaced chunk (that chunk's parents would land after its children).
|
||||
fn try_fill_with_smaller(
|
||||
slots: &mut [Option<Package>],
|
||||
start: usize,
|
||||
remaining_space: u64,
|
||||
block: &mut Vec<Package>,
|
||||
block_vsize: &mut u64,
|
||||
cluster_next: &mut [u32],
|
||||
) -> bool {
|
||||
let end = (start + LOOK_AHEAD_COUNT).min(slots.len());
|
||||
for idx in (start + 1)..end {
|
||||
let Some(pkg) = &slots[idx] else { continue };
|
||||
if pkg.vsize <= remaining_space {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
*block_vsize += pkg.vsize;
|
||||
block.push(pkg);
|
||||
return true;
|
||||
if pkg.vsize > remaining_space {
|
||||
continue;
|
||||
}
|
||||
if pkg.chunk_order != cluster_next[pkg.cluster_id as usize] {
|
||||
continue;
|
||||
}
|
||||
take(slots, idx, block, block_vsize, cluster_next);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn take(
|
||||
slots: &mut [Option<Package>],
|
||||
idx: usize,
|
||||
block: &mut Vec<Package>,
|
||||
block_vsize: &mut u64,
|
||||
cluster_next: &mut [u32],
|
||||
) {
|
||||
let pkg = slots[idx].take().unwrap();
|
||||
debug_assert_eq!(
|
||||
pkg.chunk_order, cluster_next[pkg.cluster_id as usize],
|
||||
"partitioner took a chunk out of cluster order"
|
||||
);
|
||||
cluster_next[pkg.cluster_id as usize] = pkg.chunk_order + 1;
|
||||
*block_vsize += pkg.vsize;
|
||||
block.push(pkg);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod fees;
|
||||
mod snapshot;
|
||||
mod stats;
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) mod verify;
|
||||
|
||||
pub use brk_types::RecommendedFees;
|
||||
pub use snapshot::Snapshot;
|
||||
|
||||
149
crates/brk_mempool/src/projected_blocks/verify.rs
Normal file
149
crates/brk_mempool/src/projected_blocks/verify.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use brk_rpc::Client;
|
||||
use brk_types::{Sats, SatsSigned, TxidPrefix};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
block_builder::{BLOCK_VSIZE, Package},
|
||||
entry::Entry,
|
||||
types::TxIndex,
|
||||
};
|
||||
|
||||
type PrefixSet = FxHashSet<TxidPrefix>;
|
||||
type FeeByPrefix = FxHashMap<TxidPrefix, Sats>;
|
||||
|
||||
pub struct Verifier;
|
||||
|
||||
impl Verifier {
|
||||
pub fn check(client: &Client, blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
Self::check_structure(blocks, entries);
|
||||
Self::compare_to_core(client, blocks, entries);
|
||||
}
|
||||
|
||||
fn check_structure(blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
let in_pool: PrefixSet = entries
|
||||
.iter()
|
||||
.filter_map(|e| e.as_ref().map(Entry::txid_prefix))
|
||||
.collect();
|
||||
let mut placed = PrefixSet::default();
|
||||
|
||||
for (b, block) in blocks.iter().enumerate() {
|
||||
for (p, pkg) in block.iter().enumerate() {
|
||||
let mut summed_vsize = 0u64;
|
||||
for &tx_index in &pkg.txs {
|
||||
let entry = Self::live_entry(entries, tx_index, b, p);
|
||||
Self::assert_parents_placed_first(entry, &in_pool, &placed, b, p);
|
||||
Self::place(entry, &mut placed, b, p);
|
||||
summed_vsize += u64::from(entry.vsize);
|
||||
}
|
||||
assert_eq!(
|
||||
pkg.vsize, summed_vsize,
|
||||
"block {b} pkg {p}: pkg.vsize {} != sum {summed_vsize}",
|
||||
pkg.vsize
|
||||
);
|
||||
}
|
||||
if b + 1 < blocks.len() {
|
||||
Self::assert_block_fits_budget(block, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn live_entry<'e>(
|
||||
entries: &'e [Option<Entry>],
|
||||
tx_index: TxIndex,
|
||||
b: usize,
|
||||
p: usize,
|
||||
) -> &'e Entry {
|
||||
entries[tx_index.as_usize()]
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("block {b} pkg {p}: dead tx_index {tx_index:?}"))
|
||||
}
|
||||
|
||||
fn assert_parents_placed_first(
|
||||
entry: &Entry,
|
||||
in_pool: &PrefixSet,
|
||||
placed: &PrefixSet,
|
||||
b: usize,
|
||||
p: usize,
|
||||
) {
|
||||
for parent in &entry.depends {
|
||||
if in_pool.contains(parent) && !placed.contains(parent) {
|
||||
panic!(
|
||||
"block {b} pkg {p}: {} placed before its parent",
|
||||
entry.txid
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place(entry: &Entry, placed: &mut PrefixSet, b: usize, p: usize) {
|
||||
assert!(
|
||||
placed.insert(entry.txid_prefix()),
|
||||
"block {b} pkg {p}: duplicate txid {}",
|
||||
entry.txid
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_block_fits_budget(block: &[Package], b: usize) {
|
||||
let total: u64 = block.iter().map(|pkg| pkg.vsize).sum();
|
||||
let is_oversized_singleton = block.len() == 1 && total > BLOCK_VSIZE;
|
||||
if is_oversized_singleton {
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
total <= BLOCK_VSIZE,
|
||||
"block {b}: vsize {total} exceeds {BLOCK_VSIZE}"
|
||||
);
|
||||
}
|
||||
|
||||
fn compare_to_core(client: &Client, blocks: &[Vec<Package>], entries: &[Option<Entry>]) {
|
||||
let Some(next_block) = blocks.first() else {
|
||||
return;
|
||||
};
|
||||
let core: FeeByPrefix = match client.get_block_template_txs() {
|
||||
Ok(txs) => txs
|
||||
.into_iter()
|
||||
.map(|t| (TxidPrefix::from(&t.txid), t.fee))
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
warn!("verify: getblocktemplate failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let ours: FeeByPrefix = next_block
|
||||
.iter()
|
||||
.flat_map(|pkg| &pkg.txs)
|
||||
.filter_map(|&i| entries[i.as_usize()].as_ref())
|
||||
.map(|e| (e.txid_prefix(), e.fee))
|
||||
.collect();
|
||||
|
||||
let overlap = ours.keys().filter(|k| core.contains_key(k)).count();
|
||||
let union = ours.len() + core.len() - overlap;
|
||||
let jaccard = if union == 0 {
|
||||
1.0
|
||||
} else {
|
||||
overlap as f64 / union as f64
|
||||
};
|
||||
|
||||
let ours_fee: Sats = ours.values().copied().sum();
|
||||
let core_fee: Sats = core.values().copied().sum();
|
||||
let delta = SatsSigned::from(ours_fee) - SatsSigned::from(core_fee);
|
||||
let delta_bps = if core_fee == Sats::ZERO {
|
||||
0.0
|
||||
} else {
|
||||
f64::from(delta) / f64::from(core_fee) * 10_000.0
|
||||
};
|
||||
|
||||
debug!(
|
||||
"verify block 0: txs {}/{} (overlap {}, jaccard {:.3}) | fee {}/{} (delta {:+}, {:+.1} bps)",
|
||||
ours.len(),
|
||||
core.len(),
|
||||
overlap,
|
||||
jaccard,
|
||||
ours_fee,
|
||||
core_fee,
|
||||
delta.inner(),
|
||||
delta_bps,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -343,6 +343,10 @@ impl MempoolInner {
|
||||
let entries_slice = entries.entries();
|
||||
|
||||
let blocks = build_projected_blocks(entries_slice);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
crate::projected_blocks::verify::Verifier::check(&self.client, &blocks, entries_slice);
|
||||
|
||||
let snapshot = Snapshot::build(blocks, entries_slice);
|
||||
|
||||
*self.snapshot.write() = snapshot;
|
||||
|
||||
Reference in New Issue
Block a user