solana_cost_model/
cost_model.rs

1//! 'cost_model` provides service to estimate a transaction's cost
2//! following proposed fee schedule #16984; Relevant cluster cost
3//! measuring is described by #19627
4//!
5//! The main function is `calculate_cost` which returns &TransactionCost.
6//!
7
8use {
9    crate::{block_cost_limits::*, transaction_cost::*},
10    solana_bincode::limited_deserialize,
11    solana_borsh::v1::try_from_slice_unchecked,
12    solana_builtins_default_costs::get_builtin_instruction_cost,
13    solana_compute_budget::compute_budget_limits::{
14        DEFAULT_HEAP_COST, DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_COMPUTE_UNIT_LIMIT,
15    },
16    solana_compute_budget_interface::ComputeBudgetInstruction,
17    solana_feature_set::{self as feature_set, FeatureSet},
18    solana_fee_structure::FeeStructure,
19    solana_pubkey::Pubkey,
20    solana_runtime_transaction::{
21        transaction_meta::StaticMeta, transaction_with_meta::TransactionWithMeta,
22    },
23    solana_sdk_ids::{compute_budget, system_program},
24    solana_svm_transaction::instruction::SVMInstruction,
25    solana_system_interface::{
26        instruction::SystemInstruction, MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION,
27        MAX_PERMITTED_DATA_LENGTH,
28    },
29    std::num::Saturating,
30};
31
32pub struct CostModel;
33
34#[derive(Debug, PartialEq)]
35enum SystemProgramAccountAllocation {
36    None,
37    Some(u64),
38    Failed,
39}
40
41impl CostModel {
42    pub fn calculate_cost<'a, Tx: TransactionWithMeta>(
43        transaction: &'a Tx,
44        feature_set: &FeatureSet,
45    ) -> TransactionCost<'a, Tx> {
46        if transaction.is_simple_vote_transaction() {
47            TransactionCost::SimpleVote { transaction }
48        } else {
49            let (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost) =
50                Self::get_transaction_cost(
51                    transaction,
52                    transaction.program_instructions_iter(),
53                    feature_set,
54                );
55            Self::calculate_non_vote_transaction_cost(
56                transaction,
57                transaction.program_instructions_iter(),
58                transaction.num_write_locks(),
59                programs_execution_cost,
60                loaded_accounts_data_size_cost,
61                data_bytes_cost,
62                feature_set,
63            )
64        }
65    }
66
67    // Calculate executed transaction CU cost, with actual execution and loaded accounts size
68    // costs.
69    pub fn calculate_cost_for_executed_transaction<'a, Tx: TransactionWithMeta>(
70        transaction: &'a Tx,
71        actual_programs_execution_cost: u64,
72        actual_loaded_accounts_data_size_bytes: u32,
73        feature_set: &FeatureSet,
74    ) -> TransactionCost<'a, Tx> {
75        if transaction.is_simple_vote_transaction() {
76            TransactionCost::SimpleVote { transaction }
77        } else {
78            let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost(
79                actual_loaded_accounts_data_size_bytes,
80                feature_set,
81            );
82            let instructions_data_cost =
83                Self::get_instructions_data_cost(transaction.program_instructions_iter());
84
85            Self::calculate_non_vote_transaction_cost(
86                transaction,
87                transaction.program_instructions_iter(),
88                transaction.num_write_locks(),
89                actual_programs_execution_cost,
90                loaded_accounts_data_size_cost,
91                instructions_data_cost,
92                feature_set,
93            )
94        }
95    }
96
97    /// Return an estimated total cost for a transaction given its':
98    /// - `meta` - transaction meta
99    /// - `instructions` - transaction instructions
100    /// - `num_write_locks` - number of requested write locks
101    pub fn estimate_cost<'a, Tx: StaticMeta>(
102        transaction: &'a Tx,
103        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)> + Clone,
104        num_write_locks: u64,
105        feature_set: &FeatureSet,
106    ) -> TransactionCost<'a, Tx> {
107        if transaction.is_simple_vote_transaction() {
108            return TransactionCost::SimpleVote { transaction };
109        }
110        let (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost) =
111            Self::get_transaction_cost(transaction, instructions.clone(), feature_set);
112        Self::calculate_non_vote_transaction_cost(
113            transaction,
114            instructions,
115            num_write_locks,
116            programs_execution_cost,
117            loaded_accounts_data_size_cost,
118            data_bytes_cost,
119            feature_set,
120        )
121    }
122
123    fn calculate_non_vote_transaction_cost<'a, Tx: StaticMeta>(
124        transaction: &'a Tx,
125        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)> + Clone,
126        num_write_locks: u64,
127        programs_execution_cost: u64,
128        loaded_accounts_data_size_cost: u64,
129        data_bytes_cost: u64,
130        feature_set: &FeatureSet,
131    ) -> TransactionCost<'a, Tx> {
132        let signature_cost = Self::get_signature_cost(transaction, feature_set);
133        let write_lock_cost = Self::get_write_lock_cost(num_write_locks);
134
135        let allocated_accounts_data_size =
136            Self::calculate_allocated_accounts_data_size(instructions);
137
138        let usage_cost_details = UsageCostDetails {
139            transaction,
140            signature_cost,
141            write_lock_cost,
142            data_bytes_cost,
143            programs_execution_cost,
144            loaded_accounts_data_size_cost,
145            allocated_accounts_data_size,
146        };
147
148        TransactionCost::Transaction(usage_cost_details)
149    }
150
151    /// Returns signature details and the total signature cost
152    fn get_signature_cost(transaction: &impl StaticMeta, feature_set: &FeatureSet) -> u64 {
153        let signatures_count_detail = transaction.signature_details();
154
155        let ed25519_verify_cost =
156            if feature_set.is_active(&feature_set::ed25519_precompile_verify_strict::id()) {
157                ED25519_VERIFY_STRICT_COST
158            } else {
159                ED25519_VERIFY_COST
160            };
161
162        let secp256r1_verify_cost =
163            if feature_set.is_active(&feature_set::enable_secp256r1_precompile::id()) {
164                SECP256R1_VERIFY_COST
165            } else {
166                0
167            };
168
169        signatures_count_detail
170            .num_transaction_signatures()
171            .saturating_mul(SIGNATURE_COST)
172            .saturating_add(
173                signatures_count_detail
174                    .num_secp256k1_instruction_signatures()
175                    .saturating_mul(SECP256K1_VERIFY_COST),
176            )
177            .saturating_add(
178                signatures_count_detail
179                    .num_ed25519_instruction_signatures()
180                    .saturating_mul(ed25519_verify_cost),
181            )
182            .saturating_add(
183                signatures_count_detail
184                    .num_secp256r1_instruction_signatures()
185                    .saturating_mul(secp256r1_verify_cost),
186            )
187    }
188
189    /// Returns the total write-lock cost.
190    fn get_write_lock_cost(num_write_locks: u64) -> u64 {
191        WRITE_LOCK_UNITS.saturating_mul(num_write_locks)
192    }
193
194    /// Return (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost)
195    fn get_transaction_cost<'a>(
196        meta: &impl StaticMeta,
197        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)>,
198        feature_set: &FeatureSet,
199    ) -> (u64, u64, u64) {
200        if feature_set.is_active(&feature_set::reserve_minimal_cus_for_builtin_instructions::id()) {
201            let data_bytes_cost = Self::get_instructions_data_cost(instructions);
202            let (programs_execution_cost, loaded_accounts_data_size_cost) =
203                Self::get_estimated_execution_cost(meta, feature_set);
204            (
205                programs_execution_cost,
206                loaded_accounts_data_size_cost,
207                data_bytes_cost,
208            )
209        } else {
210            Self::get_transaction_cost_without_minimal_builtin_cus(meta, instructions, feature_set)
211        }
212    }
213
214    fn get_transaction_cost_without_minimal_builtin_cus<'a>(
215        meta: &impl StaticMeta,
216        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)>,
217        feature_set: &FeatureSet,
218    ) -> (u64, u64, u64) {
219        let mut programs_execution_costs = 0u64;
220        let mut loaded_accounts_data_size_cost = 0u64;
221        let mut data_bytes_len_total = 0u64;
222        let mut compute_unit_limit_is_set = false;
223        let mut has_user_space_instructions = false;
224
225        for (program_id, instruction) in instructions {
226            let ix_execution_cost =
227                if let Some(builtin_cost) = get_builtin_instruction_cost(program_id, feature_set) {
228                    builtin_cost
229                } else {
230                    has_user_space_instructions = true;
231                    u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT)
232                };
233
234            programs_execution_costs = programs_execution_costs
235                .saturating_add(ix_execution_cost)
236                .min(u64::from(MAX_COMPUTE_UNIT_LIMIT));
237
238            data_bytes_len_total =
239                data_bytes_len_total.saturating_add(instruction.data.len() as u64);
240
241            if compute_budget::check_id(program_id) {
242                if let Ok(ComputeBudgetInstruction::SetComputeUnitLimit(_)) =
243                    try_from_slice_unchecked(instruction.data)
244                {
245                    compute_unit_limit_is_set = true;
246                }
247            }
248        }
249
250        // if failed to process compute budget instructions, the transaction
251        // will not be executed by `bank`, therefore it should be considered
252        // as no execution cost by cost model.
253        match meta
254            .compute_budget_instruction_details()
255            .sanitize_and_convert_to_compute_budget_limits(feature_set)
256        {
257            Ok(compute_budget_limits) => {
258                // if tx contained user-space instructions and a more accurate
259                // estimate available correct it, where
260                // "user-space instructions" must be specifically checked by
261                // 'compute_unit_limit_is_set' flag, because compute_budget
262                // does not distinguish builtin and bpf instructions when
263                // calculating default compute-unit-limit.
264                //
265                // (see compute_budget.rs test
266                // `test_process_mixed_instructions_without_compute_budget`)
267                if has_user_space_instructions && compute_unit_limit_is_set {
268                    programs_execution_costs = u64::from(compute_budget_limits.compute_unit_limit);
269                }
270
271                loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost(
272                    compute_budget_limits.loaded_accounts_bytes.get(),
273                    feature_set,
274                );
275            }
276            Err(_) => {
277                programs_execution_costs = 0;
278            }
279        }
280
281        (
282            programs_execution_costs,
283            loaded_accounts_data_size_cost,
284            data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST,
285        )
286    }
287
288    /// Return (programs_execution_cost, loaded_accounts_data_size_cost)
289    fn get_estimated_execution_cost(
290        transaction: &impl StaticMeta,
291        feature_set: &FeatureSet,
292    ) -> (u64, u64) {
293        // if failed to process compute_budget instructions, the transaction will not be executed
294        // by `bank`, therefore it should be considered as no execution cost by cost model.
295        let (programs_execution_costs, loaded_accounts_data_size_cost) = match transaction
296            .compute_budget_instruction_details()
297            .sanitize_and_convert_to_compute_budget_limits(feature_set)
298        {
299            Ok(compute_budget_limits) => (
300                u64::from(compute_budget_limits.compute_unit_limit),
301                Self::calculate_loaded_accounts_data_size_cost(
302                    compute_budget_limits.loaded_accounts_bytes.get(),
303                    feature_set,
304                ),
305            ),
306            Err(_) => (0, 0),
307        };
308
309        (programs_execution_costs, loaded_accounts_data_size_cost)
310    }
311
312    /// Return the instruction data bytes cost.
313    fn get_instructions_data_cost<'a>(
314        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)>,
315    ) -> u64 {
316        let ix_data_bytes_len_total: u64 = instructions
317            .map(|(_, instruction)| instruction.data.len() as u64)
318            .sum();
319
320        ix_data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST
321    }
322
323    pub fn calculate_loaded_accounts_data_size_cost(
324        loaded_accounts_data_size: u32,
325        _feature_set: &FeatureSet,
326    ) -> u64 {
327        FeeStructure::calculate_memory_usage_cost(loaded_accounts_data_size, DEFAULT_HEAP_COST)
328    }
329
330    fn calculate_account_data_size_on_deserialized_system_instruction(
331        instruction: SystemInstruction,
332    ) -> SystemProgramAccountAllocation {
333        match instruction {
334            SystemInstruction::CreateAccount { space, .. }
335            | SystemInstruction::CreateAccountWithSeed { space, .. }
336            | SystemInstruction::Allocate { space }
337            | SystemInstruction::AllocateWithSeed { space, .. } => {
338                if space > MAX_PERMITTED_DATA_LENGTH {
339                    SystemProgramAccountAllocation::Failed
340                } else {
341                    SystemProgramAccountAllocation::Some(space)
342                }
343            }
344            _ => SystemProgramAccountAllocation::None,
345        }
346    }
347
348    fn calculate_account_data_size_on_instruction(
349        program_id: &Pubkey,
350        instruction: SVMInstruction,
351    ) -> SystemProgramAccountAllocation {
352        if program_id == &system_program::id() {
353            if let Ok(instruction) =
354                limited_deserialize(instruction.data, solana_packet::PACKET_DATA_SIZE as u64)
355            {
356                Self::calculate_account_data_size_on_deserialized_system_instruction(instruction)
357            } else {
358                SystemProgramAccountAllocation::Failed
359            }
360        } else {
361            SystemProgramAccountAllocation::None
362        }
363    }
364
365    /// eventually, potentially determine account data size of all writable accounts
366    /// at the moment, calculate account data size of account creation
367    fn calculate_allocated_accounts_data_size<'a>(
368        instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)>,
369    ) -> u64 {
370        let mut tx_attempted_allocation_size = Saturating(0u64);
371        for (program_id, instruction) in instructions {
372            match Self::calculate_account_data_size_on_instruction(program_id, instruction) {
373                SystemProgramAccountAllocation::Failed => {
374                    // If any system program instructions can be statically
375                    // determined to fail, no allocations will actually be
376                    // persisted by the transaction. So return 0 here so that no
377                    // account allocation budget is used for this failed
378                    // transaction.
379                    return 0;
380                }
381                SystemProgramAccountAllocation::None => continue,
382                SystemProgramAccountAllocation::Some(ix_attempted_allocation_size) => {
383                    tx_attempted_allocation_size += ix_attempted_allocation_size;
384                }
385            }
386        }
387
388        // The runtime prevents transactions from allocating too much account
389        // data so clamp the attempted allocation size to the max amount.
390        //
391        // Note that if there are any custom bpf instructions in the transaction
392        // it's tricky to know whether a newly allocated account will be freed
393        // or not during an intermediate instruction in the transaction so we
394        // shouldn't assume that a large sum of allocations will necessarily
395        // lead to transaction failure.
396        (MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION as u64)
397            .min(tx_attempted_allocation_size.0)
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use {
404        super::*,
405        itertools::Itertools,
406        solana_compute_budget::{
407            self,
408            compute_budget_limits::{
409                DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT,
410            },
411        },
412        solana_compute_budget_interface::ComputeBudgetInstruction,
413        solana_fee_structure::ACCOUNT_DATA_COST_PAGE_SIZE,
414        solana_hash::Hash,
415        solana_instruction::Instruction,
416        solana_keypair::Keypair,
417        solana_message::{compiled_instruction::CompiledInstruction, Message},
418        solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
419        solana_sdk_ids::system_program,
420        solana_signer::Signer,
421        solana_svm_transaction::svm_message::SVMMessage,
422        solana_system_interface::instruction::{self as system_instruction},
423        solana_system_transaction as system_transaction,
424        solana_transaction::Transaction,
425    };
426
427    fn test_setup() -> (Keypair, Hash) {
428        solana_logger::setup();
429        (Keypair::new(), Hash::new_unique())
430    }
431
432    #[test]
433    fn test_calculate_allocated_accounts_data_size_no_allocation() {
434        let transaction = Transaction::new_unsigned(Message::new(
435            &[system_instruction::transfer(
436                &Pubkey::new_unique(),
437                &Pubkey::new_unique(),
438                1,
439            )],
440            Some(&Pubkey::new_unique()),
441        ));
442        let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction);
443
444        assert_eq!(
445            CostModel::calculate_allocated_accounts_data_size(
446                sanitized_tx.program_instructions_iter()
447            ),
448            0
449        );
450    }
451
452    #[test]
453    fn test_calculate_allocated_accounts_data_size_multiple_allocations() {
454        let space1 = 100;
455        let space2 = 200;
456        let transaction = Transaction::new_unsigned(Message::new(
457            &[
458                system_instruction::create_account(
459                    &Pubkey::new_unique(),
460                    &Pubkey::new_unique(),
461                    1,
462                    space1,
463                    &Pubkey::new_unique(),
464                ),
465                system_instruction::allocate(&Pubkey::new_unique(), space2),
466            ],
467            Some(&Pubkey::new_unique()),
468        ));
469        let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction);
470
471        assert_eq!(
472            CostModel::calculate_allocated_accounts_data_size(
473                sanitized_tx.program_instructions_iter()
474            ),
475            space1 + space2
476        );
477    }
478
479    #[test]
480    fn test_calculate_allocated_accounts_data_size_max_limit() {
481        let spaces = [MAX_PERMITTED_DATA_LENGTH, MAX_PERMITTED_DATA_LENGTH, 100];
482        assert!(
483            spaces.iter().copied().sum::<u64>()
484                > MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION as u64
485        );
486        let transaction = Transaction::new_unsigned(Message::new(
487            &[
488                system_instruction::create_account(
489                    &Pubkey::new_unique(),
490                    &Pubkey::new_unique(),
491                    1,
492                    spaces[0],
493                    &Pubkey::new_unique(),
494                ),
495                system_instruction::create_account(
496                    &Pubkey::new_unique(),
497                    &Pubkey::new_unique(),
498                    1,
499                    spaces[1],
500                    &Pubkey::new_unique(),
501                ),
502                system_instruction::create_account(
503                    &Pubkey::new_unique(),
504                    &Pubkey::new_unique(),
505                    1,
506                    spaces[2],
507                    &Pubkey::new_unique(),
508                ),
509            ],
510            Some(&Pubkey::new_unique()),
511        ));
512        let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction);
513
514        assert_eq!(
515            CostModel::calculate_allocated_accounts_data_size(
516                sanitized_tx.program_instructions_iter()
517            ),
518            MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION as u64,
519        );
520    }
521
522    #[test]
523    fn test_calculate_allocated_accounts_data_size_overflow() {
524        let transaction = Transaction::new_unsigned(Message::new(
525            &[
526                system_instruction::create_account(
527                    &Pubkey::new_unique(),
528                    &Pubkey::new_unique(),
529                    1,
530                    100,
531                    &Pubkey::new_unique(),
532                ),
533                system_instruction::allocate(&Pubkey::new_unique(), u64::MAX),
534            ],
535            Some(&Pubkey::new_unique()),
536        ));
537        let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction);
538
539        assert_eq!(
540            0, // SystemProgramAccountAllocation::Failed,
541            CostModel::calculate_allocated_accounts_data_size(
542                sanitized_tx.program_instructions_iter()
543            ),
544        );
545    }
546
547    #[test]
548    fn test_calculate_allocated_accounts_data_size_invalid_ix() {
549        let transaction = Transaction::new_unsigned(Message::new(
550            &[
551                system_instruction::allocate(&Pubkey::new_unique(), 100),
552                Instruction::new_with_bincode(system_program::id(), &(), vec![]),
553            ],
554            Some(&Pubkey::new_unique()),
555        ));
556        let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction);
557
558        assert_eq!(
559            0, // SystemProgramAccountAllocation::Failed,
560            CostModel::calculate_allocated_accounts_data_size(
561                sanitized_tx.program_instructions_iter()
562            ),
563        );
564    }
565
566    #[test]
567    fn test_cost_model_data_len_cost() {
568        let lamports = 0;
569        let owner = Pubkey::default();
570        let seed = String::default();
571        let space = 100;
572        let base = Pubkey::default();
573        for instruction in [
574            SystemInstruction::CreateAccount {
575                lamports,
576                space,
577                owner,
578            },
579            SystemInstruction::CreateAccountWithSeed {
580                base,
581                seed: seed.clone(),
582                lamports,
583                space,
584                owner,
585            },
586            SystemInstruction::Allocate { space },
587            SystemInstruction::AllocateWithSeed {
588                base,
589                seed,
590                space,
591                owner,
592            },
593        ] {
594            assert_eq!(
595                SystemProgramAccountAllocation::Some(space),
596                CostModel::calculate_account_data_size_on_deserialized_system_instruction(
597                    instruction
598                )
599            );
600        }
601        assert_eq!(
602            SystemProgramAccountAllocation::None,
603            CostModel::calculate_account_data_size_on_deserialized_system_instruction(
604                SystemInstruction::TransferWithSeed {
605                    lamports,
606                    from_seed: String::default(),
607                    from_owner: Pubkey::default(),
608                }
609            )
610        );
611    }
612
613    #[test]
614    fn test_cost_model_simple_transaction() {
615        let (mint_keypair, start_hash) = test_setup();
616
617        let keypair = Keypair::new();
618        let simple_transaction = RuntimeTransaction::from_transaction_for_tests(
619            system_transaction::transfer(&mint_keypair, &keypair.pubkey(), 2, start_hash),
620        );
621
622        for (feature_set, expected_execution_cost) in [
623            (
624                FeatureSet::default(),
625                solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS,
626            ),
627            (
628                FeatureSet::all_enabled(),
629                u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT),
630            ),
631        ] {
632            let (program_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) =
633                CostModel::get_transaction_cost(
634                    &simple_transaction,
635                    simple_transaction.program_instructions_iter(),
636                    &feature_set,
637                );
638
639            assert_eq!(expected_execution_cost, program_execution_cost);
640        }
641    }
642
643    #[test]
644    fn test_cost_model_token_transaction() {
645        let (mint_keypair, start_hash) = test_setup();
646
647        let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2, 0])];
648        let tx = Transaction::new_with_compiled_instructions(
649            &[&mint_keypair],
650            &[solana_pubkey::new_rand(), solana_pubkey::new_rand()],
651            start_hash,
652            vec![Pubkey::new_unique()],
653            instructions,
654        );
655        let token_transaction = RuntimeTransaction::from_transaction_for_tests(tx);
656
657        for (feature_set, expected_execution_cost) in [
658            (
659                FeatureSet::default(),
660                DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
661            ),
662            (
663                FeatureSet::all_enabled(),
664                DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
665            ),
666        ] {
667            let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) =
668                CostModel::get_transaction_cost(
669                    &token_transaction,
670                    token_transaction.program_instructions_iter(),
671                    &feature_set,
672                );
673
674            assert_eq!(expected_execution_cost, program_execution_cost);
675            assert_eq!(0, data_bytes_cost);
676        }
677    }
678
679    #[test]
680    fn test_cost_model_demoted_write_lock() {
681        let (mint_keypair, start_hash) = test_setup();
682
683        // Cannot write-lock the system program, it will be demoted when taking locks.
684        // However, the cost should be calculated as if it were taken.
685        let simple_transaction = RuntimeTransaction::from_transaction_for_tests(
686            system_transaction::transfer(&mint_keypair, &system_program::id(), 2, start_hash),
687        );
688
689        // write-lock counts towards cost, even if it is demoted.
690        let tx_cost = CostModel::calculate_cost(&simple_transaction, &FeatureSet::default());
691        assert_eq!(2 * WRITE_LOCK_UNITS, tx_cost.write_lock_cost());
692        assert_eq!(1, tx_cost.writable_accounts().count());
693    }
694
695    #[test]
696    fn test_cost_model_compute_budget_transaction() {
697        let (mint_keypair, start_hash) = test_setup();
698        let expected_cu_limit = 12_345;
699
700        let instructions = vec![
701            CompiledInstruction::new(3, &(), vec![1, 2, 0]),
702            CompiledInstruction::new_from_raw_parts(
703                4,
704                ComputeBudgetInstruction::SetComputeUnitLimit(expected_cu_limit)
705                    .pack()
706                    .unwrap(),
707                vec![],
708            ),
709        ];
710        let tx = Transaction::new_with_compiled_instructions(
711            &[&mint_keypair],
712            &[solana_pubkey::new_rand(), solana_pubkey::new_rand()],
713            start_hash,
714            vec![Pubkey::new_unique(), compute_budget::id()],
715            instructions,
716        );
717        let token_transaction = RuntimeTransaction::from_transaction_for_tests(tx);
718
719        // If cu-limit is specified, that would the cost for all programs
720        for (feature_set, expected_execution_cost) in [
721            (FeatureSet::default(), expected_cu_limit as u64),
722            (FeatureSet::all_enabled(), expected_cu_limit as u64),
723        ] {
724            let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) =
725                CostModel::get_transaction_cost(
726                    &token_transaction,
727                    token_transaction.program_instructions_iter(),
728                    &feature_set,
729                );
730
731            assert_eq!(expected_execution_cost, program_execution_cost);
732            assert_eq!(1, data_bytes_cost);
733        }
734    }
735
736    #[test]
737    fn test_cost_model_with_failed_compute_budget_transaction() {
738        let (mint_keypair, start_hash) = test_setup();
739
740        let instructions = vec![
741            CompiledInstruction::new(3, &(), vec![1, 2, 0]),
742            CompiledInstruction::new_from_raw_parts(
743                4,
744                ComputeBudgetInstruction::SetComputeUnitLimit(12_345)
745                    .pack()
746                    .unwrap(),
747                vec![],
748            ),
749            // to trigger failure in `sanitize_and_convert_to_compute_budget_limits`
750            CompiledInstruction::new_from_raw_parts(
751                4,
752                ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(0)
753                    .pack()
754                    .unwrap(),
755                vec![],
756            ),
757        ];
758        let tx = Transaction::new_with_compiled_instructions(
759            &[&mint_keypair],
760            &[solana_pubkey::new_rand(), solana_pubkey::new_rand()],
761            start_hash,
762            vec![Pubkey::new_unique(), compute_budget::id()],
763            instructions,
764        );
765        let token_transaction = RuntimeTransaction::from_transaction_for_tests(tx);
766
767        for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] {
768            let (program_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) =
769                CostModel::get_transaction_cost(
770                    &token_transaction,
771                    token_transaction.program_instructions_iter(),
772                    &feature_set,
773                );
774            assert_eq!(0, program_execution_cost);
775        }
776    }
777
778    #[test]
779    fn test_cost_model_transaction_many_transfer_instructions() {
780        let (mint_keypair, start_hash) = test_setup();
781
782        let key1 = solana_pubkey::new_rand();
783        let key2 = solana_pubkey::new_rand();
784        let instructions =
785            system_instruction::transfer_many(&mint_keypair.pubkey(), &[(key1, 1), (key2, 1)]);
786        let message = Message::new(&instructions, Some(&mint_keypair.pubkey()));
787        let tx = RuntimeTransaction::from_transaction_for_tests(Transaction::new(
788            &[&mint_keypair],
789            message,
790            start_hash,
791        ));
792
793        // expected cost for two system transfer instructions
794        for (feature_set, expected_execution_cost) in [
795            (
796                FeatureSet::default(),
797                2 * solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS,
798            ),
799            (
800                FeatureSet::all_enabled(),
801                2 * u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT),
802            ),
803        ] {
804            let (programs_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) =
805                CostModel::get_transaction_cost(&tx, tx.program_instructions_iter(), &feature_set);
806            assert_eq!(expected_execution_cost, programs_execution_cost);
807            assert_eq!(6, data_bytes_cost);
808        }
809    }
810
811    #[test]
812    fn test_cost_model_message_many_different_instructions() {
813        let (mint_keypair, start_hash) = test_setup();
814
815        // construct a transaction with multiple random instructions
816        let key1 = solana_pubkey::new_rand();
817        let key2 = solana_pubkey::new_rand();
818        let prog1 = solana_pubkey::new_rand();
819        let prog2 = solana_pubkey::new_rand();
820        let instructions = vec![
821            CompiledInstruction::new(3, &(), vec![0, 1]),
822            CompiledInstruction::new(4, &(), vec![0, 2]),
823        ];
824        let tx = RuntimeTransaction::from_transaction_for_tests(
825            Transaction::new_with_compiled_instructions(
826                &[&mint_keypair],
827                &[key1, key2],
828                start_hash,
829                vec![prog1, prog2],
830                instructions,
831            ),
832        );
833
834        for (feature_set, expected_cost) in [
835            (
836                FeatureSet::default(),
837                DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2,
838            ),
839            (
840                FeatureSet::all_enabled(),
841                DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2,
842            ),
843        ] {
844            let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) =
845                CostModel::get_transaction_cost(&tx, tx.program_instructions_iter(), &feature_set);
846            assert_eq!(expected_cost, program_execution_cost);
847            assert_eq!(0, data_bytes_cost);
848        }
849    }
850
851    #[test]
852    fn test_cost_model_sort_message_accounts_by_type() {
853        // construct a transaction with two random instructions with same signer
854        let signer1 = Keypair::new();
855        let signer2 = Keypair::new();
856        let key1 = Pubkey::new_unique();
857        let key2 = Pubkey::new_unique();
858        let prog1 = Pubkey::new_unique();
859        let prog2 = Pubkey::new_unique();
860        let instructions = vec![
861            CompiledInstruction::new(4, &(), vec![0, 2]),
862            CompiledInstruction::new(5, &(), vec![1, 3]),
863        ];
864        let tx = RuntimeTransaction::from_transaction_for_tests(
865            Transaction::new_with_compiled_instructions(
866                &[&signer1, &signer2],
867                &[key1, key2],
868                Hash::new_unique(),
869                vec![prog1, prog2],
870                instructions,
871            ),
872        );
873
874        let tx_cost = CostModel::calculate_cost(&tx, &FeatureSet::all_enabled());
875        let writable_accounts = tx_cost.writable_accounts().collect_vec();
876        assert_eq!(2 + 2, writable_accounts.len());
877        assert_eq!(signer1.pubkey(), *writable_accounts[0]);
878        assert_eq!(signer2.pubkey(), *writable_accounts[1]);
879        assert_eq!(key1, *writable_accounts[2]);
880        assert_eq!(key2, *writable_accounts[3]);
881    }
882
883    #[test]
884    fn test_cost_model_calculate_cost_all_default() {
885        let (mint_keypair, start_hash) = test_setup();
886        let tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
887            &mint_keypair,
888            &Keypair::new().pubkey(),
889            2,
890            start_hash,
891        ));
892
893        let expected_account_cost = WRITE_LOCK_UNITS * 2;
894        for (feature_set, expected_execution_cost) in [
895            (
896                FeatureSet::default(),
897                solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS,
898            ),
899            (
900                FeatureSet::all_enabled(),
901                u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT),
902            ),
903        ] {
904            const DEFAULT_PAGE_COST: u64 = 8;
905            let expected_loaded_accounts_data_size_cost =
906                solana_compute_budget::compute_budget_limits::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES
907                    .get() as u64
908                    / ACCOUNT_DATA_COST_PAGE_SIZE
909                    * DEFAULT_PAGE_COST;
910
911            let tx_cost = CostModel::calculate_cost(&tx, &feature_set);
912            assert_eq!(expected_account_cost, tx_cost.write_lock_cost());
913            assert_eq!(expected_execution_cost, tx_cost.programs_execution_cost());
914            assert_eq!(2, tx_cost.writable_accounts().count());
915            assert_eq!(
916                expected_loaded_accounts_data_size_cost,
917                tx_cost.loaded_accounts_data_size_cost()
918            );
919        }
920    }
921
922    #[test]
923    fn test_cost_model_calculate_cost_with_limit() {
924        let (mint_keypair, start_hash) = test_setup();
925        let to_keypair = Keypair::new();
926        let data_limit = 32 * 1024u32;
927        let tx =
928            RuntimeTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer(
929                &[
930                    system_instruction::transfer(&mint_keypair.pubkey(), &to_keypair.pubkey(), 2),
931                    ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_limit),
932                ],
933                Some(&mint_keypair.pubkey()),
934                &[&mint_keypair],
935                start_hash,
936            ));
937
938        let expected_account_cost = WRITE_LOCK_UNITS * 2;
939        for (feature_set, expected_execution_cost) in [
940            (
941                FeatureSet::default(),
942                solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS
943                    + solana_compute_budget_program::DEFAULT_COMPUTE_UNITS,
944            ),
945            (
946                FeatureSet::all_enabled(),
947                2 * u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT),
948            ),
949        ] {
950            let expected_loaded_accounts_data_size_cost = (data_limit as u64) / (32 * 1024) * 8;
951
952            let tx_cost = CostModel::calculate_cost(&tx, &feature_set);
953            assert_eq!(expected_account_cost, tx_cost.write_lock_cost());
954            assert_eq!(expected_execution_cost, tx_cost.programs_execution_cost());
955            assert_eq!(2, tx_cost.writable_accounts().count());
956            assert_eq!(
957                expected_loaded_accounts_data_size_cost,
958                tx_cost.loaded_accounts_data_size_cost()
959            );
960        }
961    }
962
963    #[test]
964    fn test_transaction_cost_with_mix_instruction_without_compute_budget() {
965        let (mint_keypair, start_hash) = test_setup();
966
967        let transaction =
968            RuntimeTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer(
969                &[
970                    Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
971                    system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 2),
972                ],
973                Some(&mint_keypair.pubkey()),
974                &[&mint_keypair],
975                start_hash,
976            ));
977        // transaction has one builtin instruction, and one bpf instruction, no ComputeBudget::compute_unit_limit
978        for (feature_set, expected_execution_cost) in [
979            (
980                FeatureSet::default(),
981                solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS
982                    + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
983            ),
984            (
985                FeatureSet::all_enabled(),
986                u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT)
987                    + u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT),
988            ),
989        ] {
990            let (programs_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) =
991                CostModel::get_transaction_cost(
992                    &transaction,
993                    transaction.program_instructions_iter(),
994                    &feature_set,
995                );
996
997            assert_eq!(expected_execution_cost, programs_execution_cost);
998        }
999    }
1000
1001    #[test]
1002    fn test_transaction_cost_with_mix_instruction_with_cu_limit() {
1003        let (mint_keypair, start_hash) = test_setup();
1004        let cu_limit: u32 = 12_345;
1005
1006        let transaction =
1007            RuntimeTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer(
1008                &[
1009                    system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 2),
1010                    ComputeBudgetInstruction::set_compute_unit_limit(cu_limit),
1011                ],
1012                Some(&mint_keypair.pubkey()),
1013                &[&mint_keypair],
1014                start_hash,
1015            ));
1016        for (feature_set, expected_execution_cost) in [
1017            (
1018                FeatureSet::default(),
1019                solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS
1020                    + solana_compute_budget_program::DEFAULT_COMPUTE_UNITS,
1021            ),
1022            (FeatureSet::all_enabled(), cu_limit as u64),
1023        ] {
1024            let (programs_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) =
1025                CostModel::get_transaction_cost(
1026                    &transaction,
1027                    transaction.program_instructions_iter(),
1028                    &feature_set,
1029                );
1030
1031            assert_eq!(expected_execution_cost, programs_execution_cost);
1032        }
1033    }
1034}