diff --git a/proposals/0061-fair-congestion-control.md b/proposals/0061-fair-congestion-control.md new file mode 100644 index 000000000..747806765 --- /dev/null +++ b/proposals/0061-fair-congestion-control.md @@ -0,0 +1,258 @@ +--- +simd: '0061' +title: Fair congestion control with intra-block exponential local base fee +authors: + - Ryo Onodera (Solana Labs) +category: Standard/Meta +type: Core +status: Draft +created: 2023-07-05 +feature: (fill in with feature tracking issues once accepted) +--- + +## Summary + +This feature is a fair congestion-control mechanism in the form of an extension +to local fee markets while leaving their locality of transaction fee dynamics +intact. + +To that end, it introduces a exponentially-scaled dynamic base fees to +individual local fee markets. It also attains very short feedback loop of +per-tx frequency to maintain full efficacy of Solana's peculiar execution +model compared to other blockchains: multi-threaded and low latency. + +This is realized by means of some incentive tweaks to combat against the +obvious base fee manipulation with such short intervals. + +## Motivation + +(still in half-finished...) + +- Write lock cu limit is bad (bot can lock out at the very first of block for + the entire duration of whole blocktime (400ms) +- Increased Defi activities around any volatile financial markets could starve + payment transactions for extended time +- Inter-block and linear Voluntary fee escalation with vanilla fee market + auction can't guarantee the scheduling deadline of casual payment txes (which + needs 99.99% sub-second confirmation at very minimum). + +## Overview of the Design + +This proposal tries to localize congestions by means of increasing minimum +required `cu_price`s for each write-locked addresses (= `base_cu_price`), which +is newly introduced by this proposal. This increase will be calculated +exponentially, measured by the CU consumed by each addresses at the moment. +This means a transaction must cost the sum of `requested_cu * base_cu_price` +for all of its write-locked addresses at least. This results in selectively +pricing out crowded subset of transactions waiting for block inclusion, while +allowing other transactions to be processed for block inclusion. + +In this way, under-capitalized usage demand like spl-token transfers among +wallets will still find its way for _timely_ block inclusion, accomplishing +this mechanism's advertised _fairness_ in the sense of blockspace allocation +and transaction fee isolation for each independent local fee markets even at +the time of financial market's volatility, which entices well-capitalized usage +demand in DeFi activities on chain. + +This rate-limiting gets enforced only when the cluster deemed to be congested, +meaning no idling block space is wasted when not congested. Also, those +exponentially-increased `base_cu_price`s will be decreased exponentially +likewise in terms of consumed CUs in a block (= `block_cu`), as long as its +address-specific CUs (= `address_cu`) remain to be unchanged due to the +temporal stalemate of relevant transaction processing. Collectively, each +addresses get equal amount of opportunity to execute transactions in +round-robin fashion while contributing to the decrease of other +`base_cu_price`s, assuming no exponential priority-fee premium is paid among +users to interrupt the orderly processing with such a high economical +justification. + +On top of the direct appreciation of aforementioned fairness, this proposal +also obsoletes both the existing block-wide CU limit and the account-write CU +limit to overcome their inherent unfairness and problems. Also, no global +`base_cu_price` is introduced for simplicity, relying on natural block-wide +market-rate ceiling from individual active `base_cu_prices`s. + +Also, this proposal was conceived with the intent of reinforcement of Solana's +multi-threaded, real-time, and low-latency transaction execution. Towards that +high-achieving objective, rather drastic technical and economical changes are +needed for the introduction of this consensus-level congestion control as +described below in detail. + +## Detailed Design + +### Introductory example + +Due to this proposal's inherent complexities, the following very-simplified +example is presented at first before diving into each design topic: + +Assume all transactions request 100k CU and always succeed after consuming 100k +CU. Also assume the cluster is always congested. + +Available transactions for block inclusion (= `bufferd transactions`): + +- `Tx1_AB@9` (read: transaction numbered as `1`, which write-locks both address `A` + and address `B` with `cu_price == 9`) +- `Tx2_A@8` +- `Tx3_C@8` + +(1) + +#### update freq + +why block interval is bad? + this proposal's primary objective is for congestion control, not like EIP 1559's price discovery + so non-interactiveness is desired for little or no room of active auctioning + so, quick and acute is desired much like tcp's rwin. + +### Incentive alignment + + + + + + +### `base_cu_price` Calculation and block reward adjustment + +processing model is transactions are linearized by the order of appearance in ledger entries. + +### `reserved_fee` calculation and its accruing + + +### Congestion Detection + +thread count or runnable/scheduled tx queue len? + + +### Nonconflicting transaction group + + + + + + + + + + +(i jotted this down in 10min before going to bed! pardon for being so random +writings...) + +to *determiniscally* define active thread count (`TC_a`), additionally record +transaction termination events into poh stream. + +also, derive stake-weighted average transaction execution thread count +(`TC_stake_weighted`). + +so, full is defined as the duration when `TC_a == TC_stake_weighted` (this is +updated at ~10ms intervals). + +when not full, maximize throughput of each of any single threaded transaction +executions. note that, this mode exponentially cools down any hot addresses if +any. + +when full, effectively pause any txes touching the hot state by exponentially +increasing the local base fees. so casual txes can be executed. + +so, leaders are incentivised to manipulate in this naive form. + +so, split priority fee into two parts: (1) collected, (2) accrued for the next +tx's base fee payment. The portion of (1) is calculated as if tx's cu * +`TC_stake_weighted`. (i.e. as if validator stuffed spam txes to capture the +exessive part (2) of prirotiy fee) + +in this way, there's no meaning to spam blocks by leaders. at the same time, +it's still incentivied to pack txes according to p.f. desceding order, because +leaders and clients alike are want to increse of their single threaded tps) + +also requested fee is basis for fee cals, block fullness calc, not the actual +cu. +- to prevent bad behavior, rebate 50% of (requested CU - actual CU)? + - so that leaders want txes success (want to increase actual CU) => usually + execute in the order + - specified by user + - so that users want txes fail fast (want to decrease actual CU) + - 25% is burntd and 25% is collected to leaders + - so, avoid too much requested cu. + +finally, when substantial blocks are full for extended duration, the global base-fee +will naturally starts ceiling up. That's unavoidable no matter what. + +priority fee isn't collected at all. +banking can implement this congenstion mechanism without forced consensus rules if disired for experiment + +### Example + +`TC_stake_weighted == 10` + +100 kcu + +tx1a -- tx2a -- tx3a + +tx1a: + cu 100kcu + base fee: + +tx2a: + cu 200kcu + +tx3a: + cu 300kcu + +why leaders are incentized for picking more prioritized txes even if they only receive fixed base fees? + predictable auction mechanism and ceiling base fee as much as possible + compound reserve from firstly-executed txes? + + +## Impact + +How will the implemented proposal impacts dapp developers, validators, and core contributors? + +## Security Considerations + +What security implications/considerations come with implementing this feature? +Are there any implementation-specific guidance or pitfalls? + +## Drawbacks *(Optional)* + +Why should we not do this? + +## Backwards Compatibility *(Optional)* + +Does the feature introduce any breaking changes? All incompatibilities and +consequences should be listed. + +## Alternatives Considered + +Related proposals: + +(TODO: add any relation of this to them) + +dynamic base fees +https://github.com/solana-foundation/solana-improvement-documents/pull/4 + +program rebatable account write fees: +https://github.com/solana-foundation/solana-improvement-documents/pull/16 + +asynchronous program execution: +https://github.com/solana-foundation/solana-improvement-documents/pull/45 + +increase prioritization fee: +https://github.com/solana-foundation/solana-improvement-documents/pull/50 + +bankless +https://github.com/solana-foundation/solana-improvement-documents/pull/5 + +## New Terminology + +Is there any new terminology introduced with this proposal? + +casual tx: +fairness: +block fullness in terms of number of actively-execution threads +dark/filler tx: +tx base fee: +address base fee: +reserve <=> reward +requested <=> required +buffered + diff --git a/proposals/0061-fair-congestion-control/simulation/.gitignore b/proposals/0061-fair-congestion-control/simulation/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/proposals/0061-fair-congestion-control/simulation/.gitignore @@ -0,0 +1 @@ +/target diff --git a/proposals/0061-fair-congestion-control/simulation/Cargo.lock b/proposals/0061-fair-congestion-control/simulation/Cargo.lock new file mode 100644 index 000000000..f0b690867 --- /dev/null +++ b/proposals/0061-fair-congestion-control/simulation/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "fair-congestion-control-simulation" +version = "0.1.0" diff --git a/proposals/0061-fair-congestion-control/simulation/Cargo.toml b/proposals/0061-fair-congestion-control/simulation/Cargo.toml new file mode 100644 index 000000000..cdc230295 --- /dev/null +++ b/proposals/0061-fair-congestion-control/simulation/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "fair-congestion-control-simulation" +version = "0.1.0" +edition = "2021" diff --git a/proposals/0061-fair-congestion-control/simulation/src/main.rs b/proposals/0061-fair-congestion-control/simulation/src/main.rs new file mode 100644 index 000000000..49cb63b2b --- /dev/null +++ b/proposals/0061-fair-congestion-control/simulation/src/main.rs @@ -0,0 +1,565 @@ +#![allow(incomplete_features)] +#![feature(adt_const_params)] +#![feature(const_float_bits_conv)] + +use std::cmp::Ordering::*; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +#[derive(Debug, Ord, Eq, PartialEq, PartialOrd, Clone, Copy, Default)] +struct Addr(u8); + +#[derive(Debug, Ord, Eq, PartialEq, PartialOrd, Clone, Copy, Default)] +struct TxId(u8); + +#[derive(Debug)] +struct Tx { + id: TxId, + requested_cu: u64, + supplied_fee_rate: u64, + addrs: Vec, +} + +// multiply fee by requested_cu? + +impl Tx { + fn new(id: u8, requested_cu: u64, supplied_fee_rate: u64, addrs: Vec) -> Self { + Self { + id: TxId(id), + requested_cu, + supplied_fee_rate, + addrs, + } + } +} + +#[derive(Debug)] +struct LocalFeeMarket { + required_fee_rate: u64, + reserved_fee: u64, // accrued like 106% per 1 Mcu??? depending on active tx count? + clock: u64, + reset_counter: u64, + freq: u64, + is_active: bool, +} + +impl LocalFeeMarket { + fn new(reset_counter: u64) -> Self { + Self { + required_fee_rate: MINIMUM_BASE_FEE_RATE, + reserved_fee: INITIAL_RESERVED_FEE, + clock: 0, + reset_counter, + freq: 0, + is_active: false, + } + } +} + +#[derive(Default, Debug)] +struct BaseFeeTracker { + clock: u64, + reset_counter: u64, + is_congested: bool, + fee_markets: BTreeMap, + active_txs: BTreeSet, + nonconflicting_group_count: u64, + recent_addrs: VecDeque>, + rewarded_cu: u64, + total_supplied_fee: u64, +} + +const MINIMUM_BASE_FEE_RATE: u64 = 5000; +const CU_TO_POWER: f64 = 50_000.0; +const INITIAL_RESERVED_FEE: u64 = 0; + +#[derive(Debug, PartialEq)] +enum MeasureError { + AlreadyMeasuring, + AlreadyActiveAddress, + NoAddress, + NotMeasured, + TooManyActiveThreadCount, + InsufficientSuppliedFee(u64, u64), +} +use MeasureError::*; + +#[derive(Debug, PartialEq, Eq)] +struct Policy { + congestion_threshold: usize, + recent_tx_count: usize, + maximum_thread_count: usize, + exponential_rate: u64, // can't directly use f64 yet in consts... +} + +impl Policy { + const fn new() -> Self { + Self { + congestion_threshold: 0, + recent_tx_count: 5, + maximum_thread_count: 5, + exponential_rate: 2_f64.to_bits(), + } + } + + const fn congestion_threshold(mut self, congestion_threshold: usize) -> Self { + self.congestion_threshold = congestion_threshold; + self + } + + const fn recent_tx_count(mut self, recent_tx_count: usize) -> Self { + self.recent_tx_count = recent_tx_count; + self + } + + const fn maximum_thread_count(mut self, maximum_thread_count: usize) -> Self { + self.maximum_thread_count = maximum_thread_count; + self + } + + fn exponential_rate(&self) -> f64 { + f64::from_bits(self.exponential_rate) + } +} + +impl BaseFeeTracker { + fn start_measuring(&mut self, tx: &Tx) -> Result<(), MeasureError> { + let updated_active_tx_count = self.active_txs.len() + 1; + + if updated_active_tx_count > POLICY.maximum_thread_count { + return Err(TooManyActiveThreadCount); + } + if tx.addrs.is_empty() { + return Err(NoAddress); + } + + let (is_congested, reset_counter) = + match updated_active_tx_count.cmp(&POLICY.congestion_threshold) { + Less if self.is_congested => (false, self.reset_counter + 1), + Equal | Greater if !self.is_congested => (true, self.reset_counter), + _ => (self.is_congested, self.reset_counter), + }; + + let is_new_group = tx.addrs.iter().all(|addr| { + self.fee_markets + .entry(*addr) + .or_insert_with(|| LocalFeeMarket::new(self.reset_counter)) + .freq + == 0 + }); + tx.addrs.iter().try_for_each(|addr| { + if self.fee_markets.get(addr).unwrap().is_active { + Err(AlreadyActiveAddress) + } else { + Ok(()) + } + })?; + + let heat_up_duration = tx.requested_cu; + let updated_fees = tx + .addrs + .iter() + .map(|addr| { + let market = self.fee_markets.get(addr).unwrap(); + let cool_down_duration = self.clock - market.clock; + let reserved_fee = Self::inflate_reserve(market.reserved_fee, cool_down_duration); + + if is_congested && reset_counter == market.reset_counter { + let mut required_fee_rate = market.required_fee_rate; + required_fee_rate = self.cool_down(required_fee_rate, cool_down_duration); + required_fee_rate = required_fee_rate.max(MINIMUM_BASE_FEE_RATE); + required_fee_rate = self.heat_up(required_fee_rate, heat_up_duration); + + (required_fee_rate, reserved_fee) + } else { + (MINIMUM_BASE_FEE_RATE, reserved_fee) + } + }) + .collect::>(); + let minimum_supplied_fee = updated_fees + .iter() + .map(|&(required_fee_rate, reserved_fee)| { + let required_fee = required_fee_rate * tx.requested_cu; + required_fee + .saturating_sub(reserved_fee) + .max(MINIMUM_BASE_FEE_RATE * tx.requested_cu) + }) + .sum::(); + let supplied_fee = tx.supplied_fee_rate * tx.requested_cu; + if supplied_fee < minimum_supplied_fee { + return Err(InsufficientSuppliedFee(supplied_fee, minimum_supplied_fee)); + } + if !self.active_txs.insert(tx.id) { + return Err(AlreadyMeasuring); + } + self.is_congested = is_congested; + self.reset_counter = reset_counter; + if is_new_group { + self.nonconflicting_group_count += 1; + } + self.total_supplied_fee += supplied_fee; + let total_excess_fee = (supplied_fee - minimum_supplied_fee) as f64; + + let total_required_fee = updated_fees + .iter() + .map(|(required_fee_rate, _)| required_fee_rate * tx.requested_cu) + .sum::() as f64; + for (addr, &(required_fee_rate, mut reserved_fee)) in + tx.addrs.iter().zip(updated_fees.iter()) + { + let required_fee = required_fee_rate * tx.requested_cu; + reserved_fee = reserved_fee.saturating_sub(required_fee); + reserved_fee += + (total_excess_fee * ((required_fee as f64) / total_required_fee)) as u64; + reserved_fee = Self::inflate_reserve(reserved_fee, heat_up_duration); + + let market = self.fee_markets.get_mut(addr).unwrap(); + market.required_fee_rate = required_fee_rate; + market.reserved_fee = reserved_fee; + market.reset_counter = self.reset_counter; + market.freq += 1; + market.is_active = true; + } + + self.recent_addrs.push_back(tx.addrs.clone()); + if self.recent_addrs.len() > POLICY.recent_tx_count { + let was_new_group = self + .recent_addrs + .pop_front() + .unwrap() + .iter() + .all(|expired_addr| { + let market = self.fee_markets.get_mut(expired_addr).unwrap(); + market.freq -= 1; + market.freq == 0 + }); + if was_new_group { + self.nonconflicting_group_count -= 1; + } + } + + Ok(()) + } + + fn stop_measuring(&mut self, tx: &Tx, result: Result<(), u64>) -> Result<(), MeasureError> { + if !self.active_txs.remove(&tx.id) { + return Err(NotMeasured); + } + self.clock += tx.requested_cu; + for addr in &tx.addrs { + let market = self.fee_markets.get_mut(addr).unwrap(); + market.clock = self.clock; + market.is_active = false; + } + self.rewarded_cu += tx.addrs.len() as u64 + * match result { + Ok(()) => tx.requested_cu, + Err(actual_cu) => actual_cu / 2, + }; + + Ok(()) + } + + fn heat_up(&self, fee_rate: u64, cu: u64) -> u64 { + let factor = POLICY.exponential_rate().powf(cu as f64 / CU_TO_POWER); + (fee_rate as f64 * factor) as u64 + } + + fn cool_down(&self, fee_rate: u64, cu: u64) -> u64 { + let factor = POLICY.exponential_rate().powf( + cu as f64 + / self.nonconflicting_group_count.saturating_sub(1).max(1) as f64 + / CU_TO_POWER, + ); + (fee_rate as f64 / factor) as u64 + } + + fn inflate_reserve(reserved_fee: u64, cu: u64) -> u64 { + (reserved_fee as f64 * 1.06_f64.powf(cu as f64 / 1_000_000_f64)) as u64 + } + + fn collected_fee(&self) -> u64 { + self.rewarded_cu * MINIMUM_BASE_FEE_RATE + } + + fn burnt_fee(&self) -> u64 { + self.total_supplied_fee - self.collected_fee() + } +} + +fn main() {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tracker_default() { + let tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + assert_eq!(tracker.nonconflicting_group_count, 0); + assert_eq!(tracker.burnt_fee(), 0); + assert_eq!(tracker.collected_fee(), 0); + assert_eq!(tracker.fee_markets.is_empty(), true); + format!("{:?}", tracker); + } + + #[test] + fn local_fee_market_new() { + let market = LocalFeeMarket::new(0); + format!("{:?}", market); + } + + #[test] + fn tx_new() { + let tx = Tx::new(3, 200, 5000, vec![Addr(7)]); + format!("{:?}", tx); + } + + #[test] + fn policy_new() { + assert_eq!(Policy::new(), Policy::new()); + format!( + "{:?}", + Policy::new() + .congestion_threshold(0) + .recent_tx_count(0) + .maximum_thread_count(0) + ); + } + + #[test] + fn main_() { + main(); + } + + #[test] + fn exponential_heat_up() { + let tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let cu = CU_TO_POWER as u64; + assert_eq!(tracker.heat_up(5000, cu * 0), 5000 * 1); + assert_eq!(tracker.heat_up(5000, cu * 1), 5000 * 2); + assert_eq!(tracker.heat_up(5000, cu * 2), 5000 * 4); + assert_eq!(tracker.heat_up(5000, cu * 3), 5000 * 8); + } + + #[test] + fn exponential_normal_cool_down() { + let tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let cu = CU_TO_POWER as u64; + assert_eq!(tracker.cool_down(5000 * 8, cu * 0), 5000 * 8); + assert_eq!(tracker.cool_down(5000 * 8, cu * 1), 5000 * 4); + assert_eq!(tracker.cool_down(5000 * 8, cu * 2), 5000 * 2); + assert_eq!(tracker.cool_down(5000 * 8, cu * 3), 5000 * 1); + } + + #[test] + fn exponential_slow_cool_down() { + let tracker = BaseFeeTracker { + nonconflicting_group_count: 6, + ..BaseFeeTracker::<{ Policy::new() }>::default() + }; + let cu = CU_TO_POWER as u64; + assert_eq!(tracker.cool_down(5000 * 8, cu * 0 * 5), 5000 * 8); + assert_eq!(tracker.cool_down(5000 * 8, cu * 1 * 5), 5000 * 4); + assert_eq!(tracker.cool_down(5000 * 8, cu * 2 * 5), 5000 * 2); + assert_eq!(tracker.cool_down(5000 * 8, cu * 3 * 5), 5000 * 1); + } + + #[test] + fn no_congestion() { + let mut tracker = + BaseFeeTracker::<{ Policy::new().congestion_threshold(usize::MAX) }>::default(); + let tx = Tx::new(3, 200, 5000, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.is_congested, false); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + } + + #[test] + fn no_congestion_transition() { + let mut tracker = BaseFeeTracker::<{ Policy::new().congestion_threshold(2) }>::default(); + let tx1 = Tx::new(3, 200, 1002600 / 200, vec![Addr(7)]); + let tx2 = Tx::new(4, 200, 1002600 / 200, vec![Addr(8)]); + assert_eq!(tracker.start_measuring(&tx1), Ok(())); + assert_eq!(tracker.is_congested, false); + assert_eq!(tracker.reset_counter, 0); + assert_eq!(tracker.start_measuring(&tx2), Ok(())); + assert_eq!(tracker.is_congested, true); + assert_eq!(tracker.reset_counter, 0); + assert_eq!(tracker.stop_measuring(&tx2, Ok(())), Ok(())); + assert_eq!(tracker.is_congested, true); + assert_eq!(tracker.reset_counter, 0); + assert_eq!(tracker.stop_measuring(&tx1, Ok(())), Ok(())); + assert_eq!(tracker.is_congested, true); + assert_eq!(tracker.reset_counter, 0); + assert_eq!(tracker.start_measuring(&tx1), Ok(())); + assert_eq!(tracker.is_congested, false); + assert_eq!(tracker.reset_counter, 1); + } + + #[test] + fn congestion() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let tx = Tx::new(3, 200, 1002600 / 200, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.is_congested, true); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + + assert_eq!( + tracker.start_measuring(&tx), + Err(InsufficientSuppliedFee(1002600, 1005200)) + ); + let tx = Tx::new(3, 200, 1005200 / 200, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + } + + #[test] + fn reserve() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let tx = Tx::new(3, 1, 3_000_000, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!( + tracker.fee_markets.get(&Addr(7)).unwrap().reserved_fee, + 2_995_000 + ); + assert_eq!(tracker.burnt_fee() + tracker.collected_fee(), 3_000_000); + let tx = Tx::new(3, 1, MINIMUM_BASE_FEE_RATE, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!( + tracker.fee_markets.get(&Addr(7)).unwrap().reserved_fee, + 2_990_000 + ); + } + + #[test] + fn locality() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let tx1 = Tx::new(3, 200, 1002600 / 200, vec![Addr(7)]); + let tx2 = Tx::new(4, 200, 1002600 / 200, vec![Addr(8)]); + assert_eq!(tracker.start_measuring(&tx1), Ok(())); + assert_eq!(tracker.is_congested, true); + assert_eq!(tracker.stop_measuring(&tx1, Ok(())), Ok(())); + + assert_eq!( + tracker.start_measuring(&tx1), + Err(InsufficientSuppliedFee(1002600, 1005200)) + ); + + assert_eq!(tracker.start_measuring(&tx2), Ok(())); + assert_eq!(tracker.stop_measuring(&tx2, Ok(())), Ok(())); + } + + #[test] + fn cool_down() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let tx1 = Tx::new(3, 200, 1002600 / 200, vec![Addr(7)]); + let tx2 = Tx::new(4, 200, 1002600 / 200, vec![Addr(8)]); + assert_eq!(tracker.start_measuring(&tx1), Ok(())); + assert_eq!(tracker.stop_measuring(&tx1, Ok(())), Ok(())); + + assert_eq!(tracker.start_measuring(&tx2), Ok(())); + assert_eq!(tracker.stop_measuring(&tx2, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 2); + + assert_eq!(tracker.start_measuring(&tx1), Ok(())); + assert_eq!(tracker.stop_measuring(&tx1, Ok(())), Ok(())); + + assert_eq!(tracker.start_measuring(&tx2), Ok(())); + assert_eq!(tracker.stop_measuring(&tx2, Ok(())), Ok(())); + } + + #[test] + fn insufficient_fee() { + let mut tracker = + BaseFeeTracker::<{ Policy::new().congestion_threshold(usize::MAX) }>::default(); + let tx = Tx::new(3, 200, 4999, vec![Addr(7)]); + assert_eq!( + tracker.start_measuring(&tx), + Err(InsufficientSuppliedFee(999800, 1000000)) + ); + } + + #[test] + fn burn_and_collect_with_success_tx() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let cu = 200; + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.burnt_fee(), 2600); + assert_eq!(tracker.collected_fee(), cu * MINIMUM_BASE_FEE_RATE); + } + + #[test] + fn burn_and_collect_with_fail_tx() { + let mut tracker = BaseFeeTracker::<{ Policy::new() }>::default(); + let cu = 200; + let actual_cu = 100; + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Err(actual_cu)), Ok(())); + assert_eq!(tracker.burnt_fee(), 1000000 - 1000000 / 4 + 2600); + assert_eq!( + tracker.collected_fee(), + (actual_cu / 2) * MINIMUM_BASE_FEE_RATE + ); + } + + #[test] + fn recent_tx_count() { + let mut tracker = BaseFeeTracker::< + { + Policy::new() + .congestion_threshold(usize::MAX) + .recent_tx_count(3) + }, + >::default(); + let cu = 200; + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(7)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 1); + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(8)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 2); + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(9)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 3); + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(10)]); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 3); + + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 2); + + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Ok(())); + assert_eq!(tracker.nonconflicting_group_count, 1); + } + + #[test] + fn errors() { + let cu = 200; + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(7)]); + let mut tracker = BaseFeeTracker::<{ Policy::new().maximum_thread_count(1) }>::default(); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.start_measuring(&tx), Err(TooManyActiveThreadCount)); + let mut tracker = BaseFeeTracker::<{ Policy::new().maximum_thread_count(2) }>::default(); + assert_eq!(tracker.stop_measuring(&tx, Ok(())), Err(NotMeasured)); + assert_eq!(tracker.start_measuring(&tx), Ok(())); + assert_eq!(tracker.start_measuring(&tx), Err(AlreadyActiveAddress)); + let tx = Tx::new(3, cu, 1002600 / cu, vec![Addr(8)]); + assert_eq!(tracker.start_measuring(&tx), Err(AlreadyMeasuring)); + let tx = Tx::new(3, cu, 1002600 / cu, vec![]); + assert_eq!(tracker.start_measuring(&tx), Err(NoAddress)); + } +}