From e947bbf55ac2617d216f1fcf7ec5520ddfb0eae6 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Thu, 12 Dec 2024 08:05:12 -0600 Subject: [PATCH] CostModel::estimate_cost (#3986) --- cost-model/src/cost_model.rs | 225 ++++++++++++++++++++--------- cost-model/src/transaction_cost.rs | 21 ++- 2 files changed, 170 insertions(+), 76 deletions(-) diff --git a/cost-model/src/cost_model.rs b/cost-model/src/cost_model.rs index 612492b8b1126f..f958ce6cb4eed1 100644 --- a/cost-model/src/cost_model.rs +++ b/cost-model/src/cost_model.rs @@ -12,7 +12,9 @@ use { DEFAULT_HEAP_COST, DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_COMPUTE_UNIT_LIMIT, }, solana_feature_set::{self as feature_set, FeatureSet}, - solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, + solana_runtime_transaction::{ + transaction_meta::StaticMeta, transaction_with_meta::TransactionWithMeta, + }, solana_sdk::{ borsh1::try_from_slice_unchecked, compute_budget::{self, ComputeBudgetInstruction}, @@ -46,24 +48,22 @@ impl CostModel { if transaction.is_simple_vote_transaction() { TransactionCost::SimpleVote { transaction } } else { - let signature_cost = Self::get_signature_cost(transaction, feature_set); - let write_lock_cost = Self::get_write_lock_cost(transaction, feature_set); + let num_write_locks = Self::num_write_locks(transaction, feature_set); let (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost) = - Self::get_transaction_cost(transaction, feature_set); - let allocated_accounts_data_size = - Self::calculate_allocated_accounts_data_size(transaction); - - let usage_cost_details = UsageCostDetails { + Self::get_transaction_cost( + transaction, + transaction.program_instructions_iter(), + feature_set, + ); + Self::calculate_non_vote_transaction_cost( transaction, - signature_cost, - write_lock_cost, - data_bytes_cost, + transaction.program_instructions_iter(), + num_write_locks, programs_execution_cost, loaded_accounts_data_size_cost, - allocated_accounts_data_size, - }; - - TransactionCost::Transaction(usage_cost_details) + data_bytes_cost, + feature_set, + ) } } @@ -78,35 +78,82 @@ impl CostModel { if transaction.is_simple_vote_transaction() { TransactionCost::SimpleVote { transaction } } else { - let signature_cost = Self::get_signature_cost(transaction, feature_set); - let write_lock_cost = Self::get_write_lock_cost(transaction, feature_set); - - let instructions_data_cost = Self::get_instructions_data_cost(transaction); - let allocated_accounts_data_size = - Self::calculate_allocated_accounts_data_size(transaction); - - let programs_execution_cost = actual_programs_execution_cost; + let num_write_locks = Self::num_write_locks(transaction, feature_set); let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost( actual_loaded_accounts_data_size_bytes, feature_set, ); + let instructions_data_cost = + Self::get_instructions_data_cost(transaction.program_instructions_iter()); - let usage_cost_details = UsageCostDetails { + Self::calculate_non_vote_transaction_cost( transaction, - signature_cost, - write_lock_cost, - data_bytes_cost: instructions_data_cost, - programs_execution_cost, + transaction.program_instructions_iter(), + num_write_locks, + actual_programs_execution_cost, loaded_accounts_data_size_cost, - allocated_accounts_data_size, - }; + instructions_data_cost, + feature_set, + ) + } + } - TransactionCost::Transaction(usage_cost_details) + /// Return an estimated total cost for a transaction given its': + /// - `meta` - transaction meta + /// - `instructions` - transaction instructions + /// - `num_write_locks` - number of requested write locks + pub fn estimate_cost<'a, Tx: StaticMeta>( + transaction: &'a Tx, + instructions: impl Iterator)> + Clone, + num_write_locks: u64, + feature_set: &FeatureSet, + ) -> TransactionCost<'a, Tx> { + if transaction.is_simple_vote_transaction() { + return TransactionCost::SimpleVote { transaction }; } + let (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost) = + Self::get_transaction_cost(transaction, instructions.clone(), feature_set); + Self::calculate_non_vote_transaction_cost( + transaction, + instructions, + num_write_locks, + programs_execution_cost, + loaded_accounts_data_size_cost, + data_bytes_cost, + feature_set, + ) + } + + fn calculate_non_vote_transaction_cost<'a, Tx: StaticMeta>( + transaction: &'a Tx, + instructions: impl Iterator)> + Clone, + num_write_locks: u64, + programs_execution_cost: u64, + loaded_accounts_data_size_cost: u64, + data_bytes_cost: u64, + feature_set: &FeatureSet, + ) -> TransactionCost<'a, Tx> { + let signature_cost = Self::get_signature_cost(transaction, feature_set); + let write_lock_cost = Self::get_write_lock_cost(num_write_locks); + + let allocated_accounts_data_size = + Self::calculate_allocated_accounts_data_size(instructions); + + let usage_cost_details = UsageCostDetails { + transaction, + signature_cost, + write_lock_cost, + data_bytes_cost, + programs_execution_cost, + loaded_accounts_data_size_cost, + allocated_accounts_data_size, + }; + + TransactionCost::Transaction(usage_cost_details) } /// Returns signature details and the total signature cost - fn get_signature_cost(transaction: &impl TransactionWithMeta, feature_set: &FeatureSet) -> u64 { + fn get_signature_cost(transaction: &impl StaticMeta, feature_set: &FeatureSet) -> u64 { let signatures_count_detail = transaction.signature_details(); let ed25519_verify_cost = @@ -139,38 +186,43 @@ impl CostModel { .filter_map(|(i, k)| message.is_writable(i).then_some(k)) } + /// Return the number of write-locks for a transaction. + fn num_write_locks(transaction: &impl SVMMessage, feature_set: &FeatureSet) -> u64 { + if feature_set.is_active(&feature_set::cost_model_requested_write_lock_cost::id()) { + transaction.num_write_locks() + } else { + Self::get_writable_accounts(transaction).count() as u64 + } + } + /// Returns the total write-lock cost. - fn get_write_lock_cost(transaction: &impl SVMMessage, feature_set: &FeatureSet) -> u64 { - let num_write_locks = - if feature_set.is_active(&feature_set::cost_model_requested_write_lock_cost::id()) { - transaction.num_write_locks() - } else { - Self::get_writable_accounts(transaction).count() as u64 - }; + fn get_write_lock_cost(num_write_locks: u64) -> u64 { WRITE_LOCK_UNITS.saturating_mul(num_write_locks) } /// Return (programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost) - fn get_transaction_cost( - transaction: &impl TransactionWithMeta, + fn get_transaction_cost<'a>( + meta: &impl StaticMeta, + instructions: impl Iterator)>, feature_set: &FeatureSet, ) -> (u64, u64, u64) { if feature_set.is_active(&feature_set::reserve_minimal_cus_for_builtin_instructions::id()) { - let data_bytes_cost = Self::get_instructions_data_cost(transaction); + let data_bytes_cost = Self::get_instructions_data_cost(instructions); let (programs_execution_cost, loaded_accounts_data_size_cost) = - Self::get_estimated_execution_cost(transaction, feature_set); + Self::get_estimated_execution_cost(meta, feature_set); ( programs_execution_cost, loaded_accounts_data_size_cost, data_bytes_cost, ) } else { - Self::get_transaction_cost_without_minimal_builtin_cus(transaction, feature_set) + Self::get_transaction_cost_without_minimal_builtin_cus(meta, instructions, feature_set) } } - fn get_transaction_cost_without_minimal_builtin_cus( - transaction: &impl TransactionWithMeta, + fn get_transaction_cost_without_minimal_builtin_cus<'a>( + meta: &impl StaticMeta, + instructions: impl Iterator)>, feature_set: &FeatureSet, ) -> (u64, u64, u64) { let mut programs_execution_costs = 0u64; @@ -179,7 +231,7 @@ impl CostModel { let mut compute_unit_limit_is_set = false; let mut has_user_space_instructions = false; - for (program_id, instruction) in transaction.program_instructions_iter() { + for (program_id, instruction) in instructions { let ix_execution_cost = if let Some(builtin_cost) = get_builtin_instruction_cost(program_id, feature_set) { builtin_cost @@ -207,7 +259,7 @@ impl CostModel { // if failed to process compute budget instructions, the transaction // will not be executed by `bank`, therefore it should be considered // as no execution cost by cost model. - match transaction + match meta .compute_budget_instruction_details() .sanitize_and_convert_to_compute_budget_limits(feature_set) { @@ -244,7 +296,7 @@ impl CostModel { /// Return (programs_execution_cost, loaded_accounts_data_size_cost) fn get_estimated_execution_cost( - transaction: &impl TransactionWithMeta, + transaction: &impl StaticMeta, feature_set: &FeatureSet, ) -> (u64, u64) { // if failed to process compute_budget instructions, the transaction will not be executed @@ -267,10 +319,11 @@ impl CostModel { } /// Return the instruction data bytes cost. - fn get_instructions_data_cost(transaction: &impl SVMMessage) -> u64 { - let ix_data_bytes_len_total: u64 = transaction - .instructions_iter() - .map(|instruction| instruction.data.len() as u64) + fn get_instructions_data_cost<'a>( + instructions: impl Iterator)>, + ) -> u64 { + let ix_data_bytes_len_total: u64 = instructions + .map(|(_, instruction)| instruction.data.len() as u64) .sum(); ix_data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST @@ -318,9 +371,11 @@ impl CostModel { /// eventually, potentially determine account data size of all writable accounts /// at the moment, calculate account data size of account creation - fn calculate_allocated_accounts_data_size(transaction: &impl SVMMessage) -> u64 { + fn calculate_allocated_accounts_data_size<'a>( + instructions: impl Iterator)>, + ) -> u64 { let mut tx_attempted_allocation_size: u64 = 0; - for (program_id, instruction) in transaction.program_instructions_iter() { + for (program_id, instruction) in instructions { match Self::calculate_account_data_size_on_instruction(program_id, instruction) { SystemProgramAccountAllocation::Failed => { // If any system program instructions can be statically @@ -393,7 +448,9 @@ mod tests { let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction); assert_eq!( - CostModel::calculate_allocated_accounts_data_size(&sanitized_tx), + CostModel::calculate_allocated_accounts_data_size( + sanitized_tx.program_instructions_iter() + ), 0 ); } @@ -418,7 +475,9 @@ mod tests { let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction); assert_eq!( - CostModel::calculate_allocated_accounts_data_size(&sanitized_tx), + CostModel::calculate_allocated_accounts_data_size( + sanitized_tx.program_instructions_iter() + ), space1 + space2 ); } @@ -459,7 +518,9 @@ mod tests { let sanitized_tx = RuntimeTransaction::from_transaction_for_tests(transaction); assert_eq!( - CostModel::calculate_allocated_accounts_data_size(&sanitized_tx), + CostModel::calculate_allocated_accounts_data_size( + sanitized_tx.program_instructions_iter() + ), MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION as u64, ); } @@ -483,7 +544,9 @@ mod tests { assert_eq!( 0, // SystemProgramAccountAllocation::Failed, - CostModel::calculate_allocated_accounts_data_size(&sanitized_tx), + CostModel::calculate_allocated_accounts_data_size( + sanitized_tx.program_instructions_iter() + ), ); } @@ -500,7 +563,9 @@ mod tests { assert_eq!( 0, // SystemProgramAccountAllocation::Failed, - CostModel::calculate_allocated_accounts_data_size(&sanitized_tx), + CostModel::calculate_allocated_accounts_data_size( + sanitized_tx.program_instructions_iter() + ), ); } @@ -571,7 +636,11 @@ mod tests { ), ] { let (program_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = - CostModel::get_transaction_cost(&simple_transaction, &feature_set); + CostModel::get_transaction_cost( + &simple_transaction, + simple_transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(expected_execution_cost, program_execution_cost); } @@ -605,7 +674,11 @@ mod tests { ), ] { let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = - CostModel::get_transaction_cost(&token_transaction, &feature_set); + CostModel::get_transaction_cost( + &token_transaction, + token_transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(expected_execution_cost, program_execution_cost); assert_eq!(0, data_bytes_cost); @@ -671,7 +744,11 @@ mod tests { (FeatureSet::all_enabled(), expected_cu_limit as u64), ] { let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = - CostModel::get_transaction_cost(&token_transaction, &feature_set); + CostModel::get_transaction_cost( + &token_transaction, + token_transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(expected_execution_cost, program_execution_cost); assert_eq!(1, data_bytes_cost); @@ -714,7 +791,11 @@ mod tests { for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { let (program_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = - CostModel::get_transaction_cost(&token_transaction, &feature_set); + CostModel::get_transaction_cost( + &token_transaction, + token_transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(0, program_execution_cost); } } @@ -746,7 +827,7 @@ mod tests { ), ] { let (programs_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = - CostModel::get_transaction_cost(&tx, &feature_set); + CostModel::get_transaction_cost(&tx, tx.program_instructions_iter(), &feature_set); assert_eq!(expected_execution_cost, programs_execution_cost); assert_eq!(6, data_bytes_cost); } @@ -786,7 +867,7 @@ mod tests { ), ] { let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = - CostModel::get_transaction_cost(&tx, &feature_set); + CostModel::get_transaction_cost(&tx, tx.program_instructions_iter(), &feature_set); assert_eq!(expected_cost, program_execution_cost); assert_eq!(0, data_bytes_cost); } @@ -932,7 +1013,11 @@ mod tests { ), ] { let (programs_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = - CostModel::get_transaction_cost(&transaction, &feature_set); + CostModel::get_transaction_cost( + &transaction, + transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(expected_execution_cost, programs_execution_cost); } @@ -962,7 +1047,11 @@ mod tests { (FeatureSet::all_enabled(), cu_limit as u64), ] { let (programs_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = - CostModel::get_transaction_cost(&transaction, &feature_set); + CostModel::get_transaction_cost( + &transaction, + transaction.program_instructions_iter(), + &feature_set, + ); assert_eq!(expected_execution_cost, programs_execution_cost); } diff --git a/cost-model/src/transaction_cost.rs b/cost-model/src/transaction_cost.rs index b772e0b0d7b3ad..429a1b90d4b1af 100644 --- a/cost-model/src/transaction_cost.rs +++ b/cost-model/src/transaction_cost.rs @@ -1,9 +1,8 @@ #[cfg(feature = "dev-context-only-utils")] use solana_compute_budget_instruction::compute_budget_instruction_details::ComputeBudgetInstructionDetails; use { - crate::block_cost_limits, - solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, - solana_sdk::pubkey::Pubkey, + crate::block_cost_limits, solana_runtime_transaction::transaction_meta::StaticMeta, + solana_sdk::pubkey::Pubkey, solana_svm_transaction::svm_message::SVMMessage, }; /// TransactionCost is used to represent resources required to process @@ -17,12 +16,12 @@ use { const SIMPLE_VOTE_USAGE_COST: u64 = 3428; #[derive(Debug)] -pub enum TransactionCost<'a, Tx: TransactionWithMeta> { +pub enum TransactionCost<'a, Tx> { SimpleVote { transaction: &'a Tx }, Transaction(UsageCostDetails<'a, Tx>), } -impl<'a, Tx: TransactionWithMeta> TransactionCost<'a, Tx> { +impl<'a, Tx> TransactionCost<'a, Tx> { pub fn sum(&self) -> u64 { #![allow(clippy::assertions_on_constants)] match self { @@ -90,7 +89,9 @@ impl<'a, Tx: TransactionWithMeta> TransactionCost<'a, Tx> { Self::Transaction(usage_cost) => usage_cost.write_lock_cost, } } +} +impl TransactionCost<'_, Tx> { pub fn writable_accounts(&self) -> impl Iterator { let transaction = match self { Self::SimpleVote { transaction } => transaction, @@ -102,7 +103,9 @@ impl<'a, Tx: TransactionWithMeta> TransactionCost<'a, Tx> { .enumerate() .filter_map(|(index, key)| transaction.is_writable(index).then_some(key)) } +} +impl TransactionCost<'_, Tx> { pub fn num_transaction_signatures(&self) -> u64 { match self { Self::SimpleVote { .. } => 1, @@ -136,7 +139,7 @@ impl<'a, Tx: TransactionWithMeta> TransactionCost<'a, Tx> { // costs are stored in number of 'compute unit's #[derive(Debug)] -pub struct UsageCostDetails<'a, Tx: TransactionWithMeta> { +pub struct UsageCostDetails<'a, Tx> { pub transaction: &'a Tx, pub signature_cost: u64, pub write_lock_cost: u64, @@ -146,7 +149,7 @@ pub struct UsageCostDetails<'a, Tx: TransactionWithMeta> { pub allocated_accounts_data_size: u64, } -impl<'a, Tx: TransactionWithMeta> UsageCostDetails<'a, Tx> { +impl<'a, Tx> UsageCostDetails<'a, Tx> { pub fn sum(&self) -> u64 { self.signature_cost .saturating_add(self.write_lock_cost) @@ -257,7 +260,9 @@ impl solana_runtime_transaction::transaction_meta::StaticMeta for WritableKeysTr } #[cfg(feature = "dev-context-only-utils")] -impl TransactionWithMeta for WritableKeysTransaction { +impl solana_runtime_transaction::transaction_with_meta::TransactionWithMeta + for WritableKeysTransaction +{ #[allow(refining_impl_trait)] fn as_sanitized_transaction( &self,