From 4099de52eb861964cc5da3afd418e63c0ed2cdea Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+tao-stones@users.noreply.github.com> Date: Wed, 4 Dec 2024 22:49:59 -0500 Subject: [PATCH] Fix reserve minimal compute units for builtins (#3799) - Add feature gate, issue #2562; - Implement SIMD-170; --------- Co-authored-by: Justin Starry (cherry picked from commit 3e9af14f3a145070773c719ad104b6a02aefd718) # Conflicts: # builtins-default-costs/src/lib.rs # compute-budget/src/compute_budget_limits.rs # compute-budget/src/compute_budget_processor.rs # core/src/banking_stage/consumer.rs # core/src/banking_stage/immutable_deserialized_packet.rs # core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs # cost-model/src/cost_model.rs # cost-model/src/transaction_cost.rs # programs/compute-budget-bench/benches/compute_budget.rs # programs/sbf/tests/programs.rs # runtime-transaction/benches/process_compute_budget_instructions.rs # runtime-transaction/src/compute_budget_instruction_details.rs # runtime-transaction/src/compute_budget_program_id_filter.rs # runtime-transaction/src/lib.rs # runtime-transaction/src/runtime_transaction.rs # runtime-transaction/src/runtime_transaction/sdk_transactions.rs # runtime/src/bank.rs # runtime/src/bank/tests.rs # runtime/src/prioritization_fee_cache.rs # sdk/src/feature_set.rs # svm-transaction/src/svm_message.rs # svm-transaction/src/svm_message/sanitized_message.rs # svm-transaction/src/svm_message/sanitized_transaction.rs # svm/src/transaction_processor.rs # transaction-view/src/resolved_transaction_view.rs # transaction-view/src/transaction_view.rs --- builtins-default-costs/src/lib.rs | 204 ++++++ compute-budget/src/compute_budget_limits.rs | 106 +++ .../src/compute_budget_processor.rs | 133 +++- core/src/banking_stage/consumer.rs | 15 + .../immutable_deserialized_packet.rs | 26 + .../receive_and_buffer.rs | 376 +++++++++++ cost-model/src/cost_model.rs | 270 +++++++- cost-model/src/transaction_cost.rs | 122 ++++ .../benches/compute_budget.rs | 155 +++++ programs/sbf/tests/programs.rs | 23 + .../process_compute_budget_instructions.rs | 189 ++++++ .../src/builtin_programs_filter.rs | 105 +++ .../src/compute_budget_instruction_details.rs | 524 +++++++++++++++ .../src/compute_budget_program_id_filter.rs | 36 ++ runtime-transaction/src/lib.rs | 7 + .../src/runtime_transaction.rs | 49 ++ .../runtime_transaction/sdk_transactions.rs | 346 ++++++++++ runtime/src/bank.rs | 11 + runtime/src/bank/tests.rs | 17 + runtime/src/prioritization_fee_cache.rs | 47 ++ sdk/program/src/message/sanitized.rs | 2 +- sdk/program/src/message/versions/sanitized.rs | 2 +- sdk/src/feature_set.rs | 16 + svm-transaction/src/svm_message.rs | 127 ++++ .../src/svm_message/sanitized_message.rs | 69 ++ .../src/svm_message/sanitized_transaction.rs | 64 ++ svm/src/transaction_processor.rs | 25 + .../src/resolved_transaction_view.rs | 609 ++++++++++++++++++ transaction-view/src/transaction_view.rs | 294 +++++++++ 29 files changed, 3944 insertions(+), 25 deletions(-) create mode 100644 builtins-default-costs/src/lib.rs create mode 100644 compute-budget/src/compute_budget_limits.rs create mode 100644 core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs create mode 100644 programs/compute-budget-bench/benches/compute_budget.rs create mode 100644 runtime-transaction/benches/process_compute_budget_instructions.rs create mode 100644 runtime-transaction/src/builtin_programs_filter.rs create mode 100644 runtime-transaction/src/compute_budget_instruction_details.rs create mode 100644 runtime-transaction/src/compute_budget_program_id_filter.rs create mode 100644 runtime-transaction/src/runtime_transaction/sdk_transactions.rs create mode 100644 svm-transaction/src/svm_message.rs create mode 100644 svm-transaction/src/svm_message/sanitized_message.rs create mode 100644 svm-transaction/src/svm_message/sanitized_transaction.rs create mode 100644 transaction-view/src/resolved_transaction_view.rs create mode 100644 transaction-view/src/transaction_view.rs diff --git a/builtins-default-costs/src/lib.rs b/builtins-default-costs/src/lib.rs new file mode 100644 index 00000000000000..2e167c667cc84d --- /dev/null +++ b/builtins-default-costs/src/lib.rs @@ -0,0 +1,204 @@ +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![allow(clippy::arithmetic_side_effects)] +use { + ahash::AHashMap, + lazy_static::lazy_static, + solana_sdk::{ + address_lookup_table, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, + compute_budget, ed25519_program, + feature_set::{self, FeatureSet}, + loader_v4, + pubkey::Pubkey, + secp256k1_program, + }, +}; + +/// DEVELOPER: when a builtin is migrated to sbpf, please add its corresponding +/// migration feature ID to BUILTIN_INSTRUCTION_COSTS, so the builtin's default +/// cost can be determined properly based on feature status. +/// When migration completed, eg the feature gate is enabled everywhere, please +/// remove that builtin entry from BUILTIN_INSTRUCTION_COSTS. +#[derive(Clone)] +struct BuiltinCost { + native_cost: u64, + core_bpf_migration_feature: Option, +} + +lazy_static! { + /// Number of compute units for each built-in programs + /// + /// DEVELOPER WARNING: This map CANNOT be modified without causing a + /// consensus failure because this map is used to calculate the compute + /// limit for transactions that don't specify a compute limit themselves as + /// of https://github.com/anza-xyz/agave/issues/2212. It's also used to + /// calculate the cost of a transaction which is used in replay to enforce + /// block cost limits as of + /// https://github.com/solana-labs/solana/issues/29595. + static ref BUILTIN_INSTRUCTION_COSTS: AHashMap = [ + ( + solana_stake_program::id(), + BuiltinCost { + native_cost: solana_stake_program::stake_instruction::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: Some(feature_set::migrate_stake_program_to_core_bpf::id()), + }, + ), + ( + solana_config_program::id(), + BuiltinCost { + native_cost: solana_config_program::config_processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: Some(feature_set::migrate_config_program_to_core_bpf::id()), + }, + ), + ( + solana_vote_program::id(), + BuiltinCost { + native_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + solana_system_program::id(), + BuiltinCost { + native_cost: solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + compute_budget::id(), + BuiltinCost { + native_cost: solana_compute_budget_program::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + address_lookup_table::program::id(), + BuiltinCost { + native_cost: solana_address_lookup_table_program::processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: Some( + feature_set::migrate_address_lookup_table_program_to_core_bpf::id(), + ), + }, + ), + ( + bpf_loader_upgradeable::id(), + BuiltinCost { + native_cost: solana_bpf_loader_program::UPGRADEABLE_LOADER_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + bpf_loader_deprecated::id(), + BuiltinCost { + native_cost: solana_bpf_loader_program::DEPRECATED_LOADER_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + bpf_loader::id(), + BuiltinCost { + native_cost: solana_bpf_loader_program::DEFAULT_LOADER_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + ( + loader_v4::id(), + BuiltinCost { + native_cost: solana_loader_v4_program::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: None, + }, + ), + // Note: These are precompile, run directly in bank during sanitizing; + ( + secp256k1_program::id(), + BuiltinCost { + native_cost: 0, + core_bpf_migration_feature: None, + }, + ), + ( + ed25519_program::id(), + BuiltinCost { + native_cost: 0, + core_bpf_migration_feature: None, + }, + ), + // DO NOT ADD MORE ENTRIES TO THIS MAP + ] + .iter() + .cloned() + .collect(); +} + +lazy_static! { + /// A table of 256 booleans indicates whether the first `u8` of a Pubkey exists in + /// BUILTIN_INSTRUCTION_COSTS. If the value is true, the Pubkey might be a builtin key; + /// if false, it cannot be a builtin key. This table allows for quick filtering of + /// builtin program IDs without the need for hashing. + pub static ref MAYBE_BUILTIN_KEY: [bool; 256] = { + let mut temp_table: [bool; 256] = [false; 256]; + BUILTIN_INSTRUCTION_COSTS + .keys() + .for_each(|key| temp_table[key.as_ref()[0] as usize] = true); + temp_table + }; +} + +pub fn get_builtin_instruction_cost<'a>( + program_id: &'a Pubkey, + feature_set: &'a FeatureSet, +) -> Option { + BUILTIN_INSTRUCTION_COSTS + .get(program_id) + .filter( + // Returns true if builtin program id has no core_bpf_migration_feature or feature is not activated; + // otherwise returns false because it's not considered as builtin + |builtin_cost| -> bool { + builtin_cost + .core_bpf_migration_feature + .map(|feature_id| !feature_set.is_active(&feature_id)) + .unwrap_or(true) + }, + ) + .map(|builtin_cost| builtin_cost.native_cost) +} + +#[inline] +pub fn is_builtin_program(program_id: &Pubkey) -> bool { + BUILTIN_INSTRUCTION_COSTS.contains_key(program_id) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_get_builtin_instruction_cost() { + // use native cost if no migration planned + assert_eq!( + Some(solana_compute_budget_program::DEFAULT_COMPUTE_UNITS), + get_builtin_instruction_cost(&compute_budget::id(), &FeatureSet::all_enabled()) + ); + + // use native cost if migration is planned but not activated + assert_eq!( + Some(solana_stake_program::stake_instruction::DEFAULT_COMPUTE_UNITS), + get_builtin_instruction_cost(&solana_stake_program::id(), &FeatureSet::default()) + ); + + // None if migration is planned and activated, in which case, it's no longer builtin + assert!(get_builtin_instruction_cost( + &solana_stake_program::id(), + &FeatureSet::all_enabled() + ) + .is_none()); + + // None if not builtin + assert!( + get_builtin_instruction_cost(&Pubkey::new_unique(), &FeatureSet::default()).is_none() + ); + assert!( + get_builtin_instruction_cost(&Pubkey::new_unique(), &FeatureSet::all_enabled()) + .is_none() + ); + } +} diff --git a/compute-budget/src/compute_budget_limits.rs b/compute-budget/src/compute_budget_limits.rs new file mode 100644 index 00000000000000..ac951a2014a36a --- /dev/null +++ b/compute-budget/src/compute_budget_limits.rs @@ -0,0 +1,106 @@ +use { + solana_fee_structure::FeeBudgetLimits, solana_program_entrypoint::HEAP_LENGTH, + std::num::NonZeroU32, +}; + +/// 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; +pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; +// SIMD-170 defines max CUs to be allocated for any builtin program instructions, that +// have not been migrated to sBPF programs. +pub const MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT: u32 = 3_000; +pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; +pub const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; +pub const MIN_HEAP_FRAME_BYTES: u32 = HEAP_LENGTH as u32; + +type MicroLamports = u128; + +/// There are 10^6 micro-lamports in one lamport +const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_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: NonZeroU32 = + unsafe { NonZeroU32::new_unchecked(64 * 1024 * 1024) }; + +#[derive(Clone, Copy, 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: NonZeroU32, +} + +impl Default for ComputeBudgetLimits { + fn default() -> Self { + ComputeBudgetLimits { + updated_heap_bytes: MIN_HEAP_FRAME_BYTES, + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + compute_unit_price: 0, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + } + } +} + +fn get_prioritization_fee(compute_unit_price: u64, compute_unit_limit: u64) -> u64 { + let micro_lamport_fee: MicroLamports = + (compute_unit_price as u128).saturating_mul(compute_unit_limit as u128); + micro_lamport_fee + .saturating_add(MICRO_LAMPORTS_PER_LAMPORT.saturating_sub(1) as u128) + .checked_div(MICRO_LAMPORTS_PER_LAMPORT as u128) + .and_then(|fee| u64::try_from(fee).ok()) + .unwrap_or(u64::MAX) +} + +impl From for FeeBudgetLimits { + fn from(val: ComputeBudgetLimits) -> Self { + let prioritization_fee = + get_prioritization_fee(val.compute_unit_price, u64::from(val.compute_unit_limit)); + + FeeBudgetLimits { + loaded_accounts_data_size_limit: val.loaded_accounts_bytes, + heap_cost: DEFAULT_HEAP_COST, + compute_unit_limit: u64::from(val.compute_unit_limit), + prioritization_fee, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_new_with_no_fee() { + for compute_units in [0, 1, MICRO_LAMPORTS_PER_LAMPORT, u64::MAX] { + assert_eq!(get_prioritization_fee(0, compute_units), 0); + } + } + + #[test] + fn test_new_with_compute_unit_price() { + assert_eq!( + get_prioritization_fee(MICRO_LAMPORTS_PER_LAMPORT - 1, 1), + 1, + "should round up (<1.0) lamport fee to 1 lamport" + ); + + assert_eq!(get_prioritization_fee(MICRO_LAMPORTS_PER_LAMPORT, 1), 1); + + assert_eq!( + get_prioritization_fee(MICRO_LAMPORTS_PER_LAMPORT + 1, 1), + 2, + "should round up (>1.0) lamport fee to 2 lamports" + ); + + assert_eq!(get_prioritization_fee(200, 100_000), 20); + + assert_eq!( + get_prioritization_fee(MICRO_LAMPORTS_PER_LAMPORT, u64::MAX), + u64::MAX + ); + + assert_eq!(get_prioritization_fee(u64::MAX, u64::MAX), u64::MAX); + } +} diff --git a/compute-budget/src/compute_budget_processor.rs b/compute-budget/src/compute_budget_processor.rs index edd56e382a6bf2..71f61b04674af9 100644 --- a/compute-budget/src/compute_budget_processor.rs +++ b/compute-budget/src/compute_budget_processor.rs @@ -1,4 +1,5 @@ use { +<<<<<<< HEAD:compute-budget/src/compute_budget_processor.rs crate::prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType}, solana_sdk::{ borsh1::try_from_slice_unchecked, @@ -9,6 +10,12 @@ use { pubkey::Pubkey, transaction::TransactionError, }, +======= + crate::compute_budget_instruction_details::*, + solana_compute_budget::compute_budget_limits::*, + solana_sdk::{feature_set::FeatureSet, pubkey::Pubkey, transaction::TransactionError}, + solana_svm_transaction::instruction::SVMInstruction, +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)):runtime-transaction/src/instructions_processor.rs }; /// Roughly 0.5us/page, where page is 32K; given roughly 15CU/us, the @@ -67,6 +74,7 @@ impl From for FeeBudgetLimits { /// If succeeded, the transaction's specific limits/requests (could be default) /// are retrieved and returned, pub fn process_compute_budget_instructions<'a>( +<<<<<<< HEAD:compute-budget/src/compute_budget_processor.rs instructions: impl Iterator, ) -> Result { let mut num_non_compute_budget_instructions: u32 = 0; @@ -149,6 +157,13 @@ pub fn process_compute_budget_instructions<'a>( fn sanitize_requested_heap_size(bytes: u32) -> bool { (MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&bytes) && bytes % 1024 == 0 +======= + instructions: impl Iterator)> + Clone, + feature_set: &FeatureSet, +) -> Result { + ComputeBudgetInstructionDetails::try_from(instructions)? + .sanitize_and_convert_to_compute_budget_limits(feature_set) +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)):runtime-transaction/src/instructions_processor.rs } #[cfg(test)] @@ -169,14 +184,27 @@ mod tests { macro_rules! test { ( $instructions: expr, $expected_result: expr) => { + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + test!($instructions, $expected_result, &feature_set); + } + }; + ( $instructions: expr, $expected_result: expr, $feature_set: 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(), )); +<<<<<<< HEAD:compute-budget/src/compute_budget_processor.rs let result = process_compute_budget_instructions(tx.message().program_instructions_iter()); +======= + + let result = process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&tx), + $feature_set, + ); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)):runtime-transaction/src/instructions_processor.rs assert_eq!($expected_result, result); }; } @@ -262,7 +290,21 @@ mod tests { compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, updated_heap_bytes: 40 * 1024, ..ComputeBudgetLimits::default() - }) + }), + &FeatureSet::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 + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + updated_heap_bytes: 40 * 1024, + ..ComputeBudgetLimits::default() + }), + &FeatureSet::all_enabled() ); test!( &[ @@ -303,7 +345,21 @@ mod tests { compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, updated_heap_bytes: MAX_HEAP_FRAME_BYTES, ..ComputeBudgetLimits::default() - }) + }), + &FeatureSet::default() + ); + 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 + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + updated_heap_bytes: MAX_HEAP_FRAME_BYTES, + ..ComputeBudgetLimits::default() + }), + &FeatureSet::all_enabled() ); test!( &[ @@ -410,13 +466,28 @@ mod tests { loaded_accounts_bytes: data_size, ..ComputeBudgetLimits::default() }); + test!( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + expected_result, + &FeatureSet::default() + ); + let expected_result = Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + loaded_accounts_bytes: NonZeroU32::new(data_size).unwrap(), + ..ComputeBudgetLimits::default() + }); test!( &[ ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), ], - expected_result + expected_result, + &FeatureSet::all_enabled() ); // Assert when set_loaded_accounts_data_size_limit presents, with greater than max value @@ -427,13 +498,28 @@ mod tests { loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, ..ComputeBudgetLimits::default() }); + test!( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), + Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), + ], + expected_result, + &FeatureSet::default() + ); + let expected_result = Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + ..ComputeBudgetLimits::default() + }); test!( &[ ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size), Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]), ], - expected_result + expected_result, + &FeatureSet::all_enabled() ); // Assert when set_loaded_accounts_data_size_limit is not presented @@ -483,18 +569,37 @@ mod tests { Hash::default(), )); +<<<<<<< HEAD:compute-budget/src/compute_budget_processor.rs let result = process_compute_budget_instructions(transaction.message().program_instructions_iter()); +======= + for (feature_set, expected_result) in [ + ( + FeatureSet::default(), + Ok(ComputeBudgetLimits { + compute_unit_limit: 2 * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }), + ), + ( + FeatureSet::all_enabled(), + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }), + ), + ] { + let result = process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&transaction), + &feature_set, + ); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)):runtime-transaction/src/instructions_processor.rs - // 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() - }) - ); + // 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, expected_result); + } } } diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index e87b6374b98ed0..bcfbeee9747ba6 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -589,8 +589,15 @@ impl Consumer { .filter_map(|transaction| { let round_compute_unit_price_enabled = false; // TODO get from working_bank.feature_set transaction +<<<<<<< HEAD .get_compute_budget_details(round_compute_unit_price_enabled) .map(|details| details.compute_unit_price) +======= + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(&bank.feature_set) + .ok() + .map(|limits| limits.compute_unit_price) +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) }) .minmax(); let (min_prioritization_fees, max_prioritization_fees) = @@ -751,9 +758,17 @@ impl Consumer { error_counters: &mut TransactionErrorMetrics, ) -> Result<(), TransactionError> { let fee_payer = message.fee_payer(); +<<<<<<< HEAD let budget_limits = process_compute_budget_instructions(message.program_instructions_iter())?.into(); let fee = bank.fee_structure().calculate_fee( +======= + let fee_budget_limits = FeeBudgetLimits::from(process_compute_budget_instructions( + message.program_instructions_iter(), + &bank.feature_set, + )?); + let fee = solana_fee::calculate_fee( +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) message, bank.get_lamports_per_signature(), &budget_limits, diff --git a/core/src/banking_stage/immutable_deserialized_packet.rs b/core/src/banking_stage/immutable_deserialized_packet.rs index fff7747e64e9b3..9149b77157fb77 100644 --- a/core/src/banking_stage/immutable_deserialized_packet.rs +++ b/core/src/banking_stage/immutable_deserialized_packet.rs @@ -7,6 +7,7 @@ use { }, solana_sdk::{ clock::Slot, + feature_set::FeatureSet, hash::Hash, message::{v0::LoadedAddresses, AddressLoaderError, Message, SimpleAddressLoader}, pubkey::Pubkey, @@ -38,7 +39,17 @@ pub enum DeserializedPacketError { FailedFilter(#[from] PacketFilterFailure), } +<<<<<<< HEAD #[derive(Debug, PartialEq, Eq)] +======= +lazy_static::lazy_static! { + // Make a dummy feature_set with all features enabled to + // fetch compute_unit_price and compute_unit_limit for legacy leader. + static ref FEATURE_SET: FeatureSet = FeatureSet::all_enabled(); +} + +#[derive(Debug)] +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) pub struct ImmutableDeserializedPacket { original_packet: Packet, transaction: SanitizedVersionedTransaction, @@ -56,9 +67,24 @@ impl ImmutableDeserializedPacket { let is_simple_vote = packet.meta().is_simple_vote_tx(); // drop transaction if prioritization fails. +<<<<<<< HEAD let mut compute_budget_details = sanitized_transaction .get_compute_budget_details(packet.meta().round_compute_unit_price()) .ok_or(DeserializedPacketError::PrioritizationFailure)?; +======= + let ComputeBudgetLimits { + mut compute_unit_price, + compute_unit_limit, + .. + } = process_compute_budget_instructions( + sanitized_transaction + .get_message() + .program_instructions_iter() + .map(|(pubkey, ix)| (pubkey, SVMInstruction::from(ix))), + &FEATURE_SET, + ) + .map_err(|_| DeserializedPacketError::PrioritizationFailure)?; +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) // set compute unit price to zero for vote transactions if is_simple_vote { diff --git a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs new file mode 100644 index 00000000000000..21afe448d151d3 --- /dev/null +++ b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs @@ -0,0 +1,376 @@ +use { + super::{ + scheduler_metrics::{SchedulerCountMetrics, SchedulerTimingMetrics}, + transaction_state_container::StateContainer, + }, + crate::banking_stage::{ + decision_maker::BufferedPacketsDecision, + immutable_deserialized_packet::ImmutableDeserializedPacket, + packet_deserializer::PacketDeserializer, scheduler_messages::MaxAge, + transaction_scheduler::transaction_state::SanitizedTransactionTTL, + TransactionStateContainer, + }, + arrayvec::ArrayVec, + core::time::Duration, + crossbeam_channel::RecvTimeoutError, + solana_accounts_db::account_locks::validate_account_locks, + solana_cost_model::cost_model::CostModel, + solana_measure::measure_us, + solana_runtime::{bank::Bank, bank_forks::BankForks}, + solana_runtime_transaction::{ + runtime_transaction::RuntimeTransaction, transaction_meta::StaticMeta, + transaction_with_meta::TransactionWithMeta, + }, + solana_sdk::{ + address_lookup_table::state::estimate_last_valid_slot, + clock::{Epoch, Slot, MAX_PROCESSING_AGE}, + fee::FeeBudgetLimits, + saturating_add_assign, + transaction::SanitizedTransaction, + }, + solana_svm::transaction_error_metrics::TransactionErrorMetrics, + std::sync::{Arc, RwLock}, +}; + +pub(crate) trait ReceiveAndBuffer { + type Transaction: TransactionWithMeta; + type Container: StateContainer; + + /// Returns whether the packet receiver is still connected. + fn receive_and_buffer_packets( + &mut self, + container: &mut Self::Container, + timing_metrics: &mut SchedulerTimingMetrics, + count_metrics: &mut SchedulerCountMetrics, + decision: &BufferedPacketsDecision, + ) -> bool; +} + +pub(crate) struct SanitizedTransactionReceiveAndBuffer { + /// Packet/Transaction ingress. + packet_receiver: PacketDeserializer, + bank_forks: Arc>, + + forwarding_enabled: bool, +} + +impl ReceiveAndBuffer for SanitizedTransactionReceiveAndBuffer { + type Transaction = RuntimeTransaction; + type Container = TransactionStateContainer; + + /// Returns whether the packet receiver is still connected. + fn receive_and_buffer_packets( + &mut self, + container: &mut Self::Container, + timing_metrics: &mut SchedulerTimingMetrics, + count_metrics: &mut SchedulerCountMetrics, + decision: &BufferedPacketsDecision, + ) -> bool { + let remaining_queue_capacity = container.remaining_capacity(); + + const MAX_PACKET_RECEIVE_TIME: Duration = Duration::from_millis(10); + let (recv_timeout, should_buffer) = match decision { + BufferedPacketsDecision::Consume(_) => ( + if container.is_empty() { + MAX_PACKET_RECEIVE_TIME + } else { + Duration::ZERO + }, + true, + ), + BufferedPacketsDecision::Forward => (MAX_PACKET_RECEIVE_TIME, self.forwarding_enabled), + BufferedPacketsDecision::ForwardAndHold | BufferedPacketsDecision::Hold => { + (MAX_PACKET_RECEIVE_TIME, true) + } + }; + + let (received_packet_results, receive_time_us) = measure_us!(self + .packet_receiver + .receive_packets(recv_timeout, remaining_queue_capacity, |packet| { + packet.check_excessive_precompiles()?; + Ok(packet) + })); + + timing_metrics.update(|timing_metrics| { + saturating_add_assign!(timing_metrics.receive_time_us, receive_time_us); + }); + + match received_packet_results { + Ok(receive_packet_results) => { + let num_received_packets = receive_packet_results.deserialized_packets.len(); + + count_metrics.update(|count_metrics| { + saturating_add_assign!(count_metrics.num_received, num_received_packets); + }); + + if should_buffer { + let (_, buffer_time_us) = measure_us!(self.buffer_packets( + container, + timing_metrics, + count_metrics, + receive_packet_results.deserialized_packets + )); + timing_metrics.update(|timing_metrics| { + saturating_add_assign!(timing_metrics.buffer_time_us, buffer_time_us); + }); + } else { + count_metrics.update(|count_metrics| { + saturating_add_assign!( + count_metrics.num_dropped_on_receive, + num_received_packets + ); + }); + } + } + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => return false, + } + + true + } +} + +impl SanitizedTransactionReceiveAndBuffer { + pub fn new( + packet_receiver: PacketDeserializer, + bank_forks: Arc>, + forwarding_enabled: bool, + ) -> Self { + Self { + packet_receiver, + bank_forks, + forwarding_enabled, + } + } + + fn buffer_packets( + &mut self, + container: &mut TransactionStateContainer>, + _timing_metrics: &mut SchedulerTimingMetrics, + count_metrics: &mut SchedulerCountMetrics, + packets: Vec, + ) { + // Convert to Arcs + let packets: Vec<_> = packets.into_iter().map(Arc::new).collect(); + // Sanitize packets, generate IDs, and insert into the container. + let (root_bank, working_bank) = { + let bank_forks = self.bank_forks.read().unwrap(); + let root_bank = bank_forks.root_bank(); + let working_bank = bank_forks.working_bank(); + (root_bank, working_bank) + }; + let alt_resolved_slot = root_bank.slot(); + let sanitized_epoch = root_bank.epoch(); + let transaction_account_lock_limit = working_bank.get_transaction_account_lock_limit(); + let vote_only = working_bank.vote_only_bank(); + + const CHUNK_SIZE: usize = 128; + let lock_results: [_; CHUNK_SIZE] = core::array::from_fn(|_| Ok(())); + + let mut arc_packets = ArrayVec::<_, CHUNK_SIZE>::new(); + let mut transactions = ArrayVec::<_, CHUNK_SIZE>::new(); + let mut max_ages = ArrayVec::<_, CHUNK_SIZE>::new(); + let mut fee_budget_limits_vec = ArrayVec::<_, CHUNK_SIZE>::new(); + + let mut error_counts = TransactionErrorMetrics::default(); + for chunk in packets.chunks(CHUNK_SIZE) { + let mut post_sanitization_count: usize = 0; + chunk + .iter() + .filter_map(|packet| { + packet + .build_sanitized_transaction( + vote_only, + root_bank.as_ref(), + root_bank.get_reserved_account_keys(), + ) + .map(|(tx, deactivation_slot)| (packet.clone(), tx, deactivation_slot)) + }) + .inspect(|_| saturating_add_assign!(post_sanitization_count, 1)) + .filter(|(_packet, tx, _deactivation_slot)| { + validate_account_locks( + tx.message().account_keys(), + transaction_account_lock_limit, + ) + .is_ok() + }) + .filter_map(|(packet, tx, deactivation_slot)| { + tx.compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(&working_bank.feature_set) + .map(|compute_budget| { + (packet, tx, deactivation_slot, compute_budget.into()) + }) + .ok() + }) + .for_each(|(packet, tx, deactivation_slot, fee_budget_limits)| { + arc_packets.push(packet); + transactions.push(tx); + max_ages.push(calculate_max_age( + sanitized_epoch, + deactivation_slot, + alt_resolved_slot, + )); + fee_budget_limits_vec.push(fee_budget_limits); + }); + + let check_results = working_bank.check_transactions( + &transactions, + &lock_results[..transactions.len()], + MAX_PROCESSING_AGE, + &mut error_counts, + ); + let post_lock_validation_count = transactions.len(); + + let mut post_transaction_check_count: usize = 0; + let mut num_dropped_on_capacity: usize = 0; + let mut num_buffered: usize = 0; + for ((((packet, transaction), max_age), fee_budget_limits), _check_result) in + arc_packets + .drain(..) + .zip(transactions.drain(..)) + .zip(max_ages.drain(..)) + .zip(fee_budget_limits_vec.drain(..)) + .zip(check_results) + .filter(|(_, check_result)| check_result.is_ok()) + { + saturating_add_assign!(post_transaction_check_count, 1); + + let (priority, cost) = + calculate_priority_and_cost(&transaction, &fee_budget_limits, &working_bank); + let transaction_ttl = SanitizedTransactionTTL { + transaction, + max_age, + }; + + if container.insert_new_transaction(transaction_ttl, packet, priority, cost) { + saturating_add_assign!(num_dropped_on_capacity, 1); + } + saturating_add_assign!(num_buffered, 1); + } + + // Update metrics for transactions that were dropped. + let num_dropped_on_sanitization = chunk.len().saturating_sub(post_sanitization_count); + let num_dropped_on_lock_validation = + post_sanitization_count.saturating_sub(post_lock_validation_count); + let num_dropped_on_transaction_checks = + post_lock_validation_count.saturating_sub(post_transaction_check_count); + + count_metrics.update(|count_metrics| { + saturating_add_assign!( + count_metrics.num_dropped_on_capacity, + num_dropped_on_capacity + ); + saturating_add_assign!(count_metrics.num_buffered, num_buffered); + saturating_add_assign!( + count_metrics.num_dropped_on_sanitization, + num_dropped_on_sanitization + ); + saturating_add_assign!( + count_metrics.num_dropped_on_validate_locks, + num_dropped_on_lock_validation + ); + saturating_add_assign!( + count_metrics.num_dropped_on_receive_transaction_checks, + num_dropped_on_transaction_checks + ); + }); + } + } +} + +/// Calculate priority and cost for a transaction: +/// +/// Cost is calculated through the `CostModel`, +/// and priority is calculated through a formula here that attempts to sell +/// blockspace to the highest bidder. +/// +/// The priority is calculated as: +/// P = R / (1 + C) +/// where P is the priority, R is the reward, +/// and C is the cost towards block-limits. +/// +/// Current minimum costs are on the order of several hundred, +/// so the denominator is effectively C, and the +1 is simply +/// to avoid any division by zero due to a bug - these costs +/// are calculated by the cost-model and are not direct +/// from user input. They should never be zero. +/// Any difference in the prioritization is negligible for +/// the current transaction costs. +fn calculate_priority_and_cost( + transaction: &RuntimeTransaction, + fee_budget_limits: &FeeBudgetLimits, + bank: &Bank, +) -> (u64, u64) { + let cost = CostModel::calculate_cost(transaction, &bank.feature_set).sum(); + let reward = bank.calculate_reward_for_transaction(transaction, fee_budget_limits); + + // We need a multiplier here to avoid rounding down too aggressively. + // For many transactions, the cost will be greater than the fees in terms of raw lamports. + // For the purposes of calculating prioritization, we multiply the fees by a large number so that + // the cost is a small fraction. + // An offset of 1 is used in the denominator to explicitly avoid division by zero. + const MULTIPLIER: u64 = 1_000_000; + ( + reward + .saturating_mul(MULTIPLIER) + .saturating_div(cost.saturating_add(1)), + cost, + ) +} + +/// Given the epoch, the minimum deactivation slot, and the current slot, +/// return the `MaxAge` that should be used for the transaction. This is used +/// to determine the maximum slot that a transaction will be considered valid +/// for, without re-resolving addresses or resanitizing. +/// +/// This function considers the deactivation period of Address Table +/// accounts. If the deactivation period runs past the end of the epoch, +/// then the transaction is considered valid until the end of the epoch. +/// Otherwise, the transaction is considered valid until the deactivation +/// period. +/// +/// Since the deactivation period technically uses blocks rather than +/// slots, the value used here is the lower-bound on the deactivation +/// period, i.e. the transaction's address lookups are valid until +/// AT LEAST this slot. +fn calculate_max_age( + sanitized_epoch: Epoch, + deactivation_slot: Slot, + current_slot: Slot, +) -> MaxAge { + let alt_min_expire_slot = estimate_last_valid_slot(deactivation_slot.min(current_slot)); + MaxAge { + sanitized_epoch, + alt_invalidation_slot: alt_min_expire_slot, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_max_age() { + let current_slot = 100; + let sanitized_epoch = 10; + + // ALT deactivation slot is delayed + assert_eq!( + calculate_max_age(sanitized_epoch, current_slot - 1, current_slot), + MaxAge { + sanitized_epoch, + alt_invalidation_slot: current_slot - 1 + + solana_sdk::slot_hashes::get_entries() as u64, + } + ); + + // no deactivation slot + assert_eq!( + calculate_max_age(sanitized_epoch, u64::MAX, current_slot), + MaxAge { + sanitized_epoch, + alt_invalidation_slot: current_slot + solana_sdk::slot_hashes::get_entries() as u64, + } + ); + } +} diff --git a/cost-model/src/cost_model.rs b/cost-model/src/cost_model.rs index 6599c5d0091fce..1f04f54ca2bc26 100644 --- a/cost-model/src/cost_model.rs +++ b/cost-model/src/cost_model.rs @@ -162,7 +162,29 @@ impl CostModel { tx_cost: &mut UsageCostDetails, transaction: &SanitizedTransaction, feature_set: &FeatureSet, +<<<<<<< HEAD ) { +======= + ) -> (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 (programs_execution_cost, loaded_accounts_data_size_cost) = + Self::get_estimated_execution_cost(transaction, 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) + } + } + + fn get_transaction_cost_without_minimal_builtin_cus( + transaction: &impl TransactionWithMeta, + feature_set: &FeatureSet, + ) -> (u64, u64, u64) { +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let mut programs_execution_costs = 0u64; let mut loaded_accounts_data_size_cost = 0u64; let mut data_bytes_len_total = 0u64; @@ -194,9 +216,18 @@ impl CostModel { } } +<<<<<<< HEAD // 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 process_compute_budget_instructions(transaction.message().program_instructions_iter()) +======= + // 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 + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(feature_set) +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) { Ok(compute_budget_limits) => { // if tx contained user-space instructions and a more accurate estimate available correct it, @@ -223,10 +254,39 @@ impl CostModel { tx_cost.data_bytes_cost = data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST; } +<<<<<<< HEAD fn get_instructions_data_cost( tx_cost: &mut UsageCostDetails, transaction: &SanitizedTransaction, ) { +======= + /// Return (programs_execution_cost, loaded_accounts_data_size_cost) + fn get_estimated_execution_cost( + transaction: &impl TransactionWithMeta, + feature_set: &FeatureSet, + ) -> (u64, u64) { + // 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. + let (programs_execution_costs, loaded_accounts_data_size_cost) = match transaction + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(feature_set) + { + Ok(compute_budget_limits) => ( + u64::from(compute_budget_limits.compute_unit_limit), + Self::calculate_loaded_accounts_data_size_cost( + compute_budget_limits.loaded_accounts_bytes.get(), + feature_set, + ), + ), + Err(_) => (0, 0), + }; + + (programs_execution_costs, loaded_accounts_data_size_cost) + } + + /// Return the instruction data bytes cost. + fn get_instructions_data_cost(transaction: &impl SVMMessage) -> u64 { +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let ix_data_bytes_len_total: u64 = transaction .message() .instructions() @@ -318,6 +378,14 @@ impl CostModel { mod tests { use { super::*, +<<<<<<< HEAD +======= + itertools::Itertools, + solana_compute_budget::compute_budget_limits::{ + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + }, + solana_runtime_transaction::runtime_transaction::RuntimeTransaction, +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) solana_sdk::{ compute_budget::{self, ComputeBudgetInstruction}, fee::ACCOUNT_DATA_COST_PAGE_SIZE, @@ -515,11 +583,8 @@ mod tests { let simple_transaction = SanitizedTransaction::from_transaction_for_tests( system_transaction::transfer(&mint_keypair, &keypair.pubkey(), 2, start_hash), ); - debug!( - "system_transaction simple_transaction {:?}", - simple_transaction - ); +<<<<<<< HEAD // expected cost for one system transfer instructions let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS .get(&system_program::id()) @@ -533,6 +598,23 @@ mod tests { ); assert_eq!(*expected_execution_cost, tx_cost.programs_execution_cost); assert_eq!(3, tx_cost.data_bytes_cost); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, + ), + ( + FeatureSet::all_enabled(), + u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT), + ), + ] { + let (program_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = + CostModel::get_transaction_cost(&simple_transaction, &feature_set); + + assert_eq!(expected_execution_cost, program_execution_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -550,6 +632,7 @@ mod tests { vec![Pubkey::new_unique()], instructions, ); +<<<<<<< HEAD let token_transaction = SanitizedTransaction::from_transaction_for_tests(tx); debug!("token_transaction {:?}", token_transaction); @@ -564,6 +647,26 @@ mod tests { tx_cost.programs_execution_cost ); assert_eq!(0, tx_cost.data_bytes_cost); +======= + let token_transaction = RuntimeTransaction::from_transaction_for_tests(tx); + + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + ), + ( + FeatureSet::all_enabled(), + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + ), + ] { + let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = + CostModel::get_transaction_cost(&token_transaction, &feature_set); + + assert_eq!(expected_execution_cost, program_execution_cost); + assert_eq!(0, data_bytes_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -595,12 +698,13 @@ mod tests { #[test] fn test_cost_model_compute_budget_transaction() { let (mint_keypair, start_hash) = test_setup(); + let expected_cu_limit = 12_345; let instructions = vec![ CompiledInstruction::new(3, &(), vec![1, 2, 0]), CompiledInstruction::new_from_raw_parts( 4, - ComputeBudgetInstruction::SetComputeUnitLimit(12_345) + ComputeBudgetInstruction::SetComputeUnitLimit(expected_cu_limit) .pack() .unwrap(), vec![], @@ -616,6 +720,7 @@ mod tests { vec![Pubkey::new_unique(), compute_budget::id()], instructions, ); +<<<<<<< HEAD let token_transaction = SanitizedTransaction::from_transaction_for_tests(tx); let mut tx_cost = UsageCostDetails::default(); @@ -627,6 +732,21 @@ mod tests { // If cu-limit is specified, that would the cost for all programs assert_eq!(12_345, tx_cost.programs_execution_cost); assert_eq!(1, tx_cost.data_bytes_cost); +======= + let token_transaction = RuntimeTransaction::from_transaction_for_tests(tx); + + // If cu-limit is specified, that would the cost for all programs + for (feature_set, expected_execution_cost) in [ + (FeatureSet::default(), expected_cu_limit as u64), + (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); + + assert_eq!(expected_execution_cost, program_execution_cost); + assert_eq!(1, data_bytes_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -663,6 +783,7 @@ mod tests { ); let token_transaction = SanitizedTransaction::from_transaction_for_tests(tx); +<<<<<<< HEAD let mut tx_cost = UsageCostDetails::default(); CostModel::get_transaction_cost( &mut tx_cost, @@ -670,6 +791,13 @@ mod tests { &FeatureSet::all_enabled(), ); assert_eq!(0, tx_cost.programs_execution_cost); +======= + 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); + assert_eq!(0, program_execution_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -686,9 +814,9 @@ mod tests { message, start_hash, )); - debug!("many transfer transaction {:?}", tx); // expected cost for two system transfer instructions +<<<<<<< HEAD let program_cost = BUILT_IN_INSTRUCTION_COSTS .get(&system_program::id()) .unwrap(); @@ -698,6 +826,23 @@ mod tests { CostModel::get_transaction_cost(&mut tx_cost, &tx, &FeatureSet::all_enabled()); assert_eq!(expected_cost, tx_cost.programs_execution_cost); assert_eq!(6, tx_cost.data_bytes_cost); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + 2 * solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, + ), + ( + FeatureSet::all_enabled(), + 2 * u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT), + ), + ] { + let (programs_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = + CostModel::get_transaction_cost(&tx, &feature_set); + assert_eq!(expected_execution_cost, programs_execution_cost); + assert_eq!(6, data_bytes_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -722,13 +867,30 @@ mod tests { instructions, ), ); - debug!("many random transaction {:?}", tx); +<<<<<<< HEAD let expected_cost = DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2; let mut tx_cost = UsageCostDetails::default(); CostModel::get_transaction_cost(&mut tx_cost, &tx, &FeatureSet::all_enabled()); assert_eq!(expected_cost, tx_cost.programs_execution_cost); assert_eq!(0, tx_cost.data_bytes_cost); +======= + for (feature_set, expected_cost) in [ + ( + FeatureSet::default(), + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2, + ), + ( + FeatureSet::all_enabled(), + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2, + ), + ] { + let (program_execution_cost, _loaded_accounts_data_size_cost, data_bytes_cost) = + CostModel::get_transaction_cost(&tx, &feature_set); + assert_eq!(expected_cost, program_execution_cost); + assert_eq!(0, data_bytes_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -773,6 +935,7 @@ mod tests { )); let expected_account_cost = WRITE_LOCK_UNITS * 2; +<<<<<<< HEAD let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS .get(&system_program::id()) .unwrap(); @@ -791,6 +954,34 @@ mod tests { expected_loaded_accounts_data_size_cost, tx_cost.loaded_accounts_data_size_cost() ); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, + ), + ( + FeatureSet::all_enabled(), + u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT), + ), + ] { + const DEFAULT_PAGE_COST: u64 = 8; + let expected_loaded_accounts_data_size_cost = + solana_compute_budget::compute_budget_limits::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES + .get() as u64 + / ACCOUNT_DATA_COST_PAGE_SIZE + * DEFAULT_PAGE_COST; + + let tx_cost = CostModel::calculate_cost(&tx, &feature_set); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost()); + assert_eq!(expected_execution_cost, tx_cost.programs_execution_cost()); + assert_eq!(2, tx_cost.writable_accounts().count()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost() + ); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -809,8 +1000,8 @@ mod tests { start_hash, )); - let feature_set = FeatureSet::all_enabled(); let expected_account_cost = WRITE_LOCK_UNITS * 2; +<<<<<<< HEAD let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS .get(&system_program::id()) .unwrap() @@ -827,6 +1018,30 @@ mod tests { expected_loaded_accounts_data_size_cost, tx_cost.loaded_accounts_data_size_cost() ); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS + + solana_compute_budget_program::DEFAULT_COMPUTE_UNITS, + ), + ( + FeatureSet::all_enabled(), + 2 * u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT), + ), + ] { + let expected_loaded_accounts_data_size_cost = (data_limit as u64) / (32 * 1024) * 8; + + let tx_cost = CostModel::calculate_cost(&tx, &feature_set); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost()); + assert_eq!(expected_execution_cost, tx_cost.programs_execution_cost()); + assert_eq!(2, tx_cost.writable_accounts().count()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost() + ); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] @@ -844,6 +1059,7 @@ mod tests { start_hash, )); // transaction has one builtin instruction, and one bpf instruction, no ComputeBudget::compute_unit_limit +<<<<<<< HEAD let expected_builtin_cost = *BUILT_IN_INSTRUCTION_COSTS .get(&solana_system_program::id()) .unwrap(); @@ -856,22 +1072,43 @@ mod tests { expected_builtin_cost + expected_bpf_cost as u64, tx_cost.programs_execution_cost ); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS + + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + ), + ( + FeatureSet::all_enabled(), + u64::from(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT) + + u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ), + ] { + let (programs_execution_cost, _loaded_accounts_data_size_cost, _data_bytes_cost) = + CostModel::get_transaction_cost(&transaction, &feature_set); + + assert_eq!(expected_execution_cost, programs_execution_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] fn test_transaction_cost_with_mix_instruction_with_cu_limit() { let (mint_keypair, start_hash) = test_setup(); + let cu_limit: u32 = 12_345; let transaction = SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( &[ system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 2), - ComputeBudgetInstruction::set_compute_unit_limit(12_345), + ComputeBudgetInstruction::set_compute_unit_limit(cu_limit), ], Some(&mint_keypair.pubkey()), &[&mint_keypair], start_hash, )); +<<<<<<< HEAD // transaction has one builtin instruction, and one ComputeBudget::compute_unit_limit let expected_cost = *BUILT_IN_INSTRUCTION_COSTS .get(&solana_system_program::id()) @@ -884,5 +1121,20 @@ mod tests { CostModel::get_transaction_cost(&mut tx_cost, &transaction, &FeatureSet::all_enabled()); assert_eq!(expected_cost, tx_cost.programs_execution_cost); +======= + for (feature_set, expected_execution_cost) in [ + ( + FeatureSet::default(), + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS + + solana_compute_budget_program::DEFAULT_COMPUTE_UNITS, + ), + (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); + + assert_eq!(expected_execution_cost, programs_execution_cost); + } +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } } diff --git a/cost-model/src/transaction_cost.rs b/cost-model/src/transaction_cost.rs index 4951e50036ca8b..d3b6f3c662acdb 100644 --- a/cost-model/src/transaction_cost.rs +++ b/cost-model/src/transaction_cost.rs @@ -194,6 +194,123 @@ impl UsageCostDetails { } } +<<<<<<< HEAD +======= +#[cfg(feature = "dev-context-only-utils")] +#[derive(Debug)] +pub struct WritableKeysTransaction(pub Vec); + +#[cfg(feature = "dev-context-only-utils")] +impl solana_svm_transaction::svm_message::SVMMessage for WritableKeysTransaction { + fn num_total_signatures(&self) -> u64 { + unimplemented!("WritableKeysTransaction::num_total_signatures") + } + + fn num_write_locks(&self) -> u64 { + unimplemented!("WritableKeysTransaction::num_write_locks") + } + + fn recent_blockhash(&self) -> &solana_sdk::hash::Hash { + unimplemented!("WritableKeysTransaction::recent_blockhash") + } + + fn num_instructions(&self) -> usize { + unimplemented!("WritableKeysTransaction::num_instructions") + } + + fn instructions_iter( + &self, + ) -> impl Iterator { + core::iter::empty() + } + + fn program_instructions_iter( + &self, + ) -> impl Iterator + Clone + { + core::iter::empty() + } + + fn account_keys(&self) -> solana_sdk::message::AccountKeys { + solana_sdk::message::AccountKeys::new(&self.0, None) + } + + fn fee_payer(&self) -> &Pubkey { + unimplemented!("WritableKeysTransaction::fee_payer") + } + + fn is_writable(&self, _index: usize) -> bool { + true + } + + fn is_signer(&self, _index: usize) -> bool { + unimplemented!("WritableKeysTransaction::is_signer") + } + + fn is_invoked(&self, _key_index: usize) -> bool { + unimplemented!("WritableKeysTransaction::is_invoked") + } + + fn num_lookup_tables(&self) -> usize { + unimplemented!("WritableKeysTransaction::num_lookup_tables") + } + + fn message_address_table_lookups( + &self, + ) -> impl Iterator< + Item = solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup, + > { + core::iter::empty() + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl solana_svm_transaction::svm_transaction::SVMTransaction for WritableKeysTransaction { + fn signature(&self) -> &solana_sdk::signature::Signature { + unimplemented!("WritableKeysTransaction::signature") + } + + fn signatures(&self) -> &[solana_sdk::signature::Signature] { + unimplemented!("WritableKeysTransaction::signatures") + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl solana_runtime_transaction::transaction_meta::StaticMeta for WritableKeysTransaction { + fn message_hash(&self) -> &solana_sdk::hash::Hash { + unimplemented!("WritableKeysTransaction::message_hash") + } + + fn is_simple_vote_transaction(&self) -> bool { + unimplemented!("WritableKeysTransaction::is_simple_vote_transaction") + } + + fn signature_details(&self) -> &solana_sdk::message::TransactionSignatureDetails { + const DUMMY: solana_sdk::message::TransactionSignatureDetails = + solana_sdk::message::TransactionSignatureDetails::new(0, 0, 0, 0); + &DUMMY + } + + fn compute_budget_instruction_details(&self) -> &ComputeBudgetInstructionDetails { + unimplemented!("WritableKeysTransaction::compute_budget_instruction_details") + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl TransactionWithMeta for WritableKeysTransaction { + #[allow(refining_impl_trait)] + fn as_sanitized_transaction( + &self, + ) -> std::borrow::Cow { + unimplemented!("WritableKeysTransaction::as_sanitized_transaction"); + } + + fn to_versioned_transaction(&self) -> solana_sdk::transaction::VersionedTransaction { + unimplemented!("WritableKeysTransaction::to_versioned_transaction") + } +} + +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) #[cfg(test)] mod tests { use { @@ -248,8 +365,13 @@ mod tests { // expected vote tx cost: 2 write locks, 1 sig, 1 vote ix, 8cu of loaded accounts size, let expected_vote_cost = SIMPLE_VOTE_USAGE_COST; +<<<<<<< HEAD // expected non-vote tx cost would include default loaded accounts size cost (16384) additionally let expected_none_vote_cost = 20535; +======= + // expected non-vote tx cost would include default loaded accounts size cost (16384) additionally, and 3_000 for instruction + let expected_none_vote_cost = 21443; +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let vote_cost = CostModel::calculate_cost(&vote_transaction, &FeatureSet::all_enabled()); let none_vote_cost = diff --git a/programs/compute-budget-bench/benches/compute_budget.rs b/programs/compute-budget-bench/benches/compute_budget.rs new file mode 100644 index 00000000000000..7c758d45c7ed05 --- /dev/null +++ b/programs/compute-budget-bench/benches/compute_budget.rs @@ -0,0 +1,155 @@ +use { + criterion::{black_box, criterion_group, criterion_main, Criterion}, + solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, + solana_runtime_transaction::instructions_processor::process_compute_budget_instructions, + solana_sdk::{ + compute_budget::ComputeBudgetInstruction, feature_set::FeatureSet, + instruction::CompiledInstruction, + }, + solana_svm_transaction::instruction::SVMInstruction, + std::num::NonZero, +}; + +const ONE_PAGE: u32 = 32 * 1024; +const SIXTY_FOUR_MB: u32 = 64 * 1024 * 1024; + +fn bench_request_heap_frame(c: &mut Criterion) { + let instruction = [( + solana_sdk::compute_budget::id(), + CompiledInstruction::new_from_raw_parts( + 0, + ComputeBudgetInstruction::request_heap_frame(ONE_PAGE).data, + vec![], + ), + )]; + let feature_set = FeatureSet::default(); + + c.bench_function("request_heap_limit", |bencher| { + bencher.iter(|| { + assert_eq!( + process_compute_budget_instructions( + black_box( + instruction + .iter() + .map(|(id, ix)| (id, SVMInstruction::from(ix))) + ), + black_box(&feature_set) + ), + Ok(ComputeBudgetLimits { + updated_heap_bytes: ONE_PAGE, + compute_unit_limit: 0, + compute_unit_price: 0, + loaded_accounts_bytes: NonZero::new(SIXTY_FOUR_MB).unwrap() + }) + ) + }) + }); +} + +fn bench_set_compute_unit_limit(c: &mut Criterion) { + let instruction = [( + solana_sdk::compute_budget::id(), + CompiledInstruction::new_from_raw_parts( + 0, + ComputeBudgetInstruction::set_compute_unit_limit(1024).data, + vec![], + ), + )]; + let feature_set = FeatureSet::default(); + + c.bench_function("set_compute_unit_limit", |bencher| { + bencher.iter(|| { + assert_eq!( + process_compute_budget_instructions( + black_box( + instruction + .iter() + .map(|(id, ix)| (id, SVMInstruction::from(ix))) + ), + black_box(&feature_set) + ), + Ok(ComputeBudgetLimits { + updated_heap_bytes: ONE_PAGE, + compute_unit_limit: 1024, + compute_unit_price: 0, + loaded_accounts_bytes: NonZero::new(SIXTY_FOUR_MB).unwrap() + }) + ) + }) + }); +} + +fn bench_set_compute_unit_price(c: &mut Criterion) { + let instruction = [( + solana_sdk::compute_budget::id(), + CompiledInstruction::new_from_raw_parts( + 0, + ComputeBudgetInstruction::set_compute_unit_price(1).data, + vec![], + ), + )]; + let feature_set = FeatureSet::default(); + + c.bench_function("set_compute_unit_price", |bencher| { + bencher.iter(|| { + assert_eq!( + process_compute_budget_instructions( + black_box( + instruction + .iter() + .map(|(id, ix)| (id, SVMInstruction::from(ix))) + ), + black_box(&feature_set) + ), + Ok(ComputeBudgetLimits { + updated_heap_bytes: ONE_PAGE, + compute_unit_limit: 0, + compute_unit_price: 1, + loaded_accounts_bytes: NonZero::new(SIXTY_FOUR_MB).unwrap() + }) + ) + }) + }); +} + +fn bench_set_loaded_accounts_data_size_limit(c: &mut Criterion) { + let instruction = [( + solana_sdk::compute_budget::id(), + CompiledInstruction::new_from_raw_parts( + 0, + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(1).data, + vec![], + ), + )]; + let feature_set = FeatureSet::default(); + + c.bench_function("set_loaded_accounts_data_size_limit", |bencher| { + bencher.iter(|| { + assert_eq!( + process_compute_budget_instructions( + black_box( + instruction + .iter() + .map(|(id, ix)| (id, SVMInstruction::from(ix))) + ), + black_box(&feature_set) + ), + Ok(ComputeBudgetLimits { + updated_heap_bytes: ONE_PAGE, + compute_unit_limit: 0, + compute_unit_price: 0, + loaded_accounts_bytes: NonZero::new(1).unwrap() + }) + ) + }) + }); +} + +criterion_group!( + benches, + bench_request_heap_frame, + bench_set_compute_unit_limit, + bench_set_compute_unit_price, + bench_set_loaded_accounts_data_size_limit, +); +criterion_main!(benches); diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 55802a89dcbc33..4cdced0d44d72d 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -3855,6 +3855,7 @@ fn test_program_fees() { FeeStructure::new(0.000005, 0.0, vec![(200, 0.0000005), (1400000, 0.000005)]); bank.set_fee_structure(&fee_structure); let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let feature_set = bank.feature_set.clone(); let mut bank_client = BankClient::new_shared(bank); let authority_keypair = Keypair::new(); @@ -3877,7 +3878,18 @@ fn test_program_fees() { &ReservedAccountKeys::empty_key_set(), ) .unwrap(); +<<<<<<< HEAD let expected_normal_fee = fee_structure.calculate_fee( +======= + let fee_budget_limits = FeeBudgetLimits::from( + process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&sanitized_message), + &feature_set, + ) + .unwrap_or_default(), + ); + let expected_normal_fee = solana_fee::calculate_fee( +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) &sanitized_message, congestion_multiplier, &process_compute_budget_instructions(sanitized_message.program_instructions_iter()) @@ -3905,7 +3917,18 @@ fn test_program_fees() { &ReservedAccountKeys::empty_key_set(), ) .unwrap(); +<<<<<<< HEAD let expected_prioritized_fee = fee_structure.calculate_fee( +======= + let fee_budget_limits = FeeBudgetLimits::from( + process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&sanitized_message), + &feature_set, + ) + .unwrap_or_default(), + ); + let expected_prioritized_fee = solana_fee::calculate_fee( +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) &sanitized_message, congestion_multiplier, &process_compute_budget_instructions(sanitized_message.program_instructions_iter()) diff --git a/runtime-transaction/benches/process_compute_budget_instructions.rs b/runtime-transaction/benches/process_compute_budget_instructions.rs new file mode 100644 index 00000000000000..c120b5681b5c29 --- /dev/null +++ b/runtime-transaction/benches/process_compute_budget_instructions.rs @@ -0,0 +1,189 @@ +use { + criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}, + solana_runtime_transaction::instructions_processor::process_compute_budget_instructions, + solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + feature_set::FeatureSet, + instruction::Instruction, + message::Message, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_instruction::{self}, + transaction::{SanitizedTransaction, Transaction}, + }, + solana_svm_transaction::svm_message::SVMMessage, +}; + +const NUM_TRANSACTIONS_PER_ITER: usize = 1024; +const DUMMY_PROGRAM_ID: &str = "dummmy1111111111111111111111111111111111111"; + +fn build_sanitized_transaction( + payer_keypair: &Keypair, + instructions: &[Instruction], +) -> SanitizedTransaction { + SanitizedTransaction::from_transaction_for_tests(Transaction::new_unsigned(Message::new( + instructions, + Some(&payer_keypair.pubkey()), + ))) +} + +fn bench_process_compute_budget_instructions_empty(c: &mut Criterion) { + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + c.benchmark_group("bench_process_compute_budget_instructions_empty") + .throughput(Throughput::Elements(NUM_TRANSACTIONS_PER_ITER as u64)) + .bench_function("0 instructions", |bencher| { + let tx = build_sanitized_transaction(&Keypair::new(), &[]); + bencher.iter(|| { + (0..NUM_TRANSACTIONS_PER_ITER).for_each(|_| { + assert!(process_compute_budget_instructions( + black_box(SVMMessage::program_instructions_iter(&tx)), + black_box(&feature_set), + ) + .is_ok()) + }) + }); + }); + } +} + +fn bench_process_compute_budget_instructions_no_builtins(c: &mut Criterion) { + let num_instructions = 4; + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + c.benchmark_group("bench_process_compute_budget_instructions_no_builtins") + .throughput(Throughput::Elements(NUM_TRANSACTIONS_PER_ITER as u64)) + .bench_function( + format!("{num_instructions} dummy Instructions"), + |bencher| { + let ixs: Vec<_> = (0..num_instructions) + .map(|_| { + Instruction::new_with_bincode( + DUMMY_PROGRAM_ID.parse().unwrap(), + &(), + vec![], + ) + }) + .collect(); + let tx = build_sanitized_transaction(&Keypair::new(), &ixs); + bencher.iter(|| { + (0..NUM_TRANSACTIONS_PER_ITER).for_each(|_| { + assert!(process_compute_budget_instructions( + black_box(SVMMessage::program_instructions_iter(&tx)), + black_box(&feature_set), + ) + .is_ok()) + }) + }); + }, + ); + } +} + +fn bench_process_compute_budget_instructions_compute_budgets(c: &mut Criterion) { + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + c.benchmark_group("bench_process_compute_budget_instructions_compute_budgets") + .throughput(Throughput::Elements(NUM_TRANSACTIONS_PER_ITER as u64)) + .bench_function("4 compute-budget instructions", |bencher| { + let ixs = vec![ + ComputeBudgetInstruction::request_heap_frame(40 * 1024), + ComputeBudgetInstruction::set_compute_unit_limit(u32::MAX), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(u32::MAX), + ]; + let tx = build_sanitized_transaction(&Keypair::new(), &ixs); + bencher.iter(|| { + (0..NUM_TRANSACTIONS_PER_ITER).for_each(|_| { + assert!(process_compute_budget_instructions( + black_box(SVMMessage::program_instructions_iter(&tx)), + black_box(&feature_set), + ) + .is_ok()) + }) + }); + }); + } +} + +fn bench_process_compute_budget_instructions_builtins(c: &mut Criterion) { + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + c.benchmark_group("bench_process_compute_budget_instructions_builtins") + .throughput(Throughput::Elements(NUM_TRANSACTIONS_PER_ITER as u64)) + .bench_function("4 dummy builtins", |bencher| { + let ixs = vec![ + Instruction::new_with_bincode(solana_sdk::bpf_loader::id(), &(), vec![]), + Instruction::new_with_bincode(solana_sdk::secp256k1_program::id(), &(), vec![]), + Instruction::new_with_bincode( + solana_sdk::address_lookup_table::program::id(), + &(), + vec![], + ), + Instruction::new_with_bincode(solana_sdk::loader_v4::id(), &(), vec![]), + ]; + let tx = build_sanitized_transaction(&Keypair::new(), &ixs); + bencher.iter(|| { + (0..NUM_TRANSACTIONS_PER_ITER).for_each(|_| { + assert!(process_compute_budget_instructions( + black_box(SVMMessage::program_instructions_iter(&tx)), + black_box(&feature_set), + ) + .is_ok()) + }) + }); + }); + } +} + +fn bench_process_compute_budget_instructions_mixed(c: &mut Criterion) { + let num_instructions = 355; + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + c.benchmark_group("bench_process_compute_budget_instructions_mixed") + .throughput(Throughput::Elements(NUM_TRANSACTIONS_PER_ITER as u64)) + .bench_function( + format!("{num_instructions} mixed instructions"), + |bencher| { + let payer_keypair = Keypair::new(); + let mut ixs: Vec<_> = (0..num_instructions) + .map(|_| { + Instruction::new_with_bincode( + DUMMY_PROGRAM_ID.parse().unwrap(), + &(), + vec![], + ) + }) + .collect(); + ixs.extend(vec![ + ComputeBudgetInstruction::request_heap_frame(40 * 1024), + ComputeBudgetInstruction::set_compute_unit_limit(u32::MAX), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(u32::MAX), + system_instruction::transfer( + &payer_keypair.pubkey(), + &Pubkey::new_unique(), + 1, + ), + ]); + let tx = build_sanitized_transaction(&payer_keypair, &ixs); + + bencher.iter(|| { + (0..NUM_TRANSACTIONS_PER_ITER).for_each(|_| { + assert!(process_compute_budget_instructions( + black_box(SVMMessage::program_instructions_iter(&tx)), + black_box(&feature_set), + ) + .is_ok()) + }) + }); + }, + ); + } +} + +criterion_group!( + benches, + bench_process_compute_budget_instructions_empty, + bench_process_compute_budget_instructions_no_builtins, + bench_process_compute_budget_instructions_compute_budgets, + bench_process_compute_budget_instructions_builtins, + bench_process_compute_budget_instructions_mixed, +); +criterion_main!(benches); diff --git a/runtime-transaction/src/builtin_programs_filter.rs b/runtime-transaction/src/builtin_programs_filter.rs new file mode 100644 index 00000000000000..fc935a023cfbf8 --- /dev/null +++ b/runtime-transaction/src/builtin_programs_filter.rs @@ -0,0 +1,105 @@ +use { + agave_transaction_view::static_account_keys_frame::MAX_STATIC_ACCOUNTS_PER_PACKET as FILTER_SIZE, + solana_builtins_default_costs::{is_builtin_program, MAYBE_BUILTIN_KEY}, + solana_sdk::pubkey::Pubkey, +}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum ProgramKind { + NotBuiltin, + Builtin, +} + +pub(crate) struct BuiltinProgramsFilter { + // array of slots for all possible static and sanitized program_id_index, + // each slot indicates if a program_id_index has not been checked (eg, None), + // or already checked with result (eg, Some(ProgramKind)) that can be reused. + program_kind: [Option; FILTER_SIZE as usize], +} + +impl BuiltinProgramsFilter { + pub(crate) fn new() -> Self { + BuiltinProgramsFilter { + program_kind: [None; FILTER_SIZE as usize], + } + } + + pub(crate) fn get_program_kind(&mut self, index: usize, program_id: &Pubkey) -> ProgramKind { + *self + .program_kind + .get_mut(index) + .expect("program id index is sanitized") + .get_or_insert_with(|| Self::check_program_kind(program_id)) + } + + #[inline] + fn check_program_kind(program_id: &Pubkey) -> ProgramKind { + if !MAYBE_BUILTIN_KEY[program_id.as_ref()[0] as usize] { + return ProgramKind::NotBuiltin; + } + + if is_builtin_program(program_id) { + ProgramKind::Builtin + } else { + ProgramKind::NotBuiltin + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + const DUMMY_PROGRAM_ID: &str = "dummmy1111111111111111111111111111111111111"; + + #[test] + fn get_program_kind() { + let mut test_store = BuiltinProgramsFilter::new(); + let mut index = 9; + + // initial state is Unchecked + assert!(test_store.program_kind[index].is_none()); + + // non builtin returns None + assert_eq!( + test_store.get_program_kind(index, &DUMMY_PROGRAM_ID.parse().unwrap()), + ProgramKind::NotBuiltin + ); + // but its state is now checked (eg, Some(...)) + assert_eq!( + test_store.program_kind[index], + Some(ProgramKind::NotBuiltin) + ); + // lookup same `index` will return cached data, will not lookup `program_id` + // again + assert_eq!( + test_store.get_program_kind(index, &solana_sdk::loader_v4::id()), + ProgramKind::NotBuiltin + ); + + // not-migrating builtin + index += 1; + assert_eq!( + test_store.get_program_kind(index, &solana_sdk::loader_v4::id()), + ProgramKind::Builtin, + ); + + // compute-budget + index += 1; + assert_eq!( + test_store.get_program_kind(index, &solana_sdk::compute_budget::id()), + ProgramKind::Builtin, + ); + } + + #[test] + #[should_panic(expected = "program id index is sanitized")] + fn test_get_program_kind_out_of_bound_index() { + let mut test_store = BuiltinProgramsFilter::new(); + assert_eq!( + test_store + .get_program_kind(FILTER_SIZE as usize + 1, &DUMMY_PROGRAM_ID.parse().unwrap(),), + ProgramKind::NotBuiltin + ); + } +} diff --git a/runtime-transaction/src/compute_budget_instruction_details.rs b/runtime-transaction/src/compute_budget_instruction_details.rs new file mode 100644 index 00000000000000..dba59b8f343d13 --- /dev/null +++ b/runtime-transaction/src/compute_budget_instruction_details.rs @@ -0,0 +1,524 @@ +use { + crate::{ + builtin_programs_filter::{BuiltinProgramsFilter, ProgramKind}, + compute_budget_program_id_filter::ComputeBudgetProgramIdFilter, + }, + solana_compute_budget::compute_budget_limits::*, + solana_sdk::{ + borsh1::try_from_slice_unchecked, + compute_budget::ComputeBudgetInstruction, + feature_set::{self, FeatureSet}, + instruction::InstructionError, + pubkey::Pubkey, + saturating_add_assign, + transaction::{Result, TransactionError}, + }, + solana_svm_transaction::instruction::SVMInstruction, + std::num::NonZeroU32, +}; + +#[cfg_attr(test, derive(Eq, PartialEq))] +#[cfg_attr(feature = "dev-context-only-utils", derive(Clone))] +#[derive(Default, Debug)] +pub struct ComputeBudgetInstructionDetails { + // compute-budget instruction details: + // the first field in tuple is instruction index, second field is the unsanitized value set by user + requested_compute_unit_limit: Option<(u8, u32)>, + requested_compute_unit_price: Option<(u8, u64)>, + requested_heap_size: Option<(u8, u32)>, + requested_loaded_accounts_data_size_limit: Option<(u8, u32)>, + num_non_compute_budget_instructions: u16, + // Additional builtin program counters + num_builtin_instructions: u16, + num_non_builtin_instructions: u16, +} + +impl ComputeBudgetInstructionDetails { + pub fn try_from<'a>( + instructions: impl Iterator)> + Clone, + ) -> Result { + let mut filter = ComputeBudgetProgramIdFilter::new(); + let mut compute_budget_instruction_details = ComputeBudgetInstructionDetails::default(); + + for (i, (program_id, instruction)) in instructions.clone().enumerate() { + if filter.is_compute_budget_program(instruction.program_id_index as usize, program_id) { + compute_budget_instruction_details.process_instruction(i as u8, &instruction)?; + } else { + saturating_add_assign!( + compute_budget_instruction_details.num_non_compute_budget_instructions, + 1 + ); + } + } + + if compute_budget_instruction_details + .requested_compute_unit_limit + .is_none() + { + let mut filter = BuiltinProgramsFilter::new(); + // reiterate to collect builtin details + for (program_id, instruction) in instructions { + match filter.get_program_kind(instruction.program_id_index as usize, program_id) { + ProgramKind::Builtin => { + saturating_add_assign!( + compute_budget_instruction_details.num_builtin_instructions, + 1 + ); + } + ProgramKind::NotBuiltin => { + saturating_add_assign!( + compute_budget_instruction_details.num_non_builtin_instructions, + 1 + ); + } + } + } + } + + Ok(compute_budget_instruction_details) + } + + pub fn sanitize_and_convert_to_compute_budget_limits( + &self, + feature_set: &FeatureSet, + ) -> Result { + // Sanitize requested heap size + let updated_heap_bytes = + if let Some((index, requested_heap_size)) = self.requested_heap_size { + if Self::sanitize_requested_heap_size(requested_heap_size) { + requested_heap_size + } else { + return Err(TransactionError::InstructionError( + index, + InstructionError::InvalidInstructionData, + )); + } + } else { + MIN_HEAP_FRAME_BYTES + } + .min(MAX_HEAP_FRAME_BYTES); + + // Calculate compute unit limit + let compute_unit_limit = self + .requested_compute_unit_limit + .map_or_else( + || self.calculate_default_compute_unit_limit(feature_set), + |(_index, requested_compute_unit_limit)| requested_compute_unit_limit, + ) + .min(MAX_COMPUTE_UNIT_LIMIT); + + let compute_unit_price = self + .requested_compute_unit_price + .map_or(0, |(_index, requested_compute_unit_price)| { + requested_compute_unit_price + }); + + let loaded_accounts_bytes = + if let Some((_index, requested_loaded_accounts_data_size_limit)) = + self.requested_loaded_accounts_data_size_limit + { + NonZeroU32::new(requested_loaded_accounts_data_size_limit) + .ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)? + } else { + 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 process_instruction(&mut self, index: u8, instruction: &SVMInstruction) -> Result<()> { + let invalid_instruction_data_error = + TransactionError::InstructionError(index, InstructionError::InvalidInstructionData); + let duplicate_instruction_error = TransactionError::DuplicateInstruction(index); + + match try_from_slice_unchecked(instruction.data) { + Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => { + if self.requested_heap_size.is_some() { + return Err(duplicate_instruction_error); + } + self.requested_heap_size = Some((index, bytes)); + } + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(compute_unit_limit)) => { + if self.requested_compute_unit_limit.is_some() { + return Err(duplicate_instruction_error); + } + self.requested_compute_unit_limit = Some((index, compute_unit_limit)); + } + Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => { + if self.requested_compute_unit_price.is_some() { + return Err(duplicate_instruction_error); + } + self.requested_compute_unit_price = Some((index, micro_lamports)); + } + Ok(ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes)) => { + if self.requested_loaded_accounts_data_size_limit.is_some() { + return Err(duplicate_instruction_error); + } + self.requested_loaded_accounts_data_size_limit = Some((index, bytes)); + } + _ => return Err(invalid_instruction_data_error), + } + + Ok(()) + } + + #[inline] + fn sanitize_requested_heap_size(bytes: u32) -> bool { + (MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&bytes) && bytes % 1024 == 0 + } + + fn calculate_default_compute_unit_limit(&self, feature_set: &FeatureSet) -> u32 { + if feature_set.is_active(&feature_set::reserve_minimal_cus_for_builtin_instructions::id()) { + u32::from(self.num_builtin_instructions) + .saturating_mul(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT) + .saturating_add( + u32::from(self.num_non_builtin_instructions) + .saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ) + } else { + u32::from(self.num_non_compute_budget_instructions) + .saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT) + } + } +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_sdk::{ + instruction::Instruction, + message::Message, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + transaction::{SanitizedTransaction, Transaction}, + }, + solana_svm_transaction::svm_message::SVMMessage, + }; + + fn build_sanitized_transaction(instructions: &[Instruction]) -> SanitizedTransaction { + let payer_keypair = Keypair::new(); + SanitizedTransaction::from_transaction_for_tests(Transaction::new_unsigned(Message::new( + instructions, + Some(&payer_keypair.pubkey()), + ))) + } + + #[test] + fn test_try_from_request_heap() { + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::request_heap_frame(40 * 1024), + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ]); + let expected_details = Ok(ComputeBudgetInstructionDetails { + requested_heap_size: Some((1, 40 * 1024)), + num_non_compute_budget_instructions: 2, + num_builtin_instructions: 1, + num_non_builtin_instructions: 2, + ..ComputeBudgetInstructionDetails::default() + }); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + expected_details + ); + + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::request_heap_frame(40 * 1024), + ComputeBudgetInstruction::request_heap_frame(41 * 1024), + ]); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + Err(TransactionError::DuplicateInstruction(2)) + ); + } + + #[test] + fn test_try_from_compute_unit_limit() { + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(u32::MAX), + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ]); + let expected_details = Ok(ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, u32::MAX)), + num_non_compute_budget_instructions: 2, + ..ComputeBudgetInstructionDetails::default() + }); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + expected_details + ); + + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_compute_unit_limit(0), + ComputeBudgetInstruction::set_compute_unit_limit(u32::MAX), + ]); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + Err(TransactionError::DuplicateInstruction(2)) + ); + } + + #[test] + fn test_try_from_compute_unit_price() { + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ]); + let expected_details = Ok(ComputeBudgetInstructionDetails { + requested_compute_unit_price: Some((1, u64::MAX)), + num_non_compute_budget_instructions: 2, + num_builtin_instructions: 1, + num_non_builtin_instructions: 2, + ..ComputeBudgetInstructionDetails::default() + }); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + expected_details + ); + + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_compute_unit_price(0), + ComputeBudgetInstruction::set_compute_unit_price(u64::MAX), + ]); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + Err(TransactionError::DuplicateInstruction(2)) + ); + } + + #[test] + fn test_try_from_loaded_accounts_data_size_limit() { + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(u32::MAX), + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ]); + let expected_details = Ok(ComputeBudgetInstructionDetails { + requested_loaded_accounts_data_size_limit: Some((1, u32::MAX)), + num_non_compute_budget_instructions: 2, + num_builtin_instructions: 1, + num_non_builtin_instructions: 2, + ..ComputeBudgetInstructionDetails::default() + }); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + expected_details + ); + + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(0), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(u32::MAX), + ]); + assert_eq!( + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx),), + Err(TransactionError::DuplicateInstruction(2)) + ); + } + + fn prep_feature_minimial_cus_for_builtin_instructions( + is_active: bool, + instruction_details: &ComputeBudgetInstructionDetails, + ) -> (FeatureSet, u32) { + let mut feature_set = FeatureSet::default(); + let ComputeBudgetInstructionDetails { + num_non_compute_budget_instructions, + num_builtin_instructions, + num_non_builtin_instructions, + .. + } = *instruction_details; + let expected_cu_limit = if is_active { + feature_set.activate( + &feature_set::reserve_minimal_cus_for_builtin_instructions::id(), + 0, + ); + u32::from(num_non_builtin_instructions) * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + + u32::from(num_builtin_instructions) * MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT + } else { + u32::from(num_non_compute_budget_instructions) * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + }; + + (feature_set, expected_cu_limit) + } + + #[test] + fn test_sanitize_and_convert_to_compute_budget_limits() { + // empty details, default ComputeBudgetLimits with 0 compute_unit_limits + let instruction_details = ComputeBudgetInstructionDetails::default(); + assert_eq!( + instruction_details + .sanitize_and_convert_to_compute_budget_limits(&FeatureSet::default()), + Ok(ComputeBudgetLimits { + compute_unit_limit: 0, + ..ComputeBudgetLimits::default() + }) + ); + + // no compute-budget instructions, all default ComputeBudgetLimits except cu-limit + let instruction_details = ComputeBudgetInstructionDetails { + num_non_compute_budget_instructions: 4, + num_builtin_instructions: 1, + num_non_builtin_instructions: 3, + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + Ok(ComputeBudgetLimits { + compute_unit_limit: expected_compute_unit_limit, + ..ComputeBudgetLimits::default() + }) + ); + } + + let expected_heap_size_err = Err(TransactionError::InstructionError( + 3, + InstructionError::InvalidInstructionData, + )); + // invalid: requested_heap_size can't be zero + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, 0)), + requested_compute_unit_price: Some((2, 0)), + requested_heap_size: Some((3, 0)), + requested_loaded_accounts_data_size_limit: Some((4, 1024)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + expected_heap_size_err + ); + } + + // invalid: requested_heap_size can't be less than MIN_HEAP_FRAME_BYTES + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, 0)), + requested_compute_unit_price: Some((2, 0)), + requested_heap_size: Some((3, MIN_HEAP_FRAME_BYTES - 1)), + requested_loaded_accounts_data_size_limit: Some((4, 1024)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + expected_heap_size_err + ); + } + + // invalid: requested_heap_size can't be more than MAX_HEAP_FRAME_BYTES + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, 0)), + requested_compute_unit_price: Some((2, 0)), + requested_heap_size: Some((3, MAX_HEAP_FRAME_BYTES + 1)), + requested_loaded_accounts_data_size_limit: Some((4, 1024)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + expected_heap_size_err + ); + } + + // invalid: requested_heap_size must be round by 1024 + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, 0)), + requested_compute_unit_price: Some((2, 0)), + requested_heap_size: Some((3, MIN_HEAP_FRAME_BYTES + 1024 + 1)), + requested_loaded_accounts_data_size_limit: Some((4, 1024)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + expected_heap_size_err + ); + } + + // invalid: loaded_account_data_size can't be zero + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, 0)), + requested_compute_unit_price: Some((2, 0)), + requested_heap_size: Some((3, 40 * 1024)), + requested_loaded_accounts_data_size_limit: Some((4, 0)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + Err(TransactionError::InvalidLoadedAccountsDataSizeLimit) + ); + } + + // valid: acceptable MAX + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, u32::MAX)), + requested_compute_unit_price: Some((2, u64::MAX)), + requested_heap_size: Some((3, MAX_HEAP_FRAME_BYTES)), + requested_loaded_accounts_data_size_limit: Some((4, u32::MAX)), + num_non_compute_budget_instructions: 4, + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + Ok(ComputeBudgetLimits { + updated_heap_bytes: MAX_HEAP_FRAME_BYTES, + compute_unit_limit: MAX_COMPUTE_UNIT_LIMIT, + compute_unit_price: u64::MAX, + loaded_accounts_bytes: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + }) + ); + } + + // valid + let val: u32 = 1024 * 40; + let instruction_details = ComputeBudgetInstructionDetails { + requested_compute_unit_limit: Some((1, val)), + requested_compute_unit_price: Some((2, val as u64)), + requested_heap_size: Some((3, val)), + requested_loaded_accounts_data_size_limit: Some((4, val)), + ..ComputeBudgetInstructionDetails::default() + }; + for is_active in [true, false] { + let (feature_set, _expected_compute_unit_limit) = + prep_feature_minimial_cus_for_builtin_instructions(is_active, &instruction_details); + assert_eq!( + instruction_details.sanitize_and_convert_to_compute_budget_limits(&feature_set), + Ok(ComputeBudgetLimits { + updated_heap_bytes: val, + compute_unit_limit: val, + compute_unit_price: val as u64, + loaded_accounts_bytes: NonZeroU32::new(val).unwrap(), + }) + ); + } + } +} diff --git a/runtime-transaction/src/compute_budget_program_id_filter.rs b/runtime-transaction/src/compute_budget_program_id_filter.rs new file mode 100644 index 00000000000000..59c6144a091229 --- /dev/null +++ b/runtime-transaction/src/compute_budget_program_id_filter.rs @@ -0,0 +1,36 @@ +// static account keys has max +use { + agave_transaction_view::static_account_keys_frame::MAX_STATIC_ACCOUNTS_PER_PACKET as FILTER_SIZE, + solana_builtins_default_costs::MAYBE_BUILTIN_KEY, solana_sdk::pubkey::Pubkey, +}; + +pub(crate) struct ComputeBudgetProgramIdFilter { + // array of slots for all possible static and sanitized program_id_index, + // each slot indicates if a program_id_index has not been checked (eg, None), + // or already checked with result (eg, Some(result)) that can be reused. + flags: [Option; FILTER_SIZE as usize], +} + +impl ComputeBudgetProgramIdFilter { + pub(crate) fn new() -> Self { + ComputeBudgetProgramIdFilter { + flags: [None; FILTER_SIZE as usize], + } + } + + pub(crate) fn is_compute_budget_program(&mut self, index: usize, program_id: &Pubkey) -> bool { + *self + .flags + .get_mut(index) + .expect("program id index is sanitized") + .get_or_insert_with(|| Self::check_program_id(program_id)) + } + + #[inline] + fn check_program_id(program_id: &Pubkey) -> bool { + if !MAYBE_BUILTIN_KEY[program_id.as_ref()[0] as usize] { + return false; + } + solana_sdk::compute_budget::check_id(program_id) + } +} diff --git a/runtime-transaction/src/lib.rs b/runtime-transaction/src/lib.rs index 0fdeb7c5b6bd65..bf8516a47ef205 100644 --- a/runtime-transaction/src/lib.rs +++ b/runtime-transaction/src/lib.rs @@ -1,5 +1,12 @@ #![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))] #![allow(clippy::arithmetic_side_effects)] +<<<<<<< HEAD +======= +mod builtin_programs_filter; +pub mod compute_budget_instruction_details; +mod compute_budget_program_id_filter; +pub mod instructions_processor; +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) pub mod runtime_transaction; pub mod transaction_meta; diff --git a/runtime-transaction/src/runtime_transaction.rs b/runtime-transaction/src/runtime_transaction.rs index 625ec28fdb22d8..c27b22a781738c 100644 --- a/runtime-transaction/src/runtime_transaction.rs +++ b/runtime-transaction/src/runtime_transaction.rs @@ -119,8 +119,57 @@ impl RuntimeTransaction { Ok(tx) } +<<<<<<< HEAD fn load_dynamic_metadata(&mut self) -> Result<()> { Ok(()) +======= + fn num_write_locks(&self) -> u64 { + self.transaction.num_write_locks() + } + + fn recent_blockhash(&self) -> &Hash { + self.transaction.recent_blockhash() + } + + fn num_instructions(&self) -> usize { + self.transaction.num_instructions() + } + + fn instructions_iter(&self) -> impl Iterator { + self.transaction.instructions_iter() + } + + fn program_instructions_iter(&self) -> impl Iterator + Clone { + self.transaction.program_instructions_iter() + } + + fn account_keys(&self) -> AccountKeys { + self.transaction.account_keys() + } + + fn fee_payer(&self) -> &Pubkey { + self.transaction.fee_payer() + } + + fn is_writable(&self, index: usize) -> bool { + self.transaction.is_writable(index) + } + + fn is_signer(&self, index: usize) -> bool { + self.transaction.is_signer(index) + } + + fn is_invoked(&self, key_index: usize) -> bool { + self.transaction.is_invoked(key_index) + } + + fn num_lookup_tables(&self) -> usize { + self.transaction.num_lookup_tables() + } + + fn message_address_table_lookups(&self) -> impl Iterator { + self.transaction.message_address_table_lookups() +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } } diff --git a/runtime-transaction/src/runtime_transaction/sdk_transactions.rs b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs new file mode 100644 index 00000000000000..a82e9c1217fff6 --- /dev/null +++ b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs @@ -0,0 +1,346 @@ +use { + super::{ComputeBudgetInstructionDetails, RuntimeTransaction}, + crate::{ + signature_details::get_precompile_signature_details, + transaction_meta::{StaticMeta, TransactionMeta}, + transaction_with_meta::TransactionWithMeta, + }, + solana_pubkey::Pubkey, + solana_sdk::{ + message::{AddressLoader, TransactionSignatureDetails}, + simple_vote_transaction_checker::is_simple_vote_transaction, + transaction::{ + MessageHash, Result, SanitizedTransaction, SanitizedVersionedTransaction, + VersionedTransaction, + }, + }, + solana_svm_transaction::instruction::SVMInstruction, + std::{borrow::Cow, collections::HashSet}, +}; + +impl RuntimeTransaction { + pub fn try_from( + sanitized_versioned_tx: SanitizedVersionedTransaction, + message_hash: MessageHash, + is_simple_vote_tx: Option, + ) -> Result { + let message_hash = match message_hash { + MessageHash::Precomputed(hash) => hash, + MessageHash::Compute => sanitized_versioned_tx.get_message().message.hash(), + }; + let is_simple_vote_tx = is_simple_vote_tx + .unwrap_or_else(|| is_simple_vote_transaction(&sanitized_versioned_tx)); + + let precompile_signature_details = get_precompile_signature_details( + sanitized_versioned_tx + .get_message() + .program_instructions_iter() + .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), + ); + let signature_details = TransactionSignatureDetails::new( + u64::from( + sanitized_versioned_tx + .get_message() + .message + .header() + .num_required_signatures, + ), + precompile_signature_details.num_secp256k1_instruction_signatures, + precompile_signature_details.num_ed25519_instruction_signatures, + precompile_signature_details.num_secp256r1_instruction_signatures, + ); + let compute_budget_instruction_details = ComputeBudgetInstructionDetails::try_from( + sanitized_versioned_tx + .get_message() + .program_instructions_iter() + .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), + )?; + + Ok(Self { + transaction: sanitized_versioned_tx, + meta: TransactionMeta { + message_hash, + is_simple_vote_transaction: is_simple_vote_tx, + signature_details, + compute_budget_instruction_details, + }, + }) + } +} + +impl RuntimeTransaction { + /// Create a new `RuntimeTransaction` from an + /// unsanitized `VersionedTransaction`. + pub fn try_create( + tx: VersionedTransaction, + message_hash: MessageHash, + is_simple_vote_tx: Option, + address_loader: impl AddressLoader, + reserved_account_keys: &HashSet, + ) -> Result { + let statically_loaded_runtime_tx = + RuntimeTransaction::::try_from( + SanitizedVersionedTransaction::try_from(tx)?, + message_hash, + is_simple_vote_tx, + )?; + Self::try_from( + statically_loaded_runtime_tx, + address_loader, + reserved_account_keys, + ) + } + + /// Create a new `RuntimeTransaction` from a + /// `RuntimeTransaction` that already has + /// static metadata loaded. + pub fn try_from( + statically_loaded_runtime_tx: RuntimeTransaction, + address_loader: impl AddressLoader, + reserved_account_keys: &HashSet, + ) -> Result { + let hash = *statically_loaded_runtime_tx.message_hash(); + let is_simple_vote_tx = statically_loaded_runtime_tx.is_simple_vote_transaction(); + let sanitized_transaction = SanitizedTransaction::try_new( + statically_loaded_runtime_tx.transaction, + hash, + is_simple_vote_tx, + address_loader, + reserved_account_keys, + )?; + + let mut tx = Self { + transaction: sanitized_transaction, + meta: statically_loaded_runtime_tx.meta, + }; + tx.load_dynamic_metadata()?; + + Ok(tx) + } + + fn load_dynamic_metadata(&mut self) -> Result<()> { + Ok(()) + } +} + +impl TransactionWithMeta for RuntimeTransaction { + #[inline] + fn as_sanitized_transaction(&self) -> Cow { + Cow::Borrowed(self) + } + + #[inline] + fn to_versioned_transaction(&self) -> VersionedTransaction { + self.transaction.to_versioned_transaction() + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl RuntimeTransaction { + pub fn from_transaction_for_tests(transaction: solana_sdk::transaction::Transaction) -> Self { + let versioned_transaction = VersionedTransaction::from(transaction); + Self::try_create( + versioned_transaction, + MessageHash::Compute, + None, + solana_sdk::message::SimpleAddressLoader::Disabled, + &HashSet::new(), + ) + .expect("failed to create RuntimeTransaction from Transaction") + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program::{ + system_instruction, + vote::{self, state::Vote}, + }, + solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + feature_set::FeatureSet, + hash::Hash, + instruction::Instruction, + message::Message, + reserved_account_keys::ReservedAccountKeys, + signer::{keypair::Keypair, Signer}, + transaction::{SimpleAddressLoader, Transaction, VersionedTransaction}, + }, + }; + + fn vote_sanitized_versioned_transaction() -> SanitizedVersionedTransaction { + let bank_hash = Hash::new_unique(); + let block_hash = Hash::new_unique(); + let vote_keypair = Keypair::new(); + let node_keypair = Keypair::new(); + let auth_keypair = Keypair::new(); + let votes = Vote::new(vec![1, 2, 3], bank_hash); + let vote_ix = + vote::instruction::vote(&vote_keypair.pubkey(), &auth_keypair.pubkey(), votes); + let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&node_keypair.pubkey())); + vote_tx.partial_sign(&[&node_keypair], block_hash); + vote_tx.partial_sign(&[&auth_keypair], block_hash); + + SanitizedVersionedTransaction::try_from(VersionedTransaction::from(vote_tx)).unwrap() + } + + fn non_vote_sanitized_versioned_transaction() -> SanitizedVersionedTransaction { + TestTransaction::new().to_sanitized_versioned_transaction() + } + + // Simple transfer transaction for testing, it does not support vote instruction + // because simple vote transaction will not request limits + struct TestTransaction { + from_keypair: Keypair, + hash: Hash, + instructions: Vec, + } + + impl TestTransaction { + fn new() -> Self { + let from_keypair = Keypair::new(); + let instructions = vec![system_instruction::transfer( + &from_keypair.pubkey(), + &solana_sdk::pubkey::new_rand(), + 1, + )]; + TestTransaction { + from_keypair, + hash: Hash::new_unique(), + instructions, + } + } + + fn add_compute_unit_limit(&mut self, val: u32) -> &mut TestTransaction { + self.instructions + .push(ComputeBudgetInstruction::set_compute_unit_limit(val)); + self + } + + fn add_compute_unit_price(&mut self, val: u64) -> &mut TestTransaction { + self.instructions + .push(ComputeBudgetInstruction::set_compute_unit_price(val)); + self + } + + fn add_loaded_accounts_bytes(&mut self, val: u32) -> &mut TestTransaction { + self.instructions + .push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(val)); + self + } + + fn to_sanitized_versioned_transaction(&self) -> SanitizedVersionedTransaction { + let message = Message::new(&self.instructions, Some(&self.from_keypair.pubkey())); + let tx = Transaction::new(&[&self.from_keypair], message, self.hash); + SanitizedVersionedTransaction::try_from(VersionedTransaction::from(tx)).unwrap() + } + } + + #[test] + fn test_runtime_transaction_is_vote_meta() { + fn get_is_simple_vote( + svt: SanitizedVersionedTransaction, + is_simple_vote: Option, + ) -> bool { + RuntimeTransaction::::try_from( + svt, + MessageHash::Compute, + is_simple_vote, + ) + .unwrap() + .meta + .is_simple_vote_transaction + } + + assert!(!get_is_simple_vote( + non_vote_sanitized_versioned_transaction(), + None + )); + + assert!(get_is_simple_vote( + non_vote_sanitized_versioned_transaction(), + Some(true), // override + )); + + assert!(get_is_simple_vote( + vote_sanitized_versioned_transaction(), + None + )); + + assert!(!get_is_simple_vote( + vote_sanitized_versioned_transaction(), + Some(false), // override + )); + } + + #[test] + fn test_advancing_transaction_type() { + let hash = Hash::new_unique(); + + let statically_loaded_transaction = + RuntimeTransaction::::try_from( + non_vote_sanitized_versioned_transaction(), + MessageHash::Precomputed(hash), + None, + ) + .unwrap(); + + assert_eq!(hash, *statically_loaded_transaction.message_hash()); + assert!(!statically_loaded_transaction.is_simple_vote_transaction()); + + let dynamically_loaded_transaction = RuntimeTransaction::::try_from( + statically_loaded_transaction, + SimpleAddressLoader::Disabled, + &ReservedAccountKeys::empty_key_set(), + ); + let dynamically_loaded_transaction = + dynamically_loaded_transaction.expect("created from statically loaded tx"); + + assert_eq!(hash, *dynamically_loaded_transaction.message_hash()); + assert!(!dynamically_loaded_transaction.is_simple_vote_transaction()); + } + + #[test] + fn test_runtime_transaction_static_meta() { + let hash = Hash::new_unique(); + let compute_unit_limit = 250_000; + let compute_unit_price = 1_000; + let loaded_accounts_bytes = 1_024; + let mut test_transaction = TestTransaction::new(); + + let runtime_transaction_static = + RuntimeTransaction::::try_from( + test_transaction + .add_compute_unit_limit(compute_unit_limit) + .add_compute_unit_price(compute_unit_price) + .add_loaded_accounts_bytes(loaded_accounts_bytes) + .to_sanitized_versioned_transaction(), + MessageHash::Precomputed(hash), + None, + ) + .unwrap(); + + assert_eq!(&hash, runtime_transaction_static.message_hash()); + assert!(!runtime_transaction_static.is_simple_vote_transaction()); + + let signature_details = &runtime_transaction_static.meta.signature_details; + assert_eq!(1, signature_details.num_transaction_signatures()); + assert_eq!(0, signature_details.num_secp256k1_instruction_signatures()); + assert_eq!(0, signature_details.num_ed25519_instruction_signatures()); + + for feature_set in [FeatureSet::default(), FeatureSet::all_enabled()] { + let compute_budget_limits = runtime_transaction_static + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(&feature_set) + .unwrap(); + assert_eq!(compute_unit_limit, compute_budget_limits.compute_unit_limit); + assert_eq!(compute_unit_price, compute_budget_limits.compute_unit_price); + assert_eq!( + loaded_accounts_bytes, + compute_budget_limits.loaded_accounts_bytes.get() + ); + } + } +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f0d4106cc4f9b9..07e13bb9359b84 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3092,7 +3092,18 @@ impl Bank { message: &SanitizedMessage, lamports_per_signature: u64, ) -> u64 { +<<<<<<< HEAD self.fee_structure().calculate_fee( +======= + let fee_budget_limits = FeeBudgetLimits::from( + process_compute_budget_instructions( + message.program_instructions_iter(), + &self.feature_set, + ) + .unwrap_or_default(), + ); + solana_fee::calculate_fee( +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) message, lamports_per_signature, &process_compute_budget_instructions(message.program_instructions_iter()) diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 3da66902e95ae8..9a08b19774bd34 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -10017,11 +10017,28 @@ fn calculate_test_fee( lamports_per_signature: u64, fee_structure: &FeeStructure, ) -> u64 { +<<<<<<< HEAD let budget_limits = process_compute_budget_instructions(message.program_instructions_iter()) .unwrap_or_default() .into(); fee_structure.calculate_fee(message, lamports_per_signature, &budget_limits, false, true) +======= + let fee_budget_limits = FeeBudgetLimits::from( + process_compute_budget_instructions( + message.program_instructions_iter(), + &FeatureSet::default(), + ) + .unwrap_or_default(), + ); + solana_fee::calculate_fee( + message, + lamports_per_signature == 0, + fee_structure.lamports_per_signature, + fee_budget_limits.prioritization_fee, + true, + ) +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) } #[test] diff --git a/runtime/src/prioritization_fee_cache.rs b/runtime/src/prioritization_fee_cache.rs index 796fafbb41b62a..bd4bf25f4d1aaa 100644 --- a/runtime/src/prioritization_fee_cache.rs +++ b/runtime/src/prioritization_fee_cache.rs @@ -240,9 +240,56 @@ impl PrioritizationFeeCache { ); }); } +<<<<<<< HEAD }, "send_updates", ); +======= + + let compute_budget_limits = sanitized_transaction + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(&bank.feature_set); + + let lock_result = validate_account_locks( + sanitized_transaction.account_keys(), + bank.get_transaction_account_lock_limit(), + ); + + if compute_budget_limits.is_err() || lock_result.is_err() { + continue; + } + let compute_budget_limits = compute_budget_limits.unwrap(); + + // filter out any transaction that requests zero compute_unit_limit + // since its priority fee amount is not instructive + if compute_budget_limits.compute_unit_limit == 0 { + continue; + } + + let writable_accounts = sanitized_transaction + .account_keys() + .iter() + .enumerate() + .filter(|(index, _)| sanitized_transaction.is_writable(*index)) + .map(|(_, key)| *key) + .collect(); + + self.sender + .send(CacheServiceUpdate::TransactionUpdate { + slot: bank.slot(), + bank_id: bank.bank_id(), + transaction_fee: compute_budget_limits.compute_unit_price, + writable_accounts, + }) + .unwrap_or_else(|err| { + warn!( + "prioritization fee cache transaction updates failed: {:?}", + err + ); + }); + } + }); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) self.metrics .accumulate_total_update_elapsed_us(send_updates_time.as_us()); diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 11b2efe1239456..e305aa58b2737b 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -193,7 +193,7 @@ impl SanitizedMessage { /// id. pub fn program_instructions_iter( &self, - ) -> impl Iterator { + ) -> impl Iterator + Clone { self.instructions().iter().map(move |ix| { ( self.account_keys() diff --git a/sdk/program/src/message/versions/sanitized.rs b/sdk/program/src/message/versions/sanitized.rs index 5d44f7ed4829eb..bca8be46e08406 100644 --- a/sdk/program/src/message/versions/sanitized.rs +++ b/sdk/program/src/message/versions/sanitized.rs @@ -32,7 +32,7 @@ impl SanitizedVersionedMessage { /// id. pub fn program_instructions_iter( &self, - ) -> impl Iterator { + ) -> impl Iterator + Clone { self.message.instructions().iter().map(move |ix| { ( self.message diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index ac5dbe6765f41e..9b88977e246d23 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -861,6 +861,10 @@ pub mod disable_account_loader_special_case { solana_program::declare_id!("EQUMpNFr7Nacb1sva56xn1aLfBxppEoSBH8RRVdkcD1x"); } +pub mod reserve_minimal_cus_for_builtin_instructions { + solana_pubkey::declare_id!("C9oAhLxDBm3ssWtJx1yBGzPY55r2rArHmN1pbQn6HogH"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -1068,9 +1072,21 @@ lazy_static! { (verify_retransmitter_signature::id(), "Verify retransmitter signature #1840"), (vote_only_retransmitter_signed_fec_sets::id(), "vote only on retransmitter signed fec sets"), (partitioned_epoch_rewards_superfeature::id(), "replaces enable_partitioned_epoch_reward to enable partitioned rewards at epoch boundary SIMD-0118"), +<<<<<<< HEAD:sdk/src/feature_set.rs (enable_turbine_extended_fanout_experiments::id(), "enable turbine extended fanout experiments #2373"), (deprecate_legacy_vote_ixs::id(), "Deprecate legacy vote instructions"), (disable_account_loader_special_case::id(), "Disable account loader special case"), +======= + (disable_sbpf_v1_execution::id(), "Disables execution of SBPFv1 programs"), + (reenable_sbpf_v1_execution::id(), "Re-enables execution of SBPFv1 programs"), + (remove_accounts_executable_flag_checks::id(), "Remove checks of accounts is_executable flag SIMD-0162"), + (lift_cpi_caller_restriction::id(), "Lift the restriction in CPI that the caller must have the callee as an instruction account #2202"), + (disable_account_loader_special_case::id(), "Disable account loader special case #3513"), + (accounts_lt_hash::id(), "enables lattice-based accounts hash #3333"), + (enable_secp256r1_precompile::id(), "Enable secp256r1 precompile SIMD-0075"), + (migrate_stake_program_to_core_bpf::id(), "Migrate Stake program to Core BPF SIMD-0196 #3655"), + (reserve_minimal_cus_for_builtin_instructions::id(), "Reserve minimal CUs for builtin instructions SIMD-170 #2562"), +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)):sdk/feature-set/src/lib.rs /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/svm-transaction/src/svm_message.rs b/svm-transaction/src/svm_message.rs new file mode 100644 index 00000000000000..0e60567729b296 --- /dev/null +++ b/svm-transaction/src/svm_message.rs @@ -0,0 +1,127 @@ +use { + crate::{ + instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, + }, + core::fmt::Debug, + solana_hash::Hash, + solana_message::AccountKeys, + solana_pubkey::Pubkey, + solana_sdk_ids::system_program, +}; + +mod sanitized_message; +mod sanitized_transaction; +// inlined to avoid solana-nonce dep +#[cfg(test)] +static_assertions::const_assert_eq!( + NONCED_TX_MARKER_IX_INDEX, + solana_nonce::NONCED_TX_MARKER_IX_INDEX +); +const NONCED_TX_MARKER_IX_INDEX: u8 = 0; + +// - Debug to support legacy logging +pub trait SVMMessage: Debug { + /// Returns the total number of signatures in the message. + /// This includes required transaction signatures as well as any + /// pre-compile signatures that are attached in instructions. + fn num_total_signatures(&self) -> u64; + + /// Returns the number of requested write-locks in this message. + /// This does not consider if write-locks are demoted. + fn num_write_locks(&self) -> u64; + + /// Return the recent blockhash. + fn recent_blockhash(&self) -> &Hash; + + /// Return the number of instructions in the message. + fn num_instructions(&self) -> usize; + + /// Return an iterator over the instructions in the message. + fn instructions_iter(&self) -> impl Iterator; + + /// Return an iterator over the instructions in the message, paired with + /// the pubkey of the program. + fn program_instructions_iter(&self) -> impl Iterator + Clone; + + /// Return the account keys. + fn account_keys(&self) -> AccountKeys; + + /// Return the fee-payer + fn fee_payer(&self) -> &Pubkey; + + /// Returns `true` if the account at `index` is writable. + fn is_writable(&self, index: usize) -> bool; + + /// Returns `true` if the account at `index` is signer. + fn is_signer(&self, index: usize) -> bool; + + /// Returns true if the account at the specified index is invoked as a + /// program in top-level instructions of this message. + fn is_invoked(&self, key_index: usize) -> bool; + + /// Returns true if the account at the specified index is an input to some + /// program instruction in this message. + fn is_instruction_account(&self, key_index: usize) -> bool { + if let Ok(key_index) = u8::try_from(key_index) { + self.instructions_iter() + .any(|ix| ix.accounts.contains(&key_index)) + } else { + false + } + } + + /// If the message uses a durable nonce, return the pubkey of the nonce account + fn get_durable_nonce(&self) -> Option<&Pubkey> { + let account_keys = self.account_keys(); + self.instructions_iter() + .nth(usize::from(NONCED_TX_MARKER_IX_INDEX)) + .filter( + |ix| match account_keys.get(usize::from(ix.program_id_index)) { + Some(program_id) => system_program::check_id(program_id), + _ => false, + }, + ) + .filter(|ix| { + /// Serialized value of [`SystemInstruction::AdvanceNonceAccount`]. + const SERIALIZED_ADVANCE_NONCE_ACCOUNT: [u8; 4] = 4u32.to_le_bytes(); + const SERIALIZED_SIZE: usize = SERIALIZED_ADVANCE_NONCE_ACCOUNT.len(); + + ix.data + .get(..SERIALIZED_SIZE) + .map(|data| data == SERIALIZED_ADVANCE_NONCE_ACCOUNT) + .unwrap_or(false) + }) + .and_then(|ix| { + ix.accounts.first().and_then(|idx| { + let index = usize::from(*idx); + if !self.is_writable(index) { + None + } else { + account_keys.get(index) + } + }) + }) + } + + /// For the instruction at `index`, return an iterator over input accounts + /// that are signers. + fn get_ix_signers(&self, index: usize) -> impl Iterator { + self.instructions_iter() + .nth(index) + .into_iter() + .flat_map(|ix| { + ix.accounts + .iter() + .copied() + .map(usize::from) + .filter(|index| self.is_signer(*index)) + .filter_map(|signer_index| self.account_keys().get(signer_index)) + }) + } + + /// Get the number of lookup tables. + fn num_lookup_tables(&self) -> usize; + + /// Get message address table lookups used in the message + fn message_address_table_lookups(&self) -> impl Iterator; +} diff --git a/svm-transaction/src/svm_message/sanitized_message.rs b/svm-transaction/src/svm_message/sanitized_message.rs new file mode 100644 index 00000000000000..75b98c3a9ca8e6 --- /dev/null +++ b/svm-transaction/src/svm_message/sanitized_message.rs @@ -0,0 +1,69 @@ +use { + crate::{ + instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, + svm_message::SVMMessage, + }, + solana_hash::Hash, + solana_message::{AccountKeys, SanitizedMessage}, + solana_pubkey::Pubkey, +}; + +// Implement for the "reference" `SanitizedMessage` type. +impl SVMMessage for SanitizedMessage { + fn num_total_signatures(&self) -> u64 { + SanitizedMessage::num_total_signatures(self) + } + + fn num_write_locks(&self) -> u64 { + SanitizedMessage::num_write_locks(self) + } + + fn recent_blockhash(&self) -> &Hash { + SanitizedMessage::recent_blockhash(self) + } + + fn num_instructions(&self) -> usize { + SanitizedMessage::instructions(self).len() + } + + fn instructions_iter(&self) -> impl Iterator { + SanitizedMessage::instructions(self) + .iter() + .map(SVMInstruction::from) + } + + fn program_instructions_iter(&self) -> impl Iterator + Clone { + SanitizedMessage::program_instructions_iter(self) + .map(|(pubkey, ix)| (pubkey, SVMInstruction::from(ix))) + } + + fn account_keys(&self) -> AccountKeys { + SanitizedMessage::account_keys(self) + } + + fn fee_payer(&self) -> &Pubkey { + SanitizedMessage::fee_payer(self) + } + + fn is_writable(&self, index: usize) -> bool { + SanitizedMessage::is_writable(self, index) + } + + fn is_signer(&self, index: usize) -> bool { + SanitizedMessage::is_signer(self, index) + } + + fn is_invoked(&self, key_index: usize) -> bool { + SanitizedMessage::is_invoked(self, key_index) + } + + fn num_lookup_tables(&self) -> usize { + SanitizedMessage::message_address_table_lookups(self).len() + } + + fn message_address_table_lookups(&self) -> impl Iterator { + SanitizedMessage::message_address_table_lookups(self) + .iter() + .map(SVMMessageAddressTableLookup::from) + } +} diff --git a/svm-transaction/src/svm_message/sanitized_transaction.rs b/svm-transaction/src/svm_message/sanitized_transaction.rs new file mode 100644 index 00000000000000..4bbea93cb0d215 --- /dev/null +++ b/svm-transaction/src/svm_message/sanitized_transaction.rs @@ -0,0 +1,64 @@ +use { + crate::{ + instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, + svm_message::SVMMessage, + }, + solana_hash::Hash, + solana_message::AccountKeys, + solana_pubkey::Pubkey, + solana_transaction::sanitized::SanitizedTransaction, +}; + +impl SVMMessage for SanitizedTransaction { + fn num_total_signatures(&self) -> u64 { + SVMMessage::num_total_signatures(SanitizedTransaction::message(self)) + } + + fn num_write_locks(&self) -> u64 { + SVMMessage::num_write_locks(SanitizedTransaction::message(self)) + } + + fn recent_blockhash(&self) -> &Hash { + SVMMessage::recent_blockhash(SanitizedTransaction::message(self)) + } + + fn num_instructions(&self) -> usize { + SVMMessage::num_instructions(SanitizedTransaction::message(self)) + } + + fn instructions_iter(&self) -> impl Iterator { + SVMMessage::instructions_iter(SanitizedTransaction::message(self)) + } + + fn program_instructions_iter(&self) -> impl Iterator + Clone { + SVMMessage::program_instructions_iter(SanitizedTransaction::message(self)) + } + + fn account_keys(&self) -> AccountKeys { + SVMMessage::account_keys(SanitizedTransaction::message(self)) + } + + fn fee_payer(&self) -> &Pubkey { + SVMMessage::fee_payer(SanitizedTransaction::message(self)) + } + + fn is_writable(&self, index: usize) -> bool { + SVMMessage::is_writable(SanitizedTransaction::message(self), index) + } + + fn is_signer(&self, index: usize) -> bool { + SVMMessage::is_signer(SanitizedTransaction::message(self), index) + } + + fn is_invoked(&self, key_index: usize) -> bool { + SVMMessage::is_invoked(SanitizedTransaction::message(self), key_index) + } + + fn num_lookup_tables(&self) -> usize { + SVMMessage::num_lookup_tables(SanitizedTransaction::message(self)) + } + + fn message_address_table_lookups(&self) -> impl Iterator { + SVMMessage::message_address_table_lookups(SanitizedTransaction::message(self)) + } +} diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 309b7927b437ac..bbddcb4f611834 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -429,6 +429,7 @@ impl TransactionBatchProcessor { ) -> transaction::Result { let compute_budget_limits = process_compute_budget_instructions( message.program_instructions_iter(), + &account_loader.feature_set, ) .map_err(|err| { error_counters.invalid_compute_budget += 1; @@ -1980,8 +1981,16 @@ mod tests { Some(&Pubkey::new_unique()), &Hash::new_unique(), )); +<<<<<<< HEAD let compute_budget_limits = process_compute_budget_instructions(message.program_instructions_iter()).unwrap(); +======= + let compute_budget_limits = process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&message), + &FeatureSet::default(), + ) + .unwrap(); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let fee_payer_address = message.fee_payer(); let current_epoch = 42; let rent_collector = RentCollector { @@ -2061,8 +2070,16 @@ mod tests { Some(&Pubkey::new_unique()), &Hash::new_unique(), )); +<<<<<<< HEAD let compute_budget_limits = process_compute_budget_instructions(message.program_instructions_iter()).unwrap(); +======= + let compute_budget_limits = process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&message), + &FeatureSet::default(), + ) + .unwrap(); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let fee_payer_address = message.fee_payer(); let mut rent_collector = RentCollector::default(); rent_collector.rent.lamports_per_byte_year = 1_000_000; @@ -2304,8 +2321,16 @@ mod tests { Some(&Pubkey::new_unique()), &Hash::new_unique(), )); +<<<<<<< HEAD let compute_budget_limits = process_compute_budget_instructions(message.program_instructions_iter()).unwrap(); +======= + let compute_budget_limits = process_compute_budget_instructions( + SVMMessage::program_instructions_iter(&message), + &FeatureSet::default(), + ) + .unwrap(); +>>>>>>> 3e9af14f3a (Fix reserve minimal compute units for builtins (#3799)) let fee_payer_address = message.fee_payer(); let min_balance = Rent::default().minimum_balance(nonce::State::size()); let transaction_fee = lamports_per_signature; diff --git a/transaction-view/src/resolved_transaction_view.rs b/transaction-view/src/resolved_transaction_view.rs new file mode 100644 index 00000000000000..98d395bc9dc749 --- /dev/null +++ b/transaction-view/src/resolved_transaction_view.rs @@ -0,0 +1,609 @@ +use { + crate::{ + result::{Result, TransactionViewError}, + transaction_data::TransactionData, + transaction_version::TransactionVersion, + transaction_view::TransactionView, + }, + core::{ + fmt::{Debug, Formatter}, + ops::Deref, + }, + solana_sdk::{ + bpf_loader_upgradeable, ed25519_program, + hash::Hash, + message::{v0::LoadedAddresses, AccountKeys, TransactionSignatureDetails}, + pubkey::Pubkey, + secp256k1_program, + signature::Signature, + }, + solana_sdk_ids::secp256r1_program, + solana_svm_transaction::{ + instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, + svm_message::SVMMessage, svm_transaction::SVMTransaction, + }, + std::collections::HashSet, +}; + +/// A parsed and sanitized transaction view that has had all address lookups +/// resolved. +pub struct ResolvedTransactionView { + /// The parsed and sanitized transction view. + view: TransactionView, + /// The resolved address lookups. + resolved_addresses: Option, + /// A cache for whether an address is writable. + // Sanitized transactions are guaranteed to have a maximum of 256 keys, + // because account indexing is done with a u8. + writable_cache: [bool; 256], +} + +impl Deref for ResolvedTransactionView { + type Target = TransactionView; + + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl ResolvedTransactionView { + /// Given a parsed and sanitized transaction view, and a set of resolved + /// addresses, create a resolved transaction view. + pub fn try_new( + view: TransactionView, + resolved_addresses: Option, + reserved_account_keys: &HashSet, + ) -> Result { + let resolved_addresses_ref = resolved_addresses.as_ref(); + + // verify that the number of readable and writable match up. + // This is a basic sanity check to make sure we're not passing a totally + // invalid set of resolved addresses. + // Additionally if it is a v0 transaction it *must* have resolved + // addresses, even if they are empty. + if matches!(view.version(), TransactionVersion::V0) && resolved_addresses_ref.is_none() { + return Err(TransactionViewError::AddressLookupMismatch); + } + if let Some(loaded_addresses) = resolved_addresses_ref { + if loaded_addresses.writable.len() != usize::from(view.total_writable_lookup_accounts()) + || loaded_addresses.readonly.len() + != usize::from(view.total_readonly_lookup_accounts()) + { + return Err(TransactionViewError::AddressLookupMismatch); + } + } else if view.total_writable_lookup_accounts() != 0 + || view.total_readonly_lookup_accounts() != 0 + { + return Err(TransactionViewError::AddressLookupMismatch); + } + + let writable_cache = + Self::cache_is_writable(&view, resolved_addresses_ref, reserved_account_keys); + Ok(Self { + view, + resolved_addresses, + writable_cache, + }) + } + + /// Helper function to check if an address is writable, + /// and cache the result. + /// This is done so we avoid recomputing the expensive checks each time we call + /// `is_writable` - since there is more to it than just checking index. + fn cache_is_writable( + view: &TransactionView, + resolved_addresses: Option<&LoadedAddresses>, + reserved_account_keys: &HashSet, + ) -> [bool; 256] { + // Build account keys so that we can iterate over and check if + // an address is writable. + let account_keys = AccountKeys::new(view.static_account_keys(), resolved_addresses); + + let mut is_writable_cache = [false; 256]; + let num_static_account_keys = usize::from(view.num_static_account_keys()); + let num_writable_lookup_accounts = usize::from(view.total_writable_lookup_accounts()); + let num_signed_accounts = usize::from(view.num_required_signatures()); + let num_writable_unsigned_static_accounts = + usize::from(view.num_writable_unsigned_static_accounts()); + let num_writable_signed_static_accounts = + usize::from(view.num_writable_signed_static_accounts()); + + for (index, key) in account_keys.iter().enumerate() { + let is_requested_write = { + // If the account is a resolved address, check if it is writable. + if index >= num_static_account_keys { + let loaded_address_index = index.wrapping_sub(num_static_account_keys); + loaded_address_index < num_writable_lookup_accounts + } else if index >= num_signed_accounts { + let unsigned_account_index = index.wrapping_sub(num_signed_accounts); + unsigned_account_index < num_writable_unsigned_static_accounts + } else { + index < num_writable_signed_static_accounts + } + }; + + // If the key is reserved it cannot be writable. + is_writable_cache[index] = is_requested_write && !reserved_account_keys.contains(key); + } + + // If a program account is locked, it cannot be writable unless the + // upgradable loader is present. + // However, checking for the upgradable loader is somewhat expensive, so + // we only do it if we find a writable program id. + let mut is_upgradable_loader_present = None; + for ix in view.instructions_iter() { + let program_id_index = usize::from(ix.program_id_index); + if is_writable_cache[program_id_index] + && !*is_upgradable_loader_present.get_or_insert_with(|| { + for key in account_keys.iter() { + if key == &bpf_loader_upgradeable::ID { + return true; + } + } + false + }) + { + is_writable_cache[program_id_index] = false; + } + } + + is_writable_cache + } + + fn num_readonly_accounts(&self) -> usize { + usize::from(self.view.total_readonly_lookup_accounts()) + .wrapping_add(usize::from(self.view.num_readonly_signed_static_accounts())) + .wrapping_add(usize::from( + self.view.num_readonly_unsigned_static_accounts(), + )) + } + + pub fn loaded_addresses(&self) -> Option<&LoadedAddresses> { + self.resolved_addresses.as_ref() + } + + fn signature_details(&self) -> TransactionSignatureDetails { + // counting the number of pre-processor operations separately + let mut num_secp256k1_instruction_signatures: u64 = 0; + let mut num_ed25519_instruction_signatures: u64 = 0; + let mut num_secp256r1_instruction_signatures: u64 = 0; + for (program_id, instruction) in self.program_instructions_iter() { + if secp256k1_program::check_id(program_id) { + if let Some(num_verifies) = instruction.data.first() { + num_secp256k1_instruction_signatures = + num_secp256k1_instruction_signatures.wrapping_add(u64::from(*num_verifies)); + } + } else if ed25519_program::check_id(program_id) { + if let Some(num_verifies) = instruction.data.first() { + num_ed25519_instruction_signatures = + num_ed25519_instruction_signatures.wrapping_add(u64::from(*num_verifies)); + } + } else if secp256r1_program::check_id(program_id) { + if let Some(num_verifies) = instruction.data.first() { + num_secp256r1_instruction_signatures = + num_secp256r1_instruction_signatures.wrapping_add(u64::from(*num_verifies)); + } + } + } + + TransactionSignatureDetails::new( + u64::from(self.view.num_required_signatures()), + num_secp256k1_instruction_signatures, + num_ed25519_instruction_signatures, + num_secp256r1_instruction_signatures, + ) + } +} + +impl SVMMessage for ResolvedTransactionView { + fn num_total_signatures(&self) -> u64 { + self.signature_details().total_signatures() + } + + fn num_write_locks(&self) -> u64 { + self.account_keys() + .len() + .wrapping_sub(self.num_readonly_accounts()) as u64 + } + + fn recent_blockhash(&self) -> &Hash { + self.view.recent_blockhash() + } + + fn num_instructions(&self) -> usize { + usize::from(self.view.num_instructions()) + } + + fn instructions_iter(&self) -> impl Iterator { + self.view.instructions_iter() + } + + fn program_instructions_iter( + &self, + ) -> impl Iterator< + Item = ( + &solana_sdk::pubkey::Pubkey, + solana_svm_transaction::instruction::SVMInstruction, + ), + > + Clone { + self.view.program_instructions_iter() + } + + fn account_keys(&self) -> AccountKeys { + AccountKeys::new( + self.view.static_account_keys(), + self.resolved_addresses.as_ref(), + ) + } + + fn fee_payer(&self) -> &Pubkey { + &self.view.static_account_keys()[0] + } + + fn is_writable(&self, index: usize) -> bool { + self.writable_cache.get(index).copied().unwrap_or(false) + } + + fn is_signer(&self, index: usize) -> bool { + index < usize::from(self.view.num_required_signatures()) + } + + fn is_invoked(&self, key_index: usize) -> bool { + let Ok(index) = u8::try_from(key_index) else { + return false; + }; + self.view + .instructions_iter() + .any(|ix| ix.program_id_index == index) + } + + fn num_lookup_tables(&self) -> usize { + usize::from(self.view.num_address_table_lookups()) + } + + fn message_address_table_lookups(&self) -> impl Iterator { + self.view.address_table_lookup_iter() + } +} + +impl SVMTransaction for ResolvedTransactionView { + fn signature(&self) -> &Signature { + &self.view.signatures()[0] + } + + fn signatures(&self) -> &[Signature] { + self.view.signatures() + } +} + +impl Debug for ResolvedTransactionView { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedTransactionView") + .field("view", &self.view) + .finish() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::transaction_view::SanitizedTransactionView, + solana_sdk::{ + instruction::CompiledInstruction, + message::{ + v0::{self, MessageAddressTableLookup}, + MessageHeader, VersionedMessage, + }, + signature::Signature, + system_program, sysvar, + transaction::VersionedTransaction, + }, + }; + + #[test] + fn test_expected_loaded_addresses() { + // Expected addresses passed in, but `None` was passed. + let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let transaction = VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + instructions: vec![], + account_keys: static_keys, + address_table_lookups: vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![1], + }], + recent_blockhash: Hash::default(), + }), + }; + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let result = ResolvedTransactionView::try_new(view, None, &HashSet::default()); + assert!(matches!( + result, + Err(TransactionViewError::AddressLookupMismatch) + )); + } + + #[test] + fn test_unexpected_loaded_addresses() { + // Expected no addresses passed in, but `Some` was passed. + let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let loaded_addresses = LoadedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![], + }; + let transaction = VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + instructions: vec![], + account_keys: static_keys, + address_table_lookups: vec![], + recent_blockhash: Hash::default(), + }), + }; + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let result = + ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); + assert!(matches!( + result, + Err(TransactionViewError::AddressLookupMismatch) + )); + } + + #[test] + fn test_mismatched_loaded_address_lengths() { + // Loaded addresses only has 1 writable address, no readonly. + // The message ATL has 1 writable and 1 readonly. + let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let loaded_addresses = LoadedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![], + }; + let transaction = VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + instructions: vec![], + account_keys: static_keys, + address_table_lookups: vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![1], + }], + recent_blockhash: Hash::default(), + }), + }; + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let result = + ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); + assert!(matches!( + result, + Err(TransactionViewError::AddressLookupMismatch) + )); + } + + #[test] + fn test_is_writable() { + let reserved_account_keys = HashSet::from_iter([sysvar::clock::id(), system_program::id()]); + // Create a versioned transaction. + let create_transaction_with_keys = + |static_keys: Vec, loaded_addresses: &LoadedAddresses| VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + account_keys: static_keys[..2].to_vec(), + recent_blockhash: Hash::default(), + instructions: vec![], + address_table_lookups: vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: (0..loaded_addresses.writable.len()) + .map(|x| (static_keys.len() + x) as u8) + .collect(), + readonly_indexes: (0..loaded_addresses.readonly.len()) + .map(|x| { + (static_keys.len() + loaded_addresses.writable.len() + x) as u8 + }) + .collect(), + }], + }), + }; + + let key0 = Pubkey::new_unique(); + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + { + let static_keys = vec![sysvar::clock::id(), key0]; + let loaded_addresses = LoadedAddresses { + writable: vec![key1], + readonly: vec![key2], + }; + let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses), + &reserved_account_keys, + ) + .unwrap(); + + // demote reserved static key to readonly + let expected = vec![false, false, true, false]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + + { + let static_keys = vec![system_program::id(), key0]; + let loaded_addresses = LoadedAddresses { + writable: vec![key1], + readonly: vec![key2], + }; + let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses), + &reserved_account_keys, + ) + .unwrap(); + + // demote reserved static key to readonly + let expected = vec![false, false, true, false]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + + { + let static_keys = vec![key0, key1]; + let loaded_addresses = LoadedAddresses { + writable: vec![system_program::id()], + readonly: vec![key2], + }; + let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses), + &reserved_account_keys, + ) + .unwrap(); + + // demote loaded key to readonly + let expected = vec![true, false, false, false]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + } + + #[test] + fn test_demote_writable_program() { + let reserved_account_keys = HashSet::default(); + let key0 = Pubkey::new_unique(); + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let key3 = Pubkey::new_unique(); + let key4 = Pubkey::new_unique(); + let loaded_addresses = LoadedAddresses { + writable: vec![key3, key4], + readonly: vec![], + }; + let create_transaction_with_static_keys = + |static_keys: Vec, loaded_addresses: &LoadedAddresses| VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + account_keys: static_keys, + address_table_lookups: vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: (0..loaded_addresses.writable.len()) + .map(|x| x as u8) + .collect(), + readonly_indexes: (0..loaded_addresses.readonly.len()) + .map(|x| (loaded_addresses.writable.len() + x) as u8) + .collect(), + }], + recent_blockhash: Hash::default(), + }), + }; + + // Demote writable program - static + { + let static_keys = vec![key0, key1, key2]; + let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses.clone()), + &reserved_account_keys, + ) + .unwrap(); + + let expected = vec![true, false, true, true, true]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + + // Do not demote writable program - static address: upgradable loader + { + let static_keys = vec![key0, key1, bpf_loader_upgradeable::ID]; + let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses.clone()), + &reserved_account_keys, + ) + .unwrap(); + + let expected = vec![true, true, true, true, true]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + + // Do not demote writable program - loaded address: upgradable loader + { + let static_keys = vec![key0, key1, key2]; + let loaded_addresses = LoadedAddresses { + writable: vec![key3], + readonly: vec![bpf_loader_upgradeable::ID], + }; + let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); + let bytes = bincode::serialize(&transaction).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref()).unwrap(); + + let resolved_view = ResolvedTransactionView::try_new( + view, + Some(loaded_addresses.clone()), + &reserved_account_keys, + ) + .unwrap(); + + let expected = vec![true, true, true, true, false]; + for (index, expected) in expected.into_iter().enumerate() { + assert_eq!(resolved_view.is_writable(index), expected); + } + } + } +} diff --git a/transaction-view/src/transaction_view.rs b/transaction-view/src/transaction_view.rs new file mode 100644 index 00000000000000..c186902c053f89 --- /dev/null +++ b/transaction-view/src/transaction_view.rs @@ -0,0 +1,294 @@ +use { + crate::{ + address_table_lookup_frame::AddressTableLookupIterator, + instructions_frame::InstructionsIterator, result::Result, sanitize::sanitize, + transaction_data::TransactionData, transaction_frame::TransactionFrame, + transaction_version::TransactionVersion, + }, + core::fmt::{Debug, Formatter}, + solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Signature}, + solana_svm_transaction::instruction::SVMInstruction, +}; + +// alias for convenience +pub type UnsanitizedTransactionView = TransactionView; +pub type SanitizedTransactionView = TransactionView; + +/// A view into a serialized transaction. +/// +/// This struct provides access to the transaction data without +/// deserializing it. This is done by parsing and caching metadata +/// about the layout of the serialized transaction. +/// The owned `data` is abstracted through the `TransactionData` trait, +/// so that different containers for the serialized transaction can be used. +pub struct TransactionView { + data: D, + frame: TransactionFrame, +} + +impl TransactionView { + /// Creates a new `TransactionView` without running sanitization checks. + pub fn try_new_unsanitized(data: D) -> Result { + let frame = TransactionFrame::try_new(data.data())?; + Ok(Self { data, frame }) + } + + /// Sanitizes the transaction view, returning a sanitized view on success. + pub fn sanitize(self) -> Result> { + sanitize(&self)?; + Ok(SanitizedTransactionView { + data: self.data, + frame: self.frame, + }) + } +} + +impl TransactionView { + /// Creates a new `TransactionView`, running sanitization checks. + pub fn try_new_sanitized(data: D) -> Result { + let unsanitized_view = TransactionView::try_new_unsanitized(data)?; + unsanitized_view.sanitize() + } +} + +impl TransactionView { + /// Return the number of signatures in the transaction. + #[inline] + pub fn num_signatures(&self) -> u8 { + self.frame.num_signatures() + } + + /// Return the version of the transaction. + #[inline] + pub fn version(&self) -> TransactionVersion { + self.frame.version() + } + + /// Return the number of required signatures in the transaction. + #[inline] + pub fn num_required_signatures(&self) -> u8 { + self.frame.num_required_signatures() + } + + /// Return the number of readonly signed static accounts in the transaction. + #[inline] + pub fn num_readonly_signed_static_accounts(&self) -> u8 { + self.frame.num_readonly_signed_static_accounts() + } + + /// Return the number of readonly unsigned static accounts in the transaction. + #[inline] + pub fn num_readonly_unsigned_static_accounts(&self) -> u8 { + self.frame.num_readonly_unsigned_static_accounts() + } + + /// Return the number of static account keys in the transaction. + #[inline] + pub fn num_static_account_keys(&self) -> u8 { + self.frame.num_static_account_keys() + } + + /// Return the number of instructions in the transaction. + #[inline] + pub fn num_instructions(&self) -> u16 { + self.frame.num_instructions() + } + + /// Return the number of address table lookups in the transaction. + #[inline] + pub fn num_address_table_lookups(&self) -> u8 { + self.frame.num_address_table_lookups() + } + + /// Return the number of writable lookup accounts in the transaction. + #[inline] + pub fn total_writable_lookup_accounts(&self) -> u16 { + self.frame.total_writable_lookup_accounts() + } + + /// Return the number of readonly lookup accounts in the transaction. + #[inline] + pub fn total_readonly_lookup_accounts(&self) -> u16 { + self.frame.total_readonly_lookup_accounts() + } + + /// Return the slice of signatures in the transaction. + #[inline] + pub fn signatures(&self) -> &[Signature] { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.signatures(data) } + } + + /// Return the slice of static account keys in the transaction. + #[inline] + pub fn static_account_keys(&self) -> &[Pubkey] { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.static_account_keys(data) } + } + + /// Return the recent blockhash in the transaction. + #[inline] + pub fn recent_blockhash(&self) -> &Hash { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.recent_blockhash(data) } + } + + /// Return an iterator over the instructions in the transaction. + #[inline] + pub fn instructions_iter(&self) -> InstructionsIterator { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.instructions_iter(data) } + } + + /// Return an iterator over the address table lookups in the transaction. + #[inline] + pub fn address_table_lookup_iter(&self) -> AddressTableLookupIterator { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.address_table_lookup_iter(data) } + } + + /// Return the full serialized transaction data. + #[inline] + pub fn data(&self) -> &[u8] { + self.data.data() + } + + /// Return the serialized **message** data. + /// This does not include the signatures. + #[inline] + pub fn message_data(&self) -> &[u8] { + &self.data()[usize::from(self.frame.message_offset())..] + } +} + +// Implementation that relies on sanitization checks having been run. +impl TransactionView { + /// Return an iterator over the instructions paired with their program ids. + pub fn program_instructions_iter( + &self, + ) -> impl Iterator + Clone { + self.instructions_iter().map(|ix| { + let program_id_index = usize::from(ix.program_id_index); + let program_id = &self.static_account_keys()[program_id_index]; + (program_id, ix) + }) + } + + /// Return the number of unsigned static account keys. + #[inline] + pub(crate) fn num_static_unsigned_static_accounts(&self) -> u8 { + self.num_static_account_keys() + .wrapping_sub(self.num_required_signatures()) + } + + /// Return the number of writable unsigned static accounts. + #[inline] + pub(crate) fn num_writable_unsigned_static_accounts(&self) -> u8 { + self.num_static_unsigned_static_accounts() + .wrapping_sub(self.num_readonly_unsigned_static_accounts()) + } + + /// Return the number of writable unsigned static accounts. + #[inline] + pub(crate) fn num_writable_signed_static_accounts(&self) -> u8 { + self.num_required_signatures() + .wrapping_sub(self.num_readonly_signed_static_accounts()) + } + + /// Return the total number of accounts in the transactions. + #[inline] + pub fn total_num_accounts(&self) -> u16 { + u16::from(self.num_static_account_keys()) + .wrapping_add(self.total_writable_lookup_accounts()) + .wrapping_add(self.total_readonly_lookup_accounts()) + } +} + +// Manual implementation of `Debug` - avoids bound on `D`. +// Prints nicely formatted struct-ish fields even for the iterator fields. +impl Debug for TransactionView { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TransactionView") + .field("frame", &self.frame) + .field("signatures", &self.signatures()) + .field("static_account_keys", &self.static_account_keys()) + .field("recent_blockhash", &self.recent_blockhash()) + .field("instructions", &self.instructions_iter()) + .field("address_table_lookups", &self.address_table_lookup_iter()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_sdk::{ + message::{Message, VersionedMessage}, + pubkey::Pubkey, + signature::Signature, + system_instruction::{self}, + transaction::VersionedTransaction, + }, + }; + + fn verify_transaction_view_frame(tx: &VersionedTransaction) { + let bytes = bincode::serialize(tx).unwrap(); + let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); + + assert_eq!(view.num_signatures(), tx.signatures.len() as u8); + + assert_eq!( + view.num_required_signatures(), + tx.message.header().num_required_signatures + ); + assert_eq!( + view.num_readonly_signed_static_accounts(), + tx.message.header().num_readonly_signed_accounts + ); + assert_eq!( + view.num_readonly_unsigned_static_accounts(), + tx.message.header().num_readonly_unsigned_accounts + ); + + assert_eq!( + view.num_static_account_keys(), + tx.message.static_account_keys().len() as u8 + ); + assert_eq!( + view.num_instructions(), + tx.message.instructions().len() as u16 + ); + assert_eq!( + view.num_address_table_lookups(), + tx.message + .address_table_lookups() + .map(|x| x.len() as u8) + .unwrap_or(0) + ); + } + + fn multiple_transfers() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[ + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + ], + Some(&payer), + )), + } + } + + #[test] + fn test_multiple_transfers() { + verify_transaction_view_frame(&multiple_transfers()); + } +}