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::block_template::selector::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    pub fn network_mass_per_second(&self) -> u64 {
74        self.network_blocks_per_second * self.maximum_mass_per_block
75    }
76}
77
78#[derive(Debug, Clone)]
79pub struct FeerateEstimator {
80    /// The total probability weight of current mempool ready transactions, i.e., `Σ_{tx in mempool}(tx.fee/tx.mass)^alpha`.
81    /// Note that some estimators might consider a reduced weight which excludes outliers. See [`Frontier::build_feerate_estimator`]
82    total_weight: f64,
83
84    /// The amortized time **in seconds** between transactions, given the current transaction masses present in the mempool. Or in
85    /// other words, the inverse of the transaction inclusion rate. For instance, if the average transaction mass is 2500 grams,
86    /// the block mass limit is 500,000 and the network has 10 BPS, then this number would be 1/2000 seconds.
87    inclusion_interval: f64,
88}
89
90impl FeerateEstimator {
91    pub fn new(total_weight: f64, inclusion_interval: f64) -> Self {
92        assert!(total_weight >= 0.0);
93        assert!((0f64..1f64).contains(&inclusion_interval));
94        Self { total_weight, inclusion_interval }
95    }
96
97    pub(crate) fn feerate_to_time(&self, feerate: f64) -> f64 {
98        let (c1, c2) = (self.inclusion_interval, self.total_weight);
99        c1 * c2 / feerate.powi(ALPHA) + c1
100    }
101
102    fn time_to_feerate(&self, time: f64) -> f64 {
103        let (c1, c2) = (self.inclusion_interval, self.total_weight);
104        assert!(c1 < time, "{c1}, {time}");
105        ((c1 * c2 / time) / (1f64 - c1 / time)).powf(1f64 / ALPHA as f64)
106    }
107
108    /// The antiderivative function of [`feerate_to_time`] excluding the constant shift `+ c1`
109    #[inline]
110    fn feerate_to_time_antiderivative(&self, feerate: f64) -> f64 {
111        let (c1, c2) = (self.inclusion_interval, self.total_weight);
112        c1 * c2 / (-2f64 * feerate.powi(ALPHA - 1))
113    }
114
115    /// Returns the feerate value for which the integral area is `frac` of the total area between `lower` and `upper`.
116    fn quantile(&self, lower: f64, upper: f64, frac: f64) -> f64 {
117        assert!((0f64..=1f64).contains(&frac));
118        assert!(0.0 < lower && lower <= upper, "{lower}, {upper}");
119        let (c1, c2) = (self.inclusion_interval, self.total_weight);
120        if c1 == 0.0 || c2 == 0.0 {
121            // if c1 · c2 == 0.0, the integral area is empty, so we simply return `lower`
122            return lower;
123        }
124        let z1 = self.feerate_to_time_antiderivative(lower);
125        let z2 = self.feerate_to_time_antiderivative(upper);
126        // Get the total area corresponding to `frac` of the integral area between `lower` and `upper`
127        // which can be expressed as z1 + frac * (z2 - z1)
128        let z = frac * z2 + (1f64 - frac) * z1;
129        // Calc the x value (feerate) corresponding to said area
130        ((c1 * c2) / (-2f64 * z)).powf(1f64 / (ALPHA - 1) as f64)
131    }
132
133    pub fn calc_estimations(&self, minimum_standard_feerate: f64) -> FeerateEstimations {
134        let min = minimum_standard_feerate;
135        // Choose `high` such that it provides sub-second waiting time
136        let high = self.time_to_feerate(1f64).max(min);
137        // Choose `low` feerate such that it provides sub-hour waiting time AND it covers (at least) the 0.25 quantile
138        let low = self.time_to_feerate(3600f64).max(self.quantile(min, high, 0.25));
139        // Choose `normal` feerate such that it provides sub-minute waiting time AND it covers (at least) the 0.66 quantile between low and high.
140        let normal = self.time_to_feerate(60f64).max(self.quantile(low, high, 0.66));
141        // Choose an additional point between normal and low
142        let mid = self.time_to_feerate(1800f64).max(self.quantile(min, high, 0.5));
143        /* Intuition for the above:
144               1. The quantile calculations make sure that we return interesting points on the `feerate_to_time` curve.
145               2. They also ensure that the times don't diminish too high if small increments to feerate would suffice
146                  to cover large fractions of the integral area (reflecting the position within the waiting-time distribution)
147        */
148        FeerateEstimations {
149            priority_bucket: FeerateBucket { feerate: high, estimated_seconds: self.feerate_to_time(high) },
150            normal_buckets: vec![
151                FeerateBucket { feerate: normal, estimated_seconds: self.feerate_to_time(normal) },
152                FeerateBucket { feerate: mid, estimated_seconds: self.feerate_to_time(mid) },
153            ],
154            low_buckets: vec![FeerateBucket { feerate: low, estimated_seconds: self.feerate_to_time(low) }],
155        }
156    }
157}
158
159#[derive(Clone, Debug)]
160pub struct FeeEstimateVerbose {
161    pub estimations: FeerateEstimations,
162
163    pub mempool_ready_transactions_count: u64,
164    pub mempool_ready_transactions_total_mass: u64,
165    pub network_mass_per_second: u64,
166
167    pub next_block_template_feerate_min: f64,
168    pub next_block_template_feerate_median: f64,
169    pub next_block_template_feerate_max: f64,
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use itertools::Itertools;
176
177    #[test]
178    fn test_feerate_estimations() {
179        let estimator = FeerateEstimator { total_weight: 1002283.659, inclusion_interval: 0.004f64 };
180        let estimations = estimator.calc_estimations(1.0);
181        let buckets = estimations.ordered_buckets();
182        for (i, j) in buckets.into_iter().tuple_windows() {
183            assert!(i.feerate >= j.feerate);
184        }
185        dbg!(estimations);
186    }
187
188    #[test]
189    fn test_min_feerate_estimations() {
190        let estimator = FeerateEstimator { total_weight: 0.00659, inclusion_interval: 0.004f64 };
191        let minimum_feerate = 0.755;
192        let estimations = estimator.calc_estimations(minimum_feerate);
193        println!("{estimations}");
194        let buckets = estimations.ordered_buckets();
195        assert!(buckets.last().unwrap().feerate >= minimum_feerate);
196        for (i, j) in buckets.into_iter().tuple_windows() {
197            assert!(i.feerate >= j.feerate);
198            assert!(i.estimated_seconds <= j.estimated_seconds);
199        }
200    }
201
202    #[test]
203    fn test_zero_values() {
204        let estimator = FeerateEstimator { total_weight: 0.0, inclusion_interval: 0.0 };
205        let minimum_feerate = 0.755;
206        let estimations = estimator.calc_estimations(minimum_feerate);
207        let buckets = estimations.ordered_buckets();
208        for bucket in buckets {
209            assert_eq!(minimum_feerate, bucket.feerate);
210            assert_eq!(0.0, bucket.estimated_seconds);
211        }
212
213        let estimator = FeerateEstimator { total_weight: 0.0, inclusion_interval: 0.1 };
214        let minimum_feerate = 0.755;
215        let estimations = estimator.calc_estimations(minimum_feerate);
216        let buckets = estimations.ordered_buckets();
217        for bucket in buckets {
218            assert_eq!(minimum_feerate, bucket.feerate);
219            assert_eq!(estimator.inclusion_interval, bucket.estimated_seconds);
220        }
221
222        let estimator = FeerateEstimator { total_weight: 0.1, inclusion_interval: 0.0 };
223        let minimum_feerate = 0.755;
224        let estimations = estimator.calc_estimations(minimum_feerate);
225        let buckets = estimations.ordered_buckets();
226        for bucket in buckets {
227            assert_eq!(minimum_feerate, bucket.feerate);
228            assert_eq!(0.0, bucket.estimated_seconds);
229        }
230    }
231}