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