solana_runtime/
cost_tracker.rs

1//! `cost_tracker` keeps tracking transaction cost per chained accounts as well as for entire block
2//! The main functions are:
3//! - would_fit(&tx_cost), immutable function to test if tx with tx_cost would fit into current block
4//! - add_transaction_cost(&tx_cost), mutable function to accumulate tx_cost to tracker.
5//!
6use {
7    crate::{block_cost_limits::*, cost_model::TransactionCost},
8    solana_sdk::{clock::Slot, pubkey::Pubkey, saturating_add_assign},
9    std::{cmp::Ordering, collections::HashMap},
10};
11
12const WRITABLE_ACCOUNTS_PER_BLOCK: usize = 512;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CostTrackerError {
16    /// would exceed block max limit
17    WouldExceedBlockMaxLimit,
18
19    /// would exceed vote max limit
20    WouldExceedVoteMaxLimit,
21
22    /// would exceed account max limit
23    WouldExceedAccountMaxLimit,
24
25    /// would exceed account data block limit
26    WouldExceedAccountDataBlockLimit,
27
28    /// would exceed account data total limit
29    WouldExceedAccountDataTotalLimit,
30}
31
32#[derive(AbiExample, Debug)]
33pub struct CostTracker {
34    account_cost_limit: u64,
35    block_cost_limit: u64,
36    vote_cost_limit: u64,
37    cost_by_writable_accounts: HashMap<Pubkey, u64>,
38    block_cost: u64,
39    vote_cost: u64,
40    transaction_count: u64,
41    account_data_size: u64,
42
43    /// The amount of total account data size remaining.  If `Some`, then do not add transactions
44    /// that would cause `account_data_size` to exceed this limit.
45    account_data_size_limit: Option<u64>,
46}
47
48impl Default for CostTracker {
49    fn default() -> Self {
50        // Clippy doesn't like asserts in const contexts, so need to explicitly allow them.  For
51        // more info, see this issue: https://github.com/rust-lang/rust-clippy/issues/8159
52        #![allow(clippy::assertions_on_constants)]
53        const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS);
54        const _: () = assert!(MAX_VOTE_UNITS <= MAX_BLOCK_UNITS);
55
56        Self {
57            account_cost_limit: MAX_WRITABLE_ACCOUNT_UNITS,
58            block_cost_limit: MAX_BLOCK_UNITS,
59            vote_cost_limit: MAX_VOTE_UNITS,
60            cost_by_writable_accounts: HashMap::with_capacity(WRITABLE_ACCOUNTS_PER_BLOCK),
61            block_cost: 0,
62            vote_cost: 0,
63            transaction_count: 0,
64            account_data_size: 0,
65            account_data_size_limit: None,
66        }
67    }
68}
69
70impl CostTracker {
71    /// Construct and new CostTracker and set the account data size limit.
72    #[must_use]
73    pub fn new_with_account_data_size_limit(account_data_size_limit: Option<u64>) -> Self {
74        Self {
75            account_data_size_limit,
76            ..Self::default()
77        }
78    }
79
80    /// allows to adjust limits initiated during construction
81    pub fn set_limits(
82        &mut self,
83        account_cost_limit: u64,
84        block_cost_limit: u64,
85        vote_cost_limit: u64,
86    ) {
87        self.account_cost_limit = account_cost_limit;
88        self.block_cost_limit = block_cost_limit;
89        self.vote_cost_limit = vote_cost_limit;
90    }
91
92    pub fn try_add(&mut self, tx_cost: &TransactionCost) -> Result<u64, CostTrackerError> {
93        self.would_fit(tx_cost)?;
94        self.add_transaction_cost(tx_cost);
95        Ok(self.block_cost)
96    }
97
98    /// Using user requested compute-units to track cost.
99    pub fn try_add_requested_cus(
100        &mut self,
101        write_lock_accounts: &[Pubkey],
102        requested_cus: u64,
103        is_vote: bool,
104    ) -> Result<u64, CostTrackerError> {
105        self.would_fit_internal(write_lock_accounts.iter(), requested_cus, is_vote, 0)?;
106        self.add_transaction_cost_internal(write_lock_accounts.iter(), requested_cus, is_vote, 0);
107        Ok(self.block_cost)
108    }
109
110    pub fn update_execution_cost(
111        &mut self,
112        estimated_tx_cost: &TransactionCost,
113        actual_execution_units: u64,
114    ) {
115        let estimated_execution_units = estimated_tx_cost.bpf_execution_cost;
116        match actual_execution_units.cmp(&estimated_execution_units) {
117            Ordering::Equal => (),
118            Ordering::Greater => {
119                self.add_transaction_execution_cost(
120                    estimated_tx_cost,
121                    actual_execution_units - estimated_execution_units,
122                );
123            }
124            Ordering::Less => {
125                self.sub_transaction_execution_cost(
126                    estimated_tx_cost,
127                    estimated_execution_units - actual_execution_units,
128                );
129            }
130        }
131    }
132
133    pub fn remove(&mut self, tx_cost: &TransactionCost) {
134        self.remove_transaction_cost(tx_cost);
135    }
136
137    pub fn block_cost(&self) -> u64 {
138        self.block_cost
139    }
140
141    pub fn transaction_count(&self) -> u64 {
142        self.transaction_count
143    }
144
145    pub fn report_stats(&self, bank_slot: Slot) {
146        // skip reporting if block is empty
147        if self.transaction_count == 0 {
148            return;
149        }
150
151        let (costliest_account, costliest_account_cost) = self.find_costliest_account();
152
153        datapoint_info!(
154            "cost_tracker_stats",
155            ("bank_slot", bank_slot as i64, i64),
156            ("block_cost", self.block_cost as i64, i64),
157            ("vote_cost", self.vote_cost as i64, i64),
158            ("transaction_count", self.transaction_count as i64, i64),
159            ("number_of_accounts", self.number_of_accounts() as i64, i64),
160            ("costliest_account", costliest_account.to_string(), String),
161            ("costliest_account_cost", costliest_account_cost as i64, i64),
162            ("account_data_size", self.account_data_size, i64),
163        );
164    }
165
166    fn find_costliest_account(&self) -> (Pubkey, u64) {
167        self.cost_by_writable_accounts
168            .iter()
169            .max_by_key(|(_, &cost)| cost)
170            .map(|(&pubkey, &cost)| (pubkey, cost))
171            .unwrap_or_default()
172    }
173
174    fn would_fit(&self, tx_cost: &TransactionCost) -> Result<(), CostTrackerError> {
175        self.would_fit_internal(
176            tx_cost.writable_accounts.iter(),
177            tx_cost.sum(),
178            tx_cost.is_simple_vote,
179            tx_cost.account_data_size,
180        )
181    }
182
183    fn would_fit_internal<'a>(
184        &self,
185        write_lock_accounts: impl Iterator<Item = &'a Pubkey>,
186        cost: u64,
187        is_vote: bool,
188        account_data_size: u64,
189    ) -> Result<(), CostTrackerError> {
190        let vote_cost = if is_vote { cost } else { 0 };
191
192        // check against the total package cost
193        if self.block_cost.saturating_add(cost) > self.block_cost_limit {
194            return Err(CostTrackerError::WouldExceedBlockMaxLimit);
195        }
196
197        // if vote transaction, check if it exceeds vote_transaction_limit
198        if self.vote_cost.saturating_add(vote_cost) > self.vote_cost_limit {
199            return Err(CostTrackerError::WouldExceedVoteMaxLimit);
200        }
201
202        // check if the transaction itself is more costly than the account_cost_limit
203        if cost > self.account_cost_limit {
204            return Err(CostTrackerError::WouldExceedAccountMaxLimit);
205        }
206
207        // NOTE: Check if the total accounts data size is exceeded *before* the block accounts data
208        // size.  This way, transactions are not unnecessarily retried.
209        let account_data_size = self.account_data_size.saturating_add(account_data_size);
210        if let Some(account_data_size_limit) = self.account_data_size_limit {
211            if account_data_size > account_data_size_limit {
212                return Err(CostTrackerError::WouldExceedAccountDataTotalLimit);
213            }
214        }
215
216        if account_data_size > MAX_ACCOUNT_DATA_BLOCK_LEN {
217            return Err(CostTrackerError::WouldExceedAccountDataBlockLimit);
218        }
219
220        // check each account against account_cost_limit,
221        for account_key in write_lock_accounts {
222            match self.cost_by_writable_accounts.get(account_key) {
223                Some(chained_cost) => {
224                    if chained_cost.saturating_add(cost) > self.account_cost_limit {
225                        return Err(CostTrackerError::WouldExceedAccountMaxLimit);
226                    } else {
227                        continue;
228                    }
229                }
230                None => continue,
231            }
232        }
233
234        Ok(())
235    }
236
237    fn add_transaction_cost(&mut self, tx_cost: &TransactionCost) {
238        self.add_transaction_cost_internal(
239            tx_cost.writable_accounts.iter(),
240            tx_cost.sum(),
241            tx_cost.is_simple_vote,
242            tx_cost.account_data_size,
243        )
244    }
245
246    fn add_transaction_cost_internal<'a>(
247        &mut self,
248        write_lock_accounts: impl Iterator<Item = &'a Pubkey>,
249        cost: u64,
250        is_vote: bool,
251        account_data_size: u64,
252    ) {
253        self.add_transaction_execution_cost_internal(write_lock_accounts, is_vote, cost);
254        saturating_add_assign!(self.account_data_size, account_data_size);
255        saturating_add_assign!(self.transaction_count, 1);
256    }
257
258    fn remove_transaction_cost(&mut self, tx_cost: &TransactionCost) {
259        let cost = tx_cost.sum();
260        self.sub_transaction_execution_cost(tx_cost, cost);
261        self.account_data_size = self
262            .account_data_size
263            .saturating_sub(tx_cost.account_data_size);
264        self.transaction_count = self.transaction_count.saturating_sub(1);
265    }
266
267    /// Apply additional actual execution units to cost_tracker
268    fn add_transaction_execution_cost(&mut self, tx_cost: &TransactionCost, adjustment: u64) {
269        self.add_transaction_execution_cost_internal(
270            tx_cost.writable_accounts.iter(),
271            tx_cost.is_simple_vote,
272            adjustment,
273        )
274    }
275
276    fn add_transaction_execution_cost_internal<'a>(
277        &mut self,
278        write_lock_accounts: impl Iterator<Item = &'a Pubkey>,
279        is_vote: bool,
280        adjustment: u64,
281    ) {
282        for account_key in write_lock_accounts {
283            let account_cost = self
284                .cost_by_writable_accounts
285                .entry(*account_key)
286                .or_insert(0);
287            *account_cost = account_cost.saturating_add(adjustment);
288        }
289        self.block_cost = self.block_cost.saturating_add(adjustment);
290        if is_vote {
291            self.vote_cost = self.vote_cost.saturating_add(adjustment);
292        }
293    }
294
295    /// Substract extra execution units from cost_tracker
296    fn sub_transaction_execution_cost(&mut self, tx_cost: &TransactionCost, adjustment: u64) {
297        for account_key in tx_cost.writable_accounts.iter() {
298            let account_cost = self
299                .cost_by_writable_accounts
300                .entry(*account_key)
301                .or_insert(0);
302            *account_cost = account_cost.saturating_sub(adjustment);
303        }
304        self.block_cost = self.block_cost.saturating_sub(adjustment);
305        if tx_cost.is_simple_vote {
306            self.vote_cost = self.vote_cost.saturating_sub(adjustment);
307        }
308    }
309
310    /// count number of none-zero CU accounts
311    fn number_of_accounts(&self) -> usize {
312        self.cost_by_writable_accounts
313            .values()
314            .filter(|units| **units > 0)
315            .count()
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use {
322        super::*,
323        crate::{
324            bank::Bank,
325            genesis_utils::{create_genesis_config, GenesisConfigInfo},
326        },
327        solana_sdk::{
328            hash::Hash,
329            signature::{Keypair, Signer},
330            system_transaction,
331            transaction::{
332                MessageHash, SanitizedTransaction, SimpleAddressLoader, VersionedTransaction,
333            },
334        },
335        solana_vote_program::vote_transaction,
336        std::{cmp, sync::Arc},
337    };
338
339    impl CostTracker {
340        fn new(
341            account_cost_limit: u64,
342            block_cost_limit: u64,
343            vote_cost_limit: u64,
344            account_data_size_limit: Option<u64>,
345        ) -> Self {
346            assert!(account_cost_limit <= block_cost_limit);
347            assert!(vote_cost_limit <= block_cost_limit);
348            Self {
349                account_cost_limit,
350                block_cost_limit,
351                vote_cost_limit,
352                account_data_size_limit,
353                ..Self::default()
354            }
355        }
356    }
357
358    fn test_setup() -> (Keypair, Hash) {
359        solana_logger::setup();
360        let GenesisConfigInfo {
361            genesis_config,
362            mint_keypair,
363            ..
364        } = create_genesis_config(10);
365        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
366        let start_hash = bank.last_blockhash();
367        (mint_keypair, start_hash)
368    }
369
370    fn build_simple_transaction(
371        mint_keypair: &Keypair,
372        start_hash: &Hash,
373    ) -> (SanitizedTransaction, TransactionCost) {
374        let keypair = Keypair::new();
375        let simple_transaction = SanitizedTransaction::from_transaction_for_tests(
376            system_transaction::transfer(mint_keypair, &keypair.pubkey(), 2, *start_hash),
377        );
378        let mut tx_cost = TransactionCost::new_with_capacity(1);
379        tx_cost.bpf_execution_cost = 5;
380        tx_cost.writable_accounts.push(mint_keypair.pubkey());
381
382        (simple_transaction, tx_cost)
383    }
384
385    fn build_simple_vote_transaction(
386        mint_keypair: &Keypair,
387        start_hash: &Hash,
388    ) -> (SanitizedTransaction, TransactionCost) {
389        let keypair = Keypair::new();
390        let transaction = vote_transaction::new_vote_transaction(
391            vec![42],
392            Hash::default(),
393            *start_hash,
394            mint_keypair,
395            &keypair,
396            &keypair,
397            None,
398        );
399        let vote_transaction = SanitizedTransaction::try_create(
400            VersionedTransaction::from(transaction),
401            MessageHash::Compute,
402            Some(true),
403            SimpleAddressLoader::Disabled,
404            true, // require_static_program_ids
405        )
406        .unwrap();
407        let mut tx_cost = TransactionCost::new_with_capacity(1);
408        tx_cost.bpf_execution_cost = 10;
409        tx_cost.writable_accounts.push(mint_keypair.pubkey());
410        tx_cost.is_simple_vote = true;
411
412        (vote_transaction, tx_cost)
413    }
414
415    #[test]
416    fn test_cost_tracker_initialization() {
417        let testee = CostTracker::new(10, 11, 8, None);
418        assert_eq!(10, testee.account_cost_limit);
419        assert_eq!(11, testee.block_cost_limit);
420        assert_eq!(8, testee.vote_cost_limit);
421        assert_eq!(0, testee.cost_by_writable_accounts.len());
422        assert_eq!(0, testee.block_cost);
423    }
424
425    #[test]
426    fn test_cost_tracker_ok_add_one() {
427        let (mint_keypair, start_hash) = test_setup();
428        let (_tx, tx_cost) = build_simple_transaction(&mint_keypair, &start_hash);
429        let cost = tx_cost.sum();
430
431        // build testee to have capacity for one simple transaction
432        let mut testee = CostTracker::new(cost, cost, cost, None);
433        assert!(testee.would_fit(&tx_cost).is_ok());
434        testee.add_transaction_cost(&tx_cost);
435        assert_eq!(cost, testee.block_cost);
436        assert_eq!(0, testee.vote_cost);
437        let (_costliest_account, costliest_account_cost) = testee.find_costliest_account();
438        assert_eq!(cost, costliest_account_cost);
439    }
440
441    #[test]
442    fn test_cost_tracker_ok_add_one_vote() {
443        let (mint_keypair, start_hash) = test_setup();
444        let (_tx, tx_cost) = build_simple_vote_transaction(&mint_keypair, &start_hash);
445        let cost = tx_cost.sum();
446
447        // build testee to have capacity for one simple transaction
448        let mut testee = CostTracker::new(cost, cost, cost, None);
449        assert!(testee.would_fit(&tx_cost).is_ok());
450        testee.add_transaction_cost(&tx_cost);
451        assert_eq!(cost, testee.block_cost);
452        assert_eq!(cost, testee.vote_cost);
453        let (_costliest_account, costliest_account_cost) = testee.find_costliest_account();
454        assert_eq!(cost, costliest_account_cost);
455    }
456
457    #[test]
458    fn test_cost_tracker_add_data() {
459        let (mint_keypair, start_hash) = test_setup();
460        let (_tx, mut tx_cost) = build_simple_transaction(&mint_keypair, &start_hash);
461        tx_cost.account_data_size = 1;
462        let cost = tx_cost.sum();
463
464        // build testee to have capacity for one simple transaction
465        let mut testee = CostTracker::new(cost, cost, cost, None);
466        assert!(testee.would_fit(&tx_cost).is_ok());
467        let old = testee.account_data_size;
468        testee.add_transaction_cost(&tx_cost);
469        assert_eq!(old + 1, testee.account_data_size);
470    }
471
472    #[test]
473    fn test_cost_tracker_ok_add_two_same_accounts() {
474        let (mint_keypair, start_hash) = test_setup();
475        // build two transactions with same signed account
476        let (_tx1, tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
477        let cost1 = tx_cost1.sum();
478        let (_tx2, tx_cost2) = build_simple_transaction(&mint_keypair, &start_hash);
479        let cost2 = tx_cost2.sum();
480
481        // build testee to have capacity for two simple transactions, with same accounts
482        let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2, None);
483        {
484            assert!(testee.would_fit(&tx_cost1).is_ok());
485            testee.add_transaction_cost(&tx_cost1);
486        }
487        {
488            assert!(testee.would_fit(&tx_cost2).is_ok());
489            testee.add_transaction_cost(&tx_cost2);
490        }
491        assert_eq!(cost1 + cost2, testee.block_cost);
492        assert_eq!(1, testee.cost_by_writable_accounts.len());
493        let (_ccostliest_account, costliest_account_cost) = testee.find_costliest_account();
494        assert_eq!(cost1 + cost2, costliest_account_cost);
495    }
496
497    #[test]
498    fn test_cost_tracker_ok_add_two_diff_accounts() {
499        let (mint_keypair, start_hash) = test_setup();
500        // build two transactions with diff accounts
501        let second_account = Keypair::new();
502        let (_tx1, tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
503        let cost1 = tx_cost1.sum();
504        let (_tx2, tx_cost2) = build_simple_transaction(&second_account, &start_hash);
505        let cost2 = tx_cost2.sum();
506
507        // build testee to have capacity for two simple transactions, with same accounts
508        let mut testee =
509            CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2, cost1 + cost2, None);
510        {
511            assert!(testee.would_fit(&tx_cost1).is_ok());
512            testee.add_transaction_cost(&tx_cost1);
513        }
514        {
515            assert!(testee.would_fit(&tx_cost2).is_ok());
516            testee.add_transaction_cost(&tx_cost2);
517        }
518        assert_eq!(cost1 + cost2, testee.block_cost);
519        assert_eq!(2, testee.cost_by_writable_accounts.len());
520        let (_ccostliest_account, costliest_account_cost) = testee.find_costliest_account();
521        assert_eq!(std::cmp::max(cost1, cost2), costliest_account_cost);
522    }
523
524    #[test]
525    fn test_cost_tracker_chain_reach_limit() {
526        let (mint_keypair, start_hash) = test_setup();
527        // build two transactions with same signed account
528        let (_tx1, tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
529        let cost1 = tx_cost1.sum();
530        let (_tx2, tx_cost2) = build_simple_transaction(&mint_keypair, &start_hash);
531        let cost2 = tx_cost2.sum();
532
533        // build testee to have capacity for two simple transactions, but not for same accounts
534        let mut testee =
535            CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2, cost1 + cost2, None);
536        // should have room for first transaction
537        {
538            assert!(testee.would_fit(&tx_cost1).is_ok());
539            testee.add_transaction_cost(&tx_cost1);
540        }
541        // but no more sapce on the same chain (same signer account)
542        {
543            assert!(testee.would_fit(&tx_cost2).is_err());
544        }
545    }
546
547    #[test]
548    fn test_cost_tracker_reach_limit() {
549        let (mint_keypair, start_hash) = test_setup();
550        // build two transactions with diff accounts
551        let second_account = Keypair::new();
552        let (_tx1, tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
553        let cost1 = tx_cost1.sum();
554        let (_tx2, tx_cost2) = build_simple_transaction(&second_account, &start_hash);
555        let cost2 = tx_cost2.sum();
556
557        // build testee to have capacity for each chain, but not enough room for both transactions
558        let mut testee = CostTracker::new(
559            cmp::max(cost1, cost2),
560            cost1 + cost2 - 1,
561            cost1 + cost2 - 1,
562            None,
563        );
564        // should have room for first transaction
565        {
566            assert!(testee.would_fit(&tx_cost1).is_ok());
567            testee.add_transaction_cost(&tx_cost1);
568        }
569        // but no more room for package as whole
570        {
571            assert!(testee.would_fit(&tx_cost2).is_err());
572        }
573    }
574
575    #[test]
576    fn test_cost_tracker_reach_vote_limit() {
577        let (mint_keypair, start_hash) = test_setup();
578        // build two mocking vote transactions with diff accounts
579        let second_account = Keypair::new();
580        let (_tx1, tx_cost1) = build_simple_vote_transaction(&mint_keypair, &start_hash);
581        let (_tx2, tx_cost2) = build_simple_vote_transaction(&second_account, &start_hash);
582        let cost1 = tx_cost1.sum();
583        let cost2 = tx_cost2.sum();
584
585        // build testee to have capacity for each chain, but not enough room for both votes
586        let mut testee = CostTracker::new(
587            cmp::max(cost1, cost2),
588            cost1 + cost2,
589            cost1 + cost2 - 1,
590            None,
591        );
592        // should have room for first vote
593        {
594            assert!(testee.would_fit(&tx_cost1).is_ok());
595            testee.add_transaction_cost(&tx_cost1);
596        }
597        // but no more room for package as whole
598        {
599            assert!(testee.would_fit(&tx_cost2).is_err());
600        }
601        // however there is room for none-vote tx3
602        {
603            let third_account = Keypair::new();
604            let (_tx3, tx_cost3) = build_simple_transaction(&third_account, &start_hash);
605            assert!(testee.would_fit(&tx_cost3).is_ok());
606        }
607    }
608
609    #[test]
610    fn test_cost_tracker_reach_data_block_limit() {
611        let (mint_keypair, start_hash) = test_setup();
612        // build two transactions with diff accounts
613        let second_account = Keypair::new();
614        let (_tx1, mut tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
615        let (_tx2, mut tx_cost2) = build_simple_transaction(&second_account, &start_hash);
616        tx_cost1.account_data_size = MAX_ACCOUNT_DATA_BLOCK_LEN;
617        tx_cost2.account_data_size = MAX_ACCOUNT_DATA_BLOCK_LEN + 1;
618        let cost1 = tx_cost1.sum();
619        let cost2 = tx_cost2.sum();
620
621        // build testee that passes
622        let testee = CostTracker::new(
623            cmp::max(cost1, cost2),
624            cost1 + cost2 - 1,
625            cost1 + cost2 - 1,
626            None,
627        );
628        assert!(testee.would_fit(&tx_cost1).is_ok());
629        // data is too big
630        assert_eq!(
631            testee.would_fit(&tx_cost2),
632            Err(CostTrackerError::WouldExceedAccountDataBlockLimit),
633        );
634    }
635
636    #[test]
637    fn test_cost_tracker_reach_data_total_limit() {
638        let (mint_keypair, start_hash) = test_setup();
639        // build two transactions with diff accounts
640        let second_account = Keypair::new();
641        let (_tx1, mut tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
642        let (_tx2, mut tx_cost2) = build_simple_transaction(&second_account, &start_hash);
643        let remaining_account_data_size = 1234;
644        tx_cost1.account_data_size = remaining_account_data_size;
645        tx_cost2.account_data_size = remaining_account_data_size + 1;
646        let cost1 = tx_cost1.sum();
647        let cost2 = tx_cost2.sum();
648
649        // build testee that passes
650        let testee = CostTracker::new(
651            cmp::max(cost1, cost2),
652            cost1 + cost2 - 1,
653            cost1 + cost2 - 1,
654            Some(remaining_account_data_size),
655        );
656        assert!(testee.would_fit(&tx_cost1).is_ok());
657        // data is too big
658        assert_eq!(
659            testee.would_fit(&tx_cost2),
660            Err(CostTrackerError::WouldExceedAccountDataTotalLimit),
661        );
662    }
663
664    #[test]
665    fn test_cost_tracker_remove() {
666        let (mint_keypair, start_hash) = test_setup();
667        // build two transactions with diff accounts
668        let second_account = Keypair::new();
669        let (_tx1, tx_cost1) = build_simple_transaction(&mint_keypair, &start_hash);
670        let (_tx2, tx_cost2) = build_simple_transaction(&second_account, &start_hash);
671        let cost1 = tx_cost1.sum();
672        let cost2 = tx_cost2.sum();
673        // build testee
674        let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2, None);
675
676        assert!(testee.try_add(&tx_cost1).is_ok());
677        assert!(testee.try_add(&tx_cost2).is_ok());
678        assert_eq!(cost1 + cost2, testee.block_cost);
679
680        // removing a tx_cost affects block_cost
681        testee.remove(&tx_cost1);
682        assert_eq!(cost2, testee.block_cost);
683
684        // add back tx1
685        assert!(testee.try_add(&tx_cost1).is_ok());
686        assert_eq!(cost1 + cost2, testee.block_cost);
687
688        // cannot add tx1 again, cost limit would be exceeded
689        assert!(testee.try_add(&tx_cost1).is_err());
690    }
691
692    #[test]
693    fn test_cost_tracker_try_add_is_atomic() {
694        let acct1 = Pubkey::new_unique();
695        let acct2 = Pubkey::new_unique();
696        let acct3 = Pubkey::new_unique();
697        let cost = 100;
698        let account_max = cost * 2;
699        let block_max = account_max * 3; // for three accts
700
701        let mut testee = CostTracker::new(account_max, block_max, block_max, None);
702
703        // case 1: a tx writes to 3 accounts, should success, we will have:
704        // | acct1 | $cost |
705        // | acct2 | $cost |
706        // | acct3 | $cost |
707        // and block_cost = $cost
708        {
709            let tx_cost = TransactionCost {
710                writable_accounts: vec![acct1, acct2, acct3],
711                bpf_execution_cost: cost,
712                ..TransactionCost::default()
713            };
714            assert!(testee.try_add(&tx_cost).is_ok());
715            let (_costliest_account, costliest_account_cost) = testee.find_costliest_account();
716            assert_eq!(cost, testee.block_cost);
717            assert_eq!(3, testee.cost_by_writable_accounts.len());
718            assert_eq!(cost, costliest_account_cost);
719        }
720
721        // case 2: add tx writes to acct2 with $cost, should succeed, result to
722        // | acct1 | $cost |
723        // | acct2 | $cost * 2 |
724        // | acct3 | $cost |
725        // and block_cost = $cost * 2
726        {
727            let tx_cost = TransactionCost {
728                writable_accounts: vec![acct2],
729                bpf_execution_cost: cost,
730                ..TransactionCost::default()
731            };
732            assert!(testee.try_add(&tx_cost).is_ok());
733            let (costliest_account, costliest_account_cost) = testee.find_costliest_account();
734            assert_eq!(cost * 2, testee.block_cost);
735            assert_eq!(3, testee.cost_by_writable_accounts.len());
736            assert_eq!(cost * 2, costliest_account_cost);
737            assert_eq!(acct2, costliest_account);
738        }
739
740        // case 3: add tx writes to [acct1, acct2], acct2 exceeds limit, should failed atomically,
741        // we shoudl still have:
742        // | acct1 | $cost |
743        // | acct2 | $cost * 2 |
744        // | acct3 | $cost |
745        // and block_cost = $cost * 2
746        {
747            let tx_cost = TransactionCost {
748                writable_accounts: vec![acct1, acct2],
749                bpf_execution_cost: cost,
750                ..TransactionCost::default()
751            };
752            assert!(testee.try_add(&tx_cost).is_err());
753            let (costliest_account, costliest_account_cost) = testee.find_costliest_account();
754            assert_eq!(cost * 2, testee.block_cost);
755            assert_eq!(3, testee.cost_by_writable_accounts.len());
756            assert_eq!(cost * 2, costliest_account_cost);
757            assert_eq!(acct2, costliest_account);
758        }
759    }
760
761    #[test]
762    fn test_adjust_transaction_execution_cost() {
763        let acct1 = Pubkey::new_unique();
764        let acct2 = Pubkey::new_unique();
765        let acct3 = Pubkey::new_unique();
766        let cost = 100;
767        let account_max = cost * 2;
768        let block_max = account_max * 3; // for three accts
769
770        let mut testee = CostTracker::new(account_max, block_max, block_max, None);
771        let tx_cost = TransactionCost {
772            writable_accounts: vec![acct1, acct2, acct3],
773            bpf_execution_cost: cost,
774            ..TransactionCost::default()
775        };
776        let mut expected_block_cost = tx_cost.sum();
777        let expected_tx_count = 1;
778        assert!(testee.try_add(&tx_cost).is_ok());
779        assert_eq!(expected_block_cost, testee.block_cost());
780        assert_eq!(expected_tx_count, testee.transaction_count());
781        testee
782            .cost_by_writable_accounts
783            .iter()
784            .for_each(|(_key, units)| {
785                assert_eq!(expected_block_cost, *units);
786            });
787
788        // adjust up
789        {
790            let adjustment = 50u64;
791            testee.add_transaction_execution_cost(&tx_cost, adjustment);
792            expected_block_cost += 50;
793            assert_eq!(expected_block_cost, testee.block_cost());
794            assert_eq!(expected_tx_count, testee.transaction_count());
795            testee
796                .cost_by_writable_accounts
797                .iter()
798                .for_each(|(_key, units)| {
799                    assert_eq!(expected_block_cost, *units);
800                });
801        }
802
803        // adjust down
804        {
805            let adjustment = 50u64;
806            testee.sub_transaction_execution_cost(&tx_cost, adjustment);
807            expected_block_cost -= 50;
808            assert_eq!(expected_block_cost, testee.block_cost());
809            assert_eq!(expected_tx_count, testee.transaction_count());
810            testee
811                .cost_by_writable_accounts
812                .iter()
813                .for_each(|(_key, units)| {
814                    assert_eq!(expected_block_cost, *units);
815                });
816        }
817
818        // adjust overflow
819        {
820            testee.add_transaction_execution_cost(&tx_cost, u64::MAX);
821            // expect block cost set to limit
822            assert_eq!(u64::MAX, testee.block_cost());
823            assert_eq!(expected_tx_count, testee.transaction_count());
824            testee
825                .cost_by_writable_accounts
826                .iter()
827                .for_each(|(_key, units)| {
828                    assert_eq!(u64::MAX, *units);
829                });
830        }
831
832        // adjust underflow
833        {
834            testee.sub_transaction_execution_cost(&tx_cost, u64::MAX);
835            // expect block cost set to limit
836            assert_eq!(u64::MIN, testee.block_cost());
837            assert_eq!(expected_tx_count, testee.transaction_count());
838            testee
839                .cost_by_writable_accounts
840                .iter()
841                .for_each(|(_key, units)| {
842                    assert_eq!(u64::MIN, *units);
843                });
844            // assert the number of non-empty accounts is zero, but map
845            // still contains 3 account
846            assert_eq!(0, testee.number_of_accounts());
847            assert_eq!(3, testee.cost_by_writable_accounts.len());
848        }
849    }
850
851    #[test]
852    fn test_update_execution_cost() {
853        let acct1 = Pubkey::new_unique();
854        let acct2 = Pubkey::new_unique();
855        let acct3 = Pubkey::new_unique();
856        let cost = 100;
857
858        let tx_cost = TransactionCost {
859            writable_accounts: vec![acct1, acct2, acct3],
860            bpf_execution_cost: cost,
861            ..TransactionCost::default()
862        };
863
864        let mut cost_tracker = CostTracker::default();
865
866        // Assert OK to add tx_cost
867        assert!(cost_tracker.try_add(&tx_cost).is_ok());
868        let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account();
869        assert_eq!(cost, cost_tracker.block_cost);
870        assert_eq!(cost, costliest_account_cost);
871        assert_eq!(1, cost_tracker.transaction_count);
872
873        // assert no-change if actual units is same as estimated units
874        let mut expected_cost = cost;
875        cost_tracker.update_execution_cost(&tx_cost, cost);
876        let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account();
877        assert_eq!(expected_cost, cost_tracker.block_cost);
878        assert_eq!(expected_cost, costliest_account_cost);
879        assert_eq!(1, cost_tracker.transaction_count);
880
881        // assert cost are adjusted down
882        let reduced_units = 3;
883        expected_cost -= reduced_units;
884        cost_tracker.update_execution_cost(&tx_cost, cost - reduced_units);
885        let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account();
886        assert_eq!(expected_cost, cost_tracker.block_cost);
887        assert_eq!(expected_cost, costliest_account_cost);
888        assert_eq!(1, cost_tracker.transaction_count);
889
890        // assert cost are adjusted up
891        let increased_units = 1;
892        expected_cost += increased_units;
893        cost_tracker.update_execution_cost(&tx_cost, cost + increased_units);
894        let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account();
895        assert_eq!(expected_cost, cost_tracker.block_cost);
896        assert_eq!(expected_cost, costliest_account_cost);
897        assert_eq!(1, cost_tracker.transaction_count);
898    }
899
900    #[test]
901    fn test_remove_transaction_cost() {
902        let mut cost_tracker = CostTracker::default();
903
904        let cost = 100u64;
905        let tx_cost = TransactionCost {
906            writable_accounts: vec![Pubkey::new_unique()],
907            bpf_execution_cost: cost,
908            ..TransactionCost::default()
909        };
910
911        cost_tracker.add_transaction_cost(&tx_cost);
912        // assert cost_tracker is reverted to default
913        assert_eq!(1, cost_tracker.transaction_count);
914        assert_eq!(1, cost_tracker.number_of_accounts());
915        assert_eq!(cost, cost_tracker.block_cost);
916        assert_eq!(0, cost_tracker.vote_cost);
917        assert_eq!(0, cost_tracker.account_data_size);
918
919        cost_tracker.remove_transaction_cost(&tx_cost);
920        // assert cost_tracker is reverted to default
921        assert_eq!(0, cost_tracker.transaction_count);
922        assert_eq!(0, cost_tracker.number_of_accounts());
923        assert_eq!(0, cost_tracker.block_cost);
924        assert_eq!(0, cost_tracker.vote_cost);
925        assert_eq!(0, cost_tracker.account_data_size);
926    }
927}