atlas_runtime/
prioritization_fee.rs

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