Skip to main content

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