solana_runtime/
prioritization_fee.rs

1use {
2    solana_measure::measure_us,
3    solana_sdk::{clock::Slot, pubkey::Pubkey, saturating_add_assign},
4    std::collections::HashMap,
5};
6
7#[derive(Debug, Default)]
8struct PrioritizationFeeMetrics {
9    // Count of writable accounts in slot
10    total_writable_accounts_count: u64,
11
12    // Count of writeable accounts with a minimum prioritization fee higher than the minimum transaction
13    // fee for this slot.
14    relevant_writable_accounts_count: u64,
15
16    // Count of transactions that have non-zero prioritization fee.
17    prioritized_transactions_count: u64,
18
19    // Count of transactions that have zero prioritization fee.
20    non_prioritized_transactions_count: u64,
21
22    // Count of attempted update on finalized PrioritizationFee
23    attempted_update_on_finalized_fee_count: u64,
24
25    // Total prioritization fees included in this slot.
26    total_prioritization_fee: u64,
27
28    // The minimum prioritization fee of prioritized transactions in this slot.
29    min_prioritization_fee: Option<u64>,
30
31    // The maximum prioritization fee of prioritized transactions in this slot.
32    max_prioritization_fee: u64,
33
34    // Accumulated time spent on tracking prioritization fee for each slot.
35    total_update_elapsed_us: u64,
36}
37
38impl PrioritizationFeeMetrics {
39    fn accumulate_total_prioritization_fee(&mut self, val: u64) {
40        saturating_add_assign!(self.total_prioritization_fee, val);
41    }
42
43    fn accumulate_total_update_elapsed_us(&mut self, val: u64) {
44        saturating_add_assign!(self.total_update_elapsed_us, val);
45    }
46
47    fn increment_attempted_update_on_finalized_fee_count(&mut self, val: u64) {
48        saturating_add_assign!(self.attempted_update_on_finalized_fee_count, val);
49    }
50
51    fn update_prioritization_fee(&mut self, fee: u64) {
52        if fee == 0 {
53            saturating_add_assign!(self.non_prioritized_transactions_count, 1);
54            return;
55        }
56
57        // update prioritized transaction fee metrics.
58        saturating_add_assign!(self.prioritized_transactions_count, 1);
59
60        self.max_prioritization_fee = self.max_prioritization_fee.max(fee);
61
62        self.min_prioritization_fee = Some(
63            self.min_prioritization_fee
64                .map_or(fee, |min_fee| min_fee.min(fee)),
65        );
66    }
67
68    fn report(&self, slot: Slot) {
69        datapoint_info!(
70            "block_prioritization_fee",
71            ("slot", slot as i64, i64),
72            (
73                "total_writable_accounts_count",
74                self.total_writable_accounts_count as i64,
75                i64
76            ),
77            (
78                "relevant_writable_accounts_count",
79                self.relevant_writable_accounts_count as i64,
80                i64
81            ),
82            (
83                "prioritized_transactions_count",
84                self.prioritized_transactions_count as i64,
85                i64
86            ),
87            (
88                "non_prioritized_transactions_count",
89                self.non_prioritized_transactions_count as i64,
90                i64
91            ),
92            (
93                "attempted_update_on_finalized_fee_count",
94                self.attempted_update_on_finalized_fee_count as i64,
95                i64
96            ),
97            (
98                "total_prioritization_fee",
99                self.total_prioritization_fee as i64,
100                i64
101            ),
102            (
103                "min_prioritization_fee",
104                self.min_prioritization_fee.unwrap_or(0) as i64,
105                i64
106            ),
107            (
108                "max_prioritization_fee",
109                self.max_prioritization_fee as i64,
110                i64
111            ),
112            (
113                "total_update_elapsed_us",
114                self.total_update_elapsed_us as i64,
115                i64
116            ),
117        );
118    }
119}
120
121#[derive(Debug)]
122pub enum PrioritizationFeeError {
123    // Not able to get account locks from sanitized transaction, which is required to update block
124    // minimum fees.
125    FailGetTransactionAccountLocks,
126
127    // Not able to read compute budget details, including compute-unit price, from transaction.
128    // Compute-unit price is required to update block minimum fees.
129    FailGetComputeBudgetDetails,
130
131    // Block is already finalized, trying to finalize it again is usually unexpected
132    BlockIsAlreadyFinalized,
133}
134
135/// Block minimum prioritization fee stats, includes the minimum prioritization fee for a transaction in this
136/// block; and the minimum fee for each writable account in all transactions in this block. The only relevant
137/// write account minimum fees are those greater than the block minimum transaction fee, because the minimum fee needed to land
138/// a transaction is determined by Max( min_transaction_fee, min_writable_account_fees(key), ...)
139#[derive(Debug)]
140pub struct PrioritizationFee {
141    // The minimum prioritization fee of transactions that landed in this block.
142    min_transaction_fee: u64,
143
144    // The minimum prioritization fee of each writable account in transactions in this block.
145    min_writable_account_fees: HashMap<Pubkey, u64>,
146
147    // Default to `false`, set to `true` when a block is completed, therefore the minimum fees recorded
148    // are finalized, and can be made available for use (e.g., RPC query)
149    is_finalized: bool,
150
151    // slot prioritization fee metrics
152    metrics: PrioritizationFeeMetrics,
153}
154
155impl Default for PrioritizationFee {
156    fn default() -> Self {
157        PrioritizationFee {
158            min_transaction_fee: u64::MAX,
159            min_writable_account_fees: HashMap::new(),
160            is_finalized: false,
161            metrics: PrioritizationFeeMetrics::default(),
162        }
163    }
164}
165
166impl PrioritizationFee {
167    /// Update self for minimum transaction fee in the block and minimum fee for each writable account.
168    pub fn update(&mut self, transaction_fee: u64, writable_accounts: Vec<Pubkey>) {
169        let (_, update_us) = measure_us!({
170            if !self.is_finalized {
171                if transaction_fee < self.min_transaction_fee {
172                    self.min_transaction_fee = transaction_fee;
173                }
174
175                for write_account in writable_accounts {
176                    self.min_writable_account_fees
177                        .entry(write_account)
178                        .and_modify(|write_lock_fee| {
179                            *write_lock_fee = std::cmp::min(*write_lock_fee, transaction_fee)
180                        })
181                        .or_insert(transaction_fee);
182                }
183
184                self.metrics
185                    .accumulate_total_prioritization_fee(transaction_fee);
186                self.metrics.update_prioritization_fee(transaction_fee);
187            } else {
188                self.metrics
189                    .increment_attempted_update_on_finalized_fee_count(1);
190            }
191        });
192
193        self.metrics.accumulate_total_update_elapsed_us(update_us);
194    }
195
196    /// Accounts that have minimum fees lesser or equal to the minimum fee in the block are redundant, they are
197    /// removed to reduce memory footprint when mark_block_completed() is called.
198    fn prune_irrelevant_writable_accounts(&mut self) {
199        self.metrics.total_writable_accounts_count = self.get_writable_accounts_count() as u64;
200        self.min_writable_account_fees
201            .retain(|_, account_fee| account_fee > &mut self.min_transaction_fee);
202        self.metrics.relevant_writable_accounts_count = self.get_writable_accounts_count() as u64;
203    }
204
205    pub fn mark_block_completed(&mut self) -> Result<(), PrioritizationFeeError> {
206        if self.is_finalized {
207            return Err(PrioritizationFeeError::BlockIsAlreadyFinalized);
208        }
209        self.prune_irrelevant_writable_accounts();
210        self.is_finalized = true;
211        Ok(())
212    }
213
214    pub fn get_min_transaction_fee(&self) -> Option<u64> {
215        (self.min_transaction_fee != u64::MAX).then_some(self.min_transaction_fee)
216    }
217
218    pub fn get_writable_account_fee(&self, key: &Pubkey) -> Option<u64> {
219        self.min_writable_account_fees.get(key).copied()
220    }
221
222    pub fn get_writable_account_fees(&self) -> impl Iterator<Item = (&Pubkey, &u64)> {
223        self.min_writable_account_fees.iter()
224    }
225
226    pub fn get_writable_accounts_count(&self) -> usize {
227        self.min_writable_account_fees.len()
228    }
229
230    pub fn is_finalized(&self) -> bool {
231        self.is_finalized
232    }
233
234    pub fn report_metrics(&self, slot: Slot) {
235        self.metrics.report(slot);
236
237        // report this slot's min_transaction_fee and top 10 min_writable_account_fees
238        let min_transaction_fee = self.get_min_transaction_fee().unwrap_or(0);
239        let mut accounts_fees: Vec<_> = self.get_writable_account_fees().collect();
240        accounts_fees.sort_by(|lh, rh| rh.1.cmp(lh.1));
241        datapoint_info!(
242            "block_min_prioritization_fee",
243            ("slot", slot as i64, i64),
244            ("entity", "block", String),
245            ("min_prioritization_fee", min_transaction_fee as i64, i64),
246        );
247        for (account_key, fee) in accounts_fees.iter().take(10) {
248            datapoint_trace!(
249                "block_min_prioritization_fee",
250                ("slot", slot as i64, i64),
251                ("entity", account_key.to_string(), String),
252                ("min_prioritization_fee", **fee as i64, i64),
253            );
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use {super::*, solana_pubkey::Pubkey};
261
262    #[test]
263    fn test_update_prioritization_fee() {
264        solana_logger::setup();
265        let write_account_a = Pubkey::new_unique();
266        let write_account_b = Pubkey::new_unique();
267        let write_account_c = Pubkey::new_unique();
268
269        let mut prioritization_fee = PrioritizationFee::default();
270        assert!(prioritization_fee.get_min_transaction_fee().is_none());
271
272        // Assert for 1st transaction
273        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
274        // -----------------------------------------------------------------------
275        // [5,   a, b             ]  -->  [5,     5,         5,         nil      ]
276        {
277            prioritization_fee.update(5, vec![write_account_a, write_account_b]);
278            assert_eq!(5, prioritization_fee.get_min_transaction_fee().unwrap());
279            assert_eq!(
280                5,
281                prioritization_fee
282                    .get_writable_account_fee(&write_account_a)
283                    .unwrap()
284            );
285            assert_eq!(
286                5,
287                prioritization_fee
288                    .get_writable_account_fee(&write_account_b)
289                    .unwrap()
290            );
291            assert!(prioritization_fee
292                .get_writable_account_fee(&write_account_c)
293                .is_none());
294        }
295
296        // Assert for second transaction:
297        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
298        // -----------------------------------------------------------------------
299        // [9,      b, c          ]  -->  [5,     5,         5,         9        ]
300        {
301            prioritization_fee.update(9, vec![write_account_b, write_account_c]);
302            assert_eq!(5, prioritization_fee.get_min_transaction_fee().unwrap());
303            assert_eq!(
304                5,
305                prioritization_fee
306                    .get_writable_account_fee(&write_account_a)
307                    .unwrap()
308            );
309            assert_eq!(
310                5,
311                prioritization_fee
312                    .get_writable_account_fee(&write_account_b)
313                    .unwrap()
314            );
315            assert_eq!(
316                9,
317                prioritization_fee
318                    .get_writable_account_fee(&write_account_c)
319                    .unwrap()
320            );
321        }
322
323        // Assert for third transaction:
324        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
325        // -----------------------------------------------------------------------
326        // [2,   a,    c          ]  -->  [2,     2,         5,         2        ]
327        {
328            prioritization_fee.update(2, vec![write_account_a, write_account_c]);
329            assert_eq!(2, prioritization_fee.get_min_transaction_fee().unwrap());
330            assert_eq!(
331                2,
332                prioritization_fee
333                    .get_writable_account_fee(&write_account_a)
334                    .unwrap()
335            );
336            assert_eq!(
337                5,
338                prioritization_fee
339                    .get_writable_account_fee(&write_account_b)
340                    .unwrap()
341            );
342            assert_eq!(
343                2,
344                prioritization_fee
345                    .get_writable_account_fee(&write_account_c)
346                    .unwrap()
347            );
348        }
349
350        // assert after prune, account a and c should be removed from cache to save space
351        {
352            prioritization_fee.prune_irrelevant_writable_accounts();
353            assert_eq!(1, prioritization_fee.min_writable_account_fees.len());
354            assert_eq!(2, prioritization_fee.get_min_transaction_fee().unwrap());
355            assert!(prioritization_fee
356                .get_writable_account_fee(&write_account_a)
357                .is_none());
358            assert_eq!(
359                5,
360                prioritization_fee
361                    .get_writable_account_fee(&write_account_b)
362                    .unwrap()
363            );
364            assert!(prioritization_fee
365                .get_writable_account_fee(&write_account_c)
366                .is_none());
367        }
368    }
369
370    #[test]
371    fn test_mark_block_completed() {
372        let mut prioritization_fee = PrioritizationFee::default();
373
374        assert!(prioritization_fee.mark_block_completed().is_ok());
375        assert!(prioritization_fee.mark_block_completed().is_err());
376    }
377}