use crate::{
Policy,
feerate::{FeerateEstimator, FeerateEstimatorArgs},
};
use feerate_key::FeerateTransactionKey;
use kaspa_consensus_core::{
block::TemplateTransactionSelector,
config::constants::consensus::{DEFAULT_GAS_PER_LANE_LIMIT, DEFAULT_LANES_PER_BLOCK_LIMIT},
mass::BlockLaneLimits,
subnets::SubnetworkId,
tx::{Transaction, TransactionId},
};
use kaspa_core::trace;
use rand::{Rng, distributions::Uniform, prelude::Distribution};
use search_tree::SearchTree;
use selectors::{MutatingTreeSelector, SequenceSelector, SequenceSelectorInput, TakeAllSelector};
use std::{
collections::{BTreeSet, BinaryHeap, HashMap, HashSet},
iter::FusedIterator,
sync::Arc,
};
pub(crate) mod feerate_key;
pub(crate) mod search_tree;
pub(crate) mod selectors;
const COLLISION_FACTOR: u64 = 4;
const MASS_LIMIT_FACTOR: f64 = 1.2;
const TARGET_GAP_FACTOR: f64 = 0.05;
const MAX_NULL_ATTEMPTS: usize = 8;
const INITIAL_AVG_MASS: f64 = 2036.0;
const AVG_MASS_DECAY_FACTOR: f64 = 0.99999;
const DEFAULT_BLOCK_LANE_LIMITS: BlockLaneLimits =
BlockLaneLimits { lanes_per_block: DEFAULT_LANES_PER_BLOCK_LIMIT, gas_per_lane: DEFAULT_GAS_PER_LANE_LIMIT };
pub struct Frontier {
search_tree: SearchTree,
by_lane: HashMap<SubnetworkId, BTreeSet<FeerateTransactionKey>>,
total_mass: u64,
average_transaction_mass: f64,
target_time_per_block_seconds: f64,
}
#[derive(Default)]
struct Lanes {
occupied: HashSet<SubnetworkId>,
frozen: bool,
}
struct SampleMassTracker {
sampled: u64,
gap: u64,
desired: u64,
null_attempts: usize,
target_gap: u64,
}
impl SampleMassTracker {
fn new(policy: &Policy) -> Self {
let desired = (policy.max_block_mass as f64 * MASS_LIMIT_FACTOR) as u64;
let target_gap = (policy.max_block_mass as f64 * TARGET_GAP_FACTOR) as u64;
Self { sampled: 0, gap: policy.max_block_mass, desired, null_attempts: 0, target_gap }
}
fn should_continue(&self) -> bool {
self.sampled <= self.desired || (self.null_attempts < MAX_NULL_ATTEMPTS && self.gap > self.target_gap)
}
fn record(&mut self, mass: u64) {
self.sampled = self.sampled.saturating_add(mass);
if let Some(gap) = self.gap.checked_sub(mass) {
self.gap = gap;
} else {
#[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
{
self.null_attempts += 1;
}
}
}
}
impl Frontier {
pub fn new(target_time_per_block_seconds: f64) -> Self {
Self {
search_tree: Default::default(),
by_lane: Default::default(),
total_mass: Default::default(),
average_transaction_mass: INITIAL_AVG_MASS,
target_time_per_block_seconds,
}
}
}
impl Frontier {
pub fn total_weight(&self) -> f64 {
self.search_tree.total_weight()
}
pub fn total_mass(&self) -> u64 {
self.total_mass
}
pub fn len(&self) -> usize {
self.search_tree.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn insert(&mut self, key: FeerateTransactionKey) -> bool {
let mass = key.mass;
let lane = key.lane();
if self.search_tree.insert(key.clone()) {
self.by_lane.entry(lane).or_default().insert(key);
#[allow(
clippy::arithmetic_side_effects,
reason = "test_maximum_mempool_mass_fits_u64_with_large_margin ensures that self.total_mass << u64::MAX, and mass <= max block size << u64::MAX, so self.total_mass + mass << u64::MAX"
)]
{
self.total_mass += mass;
}
self.average_transaction_mass =
self.average_transaction_mass * AVG_MASS_DECAY_FACTOR + mass as f64 * (1.0 - AVG_MASS_DECAY_FACTOR);
true
} else {
false
}
}
pub fn remove(&mut self, key: &FeerateTransactionKey) -> bool {
let mass = key.mass;
if self.search_tree.remove(key) {
let lane = key.lane();
let mut remove_lane = false;
if let Some(entries) = self.by_lane.get_mut(&lane) {
entries.remove(key);
remove_lane = entries.is_empty();
}
if remove_lane {
self.by_lane.remove(&lane);
}
#[allow(
clippy::arithmetic_side_effects,
reason = "Removing mass that was previously added to total_mass, so this cannot underflow."
)]
{
self.total_mass -= mass;
}
true
} else {
false
}
}
pub fn sample_inplace<R>(&self, rng: &mut R, policy: &Policy, _collisions: &mut u64) -> SequenceSelectorInput
where
R: Rng + ?Sized,
{
debug_assert!(!self.search_tree.is_empty(), "expected to be called only if not empty");
let mut distr = Uniform::new(0f64, self.total_weight());
let mut down_iter = self.search_tree.descending_iter();
let mut top = down_iter.next().unwrap();
let mut cache = HashSet::new();
let mut sequence = SequenceSelectorInput::default();
let mut mass = SampleMassTracker::new(policy);
let mut collisions = 0;
let mut lanes = Lanes::default();
'outer: while cache.len() < self.search_tree.len() && mass.should_continue() {
let query = distr.sample(rng);
let item = {
let mut item = self.search_tree.search(query);
while !cache.insert(item.tx.id()) {
#[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
{
collisions += 1;
}
if cache.contains(&top.tx.id()) {
loop {
match down_iter.next() {
Some(next) => top = next,
None => break 'outer,
}
if !cache.contains(&top.tx.id()) {
break;
}
}
let remaining_weight = self.search_tree.prefix_weight(top);
distr = Uniform::new(0f64, remaining_weight);
}
let query = distr.sample(rng);
item = self.search_tree.search(query);
}
item
};
if lanes.occupied.len() < policy.lanes_per_block_limit {
lanes.occupied.insert(item.lane());
} else if !lanes.occupied.contains(&item.lane()) {
lanes.frozen = true;
break;
}
sequence.push(item.tx.clone(), item.mass);
mass.record(item.mass);
}
if lanes.frozen {
self.finish_intra_lane_selection(&mut sequence, &cache, &lanes, &mut mass);
}
trace!("[mempool frontier sample inplace] collisions: {collisions}, cache: {}", cache.len());
#[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
{
*_collisions += collisions;
}
sequence
}
fn finish_intra_lane_selection(
&self,
sequence: &mut SequenceSelectorInput,
cache: &HashSet<TransactionId>,
lanes: &Lanes,
mass: &mut SampleMassTracker,
) {
let mut lane_iters = lanes
.occupied
.iter()
.filter_map(|lane| {
self.by_lane.get(lane).map(|entries| entries.iter().rev().filter(|item| !cache.contains(&item.tx.id())))
})
.collect::<Vec<_>>();
let mut heads = BinaryHeap::new();
for (idx, iter) in lane_iters.iter_mut().enumerate() {
if let Some(item) = iter.next() {
heads.push((item.clone(), idx));
}
}
while mass.should_continue() {
let Some((item, best_idx)) = heads.pop() else {
break;
};
sequence.push(item.tx.clone(), item.mass);
mass.record(item.mass);
let iter = &mut lane_iters[best_idx];
if let Some(next) = iter.next() {
heads.push((next.clone(), best_idx));
}
}
}
pub fn build_selector(&self, policy: &Policy) -> Box<dyn TemplateTransactionSelector> {
#[allow(clippy::arithmetic_side_effects, reason = "policy.max_block_mass and COLLISION_FACTOR are small enough.")]
if self.total_mass <= policy.max_block_mass {
Box::new(TakeAllSelector::new(self.search_tree.descending_iter().map(|k| k.tx.clone()).collect(), policy.clone()))
} else if self.total_mass > policy.max_block_mass * COLLISION_FACTOR {
let mut rng = rand::thread_rng();
Box::new(SequenceSelector::new(self.sample_inplace(&mut rng, policy, &mut 0), policy.clone()))
} else {
Box::new(MutatingTreeSelector::new(policy.clone(), self.search_tree.clone()))
}
}
pub fn build_selector_sample_inplace(&self, _collisions: &mut u64) -> Box<dyn TemplateTransactionSelector> {
let mut rng = rand::thread_rng();
let policy = Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS);
Box::new(SequenceSelector::new(self.sample_inplace(&mut rng, &policy, _collisions), policy))
}
pub fn build_selector_take_all(&self) -> Box<dyn TemplateTransactionSelector> {
Box::new(TakeAllSelector::new(
self.search_tree.descending_iter().map(|k| k.tx.clone()).collect(),
Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS),
))
}
pub fn build_mutating_tree_selector(&self) -> Box<dyn TemplateTransactionSelector> {
Box::new(MutatingTreeSelector::new(Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS), self.search_tree.clone()))
}
pub fn build_feerate_estimator(&self, args: FeerateEstimatorArgs) -> FeerateEstimator {
let average_transaction_mass = self.average_transaction_mass;
let bps = args.network_blocks_per_second as f64;
let mut mass_per_block = args.maximum_mass_per_block as f64;
let mut inclusion_interval = average_transaction_mass / (mass_per_block * bps);
let mut estimator = FeerateEstimator::new(self.total_weight(), inclusion_interval, self.target_time_per_block_seconds);
let mut down_iter = self.search_tree.descending_iter().peekable();
while let Some(current) = down_iter.next() {
mass_per_block -= current.mass as f64;
if mass_per_block <= average_transaction_mass {
break;
}
inclusion_interval = average_transaction_mass / (mass_per_block * bps);
let prefix_weight = down_iter.peek().map(|key| self.search_tree.prefix_weight(key)).unwrap_or_default();
let pending_estimator = FeerateEstimator::new(prefix_weight, inclusion_interval, self.target_time_per_block_seconds);
if pending_estimator.feerate_to_time(1.0) < estimator.feerate_to_time(1.0) {
estimator = pending_estimator;
} else {
break;
}
}
estimator
}
pub fn ascending_iter(&self) -> impl DoubleEndedIterator<Item = &Arc<Transaction>> + ExactSizeIterator + FusedIterator {
self.search_tree.ascending_iter().map(|key| &key.tx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mempool::config::{Config, DEFAULT_MAXIMUM_TRANSACTION_COUNT};
use feerate_key::tests::build_feerate_key;
use itertools::Itertools;
use kaspa_consensus_core::{
config::params::{DEVNET_PARAMS, MAINNET_PARAMS, SIMNET_PARAMS, TESTNET_PARAMS},
mass::BlockMassLimits,
subnets::SubnetworkId,
tx::{Transaction, TransactionInput, TransactionOutpoint},
};
use kaspa_hashes::{HasherBase, TransactionID};
use rand::{SeedableRng, rngs::StdRng, thread_rng};
use std::collections::HashMap;
use std::sync::Arc;
fn build_feerate_key_with_lane(fee: u64, mass: u64, id: u64, lane: SubnetworkId, gas: u64) -> FeerateTransactionKey {
let mut hasher = TransactionID::new();
let prev = hasher.update(id.to_le_bytes()).clone().finalize();
let input = TransactionInput::new(TransactionOutpoint::new(prev, 0), vec![], 0, 0);
let tx = Arc::new(Transaction::new(0, vec![input], vec![], 0, lane, gas, vec![]));
FeerateTransactionKey::new(fee, mass, tx)
}
#[test]
pub fn test_highly_irregular_sampling() {
let mut rng = thread_rng();
let cap = 1000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let mut fee: u64 = if i % (cap as u64 / 100) == 0 { 1000000 } else { rng.gen_range(1..10000) };
if i == 0 {
fee = 100_000_000 * 1_000_000; }
let mass: u64 = 1650;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
let mut frontier = Frontier::new(1.0);
for item in map.values().cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let _sample = frontier.sample_inplace(&mut rng, &Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS), &mut 0);
}
#[test]
pub fn test_mempool_sampling_small() {
let mut rng = thread_rng();
let cap = 2000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let fee: u64 = rng.gen_range(1..1000000);
let mass: u64 = 1650;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
let mut frontier = Frontier::new(1.0);
for item in map.values().cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let mut selector = frontier.build_selector(&Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS));
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
let mut selector = frontier.build_selector_sample_inplace(&mut 0);
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
let mut selector = frontier.build_selector_take_all();
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
let mut selector = frontier.build_selector(&Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS));
selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
}
#[test]
pub fn test_total_mass_tracking() {
let mut rng = thread_rng();
let cap = 10000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let fee: u64 = if i % (cap as u64 / 100) == 0 { 1000000 } else { rng.gen_range(1..10000) };
let mass: u64 = rng.gen_range(1..100000); let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
let len = cap / 2;
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let prev_total_mass = frontier.total_mass();
assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
let mut dup_items = frontier.search_tree.ascending_iter().take(len / 2).cloned().collect_vec();
for dup in dup_items.iter().cloned() {
(!frontier.insert(dup)).then_some(()).unwrap();
}
assert_eq!(prev_total_mass, frontier.total_mass());
assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
dup_items.iter().take(10).for_each(|k| {
map.remove(&k.tx.id());
});
for item in map.values().step_by(2) {
frontier.remove(item);
if let Some(item2) = dup_items.pop() {
frontier.insert(item2);
}
}
assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
}
#[test]
fn test_maximum_mempool_mass_fits_u64_with_large_margin() {
const MINIMUM_HEADROOM_FACTOR: u64 = 1_000_000;
let maximum_transaction_count = u64::try_from(DEFAULT_MAXIMUM_TRANSACTION_COUNT).unwrap();
for params in [&MAINNET_PARAMS, &TESTNET_PARAMS, &SIMNET_PARAMS, &DEVNET_PARAMS] {
let maximum_mempool_mass = params
.block_mass_limits
.reference()
.checked_mul(maximum_transaction_count)
.expect("maximum mempool mass must fit in u64");
let headroom_factor = u64::MAX / maximum_mempool_mass;
assert!(headroom_factor >= MINIMUM_HEADROOM_FACTOR);
}
}
#[test]
fn test_sample_inplace_respects_lane_limit_after_freeze() {
let mut rng = StdRng::seed_from_u64(42);
let lanes = [
SubnetworkId::from_namespace([1, 1, 0, 0]),
SubnetworkId::from_namespace([2, 1, 0, 0]),
SubnetworkId::from_namespace([3, 1, 0, 0]),
];
let mut frontier = Frontier::new(1.0);
for i in 0..90u64 {
let lane = lanes[(i as usize) % lanes.len()];
let fee = 1_000_000 - i;
frontier.insert(build_feerate_key_with_lane(fee, 100, i, lane, 0)).then_some(()).unwrap();
}
let mut policy = Policy::new(1_000, DEFAULT_BLOCK_LANE_LIMITS);
policy.lanes_per_block_limit = 2;
let sample = frontier.sample_inplace(&mut rng, &policy, &mut 0);
let selected_lanes = sample.iter().map(|tx| tx.tx.subnetwork_id).collect::<HashSet<_>>();
assert_eq!(selected_lanes.len(), policy.lanes_per_block_limit);
}
const EPS: f64 = 0.000001;
const TEST_MIN_FEERATES: [f64; 3] = [1.0, 100.0, 1000.0];
fn minimum_standard_feerate() -> f64 {
Config::build_default(1_000, false, BlockMassLimits::with_shared_limit(500_000), DEFAULT_BLOCK_LANE_LIMITS).minimum_feerate()
}
fn assert_valid_feerate_buckets(estimations: &crate::feerate::FeerateEstimations, min_feerate: f64) {
for bucket in estimations.ordered_buckets() {
assert!(
bucket.feerate.is_normal() && bucket.feerate >= min_feerate - EPS,
"bucket feerate {} must be finite and at least {}",
bucket.feerate,
min_feerate
);
assert!(
bucket.estimated_seconds.is_normal() && bucket.estimated_seconds > 0.0,
"bucket estimated seconds must be a finite number greater than zero"
);
}
}
#[test]
fn test_feerate_estimator() {
let mut rng = thread_rng();
let cap = 2000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let mut fee: u64 = rng.gen_range(1..1000000);
let mass: u64 = 1650;
if i <= 303 {
fee = i * 10_000_000 * 1_000_000;
}
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
for min_feerate in TEST_MIN_FEERATES {
let estimations = estimator.calc_estimations(min_feerate);
assert_valid_feerate_buckets(&estimations, min_feerate);
dbg!(len, min_feerate, &estimator);
dbg!(estimations);
}
}
}
#[test]
fn test_constant_feerate_estimator() {
let cap = 20_000;
for min_feerate in TEST_MIN_FEERATES {
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let mass: u64 = 1650;
let fee = (mass as f64 * min_feerate) as u64;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
println!();
println!("Testing a frontier with {} txs and min feerate {}...", len.min(cap), min_feerate);
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(min_feerate);
assert_valid_feerate_buckets(&estimations, min_feerate);
dbg!(len, min_feerate, estimator);
dbg!(estimations);
}
}
}
#[test]
fn test_feerate_estimator_with_low_mass_outliers() {
const MIN_FEERATE: f64 = 1.0;
const STD_FEERATE: f64 = 10.0;
const HIGH_FEERATE: f64 = 1000.0;
let cap = 20_000;
let mut frontier = Frontier::new(1.0);
for i in 0..cap as u64 {
let (mass, fee) = if i < 200 {
let mass = 1650;
(mass, (HIGH_FEERATE * mass as f64) as u64)
} else {
let mass = 90_000;
(mass, (STD_FEERATE * mass as f64) as u64)
};
let key = build_feerate_key(fee, mass, i);
frontier.insert(key).then_some(()).unwrap();
}
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(MIN_FEERATE);
let normal_feerate = estimations.normal_buckets.first().unwrap().feerate;
assert!(
normal_feerate < HIGH_FEERATE / 10.0,
"Normal bucket feerate is expected to be << high feerate due to small mass of high feerate txs ({}, {})",
normal_feerate,
HIGH_FEERATE
);
let buckets = estimations.ordered_buckets();
for b in buckets.iter() {
assert!(
b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS,
"bucket feerate must be a finite number greater or equal to the minimum standard feerate"
);
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0,
"bucket estimated seconds must be a finite number greater than zero"
);
}
dbg!(estimator);
dbg!(estimations);
}
#[test]
fn test_feerate_estimator_with_less_than_block_capacity() {
let mut map = HashMap::new();
for i in 0..304 {
let mass: u64 = 1650;
let fee = 10_000_000 * 1_000_000;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}
for len in [0, 1, 10, 100, 200, 250, 300] {
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
for min_feerate in TEST_MIN_FEERATES {
let estimations = estimator.calc_estimations(min_feerate);
let buckets = estimations.ordered_buckets();
for b in buckets.iter() {
assert!(
(b.feerate - min_feerate).abs() <= EPS,
"bucket feerate is expected to be equal to the minimum standard feerate"
);
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0 && b.estimated_seconds <= 1.0,
"bucket estimated seconds must be a finite number greater than zero & less than 1.0"
);
}
dbg!(len, min_feerate, &estimator);
dbg!(estimations);
}
}
}
#[test]
fn test_feerate_estimator_buckets_never_drop_below_minimum_standard_feerate() {
let minimum_feerate = minimum_standard_feerate();
let scenarios = [
("empty", vec![]),
("single below-floor transaction", vec![(1, 1000)]),
("single minimum-feerate transaction", vec![((minimum_feerate * 1000.0) as u64, 1000)]),
("less than one block", (0..100).map(|i| (50_000 + i, 1650)).collect_vec()),
("around one block", (0..304).map(|i| (50_000 + i, 1650)).collect_vec()),
("many low-feerate transactions", (0..2000).map(|i| (1 + i, 1650)).collect_vec()),
(
"high outliers over low-feerate backlog",
(0..2000).map(|i| if i < 10 { (50_000_000_000 + i, 1650) } else { (1 + i, 1650) }).collect_vec(),
),
(
"mixed masses over several blocks",
(0..2000)
.map(|i| {
let mass = if i % 5 == 0 { 90_000 } else { 1650 };
let fee = if i % 11 == 0 { 100_000_000 + i } else { 1 + i };
(fee, mass)
})
.collect_vec(),
),
];
for (name, txs) in scenarios {
let mut frontier = Frontier::new(1.0);
for (i, (fee, mass)) in txs.into_iter().enumerate() {
frontier.insert(build_feerate_key(fee, mass, i as u64)).then_some(()).unwrap();
}
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(minimum_feerate);
assert_valid_feerate_buckets(&estimations, minimum_feerate);
for (higher, lower) in estimations.ordered_buckets().into_iter().tuple_windows() {
assert!(
higher.feerate + EPS >= lower.feerate,
"{name}: buckets must remain ordered by decreasing feerate: {higher:?}, {lower:?}"
);
}
}
}
}