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