diff --git a/Cargo.lock b/Cargo.lock index e09b8e4c6..21f6b5518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1979,13 +1979,12 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "bc89deee4af0429081d2a518c0431ae068222a5a262a3bc6ff4d8535ec2e02fe" dependencies = [ "cc", "cty", - "libc", ] [[package]] @@ -2101,9 +2100,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "aca3c01a711f395b4257b81674c0e90e8dd1f1e62c4b7db45f684cc7a4fcb18a" dependencies = [ "libmimalloc-sys", ] @@ -3450,11 +3449,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3463,7 +3462,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -3766,6 +3765,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/crates/brk_alloc/Cargo.toml b/crates/brk_alloc/Cargo.toml index 4f6d1cc11..67ea60168 100644 --- a/crates/brk_alloc/Cargo.toml +++ b/crates/brk_alloc/Cargo.toml @@ -8,5 +8,5 @@ homepage.workspace = true repository.workspace = true [dependencies] -libmimalloc-sys = { version = "0.1.44", features = ["extended"] } -mimalloc = { version = "0.1.48", features = ["v3"] } +libmimalloc-sys = { version = "0.1.46", features = ["extended"] } +mimalloc = { version = "0.1.49" } diff --git a/crates/brk_mempool/src/block_builder/heap_entry.rs b/crates/brk_mempool/src/block_builder/heap_entry.rs index 3347ca754..38f3b72b3 100644 --- a/crates/brk_mempool/src/block_builder/heap_entry.rs +++ b/crates/brk_mempool/src/block_builder/heap_entry.rs @@ -7,10 +7,13 @@ use crate::types::PoolIndex; /// Entry in the priority heap for transaction selection. /// -/// Stores a snapshot of the score at insertion time. +/// Stores a snapshot of the score at insertion time. The `generation` field +/// lets the selector detect and skip stale entries after descendants are +/// re-pushed with updated ancestor totals. #[derive(Clone, Copy)] pub struct HeapEntry { pub pool_index: PoolIndex, + pub generation: u32, ancestor_fee: Sats, ancestor_vsize: VSize, } @@ -19,6 +22,7 @@ impl HeapEntry { pub fn new(node: &TxNode) -> Self { Self { pool_index: node.pool_index, + generation: node.generation, ancestor_fee: node.ancestor_fee, ancestor_vsize: node.ancestor_vsize, } @@ -39,7 +43,7 @@ impl HeapEntry { impl PartialEq for HeapEntry { fn eq(&self, other: &Self) -> bool { - self.pool_index == other.pool_index + self.cmp(other).is_eq() } } diff --git a/crates/brk_mempool/src/block_builder/mod.rs b/crates/brk_mempool/src/block_builder/mod.rs index 5c11f060b..c09995c53 100644 --- a/crates/brk_mempool/src/block_builder/mod.rs +++ b/crates/brk_mempool/src/block_builder/mod.rs @@ -5,28 +5,29 @@ mod partitioner; mod selector; mod tx_node; -use crate::{entry::Entry, types::SelectedTx}; +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; -/// Number of projected blocks to build. +/// Number of projected blocks to build (last one is a catch-all overflow). const NUM_BLOCKS: usize = 8; /// Build projected blocks from mempool entries. /// -/// Returns transactions grouped by projected block, sorted by fee rate. -pub fn build_projected_blocks(entries: &[Option]) -> Vec> { - // Build dependency graph +/// Returns packages grouped by projected block. Blocks 1 through +/// `NUM_BLOCKS - 1` are standard ~1MB blocks sorted by placement rate +/// descending; the final block is a catch-all containing every remaining +/// package (matches mempool.space behavior). +pub fn build_projected_blocks(entries: &[Option]) -> Vec> { let mut graph = graph::build_graph(entries); if graph.is_empty() { return Vec::new(); } - // Select transactions into packages - let packages = selector::select_packages(&mut graph, NUM_BLOCKS); - - // Partition packages into blocks + let packages = selector::select_packages(&mut graph); partitioner::partition_into_blocks(packages, NUM_BLOCKS) } diff --git a/crates/brk_mempool/src/block_builder/package.rs b/crates/brk_mempool/src/block_builder/package.rs index c84939adb..e8add93f3 100644 --- a/crates/brk_mempool/src/block_builder/package.rs +++ b/crates/brk_mempool/src/block_builder/package.rs @@ -1,20 +1,25 @@ use brk_types::FeeRate; -use crate::types::{SelectedTx, TxIndex}; +use crate::types::TxIndex; -/// A CPFP package - transactions that must be included together. +/// A CPFP package: transactions the selector decided to mine together +/// because a child pays for its parent. /// -/// When a child pays for its parent (CPFP), both must be in the same block. -/// The package fee rate is the combined rate of all transactions. +/// Carries two rates: +/// - `fee_rate` is the package's own rate (sum of fees / sum of vsizes), +/// i.e. what a miner collects per vsize when the package is mined. +/// Used for per-tx fee stats and user-facing recommendations. +/// - `placement_rate` is the key the partitioner sorts by. It's the own +/// rate clamped below by the `placement_rate` of any ancestor packages, +/// so that sorting packages by this rate descending keeps dependent +/// packages in topological order even when a child's own rate exceeds +/// its parent's (possible in branching CPFP). pub struct Package { - /// Transactions in topological order (parents before children) - pub txs: Vec, - - /// Combined vsize of all transactions + /// Transactions in topological order (parents before children). + pub txs: Vec, pub vsize: u64, - - /// Package fee rate pub fee_rate: FeeRate, + pub placement_rate: FeeRate, } impl Package { @@ -23,14 +28,12 @@ impl Package { txs: Vec::new(), vsize: 0, fee_rate, + placement_rate: fee_rate, } } pub fn add_tx(&mut self, tx_index: TxIndex, vsize: u64) { - self.txs.push(SelectedTx { - tx_index, - effective_fee_rate: self.fee_rate, - }); + self.txs.push(tx_index); self.vsize += vsize; } } diff --git a/crates/brk_mempool/src/block_builder/partitioner.rs b/crates/brk_mempool/src/block_builder/partitioner.rs index 06ff2c846..2b82cd1dd 100644 --- a/crates/brk_mempool/src/block_builder/partitioner.rs +++ b/crates/brk_mempool/src/block_builder/partitioner.rs @@ -1,103 +1,123 @@ use std::cmp::Reverse; use super::{BLOCK_VSIZE, package::Package}; -use crate::types::SelectedTx; -/// How many packages to look ahead when current doesn't fit. -const LOOK_AHEAD: usize = 100; +/// How many packages to look ahead when the current one doesn't fit. +const LOOK_AHEAD_COUNT: usize = 100; -/// Partition packages into blocks by fee rate. +/// Partition packages into blocks by placement rate. /// -/// Packages are sorted by fee rate descending, then placed into blocks. -/// When a package doesn't fit, we look ahead for smaller packages that do. -/// Atomic packages are never split across blocks. +/// The first `num_blocks - 1` blocks are packed greedily into ~`BLOCK_VSIZE` +/// 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). pub fn partition_into_blocks( mut packages: Vec, num_blocks: usize, -) -> Vec> { - packages.sort_unstable_by_key(|p| Reverse(p.fee_rate)); +) -> Vec> { + // Stable sort for deterministic output across equal placement rates. + // Topology across dependent packages is already enforced by the + // placement_rate cap in the selector. + packages.sort_by_key(|p| Reverse(p.placement_rate)); - let mut blocks: Vec> = Vec::with_capacity(num_blocks); - let mut current_block: Vec = Vec::new(); - let mut current_vsize: u64 = 0; - let mut used = vec![false; packages.len()]; + let mut slots: Vec> = packages.into_iter().map(Some).collect(); + let mut blocks: Vec> = Vec::with_capacity(num_blocks); + let normal_blocks = num_blocks.saturating_sub(1); - let mut idx = 0; - while idx < packages.len() && blocks.len() < num_blocks { - if used[idx] { + let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks); + + if blocks.len() < num_blocks { + let mut overflow: Vec = Vec::new(); + while idx < slots.len() { + if let Some(pkg) = slots[idx].take() { + overflow.push(pkg); + } idx += 1; - continue; } - - let remaining_space = BLOCK_VSIZE.saturating_sub(current_vsize); - let package = &packages[idx]; - - if package.vsize <= remaining_space { - current_block.extend(package.txs.iter().copied()); - current_vsize += package.vsize; - used[idx] = true; - idx += 1; - continue; + if !overflow.is_empty() { + blocks.push(overflow); } - - // Package doesn't fit - if current_block.is_empty() { - // Empty block: add oversized package anyway - current_block.extend(package.txs.iter().copied()); - current_vsize += package.vsize; - used[idx] = true; - idx += 1; - continue; - } - - // Look ahead for a smaller package that fits - let found_smaller = try_fill_with_smaller( - &packages, - &mut used, - idx, - remaining_space, - &mut current_block, - &mut current_vsize, - ); - - if !found_smaller { - // No package fits, finalize current block - blocks.push(std::mem::take(&mut current_block)); - current_vsize = 0; - } - } - - if !current_block.is_empty() && blocks.len() < num_blocks { - blocks.push(current_block); } blocks } -/// Try to find a smaller package in the look-ahead window that fits. -fn try_fill_with_smaller( - packages: &[Package], - used: &mut [bool], - start: usize, - remaining_space: u64, - block: &mut Vec, - block_vsize: &mut u64, -) -> bool { - let end = (start + LOOK_AHEAD).min(packages.len()); +/// Greedily pack packages into up to `target_blocks` chunks of `BLOCK_VSIZE`. +/// Returns the first `slots` index we stopped at. +fn fill_normal_blocks( + slots: &mut [Option], + blocks: &mut Vec>, + target_blocks: usize, +) -> usize { + let mut current_block: Vec = Vec::new(); + let mut current_vsize: u64 = 0; + let mut idx = 0; - for idx in (start + 1)..end { - if used[idx] { + while idx < slots.len() && blocks.len() < target_blocks { + let Some(pkg) = &slots[idx] else { + idx += 1; + continue; + }; + + 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); + idx += 1; continue; } - let package = &packages[idx]; - if package.vsize <= remaining_space { - block.extend(package.txs.iter().copied()); - *block_vsize += package.vsize; - used[idx] = true; + 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); + idx += 1; + continue; + } + + if try_fill_with_smaller( + slots, + idx, + remaining_space, + &mut current_block, + &mut current_vsize, + ) { + continue; + } + + blocks.push(std::mem::take(&mut current_block)); + current_vsize = 0; + } + + if !current_block.is_empty() && blocks.len() < target_blocks { + blocks.push(current_block); + } + + idx +} + +/// Scan the look-ahead window for a package small enough to fit in the +/// remaining space and move it into the current block. +fn try_fill_with_smaller( + slots: &mut [Option], + start: usize, + remaining_space: u64, + block: &mut Vec, + block_vsize: &mut u64, +) -> 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; } } - false } diff --git a/crates/brk_mempool/src/block_builder/selector.rs b/crates/brk_mempool/src/block_builder/selector.rs index 50bfbb20e..0eb9a7836 100644 --- a/crates/brk_mempool/src/block_builder/selector.rs +++ b/crates/brk_mempool/src/block_builder/selector.rs @@ -4,53 +4,61 @@ use brk_types::FeeRate; use rustc_hash::FxHashSet; use smallvec::SmallVec; -use super::{BLOCK_VSIZE, graph::Graph, heap_entry::HeapEntry, package::Package}; +use super::{graph::Graph, heap_entry::HeapEntry, package::Package}; use crate::types::PoolIndex; -/// Select transactions from the graph and group into CPFP packages. -pub fn select_packages(graph: &mut Graph, num_blocks: usize) -> Vec { - let target_vsize = BLOCK_VSIZE * num_blocks as u64; - let mut total_vsize: u64 = 0; - let mut packages: Vec = Vec::new(); +/// Sentinel for `package_of` entries that haven't been placed in a package yet. +const UNASSIGNED: u32 = u32::MAX; + +/// Select transactions from the graph and group them into CPFP packages, +/// running until every unselected tx has been placed into a package. +pub fn select_packages(graph: &mut Graph) -> Vec { + let mut packages: Vec = Vec::new(); + let mut package_of: Vec = vec![UNASSIGNED; graph.len()]; - // Initialize heap with all transactions let mut heap: BinaryHeap = (0..graph.len()) .map(|i| HeapEntry::new(&graph[PoolIndex::from(i)])) .collect(); while let Some(entry) = heap.pop() { let node = &graph[entry.pool_index]; - - // Skip if already selected or entry is stale - if node.selected { + if node.selected || entry.generation != node.generation { continue; } - // Package fee rate at selection time - let package_rate = FeeRate::from((node.ancestor_fee, node.ancestor_vsize)); + let own_rate = FeeRate::from((node.ancestor_fee, node.ancestor_vsize)); + let package_idx = packages.len() as u32; + let mut package = Package::new(own_rate); - // Select this tx and all unselected ancestors (parents first) - let ancestors = select_with_ancestors(graph, entry.pool_index); + for pool_idx in select_with_ancestors(graph, entry.pool_index) { + let tx = &graph[pool_idx]; + package.add_tx(tx.tx_index, u64::from(tx.vsize)); + package_of[pool_idx.as_usize()] = package_idx; + + // Cap placement_rate by any ancestor packages this tx depends on. + // select_with_ancestors returns parents before children, so a + // parent sitting in this same package already has package_of + // set to package_idx; only parents in earlier packages matter. + for &parent in &tx.parents { + let parent_pkg = package_of[parent.as_usize()]; + if parent_pkg != package_idx && parent_pkg != UNASSIGNED { + package.placement_rate = package + .placement_rate + .min(packages[parent_pkg as usize].placement_rate); + } + } - let mut package = Package::new(package_rate); - for pool_idx in ancestors { - let vsize = u64::from(graph[pool_idx].vsize); - package.add_tx(graph[pool_idx].tx_index, vsize); update_descendants(graph, pool_idx, &mut heap); } - total_vsize += package.vsize; packages.push(package); - - if total_vsize >= target_vsize { - break; - } } packages } -/// Select a tx and all its unselected ancestors in topological order. +/// Return `pool_idx` and all its unselected ancestors in topological order +/// (parents before children), marking each one selected as we go. fn select_with_ancestors(graph: &mut Graph, pool_idx: PoolIndex) -> SmallVec<[PoolIndex; 8]> { let mut result: SmallVec<[PoolIndex; 8]> = SmallVec::new(); let mut stack: SmallVec<[(PoolIndex, bool); 16]> = smallvec::smallvec![(pool_idx, false)]; @@ -76,7 +84,8 @@ fn select_with_ancestors(graph: &mut Graph, pool_idx: PoolIndex) -> SmallVec<[Po result } -/// Update descendants' ancestor scores after selecting a tx. +/// Subtract the selected tx's fee and vsize from every unselected +/// descendant's ancestor totals, and re-push updated entries to the heap. fn update_descendants( graph: &mut Graph, selected_idx: PoolIndex, @@ -96,19 +105,17 @@ fn update_descendants( } let child = &mut graph[child_idx]; - if child.selected { - continue; + + // Walk through selected intermediates: descendants behind them still + // need their ancestor totals reduced, otherwise CPFP chains with + // already-selected parents keep inflated scores and get split. + if !child.selected { + child.ancestor_fee -= selected_fee; + child.ancestor_vsize -= selected_vsize; + child.generation += 1; + heap.push(HeapEntry::new(child)); } - // Update ancestor totals - child.ancestor_fee -= selected_fee; - child.ancestor_vsize -= selected_vsize; - - // Increment generation and re-push to heap - child.generation += 1; - heap.push(HeapEntry::new(child)); - - // Continue to grandchildren stack.extend(child.children.iter().copied()); } } diff --git a/crates/brk_mempool/src/entry.rs b/crates/brk_mempool/src/entry.rs index 968d4701b..671db5efd 100644 --- a/crates/brk_mempool/src/entry.rs +++ b/crates/brk_mempool/src/entry.rs @@ -1,4 +1,4 @@ -use brk_types::{FeeRate, MempoolEntryInfo, Sats, Timestamp, Txid, TxidPrefix, VSize}; +use brk_types::{FeeRate, Sats, Timestamp, Txid, TxidPrefix, VSize}; use smallvec::SmallVec; /// A mempool transaction entry. @@ -10,6 +10,8 @@ pub struct Entry { pub txid: Txid, pub fee: Sats, pub vsize: VSize, + /// Serialized tx size in bytes (witness + non-witness), from the raw tx. + pub size: u64, /// Pre-computed ancestor fee (self + all ancestors, no double-counting) pub ancestor_fee: Sats, /// Pre-computed ancestor vsize (self + all ancestors, no double-counting) @@ -21,18 +23,6 @@ pub struct Entry { } impl Entry { - pub fn from_info(info: &MempoolEntryInfo) -> Self { - Self { - txid: info.txid.clone(), - fee: info.fee, - vsize: VSize::from(info.vsize), - ancestor_fee: info.ancestor_fee, - ancestor_vsize: VSize::from(info.ancestor_size), - depends: info.depends.iter().map(TxidPrefix::from).collect(), - first_seen: Timestamp::now(), - } - } - #[inline] pub fn fee_rate(&self) -> FeeRate { FeeRate::from((self.fee, self.vsize)) diff --git a/crates/brk_mempool/src/projected_blocks/snapshot.rs b/crates/brk_mempool/src/projected_blocks/snapshot.rs index 812c489d3..8b9bca0e4 100644 --- a/crates/brk_mempool/src/projected_blocks/snapshot.rs +++ b/crates/brk_mempool/src/projected_blocks/snapshot.rs @@ -6,36 +6,31 @@ use super::{ fees, stats::{self, BlockStats}, }; -use crate::{ - entry::Entry, - types::{SelectedTx, TxIndex}, -}; +use crate::{block_builder::Package, entry::Entry, types::TxIndex}; /// Immutable snapshot of projected blocks. #[derive(Debug, Clone, Default)] pub struct Snapshot { - /// Block structure: indices into entries Vec + /// Block structure: indices into the mempool entries Vec, in the + /// order they'd appear in the block. pub blocks: Vec>, - /// Pre-computed stats per block pub block_stats: Vec, - /// Pre-computed fee recommendations pub fees: RecommendedFees, } impl Snapshot { - /// Build snapshot from selected transactions and entries. - pub fn build(blocks: Vec>, entries: &[Option]) -> Self { + /// Build a snapshot from packages grouped by projected block. + pub fn build(blocks: Vec>, entries: &[Option]) -> Self { let block_stats: Vec = blocks .iter() - .map(|selected| stats::compute_block_stats(selected, entries)) + .map(|block| stats::compute_block_stats(block, entries)) .collect(); let fees = fees::compute_recommended_fees(&block_stats); - // Extract just the indices from selected transactions let blocks = blocks .into_iter() - .map(|selected| selected.into_iter().map(|s| s.tx_index).collect()) + .map(|block| block.into_iter().flat_map(|pkg| pkg.txs).collect()) .collect(); Self { diff --git a/crates/brk_mempool/src/projected_blocks/stats.rs b/crates/brk_mempool/src/projected_blocks/stats.rs index 0723163b7..b94b8dc59 100644 --- a/crates/brk_mempool/src/projected_blocks/stats.rs +++ b/crates/brk_mempool/src/projected_blocks/stats.rs @@ -1,11 +1,13 @@ use brk_types::{FeeRate, Sats, VSize}; -use crate::{entry::Entry, types::SelectedTx}; +use crate::{block_builder::Package, entry::Entry}; /// Statistics for a single projected block. #[derive(Debug, Clone, Default)] pub struct BlockStats { pub tx_count: u32, + /// Total serialized size of all txs in bytes (witness + non-witness). + pub total_size: u64, pub total_vsize: VSize, pub total_fee: Sats, /// Fee rate percentiles: [0%, 10%, 25%, 50%, 75%, 90%, 100%] @@ -26,28 +28,36 @@ impl BlockStats { } } -/// Compute statistics for a single block using effective fee rates from selection time. -pub fn compute_block_stats(selected: &[SelectedTx], entries: &[Option]) -> BlockStats { - if selected.is_empty() { +/// Compute statistics for a single block. Each tx contributes its +/// containing package's `fee_rate` to the percentile distribution, +/// since that's the rate the miner collects per vsize. +pub fn compute_block_stats(block: &[Package], entries: &[Option]) -> BlockStats { + if block.is_empty() { return BlockStats::default(); } let mut total_fee = Sats::default(); let mut total_vsize = VSize::default(); - let mut fee_rates: Vec = Vec::with_capacity(selected.len()); + let mut total_size: u64 = 0; + let mut fee_rates: Vec = Vec::new(); - for sel in selected { - if let Some(entry) = &entries[sel.tx_index.as_usize()] { - total_fee += entry.fee; - total_vsize += entry.vsize; - fee_rates.push(sel.effective_fee_rate); + for pkg in block { + for &tx_index in &pkg.txs { + if let Some(entry) = &entries[tx_index.as_usize()] { + total_fee += entry.fee; + total_vsize += entry.vsize; + total_size += entry.size; + fee_rates.push(pkg.fee_rate); + } } } + let tx_count = fee_rates.len() as u32; fee_rates.sort_unstable(); BlockStats { - tx_count: selected.len() as u32, + tx_count, + total_size, total_vsize, total_fee, fee_range: [ diff --git a/crates/brk_mempool/src/sync.rs b/crates/brk_mempool/src/sync.rs index cb5345117..ec3711424 100644 --- a/crates/brk_mempool/src/sync.rs +++ b/crates/brk_mempool/src/sync.rs @@ -13,8 +13,8 @@ use bitcoin::hex::DisplayHex; use brk_error::Result; use brk_rpc::Client; use brk_types::{ - AddrBytes, BlockHash, MempoolEntryInfo, MempoolInfo, Transaction, TxIn, TxOut, TxStatus, - TxWithHex, Txid, TxidPrefix, Vout, + AddrBytes, BlockHash, MempoolEntryInfo, MempoolInfo, Timestamp, Transaction, TxIn, TxOut, + TxStatus, TxWithHex, Txid, TxidPrefix, VSize, Vout, }; use derive_more::Deref; use parking_lot::{RwLock, RwLockReadGuard}; @@ -286,7 +286,19 @@ impl MempoolInner { info.add(tx, entry_info.fee); addrs.add_tx(tx, txid); - entries.insert(prefix, Entry::from_info(entry_info)); + entries.insert( + prefix, + Entry { + txid: entry_info.txid.clone(), + fee: entry_info.fee, + vsize: VSize::from(entry_info.vsize), + size: tx.total_size as u64, + ancestor_fee: entry_info.ancestor_fee, + ancestor_vsize: VSize::from(entry_info.ancestor_size), + depends: entry_info.depends.iter().map(TxidPrefix::from).collect(), + first_seen: Timestamp::now(), + }, + ); } txs.extend(new_txs); diff --git a/crates/brk_mempool/src/types/mod.rs b/crates/brk_mempool/src/types/mod.rs index 523902c9f..0f72e9a01 100644 --- a/crates/brk_mempool/src/types/mod.rs +++ b/crates/brk_mempool/src/types/mod.rs @@ -1,7 +1,5 @@ mod pool_index; -mod selected_tx; mod tx_index; pub use pool_index::PoolIndex; -pub use selected_tx::SelectedTx; pub use tx_index::TxIndex; diff --git a/crates/brk_mempool/src/types/selected_tx.rs b/crates/brk_mempool/src/types/selected_tx.rs deleted file mode 100644 index 29c335093..000000000 --- a/crates/brk_mempool/src/types/selected_tx.rs +++ /dev/null @@ -1,12 +0,0 @@ -use brk_types::FeeRate; - -use super::TxIndex; - -/// A transaction selected for a projected block. -#[derive(Debug, Clone, Copy)] -pub struct SelectedTx { - /// Index into mempool entries - pub tx_index: TxIndex, - /// Fee rate at selection time (includes CPFP) - pub effective_fee_rate: FeeRate, -} diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index 8f6bc8193..74780bd92 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -36,6 +36,7 @@ impl Query { .map(|stats| { MempoolBlock::new( stats.tx_count, + stats.total_size, stats.total_vsize, stats.total_fee, stats.fee_range, diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index f7cb5886f..a072b5e21 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -9,12 +9,11 @@ use axum::{ }; use brk_traversable::TreeNode; use brk_types::{ - CostBasisFormatted, DataRangeFormat, Date, DetailedSeriesCount, Index, IndexInfo, - PaginatedSeries, Pagination, SearchQuery, SeriesData, SeriesInfo, SeriesList, SeriesName, - SeriesSelection, SeriesSelectionLegacy, + DataRangeFormat, DetailedSeriesCount, Index, IndexInfo, PaginatedSeries, Pagination, + SearchQuery, SeriesData, SeriesInfo, SeriesList, SeriesName, SeriesSelection, + SeriesSelectionLegacy, }; -use crate::params::{CostBasisCohortParam, CostBasisParams, CostBasisQuery}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -347,91 +346,6 @@ impl ApiMetricsLegacyRoutes for ApiRouter { .not_found(), ), ) - // --- Deprecated cost basis routes --- - .api_route( - "/api/metrics/cost-basis", - get_with( - async |uri: Uri, headers: HeaderMap, State(state): State| { - state - .cached_json(&headers, CacheStrategy::Static, &uri, |q| q.cost_basis_cohorts()) - .await - }, - |op| { - op.id("get_cost_basis_cohorts_deprecated") - .metrics_tag() - .deprecated() - .summary("Available cost basis cohorts (deprecated)") - .description( - "**DEPRECATED** - Use `/api/series/cost-basis` instead.\n\n\ - Sunset date: 2027-01-01." - ) - .json_response::>() - .server_error() - }, - ), - ) - .api_route( - "/api/metrics/cost-basis/{cohort}/dates", - get_with( - async |uri: Uri, - headers: HeaderMap, - Path(params): Path, - State(state): State| { - state - .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { - q.cost_basis_dates(¶ms.cohort) - }) - .await - }, - |op| { - op.id("get_cost_basis_dates_deprecated") - .metrics_tag() - .deprecated() - .summary("Available cost basis dates (deprecated)") - .description( - "**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/dates` instead.\n\n\ - Sunset date: 2027-01-01." - ) - .json_response::>() - .not_found() - .server_error() - }, - ), - ) - .api_route( - "/api/metrics/cost-basis/{cohort}/{date}", - get_with( - async |uri: Uri, - headers: HeaderMap, - Path(params): Path, - Query(query): Query, - State(state): State| { - state - .cached_json(&headers, CacheStrategy::Static, &uri, move |q| { - q.cost_basis_formatted( - ¶ms.cohort, - params.date, - query.bucket, - query.value, - ) - }) - .await - }, - |op| { - op.id("get_cost_basis_deprecated") - .metrics_tag() - .deprecated() - .summary("Cost basis distribution (deprecated)") - .description( - "**DEPRECATED** - Use `/api/series/cost-basis/{cohort}/{date}` instead.\n\n\ - Sunset date: 2027-01-01." - ) - .json_response::() - .not_found() - .server_error() - }, - ), - ) // --- Deprecated /api/vecs/ routes (moved from series module) --- .api_route( "/api/vecs/{variant}", diff --git a/crates/brk_server/src/api/series/mod.rs b/crates/brk_server/src/api/series/mod.rs index 883a1c5d9..f8d705f5b 100644 --- a/crates/brk_server/src/api/series/mod.rs +++ b/crates/brk_server/src/api/series/mod.rs @@ -10,7 +10,7 @@ use axum::{ use brk_traversable::TreeNode; use brk_types::{ CostBasisFormatted, DataRangeFormat, Date, IndexInfo, PaginatedSeries, Pagination, SearchQuery, - SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesSelection, + SeriesCount, SeriesData, SeriesInfo, SeriesNameWithIndex, SeriesSelection, Version, }; use crate::{ @@ -383,8 +383,9 @@ impl ApiSeriesRoutes for ApiRouter { Path(params): Path, Query(query): Query, State(state): State| { + let strategy = state.date_cache(Version::ONE, params.date); state - .cached_json(&headers, CacheStrategy::Static, &uri, move |q| { + .cached_json(&headers, strategy, &uri, move |q| { q.cost_basis_formatted( ¶ms.cohort, params.date, diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index 047366a65..76cc1222c 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -14,13 +14,14 @@ use axum::{ }; use brk_query::AsyncQuery; use brk_types::{ - Addr, BlockHash, BlockHashPrefix, Height, ONE_HOUR_IN_SEC, Timestamp as BrkTimestamp, Txid, - Version, + Addr, BlockHash, BlockHashPrefix, Date, Height, ONE_HOUR_IN_SEC, Timestamp as BrkTimestamp, + Txid, Version, }; use derive_more::Deref; use jiff::Timestamp; use quick_cache::sync::{Cache, GuardResult}; use serde::Serialize; +use vecdb::ReadableVec; use crate::{ CacheParams, CacheStrategy, Error, Website, @@ -59,6 +60,28 @@ impl AppState { } } + /// `Immutable` if `date` is strictly before the indexed tip's date, `Tip` otherwise. + /// For per-date files that keep being rewritten while the tip is still within the + /// date's day, then settle once the tip crosses the day boundary. + pub fn date_cache(&self, version: Version, date: Date) -> CacheStrategy { + self.sync(|q| { + let height = q.indexed_height(); + q.indexer() + .vecs + .blocks + .timestamp + .collect_one(height) + .map(|ts| { + if date < Date::from(ts) { + CacheStrategy::Immutable(version) + } else { + CacheStrategy::Tip + } + }) + .unwrap_or(CacheStrategy::Tip) + }) + } + /// Smart address caching: checks mempool activity first (unless `chain_only`), then on-chain. /// - Address has mempool txs → `MempoolHash(addr_specific_hash)` /// - No mempool, has on-chain activity → `BlockBound(last_activity_block)` diff --git a/crates/brk_types/src/mempool_block.rs b/crates/brk_types/src/mempool_block.rs index ff86837c7..b0943ef68 100644 --- a/crates/brk_types/src/mempool_block.rs +++ b/crates/brk_types/src/mempool_block.rs @@ -7,8 +7,8 @@ use crate::{FeeRate, Sats, VSize}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MempoolBlock { - /// Total block size in weight units - #[schemars(example = 3993472)] + /// Total serialized block size in bytes (witness + non-witness). + #[schemars(example = 1604417)] pub block_size: u64, /// Total block virtual size in vbytes @@ -47,14 +47,14 @@ fn example_fee_range() -> [FeeRate; 7] { impl MempoolBlock { pub fn new( tx_count: u32, + total_size: u64, total_vsize: VSize, total_fee: Sats, fee_range: [FeeRate; 7], ) -> Self { - let vsize_f64 = *total_vsize as f64; Self { - block_size: *total_vsize * 4, // weight = vsize * 4 - block_v_size: vsize_f64, + block_size: total_size, + block_v_size: *total_vsize as f64, n_tx: tx_count, total_fees: total_fee, median_fee: fee_range[3], diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 021595806..942d9dd74 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -626,7 +626,7 @@ Matches mempool.space/bitcoin-cli behavior. * Block info in a mempool.space like format for fee estimation. * * @typedef {Object} MempoolBlock - * @property {number} blockSize - Total block size in weight units + * @property {number} blockSize - Total serialized block size in bytes (witness + non-witness). * @property {number} blockVSize - Total block virtual size in vbytes * @property {number} nTx - Number of transactions in the projected block * @property {Sats} totalFees - Total fees in satoshis diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 65f328319..2d71d8981 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -967,7 +967,7 @@ class MempoolBlock(TypedDict): Block info in a mempool.space like format for fee estimation. Attributes: - blockSize: Total block size in weight units + blockSize: Total serialized block size in bytes (witness + non-witness). blockVSize: Total block virtual size in vbytes nTx: Number of transactions in the projected block totalFees: Total fees in satoshis