solana_runtime/
prioritization_fee.rs

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