solana_fee_calculator/
lib.rs

1//! Calculation of transaction fees.
2#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
3#![allow(clippy::arithmetic_side_effects)]
4#![no_std]
5#![cfg_attr(docsrs, feature(doc_auto_cfg))]
6use log::*;
7#[cfg(feature = "frozen-abi")]
8extern crate std;
9
10#[repr(C)]
11#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))]
12#[cfg_attr(
13    feature = "serde",
14    derive(serde_derive::Serialize, serde_derive::Deserialize)
15)]
16#[derive(Default, PartialEq, Eq, Clone, Copy, Debug)]
17#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
18pub struct FeeCalculator {
19    /// The current cost of a signature.
20    ///
21    /// This amount may increase/decrease over time based on cluster processing
22    /// load.
23    pub lamports_per_signature: u64,
24}
25
26impl FeeCalculator {
27    pub fn new(lamports_per_signature: u64) -> Self {
28        Self {
29            lamports_per_signature,
30        }
31    }
32}
33
34#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))]
35#[cfg_attr(
36    feature = "serde",
37    derive(serde_derive::Serialize, serde_derive::Deserialize)
38)]
39#[derive(PartialEq, Eq, Clone, Debug)]
40#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
41pub struct FeeRateGovernor {
42    // The current cost of a signature  This amount may increase/decrease over time based on
43    // cluster processing load.
44    #[cfg_attr(feature = "serde", serde(skip))]
45    pub lamports_per_signature: u64,
46
47    // The target cost of a signature when the cluster is operating around target_signatures_per_slot
48    // signatures
49    pub target_lamports_per_signature: u64,
50
51    // Used to estimate the desired processing capacity of the cluster.  As the signatures for
52    // recent slots are fewer/greater than this value, lamports_per_signature will decrease/increase
53    // for the next slot.  A value of 0 disables lamports_per_signature fee adjustments
54    pub target_signatures_per_slot: u64,
55
56    pub min_lamports_per_signature: u64,
57    pub max_lamports_per_signature: u64,
58
59    // What portion of collected fees are to be destroyed, as a fraction of u8::MAX
60    pub burn_percent: u8,
61}
62
63pub const DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 10_000;
64const DEFAULT_MS_PER_SLOT: u64 = 400;
65#[cfg(test)]
66static_assertions::const_assert_eq!(DEFAULT_MS_PER_SLOT, solana_clock::DEFAULT_MS_PER_SLOT);
67pub const DEFAULT_TARGET_SIGNATURES_PER_SLOT: u64 = 50 * DEFAULT_MS_PER_SLOT;
68
69// Percentage of tx fees to burn
70pub const DEFAULT_BURN_PERCENT: u8 = 50;
71
72impl Default for FeeRateGovernor {
73    fn default() -> Self {
74        Self {
75            lamports_per_signature: 0,
76            target_lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE,
77            target_signatures_per_slot: DEFAULT_TARGET_SIGNATURES_PER_SLOT,
78            min_lamports_per_signature: 0,
79            max_lamports_per_signature: 0,
80            burn_percent: DEFAULT_BURN_PERCENT,
81        }
82    }
83}
84
85impl FeeRateGovernor {
86    pub fn new(target_lamports_per_signature: u64, target_signatures_per_slot: u64) -> Self {
87        let base_fee_rate_governor = Self {
88            target_lamports_per_signature,
89            lamports_per_signature: target_lamports_per_signature,
90            target_signatures_per_slot,
91            ..FeeRateGovernor::default()
92        };
93
94        Self::new_derived(&base_fee_rate_governor, 0)
95    }
96
97    pub fn new_derived(
98        base_fee_rate_governor: &FeeRateGovernor,
99        latest_signatures_per_slot: u64,
100    ) -> Self {
101        let mut me = base_fee_rate_governor.clone();
102
103        if me.target_signatures_per_slot > 0 {
104            // lamports_per_signature can range from 50% to 1000% of
105            // target_lamports_per_signature
106            me.min_lamports_per_signature = core::cmp::max(1, me.target_lamports_per_signature / 2);
107            me.max_lamports_per_signature = me.target_lamports_per_signature * 10;
108
109            // What the cluster should charge at `latest_signatures_per_slot`
110            let desired_lamports_per_signature =
111                me.max_lamports_per_signature
112                    .min(me.min_lamports_per_signature.max(
113                        me.target_lamports_per_signature
114                            * core::cmp::min(latest_signatures_per_slot, u32::MAX as u64)
115                            / me.target_signatures_per_slot,
116                    ));
117
118            trace!(
119                "desired_lamports_per_signature: {}",
120                desired_lamports_per_signature
121            );
122
123            let gap = desired_lamports_per_signature as i64
124                - base_fee_rate_governor.lamports_per_signature as i64;
125
126            if gap == 0 {
127                me.lamports_per_signature = desired_lamports_per_signature;
128            } else {
129                // Adjust fee by 5% of target_lamports_per_signature to produce a smooth
130                // increase/decrease in fees over time.
131                let gap_adjust =
132                    core::cmp::max(1, me.target_lamports_per_signature / 20) as i64 * gap.signum();
133
134                trace!(
135                    "lamports_per_signature gap is {}, adjusting by {}",
136                    gap,
137                    gap_adjust
138                );
139
140                me.lamports_per_signature =
141                    me.max_lamports_per_signature
142                        .min(me.min_lamports_per_signature.max(
143                            (base_fee_rate_governor.lamports_per_signature as i64 + gap_adjust)
144                                as u64,
145                        ));
146            }
147        } else {
148            me.lamports_per_signature = base_fee_rate_governor.target_lamports_per_signature;
149            me.min_lamports_per_signature = me.target_lamports_per_signature;
150            me.max_lamports_per_signature = me.target_lamports_per_signature;
151        }
152        debug!(
153            "new_derived(): lamports_per_signature: {}",
154            me.lamports_per_signature
155        );
156        me
157    }
158
159    pub fn clone_with_lamports_per_signature(&self, lamports_per_signature: u64) -> Self {
160        Self {
161            lamports_per_signature,
162            ..*self
163        }
164    }
165
166    /// calculate unburned fee from a fee total, returns (unburned, burned)
167    pub fn burn(&self, fees: u64) -> (u64, u64) {
168        let burned = fees * u64::from(self.burn_percent) / 100;
169        (fees - burned, burned)
170    }
171
172    /// create a FeeCalculator based on current cluster signature throughput
173    pub fn create_fee_calculator(&self) -> FeeCalculator {
174        FeeCalculator::new(self.lamports_per_signature)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_fee_rate_governor_burn() {
184        let mut fee_rate_governor = FeeRateGovernor::default();
185        assert_eq!(fee_rate_governor.burn(2), (1, 1));
186
187        fee_rate_governor.burn_percent = 0;
188        assert_eq!(fee_rate_governor.burn(2), (2, 0));
189
190        fee_rate_governor.burn_percent = 100;
191        assert_eq!(fee_rate_governor.burn(2), (0, 2));
192    }
193
194    #[test]
195    fn test_fee_rate_governor_derived_default() {
196        solana_logger::setup();
197
198        let f0 = FeeRateGovernor::default();
199        assert_eq!(
200            f0.target_signatures_per_slot,
201            DEFAULT_TARGET_SIGNATURES_PER_SLOT
202        );
203        assert_eq!(
204            f0.target_lamports_per_signature,
205            DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE
206        );
207        assert_eq!(f0.lamports_per_signature, 0);
208
209        let f1 = FeeRateGovernor::new_derived(&f0, DEFAULT_TARGET_SIGNATURES_PER_SLOT);
210        assert_eq!(
211            f1.target_signatures_per_slot,
212            DEFAULT_TARGET_SIGNATURES_PER_SLOT
213        );
214        assert_eq!(
215            f1.target_lamports_per_signature,
216            DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE
217        );
218        assert_eq!(
219            f1.lamports_per_signature,
220            DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2
221        ); // min
222    }
223
224    #[test]
225    fn test_fee_rate_governor_derived_adjust() {
226        solana_logger::setup();
227
228        let mut f = FeeRateGovernor {
229            target_lamports_per_signature: 100,
230            target_signatures_per_slot: 100,
231            ..FeeRateGovernor::default()
232        };
233        f = FeeRateGovernor::new_derived(&f, 0);
234
235        // Ramp fees up
236        let mut count = 0;
237        loop {
238            let last_lamports_per_signature = f.lamports_per_signature;
239
240            f = FeeRateGovernor::new_derived(&f, u64::MAX);
241            info!("[up] f.lamports_per_signature={}", f.lamports_per_signature);
242
243            // some maximum target reached
244            if f.lamports_per_signature == last_lamports_per_signature {
245                break;
246            }
247            // shouldn't take more than 1000 steps to get to minimum
248            assert!(count < 1000);
249            count += 1;
250        }
251
252        // Ramp fees down
253        let mut count = 0;
254        loop {
255            let last_lamports_per_signature = f.lamports_per_signature;
256            f = FeeRateGovernor::new_derived(&f, 0);
257
258            info!(
259                "[down] f.lamports_per_signature={}",
260                f.lamports_per_signature
261            );
262
263            // some minimum target reached
264            if f.lamports_per_signature == last_lamports_per_signature {
265                break;
266            }
267
268            // shouldn't take more than 1000 steps to get to minimum
269            assert!(count < 1000);
270            count += 1;
271        }
272
273        // Arrive at target rate
274        let mut count = 0;
275        while f.lamports_per_signature != f.target_lamports_per_signature {
276            f = FeeRateGovernor::new_derived(&f, f.target_signatures_per_slot);
277            info!(
278                "[target] f.lamports_per_signature={}",
279                f.lamports_per_signature
280            );
281            // shouldn't take more than 100 steps to get to target
282            assert!(count < 100);
283            count += 1;
284        }
285    }
286}