Skip to main content

kaspa_mining/feerate/
mod.rs

1//! See the accompanying fee_estimation.ipynb Jupyter Notebook which details the reasoning
2//! behind this fee estimator.
3
4use crate::mempool::model::frontier::selectors::ALPHA;
5use itertools::Itertools;
6use std::fmt::Display;
7
8/// A type representing fee/mass of a transaction in `sompi/gram` units.
9/// Given a feerate value recommendation, calculate the required fee by
10/// taking the transaction mass and multiplying it by feerate: `fee = feerate * mass(tx)`
11pub type Feerate = f64;
12
13#[derive(Clone, Copy, Debug)]
14pub struct FeerateBucket {
15    pub feerate: f64,
16    pub estimated_seconds: f64,
17}
18
19impl Display for FeerateBucket {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "({:.4}, {:.4}s)", self.feerate, self.estimated_seconds)
22    }
23}
24
25#[derive(Clone, Debug)]
26pub struct FeerateEstimations {
27    /// *Top-priority* feerate bucket. Provides an estimation of the feerate required for sub-second DAG inclusion.
28    ///
29    /// Note: for all buckets, feerate values represent fee/mass of a transaction in `sompi/gram` units.
30    /// Given a feerate value recommendation, calculate the required fee by
31    /// taking the transaction mass and multiplying it by feerate: `fee = feerate * mass(tx)`
32    pub priority_bucket: FeerateBucket,
33
34    /// A vector of *normal* priority feerate values. The first value of this vector is guaranteed to exist and
35    /// provide an estimation for sub-*minute* DAG inclusion. All other values will have shorter estimation
36    /// times than all `low_bucket` values. Therefor by chaining `[priority] | normal | low` and interpolating
37    /// between them, one can compose a complete feerate function on the client side. The API makes an effort
38    /// to sample enough "interesting" points on the feerate-to-time curve, so that the interpolation is meaningful.
39    pub normal_buckets: Vec<FeerateBucket>,
40
41    /// A vector of *low* priority feerate values. The first value of this vector is guaranteed to
42    /// exist and provide an estimation for sub-*hour* DAG inclusion.
43    pub low_buckets: Vec<FeerateBucket>,
44}
45
46impl FeerateEstimations {
47    pub fn ordered_buckets(&self) -> Vec<FeerateBucket> {
48        std::iter::once(self.priority_bucket)
49            .chain(self.normal_buckets.iter().copied())
50            .chain(self.low_buckets.iter().copied())
51            .collect()
52    }
53}
54
55impl Display for FeerateEstimations {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "(fee/mass, secs) priority: {}, ", self.priority_bucket)?;
58        write!(f, "normal: {}, ", self.normal_buckets.iter().format(", "))?;
59        write!(f, "low: {}", self.low_buckets.iter().format(", "))
60    }
61}
62
63pub struct FeerateEstimatorArgs {
64    pub network_blocks_per_second: u64,
65    pub maximum_mass_per_block: u64,
66}
67
68impl FeerateEstimatorArgs {
69    pub fn new(network_blocks_per_second: u64, maximum_mass_per_block: u64) -> Self {
70        Self { network_blocks_per_second, maximum_mass_per_block }
71    }
72
73    #[allow(clippy::arithmetic_side_effects, reason = "Both values are relatively small.")]
74    pub fn network_mass_per_second(&self) -> u64 {
75        self.network_blocks_per_second * self.maximum_mass_per_block
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct FeerateEstimator {
81    /// The total probability weight of current mempool ready transactions, i.e., `Σ_{tx in mempool}(tx.fee/tx.mass)^alpha`.
82    /// Note that some estimators might consider a reduced weight which excludes outliers. See [`Frontier::build_feerate_estimator`]
83    total_weight: f64,
84
85    /// The amortized time **in seconds** between transactions, given the current transaction masses present in the mempool. Or in
86    /// other words, the inverse of the transaction inclusion rate. For instance, if the average transaction mass is 2500 grams,
87    /// the block mass limit is 500,000 and the network has 10 BPS, then this number would be 1/2000 seconds.
88    inclusion_interval: f64,
89
90    target_time_per_block_seconds: f64,
91}
92
93impl FeerateEstimator {
94    pub fn new(total_weight: f64, inclusion_interval: f64, target_time_per_block_seconds: f64) -> Self {
95        assert!(total_weight >= 0.0);
96        assert!((0f64..1f64).contains(&inclusion_interval));
97        Self { total_weight, inclusion_interval, target_time_per_block_seconds }
98    }
99
100    pub(crate) fn feerate_to_time(&self, feerate: f64) -> f64 {
101        let (c1, c2) = (self.inclusion_interval, self.total_weight);
102        c1 * c2 / feerate.powi(ALPHA) + c1
103    }
104
105    fn time_to_feerate(&self, time: f64) -> f64 {
106        let (c1, c2) = (self.inclusion_interval, self.total_weight);
107        assert!(c1 < time, "{c1}, {time}");
108        ((c1 * c2 / time) / (1f64 - c1 / time)).powf(1f64 / ALPHA as f64)
109    }
110
111    /// The antiderivative function of [`feerate_to_time`] excluding the constant shift `+ c1`
112    #[inline]
113    fn feerate_to_time_antiderivative(&self, feerate: f64) -> f64 {
114        let (c1, c2) = (self.inclusion_interval, self.total_weight);
115        c1 * c2 / (-2f64 * feerate.powi(ALPHA - 1))
116    }
117
118    /// Returns the feerate value for which the integral area is `frac` of the total area between `lower` and `upper`.
119    fn quantile(&self, lower: f64, upper: f64, frac: f64) -> f64 {
120        assert!((0f64..=1f64).contains(&frac));
121        if lower == upper {
122            // A collapsed interval has only one quantile; avoid formula roundoff.
123            return lower;
124        }
125        assert!(0.0 < lower && lower <= upper, "{lower}, {upper}");
126        let (c1, c2) = (self.inclusion_interval, self.total_weight);
127        if c1 == 0.0 || c2 == 0.0 {
128            // if c1 · c2 == 0.0, the integral area is empty, so we simply return `lower`
129            return lower;
130        }
131        let z1 = self.feerate_to_time_antiderivative(lower);
132        let z2 = self.feerate_to_time_antiderivative(upper);
133        // Get the total area corresponding to `frac` of the integral area between `lower` and `upper`
134        // which can be expressed as z1 + frac * (z2 - z1)
135        let z = frac * z2 + (1f64 - frac) * z1;
136        // Calc the x value (feerate) corresponding to said area
137        ((c1 * c2) / (-2f64 * z)).powf(1f64 / (ALPHA - 1) as f64)
138    }
139
140    pub fn calc_estimations(&self, minimum_standard_feerate: f64) -> FeerateEstimations {
141        let min = minimum_standard_feerate;
142        // Choose `high` such that the transaction is expected to be included in the next block.
143        let high = self.time_to_feerate(self.target_time_per_block_seconds).max(min);
144        // Choose `low` feerate such that it provides sub-hour waiting time AND it covers (at least) the 0.25 quantile
145        let low = self.time_to_feerate(3600f64).max(self.quantile(min, high, 0.25));
146        // Choose `normal` feerate such that it provides sub-minute waiting time AND it covers (at least) the 0.66 quantile between low and high.
147        let normal = self.time_to_feerate(60f64).max(self.quantile(low, high, 0.66));
148        // Choose an additional point between normal and low
149        let mid = self.time_to_feerate(1800f64).max(self.quantile(min, high, 0.5));
150        /* Intuition for the above:
151               1. The quantile calculations make sure that we return interesting points on the `feerate_to_time` curve.
152               2. They also ensure that the times don't diminish too high if small increments to feerate would suffice
153                  to cover large fractions of the integral area (reflecting the position within the waiting-time distribution)
154        */
155        FeerateEstimations {
156            priority_bucket: FeerateBucket { feerate: high, estimated_seconds: self.feerate_to_time(high) },
157            normal_buckets: vec![
158                FeerateBucket { feerate: normal, estimated_seconds: self.feerate_to_time(normal) },
159                FeerateBucket { feerate: mid, estimated_seconds: self.feerate_to_time(mid) },
160            ],
161            low_buckets: vec![FeerateBucket { feerate: low, estimated_seconds: self.feerate_to_time(low) }],
162        }
163    }
164}
165
166#[derive(Clone, Debug)]
167pub struct FeeEstimateVerbose {
168    pub estimations: FeerateEstimations,
169
170    pub mempool_ready_transactions_count: u64,
171    pub mempool_ready_transactions_total_mass: u64,
172    pub network_mass_per_second: u64,
173
174    pub next_block_template_feerate_min: f64,
175    pub next_block_template_feerate_median: f64,
176    pub next_block_template_feerate_max: f64,
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use itertools::Itertools;
183
184    #[test]
185    fn test_feerate_estimations() {
186        let estimator =
187            FeerateEstimator { total_weight: 1002283.659, inclusion_interval: 0.004f64, target_time_per_block_seconds: 1.0 };
188        let estimations = estimator.calc_estimations(1.0);
189        let buckets = estimations.ordered_buckets();
190        for (i, j) in buckets.into_iter().tuple_windows() {
191            assert!(i.feerate >= j.feerate);
192        }
193        dbg!(estimations);
194    }
195
196    #[test]
197    fn test_min_feerate_estimations() {
198        let estimator = FeerateEstimator { total_weight: 0.00659, inclusion_interval: 0.004f64, target_time_per_block_seconds: 1.0 };
199        let minimum_feerate = 0.755;
200        let estimations = estimator.calc_estimations(minimum_feerate);
201        println!("{estimations}");
202        let buckets = estimations.ordered_buckets();
203        assert!(buckets.last().unwrap().feerate >= minimum_feerate);
204        for (i, j) in buckets.into_iter().tuple_windows() {
205            assert!(i.feerate >= j.feerate);
206            assert!(i.estimated_seconds <= j.estimated_seconds);
207        }
208    }
209
210    #[test]
211    fn test_zero_values() {
212        let estimator = FeerateEstimator { total_weight: 0.0, inclusion_interval: 0.0, target_time_per_block_seconds: 1.0 };
213        let minimum_feerate = 0.755;
214        let estimations = estimator.calc_estimations(minimum_feerate);
215        let buckets = estimations.ordered_buckets();
216        for bucket in buckets {
217            assert_eq!(minimum_feerate, bucket.feerate);
218            assert_eq!(0.0, bucket.estimated_seconds);
219        }
220
221        let estimator = FeerateEstimator { total_weight: 0.0, inclusion_interval: 0.1, target_time_per_block_seconds: 1.0 };
222        let minimum_feerate = 0.755;
223        let estimations = estimator.calc_estimations(minimum_feerate);
224        let buckets = estimations.ordered_buckets();
225        for bucket in buckets {
226            assert_eq!(minimum_feerate, bucket.feerate);
227            assert_eq!(estimator.inclusion_interval, bucket.estimated_seconds);
228        }
229
230        let estimator = FeerateEstimator { total_weight: 0.1, inclusion_interval: 0.0, target_time_per_block_seconds: 1.0 };
231        let minimum_feerate = 0.755;
232        let estimations = estimator.calc_estimations(minimum_feerate);
233        let buckets = estimations.ordered_buckets();
234        for bucket in buckets {
235            assert_eq!(minimum_feerate, bucket.feerate);
236            assert_eq!(0.0, bucket.estimated_seconds);
237        }
238    }
239}