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