miraland_runtime/
prioritization_fee.rs

1use {
2    miraland_measure::measure,
3    miraland_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(
169        &mut self,
170        transaction_fee: u64,
171        writable_accounts: &[Pubkey],
172    ) -> Result<(), PrioritizationFeeError> {
173        let (_, update_time) = measure!(
174            {
175                if !self.is_finalized {
176                    if transaction_fee < self.min_transaction_fee {
177                        self.min_transaction_fee = transaction_fee;
178                    }
179
180                    for write_account in writable_accounts.iter() {
181                        self.min_writable_account_fees
182                            .entry(*write_account)
183                            .and_modify(|write_lock_fee| {
184                                *write_lock_fee = std::cmp::min(*write_lock_fee, transaction_fee)
185                            })
186                            .or_insert(transaction_fee);
187                    }
188
189                    self.metrics
190                        .accumulate_total_prioritization_fee(transaction_fee);
191                    self.metrics.update_prioritization_fee(transaction_fee);
192                } else {
193                    self.metrics
194                        .increment_attempted_update_on_finalized_fee_count(1);
195                }
196            },
197            "update_time",
198        );
199
200        self.metrics
201            .accumulate_total_update_elapsed_us(update_time.as_us());
202        Ok(())
203    }
204
205    /// Accounts that have minimum fees lesser or equal to the minimum fee in the block are redundant, they are
206    /// removed to reduce memory footprint when mark_block_completed() is called.
207    fn prune_irrelevant_writable_accounts(&mut self) {
208        self.metrics.total_writable_accounts_count = self.get_writable_accounts_count() as u64;
209        self.min_writable_account_fees
210            .retain(|_, account_fee| account_fee > &mut self.min_transaction_fee);
211        self.metrics.relevant_writable_accounts_count = self.get_writable_accounts_count() as u64;
212    }
213
214    pub fn mark_block_completed(&mut self) -> Result<(), PrioritizationFeeError> {
215        if self.is_finalized {
216            return Err(PrioritizationFeeError::BlockIsAlreadyFinalized);
217        }
218        self.prune_irrelevant_writable_accounts();
219        self.is_finalized = true;
220        Ok(())
221    }
222
223    pub fn get_min_transaction_fee(&self) -> Option<u64> {
224        (self.min_transaction_fee != u64::MAX).then_some(self.min_transaction_fee)
225    }
226
227    pub fn get_writable_account_fee(&self, key: &Pubkey) -> Option<u64> {
228        self.min_writable_account_fees.get(key).copied()
229    }
230
231    pub fn get_writable_account_fees(&self) -> impl Iterator<Item = (&Pubkey, &u64)> {
232        self.min_writable_account_fees.iter()
233    }
234
235    pub fn get_writable_accounts_count(&self) -> usize {
236        self.min_writable_account_fees.len()
237    }
238
239    pub fn is_finalized(&self) -> bool {
240        self.is_finalized
241    }
242
243    pub fn report_metrics(&self, slot: Slot) {
244        self.metrics.report(slot);
245
246        // report this slot's min_transaction_fee and top 10 min_writable_account_fees
247        let min_transaction_fee = self.get_min_transaction_fee().unwrap_or(0);
248        let mut accounts_fees: Vec<_> = self.get_writable_account_fees().collect();
249        accounts_fees.sort_by(|lh, rh| rh.1.cmp(lh.1));
250        datapoint_info!(
251            "block_min_prioritization_fee",
252            ("slot", slot as i64, i64),
253            ("entity", "block", String),
254            ("min_prioritization_fee", min_transaction_fee as i64, i64),
255        );
256        for (account_key, fee) in accounts_fees.iter().take(10) {
257            datapoint_trace!(
258                "block_min_prioritization_fee",
259                ("slot", slot as i64, i64),
260                ("entity", account_key.to_string(), String),
261                ("min_prioritization_fee", **fee as i64, i64),
262            );
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use {super::*, miraland_sdk::pubkey::Pubkey};
270
271    #[test]
272    fn test_update_prioritization_fee() {
273        miraland_logger::setup();
274        let write_account_a = Pubkey::new_unique();
275        let write_account_b = Pubkey::new_unique();
276        let write_account_c = Pubkey::new_unique();
277
278        let mut prioritization_fee = PrioritizationFee::default();
279        assert!(prioritization_fee.get_min_transaction_fee().is_none());
280
281        // Assert for 1st transaction
282        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
283        // -----------------------------------------------------------------------
284        // [5,   a, b             ]  -->  [5,     5,         5,         nil      ]
285        {
286            assert!(prioritization_fee
287                .update(5, &[write_account_a, write_account_b])
288                .is_ok());
289            assert_eq!(5, prioritization_fee.get_min_transaction_fee().unwrap());
290            assert_eq!(
291                5,
292                prioritization_fee
293                    .get_writable_account_fee(&write_account_a)
294                    .unwrap()
295            );
296            assert_eq!(
297                5,
298                prioritization_fee
299                    .get_writable_account_fee(&write_account_b)
300                    .unwrap()
301            );
302            assert!(prioritization_fee
303                .get_writable_account_fee(&write_account_c)
304                .is_none());
305        }
306
307        // Assert for second transaction:
308        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
309        // -----------------------------------------------------------------------
310        // [9,      b, c          ]  -->  [5,     5,         5,         9        ]
311        {
312            assert!(prioritization_fee
313                .update(9, &[write_account_b, write_account_c])
314                .is_ok());
315            assert_eq!(5, prioritization_fee.get_min_transaction_fee().unwrap());
316            assert_eq!(
317                5,
318                prioritization_fee
319                    .get_writable_account_fee(&write_account_a)
320                    .unwrap()
321            );
322            assert_eq!(
323                5,
324                prioritization_fee
325                    .get_writable_account_fee(&write_account_b)
326                    .unwrap()
327            );
328            assert_eq!(
329                9,
330                prioritization_fee
331                    .get_writable_account_fee(&write_account_c)
332                    .unwrap()
333            );
334        }
335
336        // Assert for third transaction:
337        // [fee, write_accounts...]  -->  [block, account_a, account_b, account_c]
338        // -----------------------------------------------------------------------
339        // [2,   a,    c          ]  -->  [2,     2,         5,         2        ]
340        {
341            assert!(prioritization_fee
342                .update(2, &[write_account_a, write_account_c])
343                .is_ok());
344            assert_eq!(2, prioritization_fee.get_min_transaction_fee().unwrap());
345            assert_eq!(
346                2,
347                prioritization_fee
348                    .get_writable_account_fee(&write_account_a)
349                    .unwrap()
350            );
351            assert_eq!(
352                5,
353                prioritization_fee
354                    .get_writable_account_fee(&write_account_b)
355                    .unwrap()
356            );
357            assert_eq!(
358                2,
359                prioritization_fee
360                    .get_writable_account_fee(&write_account_c)
361                    .unwrap()
362            );
363        }
364
365        // assert after prune, account a and c should be removed from cache to save space
366        {
367            prioritization_fee.prune_irrelevant_writable_accounts();
368            assert_eq!(1, prioritization_fee.min_writable_account_fees.len());
369            assert_eq!(2, prioritization_fee.get_min_transaction_fee().unwrap());
370            assert!(prioritization_fee
371                .get_writable_account_fee(&write_account_a)
372                .is_none());
373            assert_eq!(
374                5,
375                prioritization_fee
376                    .get_writable_account_fee(&write_account_b)
377                    .unwrap()
378            );
379            assert!(prioritization_fee
380                .get_writable_account_fee(&write_account_c)
381                .is_none());
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}