From c73bebe9847ecd5a1cbffa96bf03e03a7683232f Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+taozhu-chicago@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:10:42 -0500 Subject: [PATCH] Split compute budget instructions process from struct itself (#33513) * Split compute budget instruction processing from ComputeBudget struct itself, allow compute_budget_instructions be processed elsewhere without having to instantiate ComputeBudget * updated tests --- accounts-db/src/accounts.rs | 52 +- cost-model/src/cost_model.rs | 69 +- program-runtime/src/compute_budget.rs | 648 +----------------- .../src/compute_budget_processor.rs | 619 +++++++++++++++++ program-runtime/src/invoke_context.rs | 11 +- program-runtime/src/lib.rs | 1 + programs/sbf/tests/programs.rs | 17 +- runtime/src/bank.rs | 67 +- runtime/src/bank/tests.rs | 34 +- runtime/src/transaction_priority_details.rs | 40 +- 10 files changed, 796 insertions(+), 762 deletions(-) create mode 100644 program-runtime/src/compute_budget_processor.rs diff --git a/accounts-db/src/accounts.rs b/accounts-db/src/accounts.rs index 47b372d98..4ff891fc8 100644 --- a/accounts-db/src/accounts.rs +++ b/accounts-db/src/accounts.rs @@ -25,7 +25,7 @@ use { itertools::Itertools, log::*, solana_program_runtime::{ - compute_budget::{self, ComputeBudget}, + compute_budget_processor::process_compute_budget_instructions, loaded_programs::LoadedProgramsForTxBatch, }, solana_sdk::{ @@ -35,9 +35,8 @@ use { bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::{BankId, Slot}, feature_set::{ - self, add_set_tx_loaded_accounts_data_size_instruction, - include_loaded_accounts_data_size_in_fee_calculation, - remove_congestion_multiplier_from_fee_calculation, remove_deprecated_request_unit_ix, + self, include_loaded_accounts_data_size_in_fee_calculation, + remove_congestion_multiplier_from_fee_calculation, simplify_writable_program_account_check, FeatureSet, }, fee::FeeStructure, @@ -247,15 +246,16 @@ impl Accounts { feature_set: &FeatureSet, ) -> Result> { if feature_set.is_active(&feature_set::cap_transaction_accounts_data_size::id()) { - let mut compute_budget = - ComputeBudget::new(compute_budget::MAX_COMPUTE_UNIT_LIMIT as u64); - let _process_transaction_result = compute_budget.process_instructions( + let compute_budget_limits = process_compute_budget_instructions( tx.message().program_instructions_iter(), - !feature_set.is_active(&remove_deprecated_request_unit_ix::id()), - feature_set.is_active(&add_set_tx_loaded_accounts_data_size_instruction::id()), - ); + feature_set, + ) + .unwrap_or_default(); // sanitize against setting size limit to zero - NonZeroUsize::new(compute_budget.loaded_accounts_data_size_limit).map_or( + NonZeroUsize::new( + usize::try_from(compute_budget_limits.loaded_accounts_bytes).unwrap_or_default(), + ) + .map_or( Err(TransactionError::InvalidLoadedAccountsDataSizeLimit), |v| Ok(Some(v)), ) @@ -722,7 +722,7 @@ impl Accounts { fee_structure.calculate_fee( tx.message(), lamports_per_signature, - &ComputeBudget::fee_budget_limits(tx.message().program_instructions_iter(), feature_set), + &process_compute_budget_instructions(tx.message().program_instructions_iter(), feature_set).unwrap_or_default().into(), feature_set.is_active(&remove_congestion_multiplier_from_fee_calculation::id()), feature_set.is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()), ) @@ -1474,8 +1474,9 @@ mod tests { transaction_results::{DurableNonceFee, TransactionExecutionDetails}, }, assert_matches::assert_matches, - solana_program_runtime::prioritization_fee::{ - PrioritizationFeeDetails, PrioritizationFeeType, + solana_program_runtime::{ + compute_budget_processor, + prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType}, }, solana_sdk::{ account::{AccountSharedData, WritableAccount}, @@ -1751,13 +1752,15 @@ mod tests { ); let mut feature_set = FeatureSet::all_enabled(); - feature_set.deactivate(&remove_deprecated_request_unit_ix::id()); + feature_set.deactivate(&solana_sdk::feature_set::remove_deprecated_request_unit_ix::id()); let message = SanitizedMessage::try_from(tx.message().clone()).unwrap(); let fee = FeeStructure::default().calculate_fee( &message, lamports_per_signature, - &ComputeBudget::fee_budget_limits(message.program_instructions_iter(), &feature_set), + &process_compute_budget_instructions(message.program_instructions_iter(), &feature_set) + .unwrap_or_default() + .into(), true, false, ); @@ -4253,7 +4256,11 @@ mod tests { let result_no_limit = Ok(None); let result_default_limit = Ok(Some( - NonZeroUsize::new(compute_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES).unwrap(), + NonZeroUsize::new( + usize::try_from(compute_budget_processor::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES) + .unwrap(), + ) + .unwrap(), )); let result_requested_limit: Result> = Ok(Some(NonZeroUsize::new(99).unwrap())); @@ -4281,7 +4288,10 @@ mod tests { // if tx doesn't set limit, then default limit (64MiB) // if tx sets limit, then requested limit // if tx sets limit to zero, then TransactionError::InvalidLoadedAccountsDataSizeLimit - feature_set.activate(&add_set_tx_loaded_accounts_data_size_instruction::id(), 0); + feature_set.activate( + &solana_sdk::feature_set::add_set_tx_loaded_accounts_data_size_instruction::id(), + 0, + ); test(tx_not_set_limit, &feature_set, &result_default_limit); test(tx_set_limit_99, &feature_set, &result_requested_limit); test(tx_set_limit_0, &feature_set, &result_invalid_limit); @@ -4316,13 +4326,15 @@ mod tests { ); let mut feature_set = FeatureSet::all_enabled(); - feature_set.deactivate(&remove_deprecated_request_unit_ix::id()); + feature_set.deactivate(&solana_sdk::feature_set::remove_deprecated_request_unit_ix::id()); let message = SanitizedMessage::try_from(tx.message().clone()).unwrap(); let fee = FeeStructure::default().calculate_fee( &message, lamports_per_signature, - &ComputeBudget::fee_budget_limits(message.program_instructions_iter(), &feature_set), + &process_compute_budget_instructions(message.program_instructions_iter(), &feature_set) + .unwrap_or_default() + .into(), true, false, ); diff --git a/cost-model/src/cost_model.rs b/cost-model/src/cost_model.rs index 0e8d69542..bb3e296d6 100644 --- a/cost-model/src/cost_model.rs +++ b/cost-model/src/cost_model.rs @@ -8,17 +8,17 @@ use { crate::{block_cost_limits::*, transaction_cost::*}, log::*, - solana_program_runtime::compute_budget::{ - ComputeBudget, DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_COMPUTE_UNIT_LIMIT, + solana_program_runtime::{ + compute_budget::DEFAULT_HEAP_COST, + compute_budget_processor::{ + process_compute_budget_instructions, ComputeBudgetLimits, + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_COMPUTE_UNIT_LIMIT, + }, }, solana_sdk::{ borsh0_10::try_from_slice_unchecked, compute_budget::{self, ComputeBudgetInstruction}, - feature_set::{ - add_set_tx_loaded_accounts_data_size_instruction, - include_loaded_accounts_data_size_in_fee_calculation, - remove_deprecated_request_unit_ix, FeatureSet, - }, + feature_set::{include_loaded_accounts_data_size_in_fee_calculation, FeatureSet}, fee::FeeStructure, instruction::CompiledInstruction, program_utils::limited_deserialize, @@ -62,10 +62,12 @@ impl CostModel { // to set limit, `compute_budget.loaded_accounts_data_size_limit` is set to default // limit of 64MB; which will convert to (64M/32K)*8CU = 16_000 CUs // - pub fn calculate_loaded_accounts_data_size_cost(compute_budget: &ComputeBudget) -> u64 { + pub fn calculate_loaded_accounts_data_size_cost( + compute_budget_limits: &ComputeBudgetLimits, + ) -> u64 { FeeStructure::calculate_memory_usage_cost( - compute_budget.loaded_accounts_data_size_limit, - compute_budget.heap_cost, + usize::try_from(compute_budget_limits.loaded_accounts_bytes).unwrap(), + DEFAULT_HEAP_COST, ) } @@ -128,32 +130,28 @@ impl CostModel { } // calculate bpf cost based on compute budget instructions - let mut compute_budget = ComputeBudget::default(); - - let result = compute_budget.process_instructions( - transaction.message().program_instructions_iter(), - !feature_set.is_active(&remove_deprecated_request_unit_ix::id()), - feature_set.is_active(&add_set_tx_loaded_accounts_data_size_instruction::id()), - ); // 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 result { - Ok(_) => { + match process_compute_budget_instructions( + transaction.message().program_instructions_iter(), + feature_set, + ) { + Ok(compute_budget_limits) => { // if tx contained user-space instructions and a more accurate estimate available correct it, // where "user-space instructions" must be specifically checked by // 'compute_unit_limit_is_set' flag, because compute_budget does not distinguish // builtin and bpf instructions when calculating default compute-unit-limit. (see // compute_budget.rs test `test_process_mixed_instructions_without_compute_budget`) if bpf_costs > 0 && compute_unit_limit_is_set { - bpf_costs = compute_budget.compute_unit_limit + bpf_costs = u64::from(compute_budget_limits.compute_unit_limit); } if feature_set .is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()) { loaded_accounts_data_size_cost = - Self::calculate_loaded_accounts_data_size_cost(&compute_budget); + Self::calculate_loaded_accounts_data_size_cost(&compute_budget_limits); } } Err(_) => { @@ -545,7 +543,8 @@ mod tests { // default loaded_accounts_data_size_limit const DEFAULT_PAGE_COST: u64 = 8; let expected_loaded_accounts_data_size_cost = - solana_program_runtime::compute_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES as u64 + solana_program_runtime::compute_budget_processor::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES + as u64 / ACCOUNT_DATA_COST_PAGE_SIZE * DEFAULT_PAGE_COST; @@ -663,36 +662,36 @@ mod tests { #[allow(clippy::field_reassign_with_default)] #[test] fn test_calculate_loaded_accounts_data_size_cost() { - let mut compute_budget = ComputeBudget::default(); + let mut compute_budget_limits = ComputeBudgetLimits::default(); // accounts data size are priced in block of 32K, ... // ... requesting less than 32K should still be charged as one block - compute_budget.loaded_accounts_data_size_limit = 31_usize * 1024; + compute_budget_limits.loaded_accounts_bytes = 31 * 1024; assert_eq!( - compute_budget.heap_cost, - CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) + DEFAULT_HEAP_COST, + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget_limits) ); // ... requesting exact 32K should be charged as one block - compute_budget.loaded_accounts_data_size_limit = 32_usize * 1024; + compute_budget_limits.loaded_accounts_bytes = 32 * 1024; assert_eq!( - compute_budget.heap_cost, - CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) + DEFAULT_HEAP_COST, + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget_limits) ); // ... requesting slightly above 32K should be charged as 2 block - compute_budget.loaded_accounts_data_size_limit = 33_usize * 1024; + compute_budget_limits.loaded_accounts_bytes = 33 * 1024; assert_eq!( - compute_budget.heap_cost * 2, - CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) + DEFAULT_HEAP_COST * 2, + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget_limits) ); // ... requesting exact 64K should be charged as 2 block - compute_budget.loaded_accounts_data_size_limit = 64_usize * 1024; + compute_budget_limits.loaded_accounts_bytes = 64 * 1024; assert_eq!( - compute_budget.heap_cost * 2, - CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) + DEFAULT_HEAP_COST * 2, + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget_limits) ); } diff --git a/program-runtime/src/compute_budget.rs b/program-runtime/src/compute_budget.rs index f9239224b..a568162c1 100644 --- a/program-runtime/src/compute_budget.rs +++ b/program-runtime/src/compute_budget.rs @@ -1,28 +1,11 @@ use { - crate::prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType}, + crate::compute_budget_processor::{self, process_compute_budget_instructions}, solana_sdk::{ - borsh0_10::try_from_slice_unchecked, - compute_budget::{self, ComputeBudgetInstruction}, - entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES, - feature_set::{ - add_set_tx_loaded_accounts_data_size_instruction, remove_deprecated_request_unit_ix, - FeatureSet, - }, - fee::FeeBudgetLimits, - instruction::{CompiledInstruction, InstructionError}, - pubkey::Pubkey, - transaction::TransactionError, + feature_set::FeatureSet, instruction::CompiledInstruction, pubkey::Pubkey, + transaction::Result, }, }; -/// The total accounts data a transaction can load is limited to 64MiB to not break -/// anyone in Mainnet-beta today. It can be set by set_loaded_accounts_data_size_limit instruction -pub const MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES: usize = 64 * 1024 * 1024; - -pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; -pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; -const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; - #[cfg(RUSTC_WITH_SPECIALIZATION)] impl ::solana_frozen_abi::abi_example::AbiExample for ComputeBudget { fn example() -> Self { @@ -31,6 +14,10 @@ impl ::solana_frozen_abi::abi_example::AbiExample for ComputeBudget { } } +/// Roughly 0.5us/page, where page is 32K; given roughly 15CU/us, the +/// default heap page cost = 0.5 * 15 ~= 8CU/page +pub const DEFAULT_HEAP_COST: u64 = 8; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ComputeBudget { /// Number of compute units that a transaction or individual instruction is @@ -118,9 +105,6 @@ pub struct ComputeBudget { pub alt_bn128_pairing_one_pair_cost_other: u64, /// Big integer modular exponentiation cost pub big_modular_exponentiation_cost: u64, - /// Maximum accounts data size, in bytes, that a transaction is allowed to load; The - /// value is capped by MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES to prevent overuse of memory. - pub loaded_accounts_data_size_limit: usize, /// Coefficient `a` of the quadratic function which determines the number /// of compute units consumed to call poseidon syscall for a given number /// of inputs. @@ -143,7 +127,7 @@ pub struct ComputeBudget { impl Default for ComputeBudget { fn default() -> Self { - Self::new(MAX_COMPUTE_UNIT_LIMIT as u64) + Self::new(compute_budget_processor::MAX_COMPUTE_UNIT_LIMIT as u64) } } @@ -180,14 +164,13 @@ impl ComputeBudget { curve25519_ristretto_msm_base_cost: 2303, curve25519_ristretto_msm_incremental_cost: 788, heap_size: u32::try_from(solana_sdk::entrypoint::HEAP_LENGTH).unwrap(), - heap_cost: 8, + heap_cost: DEFAULT_HEAP_COST, mem_op_base_cost: 10, alt_bn128_addition_cost: 334, alt_bn128_multiplication_cost: 3_840, alt_bn128_pairing_one_pair_cost_first: 36_364, alt_bn128_pairing_one_pair_cost_other: 12_121, big_modular_exponentiation_cost: 33, - loaded_accounts_data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, poseidon_cost_coefficient_a: 61, poseidon_cost_coefficient_c: 542, get_remaining_compute_units_cost: 100, @@ -198,127 +181,16 @@ impl ComputeBudget { } } - pub fn process_instructions<'a>( - &mut self, - instructions: impl Iterator, - support_request_units_deprecated: bool, - support_set_loaded_accounts_data_size_limit_ix: bool, - ) -> Result { - let mut num_non_compute_budget_instructions: u32 = 0; - let mut updated_compute_unit_limit = None; - let mut requested_heap_size = None; - let mut prioritization_fee = None; - let mut updated_loaded_accounts_data_size_limit = None; - - for (i, (program_id, instruction)) in instructions.enumerate() { - if compute_budget::check_id(program_id) { - let invalid_instruction_data_error = TransactionError::InstructionError( - i as u8, - InstructionError::InvalidInstructionData, - ); - let duplicate_instruction_error = TransactionError::DuplicateInstruction(i as u8); - - match try_from_slice_unchecked(&instruction.data) { - Ok(ComputeBudgetInstruction::RequestUnitsDeprecated { - units: compute_unit_limit, - additional_fee, - }) if support_request_units_deprecated => { - if updated_compute_unit_limit.is_some() { - return Err(duplicate_instruction_error); - } - if prioritization_fee.is_some() { - return Err(duplicate_instruction_error); - } - updated_compute_unit_limit = Some(compute_unit_limit); - prioritization_fee = - Some(PrioritizationFeeType::Deprecated(additional_fee as u64)); - } - Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => { - if requested_heap_size.is_some() { - return Err(duplicate_instruction_error); - } - requested_heap_size = Some((bytes, i as u8)); - } - Ok(ComputeBudgetInstruction::SetComputeUnitLimit(compute_unit_limit)) => { - if updated_compute_unit_limit.is_some() { - return Err(duplicate_instruction_error); - } - updated_compute_unit_limit = Some(compute_unit_limit); - } - Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => { - if prioritization_fee.is_some() { - return Err(duplicate_instruction_error); - } - prioritization_fee = - Some(PrioritizationFeeType::ComputeUnitPrice(micro_lamports)); - } - Ok(ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes)) - if support_set_loaded_accounts_data_size_limit_ix => - { - if updated_loaded_accounts_data_size_limit.is_some() { - return Err(duplicate_instruction_error); - } - updated_loaded_accounts_data_size_limit = Some(bytes as usize); - } - _ => return Err(invalid_instruction_data_error), - } - } else { - // only include non-request instructions in default max calc - num_non_compute_budget_instructions = - num_non_compute_budget_instructions.saturating_add(1); - } - } - - if let Some((bytes, i)) = requested_heap_size { - if bytes > MAX_HEAP_FRAME_BYTES - || bytes < MIN_HEAP_FRAME_BYTES as u32 - || bytes % 1024 != 0 - { - return Err(TransactionError::InstructionError( - i, - InstructionError::InvalidInstructionData, - )); - } - self.heap_size = bytes; - } - - let compute_unit_limit = updated_compute_unit_limit - .unwrap_or_else(|| { - num_non_compute_budget_instructions - .saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT) - }) - .min(MAX_COMPUTE_UNIT_LIMIT); - self.compute_unit_limit = u64::from(compute_unit_limit); - - self.loaded_accounts_data_size_limit = updated_loaded_accounts_data_size_limit - .unwrap_or(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES) - .min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES); - - Ok(prioritization_fee - .map(|fee_type| PrioritizationFeeDetails::new(fee_type, self.compute_unit_limit)) - .unwrap_or_default()) - } - - pub fn fee_budget_limits<'a>( + pub fn try_from_instructions<'a>( instructions: impl Iterator, feature_set: &FeatureSet, - ) -> FeeBudgetLimits { - let mut compute_budget = Self::default(); - - let prioritization_fee_details = compute_budget - .process_instructions( - instructions, - !feature_set.is_active(&remove_deprecated_request_unit_ix::id()), - feature_set.is_active(&add_set_tx_loaded_accounts_data_size_instruction::id()), - ) - .unwrap_or_default(); - - FeeBudgetLimits { - loaded_accounts_data_size_limit: compute_budget.loaded_accounts_data_size_limit, - heap_cost: compute_budget.heap_cost, - compute_unit_limit: compute_budget.compute_unit_limit, - prioritization_fee: prioritization_fee_details.get_fee(), - } + ) -> Result { + let compute_budget_limits = process_compute_budget_instructions(instructions, feature_set)?; + Ok(ComputeBudget { + compute_unit_limit: u64::from(compute_budget_limits.compute_unit_limit), + heap_size: compute_budget_limits.updated_heap_bytes, + ..ComputeBudget::default() + }) } /// Returns cost of the Poseidon hash function for the given number of @@ -350,489 +222,3 @@ impl ComputeBudget { Some(final_result) } } - -#[cfg(test)] -mod tests { - use { - super::*, - solana_sdk::{ - hash::Hash, - instruction::Instruction, - message::Message, - pubkey::Pubkey, - signature::Keypair, - signer::Signer, - system_instruction::{self}, - transaction::{SanitizedTransaction, Transaction}, - }, - }; - - macro_rules! test { - ( $instructions: expr, $expected_result: expr, $expected_budget: expr, $support_set_loaded_accounts_data_size_limit_ix: expr ) => { - let payer_keypair = Keypair::new(); - let tx = SanitizedTransaction::from_transaction_for_tests(Transaction::new( - &[&payer_keypair], - Message::new($instructions, Some(&payer_keypair.pubkey())), - Hash::default(), - )); - let mut compute_budget = ComputeBudget::default(); - let result = compute_budget.process_instructions( - tx.message().program_instructions_iter(), - false, /*not support request_units_deprecated*/ - $support_set_loaded_accounts_data_size_limit_ix, - ); - assert_eq!($expected_result, result); - assert_eq!(compute_budget, $expected_budget); - }; - ( $instructions: expr, $expected_result: expr, $expected_budget: expr) => { - test!($instructions, $expected_result, $expected_budget, false); - }; - } - - #[test] - fn test_process_instructions() { - // Units - test!( - &[], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: 0, - ..ComputeBudget::default() - } - ); - test!( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(1), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: 1, - ..ComputeBudget::default() - } - ); - test!( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT + 1), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT as u64, - ..ComputeBudget::default() - } - ); - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT as u64, - ..ComputeBudget::default() - } - ); - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_compute_unit_limit(1), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: 1, - ..ComputeBudget::default() - } - ); - - test!( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(1), - ComputeBudgetInstruction::set_compute_unit_price(42) - ], - Ok(PrioritizationFeeDetails::new( - PrioritizationFeeType::ComputeUnitPrice(42), - 1 - )), - ComputeBudget { - compute_unit_limit: 1, - ..ComputeBudget::default() - } - ); - - // HeapFrame - test!( - &[], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: 0, - ..ComputeBudget::default() - } - ); - test!( - &[ - ComputeBudgetInstruction::request_heap_frame(40 * 1024), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - heap_size: 40 * 1024, - ..ComputeBudget::default() - } - ); - test!( - &[ - ComputeBudgetInstruction::request_heap_frame(40 * 1024 + 1), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default() - ); - test!( - &[ - ComputeBudgetInstruction::request_heap_frame(31 * 1024), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default() - ); - test!( - &[ - ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES + 1), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default() - ); - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - heap_size: MAX_HEAP_FRAME_BYTES, - ..ComputeBudget::default() - } - ); - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::request_heap_frame(1), - ], - Err(TransactionError::InstructionError( - 3, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default() - ); - - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 7, - ..ComputeBudget::default() - } - ); - - // Combined - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), - ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), - ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), - ], - Ok(PrioritizationFeeDetails::new( - PrioritizationFeeType::ComputeUnitPrice(u64::MAX), - MAX_COMPUTE_UNIT_LIMIT as u64, - )), - ComputeBudget { - compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT as u64, - heap_size: MAX_HEAP_FRAME_BYTES, - ..ComputeBudget::default() - } - ); - - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_compute_unit_limit(1), - ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), - ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), - ], - Ok(PrioritizationFeeDetails::new( - PrioritizationFeeType::ComputeUnitPrice(u64::MAX), - 1 - )), - ComputeBudget { - compute_unit_limit: 1, - heap_size: MAX_HEAP_FRAME_BYTES, - ..ComputeBudget::default() - } - ); - - // Duplicates - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), - ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT - 1), - ], - Err(TransactionError::DuplicateInstruction(2)), - ComputeBudget::default() - ); - - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::request_heap_frame(MIN_HEAP_FRAME_BYTES as u32), - ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), - ], - Err(TransactionError::DuplicateInstruction(2)), - ComputeBudget::default() - ); - - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_compute_unit_price(0), - ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), - ], - Err(TransactionError::DuplicateInstruction(2)), - ComputeBudget::default() - ); - - // deprecated - test!( - &[Instruction::new_with_borsh( - compute_budget::id(), - &compute_budget::ComputeBudgetInstruction::RequestUnitsDeprecated { - units: 1_000, - additional_fee: 10 - }, - vec![] - )], - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default() - ); - } - - #[test] - fn test_process_loaded_accounts_data_size_limit_instruction() { - // Assert for empty instructions, change value of support_set_loaded_accounts_data_size_limit_ix - // will not change results, which should all be default - for support_set_loaded_accounts_data_size_limit_ix in [true, false] { - test!( - &[], - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: 0, - ..ComputeBudget::default() - }, - support_set_loaded_accounts_data_size_limit_ix - ); - } - - // Assert when set_loaded_accounts_data_size_limit presents, - // if support_set_loaded_accounts_data_size_limit_ix then - // budget is set with data_size - // else - // return InstructionError - let data_size: usize = 1; - for support_set_loaded_accounts_data_size_limit_ix in [true, false] { - let (expected_result, expected_budget) = - if support_set_loaded_accounts_data_size_limit_ix { - ( - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - loaded_accounts_data_size_limit: data_size, - ..ComputeBudget::default() - }, - ) - } else { - ( - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default(), - ) - }; - - test!( - &[ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size as u32), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - expected_result, - expected_budget, - support_set_loaded_accounts_data_size_limit_ix - ); - } - - // Assert when set_loaded_accounts_data_size_limit presents, with greater than max value - // if support_set_loaded_accounts_data_size_limit_ix then - // budget is set to max data size - // else - // return InstructionError - let data_size: usize = MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES + 1; - for support_set_loaded_accounts_data_size_limit_ix in [true, false] { - let (expected_result, expected_budget) = - if support_set_loaded_accounts_data_size_limit_ix { - ( - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - loaded_accounts_data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, - ..ComputeBudget::default() - }, - ) - } else { - ( - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default(), - ) - }; - - test!( - &[ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size as u32), - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ], - expected_result, - expected_budget, - support_set_loaded_accounts_data_size_limit_ix - ); - } - - // Assert when set_loaded_accounts_data_size_limit is not presented - // if support_set_loaded_accounts_data_size_limit_ix then - // budget is set to default data size - // else - // return - for support_set_loaded_accounts_data_size_limit_ix in [true, false] { - let (expected_result, expected_budget) = ( - Ok(PrioritizationFeeDetails::default()), - ComputeBudget { - compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - loaded_accounts_data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, - ..ComputeBudget::default() - }, - ); - - test!( - &[Instruction::new_with_bincode( - Pubkey::new_unique(), - &0_u8, - vec![] - ),], - expected_result, - expected_budget, - support_set_loaded_accounts_data_size_limit_ix - ); - } - - // Assert when set_loaded_accounts_data_size_limit presents more than once, - // if support_set_loaded_accounts_data_size_limit_ix then - // return DuplicateInstruction - // else - // return InstructionError - let data_size: usize = MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES; - for support_set_loaded_accounts_data_size_limit_ix in [true, false] { - let (expected_result, expected_budget) = - if support_set_loaded_accounts_data_size_limit_ix { - ( - Err(TransactionError::DuplicateInstruction(2)), - ComputeBudget::default(), - ) - } else { - ( - Err(TransactionError::InstructionError( - 1, - InstructionError::InvalidInstructionData, - )), - ComputeBudget::default(), - ) - }; - - test!( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size as u32), - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size as u32), - ], - expected_result, - expected_budget, - support_set_loaded_accounts_data_size_limit_ix - ); - } - } - - #[test] - fn test_process_mixed_instructions_without_compute_budget() { - let payer_keypair = Keypair::new(); - - let transaction = - SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( - &[ - Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), - system_instruction::transfer(&payer_keypair.pubkey(), &Pubkey::new_unique(), 2), - ], - Some(&payer_keypair.pubkey()), - &[&payer_keypair], - Hash::default(), - )); - - let mut compute_budget = ComputeBudget::default(); - let result = compute_budget.process_instructions( - transaction.message().program_instructions_iter(), - false, //not support request_units_deprecated - true, //support_set_loaded_accounts_data_size_limit_ix, - ); - - // assert process_instructions will be successful with default, - assert_eq!(Ok(PrioritizationFeeDetails::default()), result); - // assert the default compute_unit_limit is 2 times default: one for bpf ix, one for - // builtin ix. - assert_eq!( - compute_budget, - ComputeBudget { - compute_unit_limit: 2 * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, - ..ComputeBudget::default() - } - ); - } -} diff --git a/program-runtime/src/compute_budget_processor.rs b/program-runtime/src/compute_budget_processor.rs new file mode 100644 index 000000000..be5e642fa --- /dev/null +++ b/program-runtime/src/compute_budget_processor.rs @@ -0,0 +1,619 @@ +//! Process compute_budget instructions to extract and sanitize limits. +use { + crate::{ + compute_budget::DEFAULT_HEAP_COST, + prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType}, + }, + solana_sdk::{ + borsh0_10::try_from_slice_unchecked, + compute_budget::{self, ComputeBudgetInstruction}, + entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES, + feature_set::{ + add_set_tx_loaded_accounts_data_size_instruction, remove_deprecated_request_unit_ix, + FeatureSet, + }, + fee::FeeBudgetLimits, + instruction::{CompiledInstruction, InstructionError}, + pubkey::Pubkey, + transaction::TransactionError, + }, +}; + +const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; +pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; +pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; + +/// The total accounts data a transaction can load is limited to 64MiB to not break +/// anyone in Mainnet-beta today. It can be set by set_loaded_accounts_data_size_limit instruction +pub const MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES: u32 = 64 * 1024 * 1024; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ComputeBudgetLimits { + pub updated_heap_bytes: u32, + pub compute_unit_limit: u32, + pub compute_unit_price: u64, + pub loaded_accounts_bytes: u32, +} + +impl Default for ComputeBudgetLimits { + fn default() -> Self { + ComputeBudgetLimits { + updated_heap_bytes: u32::try_from(MIN_HEAP_FRAME_BYTES).unwrap(), + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + compute_unit_price: 0, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + } + } +} + +impl From for FeeBudgetLimits { + fn from(val: ComputeBudgetLimits) -> Self { + let prioritization_fee_details = PrioritizationFeeDetails::new( + PrioritizationFeeType::ComputeUnitPrice(val.compute_unit_price), + u64::from(val.compute_unit_limit), + ); + FeeBudgetLimits { + // NOTE - usize::from(u32).unwrap() may fail if target is 16-bit and + // `loaded_accounts_bytes` is greater than u16::MAX. In that case, panic is proper. + loaded_accounts_data_size_limit: usize::try_from(val.loaded_accounts_bytes).unwrap(), + heap_cost: DEFAULT_HEAP_COST, + compute_unit_limit: u64::from(val.compute_unit_limit), + prioritization_fee: prioritization_fee_details.get_fee(), + } + } +} + +/// Processing compute_budget could be part of tx sanitizing, failed to process +/// these instructions will drop the transaction eventually without execution, +/// may as well fail it early. +/// If succeeded, the transaction's specific limits/requests (could be default) +/// are retrieved and returned, +pub fn process_compute_budget_instructions<'a>( + instructions: impl Iterator, + feature_set: &FeatureSet, +) -> Result { + let support_request_units_deprecated = + !feature_set.is_active(&remove_deprecated_request_unit_ix::id()); + let support_set_loaded_accounts_data_size_limit_ix = + feature_set.is_active(&add_set_tx_loaded_accounts_data_size_instruction::id()); + + let mut num_non_compute_budget_instructions: u32 = 0; + let mut updated_compute_unit_limit = None; + let mut updated_compute_unit_price = None; + let mut requested_heap_size = None; + let mut updated_loaded_accounts_data_size_limit = None; + + for (i, (program_id, instruction)) in instructions.enumerate() { + if compute_budget::check_id(program_id) { + let invalid_instruction_data_error = TransactionError::InstructionError( + i as u8, + InstructionError::InvalidInstructionData, + ); + let duplicate_instruction_error = TransactionError::DuplicateInstruction(i as u8); + + match try_from_slice_unchecked(&instruction.data) { + Ok(ComputeBudgetInstruction::RequestUnitsDeprecated { + units: compute_unit_limit, + additional_fee, + }) if support_request_units_deprecated => { + if updated_compute_unit_limit.is_some() { + return Err(duplicate_instruction_error); + } + if updated_compute_unit_price.is_some() { + return Err(duplicate_instruction_error); + } + updated_compute_unit_limit = Some(compute_unit_limit); + updated_compute_unit_price = + support_deprecated_requested_units(additional_fee, compute_unit_limit); + } + Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => { + if requested_heap_size.is_some() { + return Err(duplicate_instruction_error); + } + if sanitize_requested_heap_size(bytes) { + requested_heap_size = Some(bytes); + } else { + return Err(invalid_instruction_data_error); + } + } + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(compute_unit_limit)) => { + if updated_compute_unit_limit.is_some() { + return Err(duplicate_instruction_error); + } + updated_compute_unit_limit = Some(compute_unit_limit); + } + Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => { + if updated_compute_unit_price.is_some() { + return Err(duplicate_instruction_error); + } + updated_compute_unit_price = Some(micro_lamports); + } + Ok(ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes)) + if support_set_loaded_accounts_data_size_limit_ix => + { + if updated_loaded_accounts_data_size_limit.is_some() { + return Err(duplicate_instruction_error); + } + updated_loaded_accounts_data_size_limit = Some(bytes); + } + _ => return Err(invalid_instruction_data_error), + } + } else { + // only include non-request instructions in default max calc + num_non_compute_budget_instructions = + num_non_compute_budget_instructions.saturating_add(1); + } + } + + // sanitize limits + let updated_heap_bytes = requested_heap_size + .unwrap_or(u32::try_from(MIN_HEAP_FRAME_BYTES).unwrap()) // loader's default heap_size + .min(MAX_HEAP_FRAME_BYTES); + + let compute_unit_limit = updated_compute_unit_limit + .unwrap_or_else(|| { + num_non_compute_budget_instructions + .saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT) + }) + .min(MAX_COMPUTE_UNIT_LIMIT); + + let compute_unit_price = updated_compute_unit_price.unwrap_or(0); + + let loaded_accounts_bytes = updated_loaded_accounts_data_size_limit + .unwrap_or(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES) + .min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES); + + Ok(ComputeBudgetLimits { + updated_heap_bytes, + compute_unit_limit, + compute_unit_price, + loaded_accounts_bytes, + }) +} + +fn sanitize_requested_heap_size(bytes: u32) -> bool { + (u32::try_from(MIN_HEAP_FRAME_BYTES).unwrap()..=MAX_HEAP_FRAME_BYTES).contains(&bytes) + && bytes % 1024 == 0 +} + +// Supports request_units_derpecated ix, returns cu_price if available. +fn support_deprecated_requested_units(additional_fee: u32, compute_unit_limit: u32) -> Option { + // TODO: remove support of 'Deprecated' after feature remove_deprecated_request_unit_ix::id() is activated + const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000; + + let micro_lamport_fee = + (additional_fee as u128).saturating_mul(MICRO_LAMPORTS_PER_LAMPORT as u128); + micro_lamport_fee + .checked_div(compute_unit_limit as u128) + .map(|cu_price| u64::try_from(cu_price).unwrap_or(u64::MAX)) +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_sdk::{ + hash::Hash, + instruction::Instruction, + message::Message, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_instruction::{self}, + transaction::{SanitizedTransaction, Transaction}, + }, + }; + + macro_rules! test { + ( $instructions: expr, $expected_result: expr, $support_set_loaded_accounts_data_size_limit_ix: expr ) => { + let payer_keypair = Keypair::new(); + let tx = SanitizedTransaction::from_transaction_for_tests(Transaction::new( + &[&payer_keypair], + Message::new($instructions, Some(&payer_keypair.pubkey())), + Hash::default(), + )); + let mut feature_set = FeatureSet::default(); + feature_set.activate(&remove_deprecated_request_unit_ix::id(), 0); + if $support_set_loaded_accounts_data_size_limit_ix { + feature_set.activate(&add_set_tx_loaded_accounts_data_size_instruction::id(), 0); + } + let result = process_compute_budget_instructions( + tx.message().program_instructions_iter(), + &feature_set, + ); + assert_eq!($expected_result, result); + }; + ( $instructions: expr, $expected_result: expr ) => { + test!($instructions, $expected_result, false); + }; + } + + #[test] + fn test_process_instructions() { + // Units + test!( + &[], + Ok(ComputeBudgetLimits { + compute_unit_limit: 0, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: 1, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT + 1), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(1), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: 1, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1), + ComputeBudgetInstruction::set_compute_unit_price(42) + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: 1, + compute_unit_price: 42, + ..ComputeBudgetLimits::default() + }) + ); + + // HeapFrame + test!( + &[], + Ok(ComputeBudgetLimits { + compute_unit_limit: 0, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + ComputeBudgetInstruction::request_heap_frame(40 * 1024), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + updated_heap_bytes: 40 * 1024, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + ComputeBudgetInstruction::request_heap_frame(40 * 1024 + 1), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + ); + test!( + &[ + ComputeBudgetInstruction::request_heap_frame(31 * 1024), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + ); + test!( + &[ + ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES + 1), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + updated_heap_bytes: MAX_HEAP_FRAME_BYTES, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::request_heap_frame(1), + ], + Err(TransactionError::InstructionError( + 3, + InstructionError::InvalidInstructionData, + )) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT * 7, + ..ComputeBudgetLimits::default() + }) + ); + + // Combined + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ], + Ok(ComputeBudgetLimits { + compute_unit_price: u64::MAX, + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + updated_heap_bytes: MAX_HEAP_FRAME_BYTES, + ..ComputeBudgetLimits::default() + }) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(1), + ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ], + Ok(ComputeBudgetLimits { + compute_unit_price: u64::MAX, + compute_unit_limit: 1, + updated_heap_bytes: MAX_HEAP_FRAME_BYTES, + ..ComputeBudgetLimits::default() + }) + ); + + // Duplicates + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT - 1), + ], + Err(TransactionError::DuplicateInstruction(2)) + ); + + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::request_heap_frame(MIN_HEAP_FRAME_BYTES as u32), + ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), + ], + Err(TransactionError::DuplicateInstruction(2)) + ); + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_compute_unit_price(0), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ], + Err(TransactionError::DuplicateInstruction(2)) + ); + + // deprecated + test!( + &[Instruction::new_with_borsh( + compute_budget::id(), + &compute_budget::ComputeBudgetInstruction::RequestUnitsDeprecated { + units: 1_000, + additional_fee: 10 + }, + vec![] + )], + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + ); + } + + #[test] + fn test_process_loaded_accounts_data_size_limit_instruction() { + // Assert for empty instructions, change value of support_set_loaded_accounts_data_size_limit_ix + // will not change results, which should all be default + for support_set_loaded_accounts_data_size_limit_ix in [true, false] { + test!( + &[], + Ok(ComputeBudgetLimits { + compute_unit_limit: 0, + ..ComputeBudgetLimits::default() + }), + support_set_loaded_accounts_data_size_limit_ix + ); + } + + // Assert when set_loaded_accounts_data_size_limit presents, + // if support_set_loaded_accounts_data_size_limit_ix then + // budget is set with data_size + // else + // return InstructionError + let data_size = 1; + for support_set_loaded_accounts_data_size_limit_ix in [true, false] { + let expected_result = if support_set_loaded_accounts_data_size_limit_ix { + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + loaded_accounts_bytes: data_size, + ..ComputeBudgetLimits::default() + }) + } else { + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + }; + + test!( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + expected_result, + support_set_loaded_accounts_data_size_limit_ix + ); + } + + // Assert when set_loaded_accounts_data_size_limit presents, with greater than max value + // if support_set_loaded_accounts_data_size_limit_ix then + // budget is set to max data size + // else + // return InstructionError + let data_size = MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES + 1; + for support_set_loaded_accounts_data_size_limit_ix in [true, false] { + let expected_result = if support_set_loaded_accounts_data_size_limit_ix { + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + ..ComputeBudgetLimits::default() + }) + } else { + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )) + }; + + test!( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + expected_result, + support_set_loaded_accounts_data_size_limit_ix + ); + } + + // Assert when set_loaded_accounts_data_size_limit is not presented + // if support_set_loaded_accounts_data_size_limit_ix then + // budget is set to default data size + // else + // return + for support_set_loaded_accounts_data_size_limit_ix in [true, false] { + let expected_result = Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + ..ComputeBudgetLimits::default() + }); + + test!( + &[Instruction::new_with_bincode( + Pubkey::new_unique(), + &0_u8, + vec![] + ),], + expected_result, + support_set_loaded_accounts_data_size_limit_ix + ); + } + + // Assert when set_loaded_accounts_data_size_limit presents more than once, + // if support_set_loaded_accounts_data_size_limit_ix then + // return DuplicateInstruction + // else + // return InstructionError + let data_size = MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES; + for support_set_loaded_accounts_data_size_limit_ix in [true, false] { + let expected_result = if support_set_loaded_accounts_data_size_limit_ix { + Err(TransactionError::DuplicateInstruction(2)) + } else { + Err(TransactionError::InstructionError( + 1, + InstructionError::InvalidInstructionData, + )) + }; + + test!( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + ], + expected_result, + support_set_loaded_accounts_data_size_limit_ix + ); + } + } + + #[test] + fn test_process_mixed_instructions_without_compute_budget() { + let payer_keypair = Keypair::new(); + + let transaction = + SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[ + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + system_instruction::transfer(&payer_keypair.pubkey(), &Pubkey::new_unique(), 2), + ], + Some(&payer_keypair.pubkey()), + &[&payer_keypair], + Hash::default(), + )); + + let mut feature_set = FeatureSet::default(); + feature_set.activate(&remove_deprecated_request_unit_ix::id(), 0); + feature_set.activate(&add_set_tx_loaded_accounts_data_size_instruction::id(), 0); + + let result = process_compute_budget_instructions( + transaction.message().program_instructions_iter(), + &feature_set, + ); + + // assert process_instructions will be successful with default, + // and the default compute_unit_limit is 2 times default: one for bpf ix, one for + // builtin ix. + assert_eq!( + result, + Ok(ComputeBudgetLimits { + compute_unit_limit: 2 * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }) + ); + } +} diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 9fbe42d8d..566a98dab 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -756,7 +756,7 @@ pub fn mock_process_instruction TransactionExecutionResult::NotExecuted(e.clone()), (Ok(loaded_transaction), nonce) => { - let compute_budget = if let Some(compute_budget) = - self.runtime_config.compute_budget - { - compute_budget - } else { - let mut compute_budget = - ComputeBudget::new(compute_budget::MAX_COMPUTE_UNIT_LIMIT as u64); - - let mut compute_budget_process_transaction_time = - Measure::start("compute_budget_process_transaction_time"); - let process_transaction_result = compute_budget.process_instructions( - tx.message().program_instructions_iter(), - !self - .feature_set - .is_active(&remove_deprecated_request_unit_ix::id()), - self.feature_set - .is_active(&add_set_tx_loaded_accounts_data_size_instruction::id()), - ); - compute_budget_process_transaction_time.stop(); - saturating_add_assign!( - timings - .execute_accessories - .compute_budget_process_transaction_us, - compute_budget_process_transaction_time.as_us() - ); - if let Err(err) = process_transaction_result { - return TransactionExecutionResult::NotExecuted(err); - } - compute_budget - }; + let compute_budget = + if let Some(compute_budget) = self.runtime_config.compute_budget { + compute_budget + } else { + let mut compute_budget_process_transaction_time = + Measure::start("compute_budget_process_transaction_time"); + let maybe_compute_budget = ComputeBudget::try_from_instructions( + tx.message().program_instructions_iter(), + &self.feature_set, + ); + compute_budget_process_transaction_time.stop(); + saturating_add_assign!( + timings + .execute_accessories + .compute_budget_process_transaction_us, + compute_budget_process_transaction_time.as_us() + ); + if let Err(err) = maybe_compute_budget { + return TransactionExecutionResult::NotExecuted(err); + } + maybe_compute_budget.unwrap() + }; let result = self.execute_loaded_transaction( tx, diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 82393ef71..cd1e22759 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -46,7 +46,8 @@ use { }, solana_logger, solana_program_runtime::{ - compute_budget::{self, ComputeBudget, MAX_COMPUTE_UNIT_LIMIT}, + compute_budget::ComputeBudget, + compute_budget_processor::{self, MAX_COMPUTE_UNIT_LIMIT}, declare_process_instruction, invoke_context::mock_process_instruction, loaded_programs::{LoadedProgram, LoadedProgramType, DELAY_VISIBILITY_SLOT_OFFSET}, @@ -10120,7 +10121,9 @@ fn test_compute_budget_program_noop() { assert_eq!( *compute_budget, ComputeBudget { - compute_unit_limit: compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + compute_unit_limit: u64::from( + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + ), heap_size: 48 * 1024, ..ComputeBudget::default() } @@ -10133,7 +10136,7 @@ fn test_compute_budget_program_noop() { let message = Message::new( &[ ComputeBudgetInstruction::set_compute_unit_limit( - compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, ), ComputeBudgetInstruction::request_heap_frame(48 * 1024), Instruction::new_with_bincode(program_id, &0, vec![]), @@ -10163,7 +10166,9 @@ fn test_compute_request_instruction() { assert_eq!( *compute_budget, ComputeBudget { - compute_unit_limit: compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + compute_unit_limit: u64::from( + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + ), heap_size: 48 * 1024, ..ComputeBudget::default() } @@ -10176,7 +10181,7 @@ fn test_compute_request_instruction() { let message = Message::new( &[ ComputeBudgetInstruction::set_compute_unit_limit( - compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, ), ComputeBudgetInstruction::request_heap_frame(48 * 1024), Instruction::new_with_bincode(program_id, &0, vec![]), @@ -10213,7 +10218,9 @@ fn test_failed_compute_request_instruction() { assert_eq!( *compute_budget, ComputeBudget { - compute_unit_limit: compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + compute_unit_limit: u64::from( + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + ), heap_size: 48 * 1024, ..ComputeBudget::default() } @@ -10444,14 +10451,19 @@ fn calculate_test_fee( remove_congestion_multiplier: bool, ) -> u64 { let mut feature_set = FeatureSet::all_enabled(); - feature_set.deactivate(&remove_deprecated_request_unit_ix::id()); + feature_set.deactivate(&solana_sdk::feature_set::remove_deprecated_request_unit_ix::id()); if !support_set_accounts_data_size_limit_ix { - feature_set.deactivate(&include_loaded_accounts_data_size_in_fee_calculation::id()); + feature_set.deactivate( + &solana_sdk::feature_set::include_loaded_accounts_data_size_in_fee_calculation::id(), + ); } let budget_limits = - ComputeBudget::fee_budget_limits(message.program_instructions_iter(), &feature_set); + process_compute_budget_instructions(message.program_instructions_iter(), &feature_set) + .unwrap_or_default() + .into(); + fee_structure.calculate_fee( message, lamports_per_signature, @@ -11478,7 +11490,9 @@ fn test_rent_state_list_len() { ); let compute_budget = bank.runtime_config.compute_budget.unwrap_or_else(|| { - ComputeBudget::new(compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64) + ComputeBudget::new(u64::from( + compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + )) }); let transaction_context = TransactionContext::new( loaded_txs[0].0.as_ref().unwrap().accounts.clone(), diff --git a/runtime/src/transaction_priority_details.rs b/runtime/src/transaction_priority_details.rs index 0d0a94df4..401f3e878 100644 --- a/runtime/src/transaction_priority_details.rs +++ b/runtime/src/transaction_priority_details.rs @@ -1,6 +1,7 @@ use { - solana_program_runtime::compute_budget::ComputeBudget, + solana_program_runtime::compute_budget_processor::process_compute_budget_instructions, solana_sdk::{ + feature_set::FeatureSet, instruction::CompiledInstruction, pubkey::Pubkey, transaction::{SanitizedTransaction, SanitizedVersionedTransaction}, @@ -23,18 +24,17 @@ pub trait GetTransactionPriorityDetails { instructions: impl Iterator, _round_compute_unit_price_enabled: bool, ) -> Option { - let mut compute_budget = ComputeBudget::default(); - let prioritization_fee_details = compute_budget - .process_instructions( - instructions, - true, // supports prioritization by request_units_deprecated instruction - true, // enable support set accounts data size instruction - // TODO: round_compute_unit_price_enabled: bool - ) - .ok()?; + let mut feature_set = FeatureSet::default(); + feature_set.activate( + &solana_sdk::feature_set::add_set_tx_loaded_accounts_data_size_instruction::id(), + 0, + ); + + let compute_budget_limits = + process_compute_budget_instructions(instructions, &feature_set).ok()?; Some(TransactionPriorityDetails { - priority: prioritization_fee_details.get_priority(), - compute_unit_limit: compute_budget.compute_unit_limit, + priority: compute_budget_limits.compute_unit_price, + compute_unit_limit: u64::from(compute_budget_limits.compute_unit_limit), }) } } @@ -98,8 +98,8 @@ mod tests { Some(TransactionPriorityDetails { priority: 0, compute_unit_limit: - solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT - as u64 + solana_program_runtime::compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + as u64, }) ); @@ -111,8 +111,8 @@ mod tests { Some(TransactionPriorityDetails { priority: 0, compute_unit_limit: - solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT - as u64 + solana_program_runtime::compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + as u64, }) ); } @@ -174,8 +174,8 @@ mod tests { Some(TransactionPriorityDetails { priority: requested_price, compute_unit_limit: - solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT - as u64 + solana_program_runtime::compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + as u64, }) ); @@ -187,8 +187,8 @@ mod tests { Some(TransactionPriorityDetails { priority: requested_price, compute_unit_limit: - solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT - as u64 + solana_program_runtime::compute_budget_processor::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + as u64, }) ); }