1use crate::{
2 Policy,
3 feerate::{FeerateEstimator, FeerateEstimatorArgs},
4};
5
6use feerate_key::FeerateTransactionKey;
7use kaspa_consensus_core::{
8 block::TemplateTransactionSelector,
9 config::constants::consensus::{DEFAULT_GAS_PER_LANE_LIMIT, DEFAULT_LANES_PER_BLOCK_LIMIT},
10 mass::BlockLaneLimits,
11 subnets::SubnetworkId,
12 tx::{Transaction, TransactionId},
13};
14use kaspa_core::trace;
15use rand::{Rng, distributions::Uniform, prelude::Distribution};
16use search_tree::SearchTree;
17use selectors::{MutatingTreeSelector, SequenceSelector, SequenceSelectorInput, TakeAllSelector};
18use std::{
19 collections::{BTreeSet, BinaryHeap, HashMap, HashSet},
20 iter::FusedIterator,
21 sync::Arc,
22};
23
24pub(crate) mod feerate_key;
25pub(crate) mod search_tree;
26pub(crate) mod selectors;
27
28const COLLISION_FACTOR: u64 = 4;
32
33const MASS_LIMIT_FACTOR: f64 = 1.2;
36
37const TARGET_GAP_FACTOR: f64 = 0.05;
39
40const MAX_NULL_ATTEMPTS: usize = 8;
43
44const INITIAL_AVG_MASS: f64 = 2036.0;
46
47const AVG_MASS_DECAY_FACTOR: f64 = 0.99999;
49
50const DEFAULT_BLOCK_LANE_LIMITS: BlockLaneLimits =
51 BlockLaneLimits { lanes_per_block: DEFAULT_LANES_PER_BLOCK_LIMIT, gas_per_lane: DEFAULT_GAS_PER_LANE_LIMIT };
52
53pub struct Frontier {
57 search_tree: SearchTree,
59
60 by_lane: HashMap<SubnetworkId, BTreeSet<FeerateTransactionKey>>,
62
63 total_mass: u64,
65
66 average_transaction_mass: f64,
68
69 target_time_per_block_seconds: f64,
70}
71
72#[derive(Default)]
73struct Lanes {
74 occupied: HashSet<SubnetworkId>,
75 frozen: bool,
76}
77
78struct SampleMassTracker {
79 sampled: u64,
83
84 gap: u64,
87
88 desired: u64,
90
91 null_attempts: usize,
93
94 target_gap: u64,
96}
97
98impl SampleMassTracker {
99 fn new(policy: &Policy) -> Self {
100 let desired = (policy.max_block_mass as f64 * MASS_LIMIT_FACTOR) as u64;
105
106 let target_gap = (policy.max_block_mass as f64 * TARGET_GAP_FACTOR) as u64;
109
110 Self { sampled: 0, gap: policy.max_block_mass, desired, null_attempts: 0, target_gap }
111 }
112
113 fn should_continue(&self) -> bool {
115 self.sampled <= self.desired || (self.null_attempts < MAX_NULL_ATTEMPTS && self.gap > self.target_gap)
119 }
120
121 fn record(&mut self, mass: u64) {
123 self.sampled = self.sampled.saturating_add(mass);
124
125 if let Some(gap) = self.gap.checked_sub(mass) {
126 self.gap = gap;
127 } else {
128 #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
131 {
132 self.null_attempts += 1;
133 }
134 }
135 }
136}
137
138impl Frontier {
139 pub fn new(target_time_per_block_seconds: f64) -> Self {
140 Self {
141 search_tree: Default::default(),
142 by_lane: Default::default(),
143 total_mass: Default::default(),
144 average_transaction_mass: INITIAL_AVG_MASS,
145 target_time_per_block_seconds,
146 }
147 }
148}
149
150impl Frontier {
151 pub fn total_weight(&self) -> f64 {
152 self.search_tree.total_weight()
153 }
154
155 pub fn total_mass(&self) -> u64 {
156 self.total_mass
157 }
158
159 pub fn len(&self) -> usize {
160 self.search_tree.len()
161 }
162
163 pub fn is_empty(&self) -> bool {
164 self.len() == 0
165 }
166
167 pub fn insert(&mut self, key: FeerateTransactionKey) -> bool {
168 let mass = key.mass;
169 let lane = key.lane();
170 if self.search_tree.insert(key.clone()) {
171 self.by_lane.entry(lane).or_default().insert(key);
172
173 #[allow(
174 clippy::arithmetic_side_effects,
175 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"
176 )]
177 {
178 self.total_mass += mass;
179 }
180 self.average_transaction_mass =
184 self.average_transaction_mass * AVG_MASS_DECAY_FACTOR + mass as f64 * (1.0 - AVG_MASS_DECAY_FACTOR);
185 true
186 } else {
187 false
188 }
189 }
190
191 pub fn remove(&mut self, key: &FeerateTransactionKey) -> bool {
192 let mass = key.mass;
193 if self.search_tree.remove(key) {
194 let lane = key.lane();
195 let mut remove_lane = false;
196 if let Some(entries) = self.by_lane.get_mut(&lane) {
197 entries.remove(key);
198 remove_lane = entries.is_empty();
199 }
200 if remove_lane {
201 self.by_lane.remove(&lane);
202 }
203
204 #[allow(
205 clippy::arithmetic_side_effects,
206 reason = "Removing mass that was previously added to total_mass, so this cannot underflow."
207 )]
208 {
209 self.total_mass -= mass;
210 }
211 true
212 } else {
213 false
214 }
215 }
216
217 pub fn sample_inplace<R>(&self, rng: &mut R, policy: &Policy, _collisions: &mut u64) -> SequenceSelectorInput
250 where
251 R: Rng + ?Sized,
252 {
253 debug_assert!(!self.search_tree.is_empty(), "expected to be called only if not empty");
254
255 let mut distr = Uniform::new(0f64, self.total_weight());
256 let mut down_iter = self.search_tree.descending_iter();
257 let mut top = down_iter.next().unwrap();
258 let mut cache = HashSet::new();
259 let mut sequence = SequenceSelectorInput::default();
260 let mut mass = SampleMassTracker::new(policy);
261 let mut collisions = 0;
262 let mut lanes = Lanes::default();
263
264 'outer: while cache.len() < self.search_tree.len() && mass.should_continue() {
266 let query = distr.sample(rng);
267 let item = {
268 let mut item = self.search_tree.search(query);
269 while !cache.insert(item.tx.id()) {
270 #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
271 {
272 collisions += 1;
273 }
274 if cache.contains(&top.tx.id()) {
276 loop {
277 match down_iter.next() {
278 Some(next) => top = next,
279 None => break 'outer,
280 }
281 if !cache.contains(&top.tx.id()) {
283 break;
284 }
285 }
286 let remaining_weight = self.search_tree.prefix_weight(top);
287 distr = Uniform::new(0f64, remaining_weight);
288 }
289 let query = distr.sample(rng);
290 item = self.search_tree.search(query);
291 }
292 item
293 };
294 if lanes.occupied.len() < policy.lanes_per_block_limit {
295 lanes.occupied.insert(item.lane());
296 } else if !lanes.occupied.contains(&item.lane()) {
297 lanes.frozen = true;
300 break;
301 }
302 sequence.push(item.tx.clone(), item.mass);
303 mass.record(item.mass);
304 }
305
306 if lanes.frozen {
307 self.finish_intra_lane_selection(&mut sequence, &cache, &lanes, &mut mass);
308 }
309 trace!("[mempool frontier sample inplace] collisions: {collisions}, cache: {}", cache.len());
310 #[allow(clippy::arithmetic_side_effects, reason = "ARITH-SAFETY(COUNTER)")]
311 {
312 *_collisions += collisions;
313 }
314 sequence
315 }
316
317 fn finish_intra_lane_selection(
326 &self,
327 sequence: &mut SequenceSelectorInput,
328 cache: &HashSet<TransactionId>,
329 lanes: &Lanes,
330 mass: &mut SampleMassTracker,
331 ) {
332 let mut lane_iters = lanes
333 .occupied
334 .iter()
335 .filter_map(|lane| {
336 self.by_lane.get(lane).map(|entries| entries.iter().rev().filter(|item| !cache.contains(&item.tx.id())))
337 })
338 .collect::<Vec<_>>();
339 let mut heads = BinaryHeap::new();
340
341 for (idx, iter) in lane_iters.iter_mut().enumerate() {
343 if let Some(item) = iter.next() {
344 heads.push((item.clone(), idx));
345 }
346 }
347
348 while mass.should_continue() {
350 let Some((item, best_idx)) = heads.pop() else {
351 break;
352 };
353
354 sequence.push(item.tx.clone(), item.mass);
355 mass.record(item.mass);
356
357 let iter = &mut lane_iters[best_idx];
359 if let Some(next) = iter.next() {
360 heads.push((next.clone(), best_idx));
361 }
362 }
363 }
364
365 pub fn build_selector(&self, policy: &Policy) -> Box<dyn TemplateTransactionSelector> {
380 #[allow(clippy::arithmetic_side_effects, reason = "policy.max_block_mass and COLLISION_FACTOR are small enough.")]
381 if self.total_mass <= policy.max_block_mass {
382 Box::new(TakeAllSelector::new(self.search_tree.descending_iter().map(|k| k.tx.clone()).collect(), policy.clone()))
384 } else if self.total_mass > policy.max_block_mass * COLLISION_FACTOR {
385 let mut rng = rand::thread_rng();
386 Box::new(SequenceSelector::new(self.sample_inplace(&mut rng, policy, &mut 0), policy.clone()))
387 } else {
388 Box::new(MutatingTreeSelector::new(policy.clone(), self.search_tree.clone()))
389 }
390 }
391
392 pub fn build_selector_sample_inplace(&self, _collisions: &mut u64) -> Box<dyn TemplateTransactionSelector> {
394 let mut rng = rand::thread_rng();
395 let policy = Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS);
396 Box::new(SequenceSelector::new(self.sample_inplace(&mut rng, &policy, _collisions), policy))
397 }
398
399 pub fn build_selector_take_all(&self) -> Box<dyn TemplateTransactionSelector> {
401 Box::new(TakeAllSelector::new(
402 self.search_tree.descending_iter().map(|k| k.tx.clone()).collect(),
403 Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS),
404 ))
405 }
406
407 pub fn build_mutating_tree_selector(&self) -> Box<dyn TemplateTransactionSelector> {
409 Box::new(MutatingTreeSelector::new(Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS), self.search_tree.clone()))
410 }
411
412 pub fn build_feerate_estimator(&self, args: FeerateEstimatorArgs) -> FeerateEstimator {
414 let average_transaction_mass = self.average_transaction_mass;
415 let bps = args.network_blocks_per_second as f64;
416 let mut mass_per_block = args.maximum_mass_per_block as f64;
417 let mut inclusion_interval = average_transaction_mass / (mass_per_block * bps);
418 let mut estimator = FeerateEstimator::new(self.total_weight(), inclusion_interval, self.target_time_per_block_seconds);
419
420 let mut down_iter = self.search_tree.descending_iter().peekable();
422 while let Some(current) = down_iter.next() {
423 mass_per_block -= current.mass as f64;
428 if mass_per_block <= average_transaction_mass {
429 break;
431 }
432
433 inclusion_interval = average_transaction_mass / (mass_per_block * bps);
436
437 let prefix_weight = down_iter.peek().map(|key| self.search_tree.prefix_weight(key)).unwrap_or_default();
439 let pending_estimator = FeerateEstimator::new(prefix_weight, inclusion_interval, self.target_time_per_block_seconds);
440
441 if pending_estimator.feerate_to_time(1.0) < estimator.feerate_to_time(1.0) {
443 estimator = pending_estimator;
444 } else {
445 break;
448 }
449 }
450 estimator
451 }
452
453 pub fn ascending_iter(&self) -> impl DoubleEndedIterator<Item = &Arc<Transaction>> + ExactSizeIterator + FusedIterator {
455 self.search_tree.ascending_iter().map(|key| &key.tx)
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::mempool::config::{Config, DEFAULT_MAXIMUM_TRANSACTION_COUNT};
463 use feerate_key::tests::build_feerate_key;
464 use itertools::Itertools;
465 use kaspa_consensus_core::{
466 config::params::{DEVNET_PARAMS, MAINNET_PARAMS, SIMNET_PARAMS, TESTNET_PARAMS},
467 mass::BlockMassLimits,
468 subnets::SubnetworkId,
469 tx::{Transaction, TransactionInput, TransactionOutpoint},
470 };
471 use kaspa_hashes::{HasherBase, TransactionID};
472 use rand::{SeedableRng, rngs::StdRng, thread_rng};
473 use std::collections::HashMap;
474 use std::sync::Arc;
475
476 fn build_feerate_key_with_lane(fee: u64, mass: u64, id: u64, lane: SubnetworkId, gas: u64) -> FeerateTransactionKey {
477 let mut hasher = TransactionID::new();
478 let prev = hasher.update(id.to_le_bytes()).clone().finalize();
479 let input = TransactionInput::new(TransactionOutpoint::new(prev, 0), vec![], 0, 0);
480 let tx = Arc::new(Transaction::new(0, vec![input], vec![], 0, lane, gas, vec![]));
481 FeerateTransactionKey::new(fee, mass, tx)
482 }
483
484 #[test]
485 pub fn test_highly_irregular_sampling() {
486 let mut rng = thread_rng();
487 let cap = 1000;
488 let mut map = HashMap::with_capacity(cap);
489 for i in 0..cap as u64 {
490 let mut fee: u64 = if i % (cap as u64 / 100) == 0 { 1000000 } else { rng.gen_range(1..10000) };
491 if i == 0 {
492 fee = 100_000_000 * 1_000_000; }
495 let mass: u64 = 1650;
496 let key = build_feerate_key(fee, mass, i);
497 map.insert(key.tx.id(), key);
498 }
499
500 let mut frontier = Frontier::new(1.0);
501 for item in map.values().cloned() {
502 frontier.insert(item).then_some(()).unwrap();
503 }
504
505 let _sample = frontier.sample_inplace(&mut rng, &Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS), &mut 0);
506 }
507
508 #[test]
509 pub fn test_mempool_sampling_small() {
510 let mut rng = thread_rng();
511 let cap = 2000;
512 let mut map = HashMap::with_capacity(cap);
513 for i in 0..cap as u64 {
514 let fee: u64 = rng.gen_range(1..1000000);
515 let mass: u64 = 1650;
516 let key = build_feerate_key(fee, mass, i);
517 map.insert(key.tx.id(), key);
518 }
519
520 let mut frontier = Frontier::new(1.0);
521 for item in map.values().cloned() {
522 frontier.insert(item).then_some(()).unwrap();
523 }
524
525 let mut selector = frontier.build_selector(&Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS));
526 selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
527
528 let mut selector = frontier.build_selector_sample_inplace(&mut 0);
529 selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
530
531 let mut selector = frontier.build_selector_take_all();
532 selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
533
534 let mut selector = frontier.build_selector(&Policy::new(500_000, DEFAULT_BLOCK_LANE_LIMITS));
535 selector.select_transactions().iter().map(|k| k.gas).sum::<u64>();
536 }
537
538 #[test]
539 pub fn test_total_mass_tracking() {
540 let mut rng = thread_rng();
541 let cap = 10000;
542 let mut map = HashMap::with_capacity(cap);
543 for i in 0..cap as u64 {
544 let fee: u64 = if i % (cap as u64 / 100) == 0 { 1000000 } else { rng.gen_range(1..10000) };
545 let mass: u64 = rng.gen_range(1..100000); let key = build_feerate_key(fee, mass, i);
547 map.insert(key.tx.id(), key);
548 }
549
550 let len = cap / 2;
551 let mut frontier = Frontier::new(1.0);
552 for item in map.values().take(len).cloned() {
553 frontier.insert(item).then_some(()).unwrap();
554 }
555
556 let prev_total_mass = frontier.total_mass();
557 assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
559
560 let mut dup_items = frontier.search_tree.ascending_iter().take(len / 2).cloned().collect_vec();
562 for dup in dup_items.iter().cloned() {
563 (!frontier.insert(dup)).then_some(()).unwrap();
564 }
565 assert_eq!(prev_total_mass, frontier.total_mass());
566 assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
567
568 dup_items.iter().take(10).for_each(|k| {
570 map.remove(&k.tx.id());
571 });
572
573 for item in map.values().step_by(2) {
575 frontier.remove(item);
576 if let Some(item2) = dup_items.pop() {
577 frontier.insert(item2);
578 }
579 }
580 assert_eq!(frontier.total_mass(), frontier.search_tree.ascending_iter().map(|k| k.mass).sum::<u64>());
581 }
582
583 #[test]
584 fn test_maximum_mempool_mass_fits_u64_with_large_margin() {
585 const MINIMUM_HEADROOM_FACTOR: u64 = 1_000_000;
586
587 let maximum_transaction_count = u64::try_from(DEFAULT_MAXIMUM_TRANSACTION_COUNT).unwrap();
588 for params in [&MAINNET_PARAMS, &TESTNET_PARAMS, &SIMNET_PARAMS, &DEVNET_PARAMS] {
589 let maximum_mempool_mass = params
590 .block_mass_limits
591 .reference()
592 .checked_mul(maximum_transaction_count)
593 .expect("maximum mempool mass must fit in u64");
594 let headroom_factor = u64::MAX / maximum_mempool_mass;
595 assert!(headroom_factor >= MINIMUM_HEADROOM_FACTOR);
596 }
597 }
598
599 #[test]
600 fn test_sample_inplace_respects_lane_limit_after_freeze() {
601 let mut rng = StdRng::seed_from_u64(42);
602 let lanes = [
603 SubnetworkId::from_namespace([1, 1, 0, 0]),
604 SubnetworkId::from_namespace([2, 1, 0, 0]),
605 SubnetworkId::from_namespace([3, 1, 0, 0]),
606 ];
607
608 let mut frontier = Frontier::new(1.0);
609 for i in 0..90u64 {
610 let lane = lanes[(i as usize) % lanes.len()];
611 let fee = 1_000_000 - i;
612 frontier.insert(build_feerate_key_with_lane(fee, 100, i, lane, 0)).then_some(()).unwrap();
613 }
614
615 let mut policy = Policy::new(1_000, DEFAULT_BLOCK_LANE_LIMITS);
616 policy.lanes_per_block_limit = 2;
617 let sample = frontier.sample_inplace(&mut rng, &policy, &mut 0);
618 let selected_lanes = sample.iter().map(|tx| tx.tx.subnetwork_id).collect::<HashSet<_>>();
619
620 assert_eq!(selected_lanes.len(), policy.lanes_per_block_limit);
621 }
622
623 const EPS: f64 = 0.000001;
625 const TEST_MIN_FEERATES: [f64; 3] = [1.0, 100.0, 1000.0];
627
628 fn minimum_standard_feerate() -> f64 {
629 Config::build_default(1_000, false, BlockMassLimits::with_shared_limit(500_000), DEFAULT_BLOCK_LANE_LIMITS).minimum_feerate()
630 }
631
632 fn assert_valid_feerate_buckets(estimations: &crate::feerate::FeerateEstimations, min_feerate: f64) {
633 for bucket in estimations.ordered_buckets() {
634 assert!(
636 bucket.feerate.is_normal() && bucket.feerate >= min_feerate - EPS,
637 "bucket feerate {} must be finite and at least {}",
638 bucket.feerate,
639 min_feerate
640 );
641 assert!(
642 bucket.estimated_seconds.is_normal() && bucket.estimated_seconds > 0.0,
643 "bucket estimated seconds must be a finite number greater than zero"
644 );
645 }
646 }
647
648 #[test]
649 fn test_feerate_estimator() {
650 let mut rng = thread_rng();
651 let cap = 2000;
652 let mut map = HashMap::with_capacity(cap);
653 for i in 0..cap as u64 {
654 let mut fee: u64 = rng.gen_range(1..1000000);
655 let mass: u64 = 1650;
656 if i <= 303 {
658 fee = i * 10_000_000 * 1_000_000;
660 }
661 let key = build_feerate_key(fee, mass, i);
662 map.insert(key.tx.id(), key);
663 }
664
665 for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
666 let mut frontier = Frontier::new(1.0);
667 for item in map.values().take(len).cloned() {
668 frontier.insert(item).then_some(()).unwrap();
669 }
670
671 let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
672 let estimator = frontier.build_feerate_estimator(args);
674
675 for min_feerate in TEST_MIN_FEERATES {
676 let estimations = estimator.calc_estimations(min_feerate);
677 assert_valid_feerate_buckets(&estimations, min_feerate);
678 dbg!(len, min_feerate, &estimator);
679 dbg!(estimations);
680 }
681 }
682 }
683
684 #[test]
685 fn test_constant_feerate_estimator() {
686 let cap = 20_000;
687 for min_feerate in TEST_MIN_FEERATES {
688 let mut map = HashMap::with_capacity(cap);
689 for i in 0..cap as u64 {
690 let mass: u64 = 1650;
691 let fee = (mass as f64 * min_feerate) as u64;
692 let key = build_feerate_key(fee, mass, i);
693 map.insert(key.tx.id(), key);
694 }
695
696 for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
697 println!();
698 println!("Testing a frontier with {} txs and min feerate {}...", len.min(cap), min_feerate);
699 let mut frontier = Frontier::new(1.0);
700 for item in map.values().take(len).cloned() {
701 frontier.insert(item).then_some(()).unwrap();
702 }
703
704 let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
705 let estimator = frontier.build_feerate_estimator(args);
707 let estimations = estimator.calc_estimations(min_feerate);
708 assert_valid_feerate_buckets(&estimations, min_feerate);
709 dbg!(len, min_feerate, estimator);
710 dbg!(estimations);
711 }
712 }
713 }
714
715 #[test]
716 fn test_feerate_estimator_with_low_mass_outliers() {
717 const MIN_FEERATE: f64 = 1.0;
718 const STD_FEERATE: f64 = 10.0;
719 const HIGH_FEERATE: f64 = 1000.0;
720
721 let cap = 20_000;
722 let mut frontier = Frontier::new(1.0);
723 for i in 0..cap as u64 {
724 let (mass, fee) = if i < 200 {
725 let mass = 1650;
726 (mass, (HIGH_FEERATE * mass as f64) as u64)
727 } else {
728 let mass = 90_000;
729 (mass, (STD_FEERATE * mass as f64) as u64)
730 };
731 let key = build_feerate_key(fee, mass, i);
732 frontier.insert(key).then_some(()).unwrap();
733 }
734
735 let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
736 let estimator = frontier.build_feerate_estimator(args);
738 let estimations = estimator.calc_estimations(MIN_FEERATE);
739
740 let normal_feerate = estimations.normal_buckets.first().unwrap().feerate;
742 assert!(
743 normal_feerate < HIGH_FEERATE / 10.0,
744 "Normal bucket feerate is expected to be << high feerate due to small mass of high feerate txs ({}, {})",
745 normal_feerate,
746 HIGH_FEERATE
747 );
748
749 let buckets = estimations.ordered_buckets();
750 for b in buckets.iter() {
752 assert!(
753 b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS,
754 "bucket feerate must be a finite number greater or equal to the minimum standard feerate"
755 );
756 assert!(
757 b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0,
758 "bucket estimated seconds must be a finite number greater than zero"
759 );
760 }
761 dbg!(estimator);
762 dbg!(estimations);
763 }
764
765 #[test]
766 fn test_feerate_estimator_with_less_than_block_capacity() {
767 let mut map = HashMap::new();
768 for i in 0..304 {
769 let mass: u64 = 1650;
770 let fee = 10_000_000 * 1_000_000;
771 let key = build_feerate_key(fee, mass, i);
772 map.insert(key.tx.id(), key);
773 }
774
775 for len in [0, 1, 10, 100, 200, 250, 300] {
777 let mut frontier = Frontier::new(1.0);
778 for item in map.values().take(len).cloned() {
779 frontier.insert(item).then_some(()).unwrap();
780 }
781
782 let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
783 let estimator = frontier.build_feerate_estimator(args);
785
786 for min_feerate in TEST_MIN_FEERATES {
787 let estimations = estimator.calc_estimations(min_feerate);
788 let buckets = estimations.ordered_buckets();
789 for b in buckets.iter() {
791 assert!(
793 (b.feerate - min_feerate).abs() <= EPS,
794 "bucket feerate is expected to be equal to the minimum standard feerate"
795 );
796 assert!(
797 b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0 && b.estimated_seconds <= 1.0,
798 "bucket estimated seconds must be a finite number greater than zero & less than 1.0"
799 );
800 }
801 dbg!(len, min_feerate, &estimator);
802 dbg!(estimations);
803 }
804 }
805 }
806
807 #[test]
808 fn test_feerate_estimator_buckets_never_drop_below_minimum_standard_feerate() {
809 let minimum_feerate = minimum_standard_feerate();
810 let scenarios = [
811 ("empty", vec![]),
812 ("single below-floor transaction", vec![(1, 1000)]),
813 ("single minimum-feerate transaction", vec![((minimum_feerate * 1000.0) as u64, 1000)]),
814 ("less than one block", (0..100).map(|i| (50_000 + i, 1650)).collect_vec()),
815 ("around one block", (0..304).map(|i| (50_000 + i, 1650)).collect_vec()),
816 ("many low-feerate transactions", (0..2000).map(|i| (1 + i, 1650)).collect_vec()),
817 (
818 "high outliers over low-feerate backlog",
819 (0..2000).map(|i| if i < 10 { (50_000_000_000 + i, 1650) } else { (1 + i, 1650) }).collect_vec(),
820 ),
821 (
822 "mixed masses over several blocks",
823 (0..2000)
824 .map(|i| {
825 let mass = if i % 5 == 0 { 90_000 } else { 1650 };
826 let fee = if i % 11 == 0 { 100_000_000 + i } else { 1 + i };
827 (fee, mass)
828 })
829 .collect_vec(),
830 ),
831 ];
832
833 for (name, txs) in scenarios {
834 let mut frontier = Frontier::new(1.0);
835 for (i, (fee, mass)) in txs.into_iter().enumerate() {
836 frontier.insert(build_feerate_key(fee, mass, i as u64)).then_some(()).unwrap();
837 }
838
839 let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
840 let estimator = frontier.build_feerate_estimator(args);
841 let estimations = estimator.calc_estimations(minimum_feerate);
842 assert_valid_feerate_buckets(&estimations, minimum_feerate);
843
844 for (higher, lower) in estimations.ordered_buckets().into_iter().tuple_windows() {
845 assert!(
846 higher.feerate + EPS >= lower.feerate,
847 "{name}: buckets must remain ordered by decreasing feerate: {higher:?}, {lower:?}"
848 );
849 }
850 }
851 }
852}