global: reused + mempool + favicon

This commit is contained in:
nym21
2026-04-23 23:13:39 +02:00
parent ce00de5da8
commit e4496742a4
77 changed files with 2631 additions and 1624 deletions

View File

@@ -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]);

View File

@@ -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(

View File

@@ -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;

View File

@@ -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,
}
}

View File

@@ -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);
}

View File

@@ -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;

View 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,
);
}
}

View File

@@ -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;