diff --git a/Cargo.lock b/Cargo.lock index c0de14e91c1cba..62d42b1f02e643 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6121,6 +6121,7 @@ dependencies = [ "solana-stake-program", "solana-system-program", "solana-vote-program", + "static_assertions", ] [[package]] diff --git a/builtins-default-costs/Cargo.toml b/builtins-default-costs/Cargo.toml index bdd29a267280b5..75b84be0f38b60 100644 --- a/builtins-default-costs/Cargo.toml +++ b/builtins-default-costs/Cargo.toml @@ -33,6 +33,7 @@ name = "solana_builtins_default_costs" [dev-dependencies] rand = "0.8.5" +static_assertions = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -42,6 +43,7 @@ frozen-abi = [ "dep:solana-frozen-abi", "solana-vote-program/frozen-abi", ] +dev-context-only-utils = [] [lints] workspace = true diff --git a/builtins-default-costs/src/lib.rs b/builtins-default-costs/src/lib.rs index 2e167c667cc84d..a49dd7799a29d1 100644 --- a/builtins-default-costs/src/lib.rs +++ b/builtins-default-costs/src/lib.rs @@ -13,15 +13,68 @@ use { }, }; +#[derive(Clone)] +pub struct MigratingBuiltinCost { + native_cost: u64, + core_bpf_migration_feature: Pubkey, + // encoding positional information explicitly for migration feature item, + // its value must be correctly corresponding to this object's position + // in MIGRATING_BUILTINS_COSTS, otherwise a const validation + // `validate_position(MIGRATING_BUILTINS_COSTS)` will fail at compile time. + position: usize, +} + +#[derive(Clone)] +pub struct NotMigratingBuiltinCost { + native_cost: u64, +} + /// 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. +/// migration feature ID to BUILTIN_INSTRUCTION_COSTS, and move it from +/// NON_MIGRATING_BUILTINS_COSTS to MIGRATING_BUILTINS_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. +/// remove that builtin entry from MIGRATING_BUILTINS_COSTS. #[derive(Clone)] -struct BuiltinCost { - native_cost: u64, - core_bpf_migration_feature: Option, +pub enum BuiltinCost { + Migrating(MigratingBuiltinCost), + NotMigrating(NotMigratingBuiltinCost), +} + +impl BuiltinCost { + pub fn native_cost(&self) -> u64 { + match self { + BuiltinCost::Migrating(MigratingBuiltinCost { native_cost, .. }) => *native_cost, + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost }) => *native_cost, + } + } + + pub fn core_bpf_migration_feature(&self) -> Option<&Pubkey> { + match self { + BuiltinCost::Migrating(MigratingBuiltinCost { + core_bpf_migration_feature, + .. + }) => Some(core_bpf_migration_feature), + BuiltinCost::NotMigrating(_) => None, + } + } + + pub fn position(&self) -> Option { + match self { + BuiltinCost::Migrating(MigratingBuiltinCost { position, .. }) => Some(*position), + BuiltinCost::NotMigrating(_) => None, + } + } + + fn has_migrated(&self, feature_set: &FeatureSet) -> bool { + match self { + BuiltinCost::Migrating(MigratingBuiltinCost { + core_bpf_migration_feature, + .. + }) => feature_set.is_active(core_bpf_migration_feature), + BuiltinCost::NotMigrating(_) => false, + } + } } lazy_static! { @@ -34,100 +87,110 @@ lazy_static! { /// 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 = [ + static ref BUILTIN_INSTRUCTION_COSTS: AHashMap = + MIGRATING_BUILTINS_COSTS + .iter() + .chain(NON_MIGRATING_BUILTINS_COSTS.iter()) + .cloned() + .collect(); + // DO NOT ADD MORE ENTRIES TO THIS MAP +} + +/// DEVELOPER WARNING: please do not add new entry into MIGRATING_BUILTINS_COSTS or +/// NON_MIGRATING_BUILTINS_COSTS, do so will modify BUILTIN_INSTRUCTION_COSTS therefore +/// cause consensus failure. However, when a builtin started being migrated to core bpf, +/// it MUST be moved from NON_MIGRATING_BUILTINS_COSTS to MIGRATING_BUILTINS_COSTS, then +/// correctly furnishing `core_bpf_migration_feature`. +/// +#[allow(dead_code)] +const TOTAL_COUNT_BUILTS: usize = 12; +#[cfg(test)] +static_assertions::const_assert_eq!( + MIGRATING_BUILTINS_COSTS.len() + NON_MIGRATING_BUILTINS_COSTS.len(), + TOTAL_COUNT_BUILTS +); + +pub const MIGRATING_BUILTINS_COSTS: &[(Pubkey, BuiltinCost)] = &[ ( solana_stake_program::id(), - BuiltinCost { + BuiltinCost::Migrating(MigratingBuiltinCost { native_cost: solana_stake_program::stake_instruction::DEFAULT_COMPUTE_UNITS, - core_bpf_migration_feature: Some(feature_set::migrate_stake_program_to_core_bpf::id()), - }, + core_bpf_migration_feature: feature_set::migrate_stake_program_to_core_bpf::id(), + position: 0, + }), ), ( solana_config_program::id(), - BuiltinCost { + BuiltinCost::Migrating(MigratingBuiltinCost { native_cost: solana_config_program::config_processor::DEFAULT_COMPUTE_UNITS, - core_bpf_migration_feature: Some(feature_set::migrate_config_program_to_core_bpf::id()), - }, + core_bpf_migration_feature: feature_set::migrate_config_program_to_core_bpf::id(), + position: 1, + }), ), + ( + address_lookup_table::program::id(), + BuiltinCost::Migrating(MigratingBuiltinCost { + native_cost: solana_address_lookup_table_program::processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: + feature_set::migrate_address_lookup_table_program_to_core_bpf::id(), + position: 2, + }), + ), +]; + +pub const NON_MIGRATING_BUILTINS_COSTS: &[(Pubkey, BuiltinCost)] = &[ ( solana_vote_program::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, - core_bpf_migration_feature: None, - }, + }), ), ( solana_system_program::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, - core_bpf_migration_feature: None, - }, + }), ), ( compute_budget::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { 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 { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: solana_bpf_loader_program::UPGRADEABLE_LOADER_COMPUTE_UNITS, - core_bpf_migration_feature: None, - }, + }), ), ( bpf_loader_deprecated::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: solana_bpf_loader_program::DEPRECATED_LOADER_COMPUTE_UNITS, - core_bpf_migration_feature: None, - }, + }), ), ( bpf_loader::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: solana_bpf_loader_program::DEFAULT_LOADER_COMPUTE_UNITS, - core_bpf_migration_feature: None, - }, + }), ), ( loader_v4::id(), - BuiltinCost { + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { 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, - }, + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: 0 }), ), ( ed25519_program::id(), - BuiltinCost { - native_cost: 0, - core_bpf_migration_feature: None, - }, + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: 0 }), ), - // 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 @@ -149,28 +212,84 @@ pub fn get_builtin_instruction_cost<'a>( ) -> 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) + .filter(|builtin_cost| !builtin_cost.has_migrated(feature_set)) + .map(|builtin_cost| builtin_cost.native_cost()) +} + +pub enum BuiltinMigrationFeatureIndex { + NotBuiltin, + BuiltinNoMigrationFeature, + BuiltinWithMigrationFeature(usize), +} + +pub fn get_builtin_migration_feature_index(program_id: &Pubkey) -> BuiltinMigrationFeatureIndex { + BUILTIN_INSTRUCTION_COSTS.get(program_id).map_or( + BuiltinMigrationFeatureIndex::NotBuiltin, + |builtin_cost| { + builtin_cost.position().map_or( + BuiltinMigrationFeatureIndex::BuiltinNoMigrationFeature, + BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature, + ) + }, + ) +} + +/// const function validates `position` correctness at compile time. +#[allow(dead_code)] +const fn validate_position(migrating_builtins: &[(Pubkey, BuiltinCost)]) { + let mut index = 0; + while index < migrating_builtins.len() { + match migrating_builtins[index].1 { + BuiltinCost::Migrating(MigratingBuiltinCost { position, .. }) => assert!( + position == index, + "migration feture must exist and at correct position" + ), + BuiltinCost::NotMigrating(_) => { + panic!("migration feture must exist and at correct position") + } + } + index += 1; + } } +const _: () = validate_position(MIGRATING_BUILTINS_COSTS); -#[inline] -pub fn is_builtin_program(program_id: &Pubkey) -> bool { - BUILTIN_INSTRUCTION_COSTS.contains_key(program_id) +/// Helper function to return ref of migration feature Pubkey at position `index` +/// from MIGRATING_BUILTINS_COSTS +pub fn get_migration_feature_id(index: usize) -> &'static Pubkey { + MIGRATING_BUILTINS_COSTS + .get(index) + .expect("valid index of MIGRATING_BUILTINS_COSTS") + .1 + .core_bpf_migration_feature() + .expect("migrating builtin") +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn get_migration_feature_position(feature_id: &Pubkey) -> usize { + MIGRATING_BUILTINS_COSTS + .iter() + .position(|(_, c)| c.core_bpf_migration_feature().expect("migrating builtin") == feature_id) + .unwrap() } #[cfg(test)] mod test { use super::*; + #[test] + fn test_const_builtin_cost_arrays() { + // sanity check to make sure built-ins are declared in the correct array + assert!(MIGRATING_BUILTINS_COSTS + .iter() + .enumerate() + .all(|(index, (_, c))| { + c.core_bpf_migration_feature().is_some() && c.position() == Some(index) + })); + assert!(NON_MIGRATING_BUILTINS_COSTS + .iter() + .all(|(_, c)| c.core_bpf_migration_feature().is_none())); + } + #[test] fn test_get_builtin_instruction_cost() { // use native cost if no migration planned @@ -201,4 +320,65 @@ mod test { .is_none() ); } + + #[test] + fn test_get_builtin_migration_feature_index() { + assert!(matches!( + get_builtin_migration_feature_index(&Pubkey::new_unique()), + BuiltinMigrationFeatureIndex::NotBuiltin + )); + assert!(matches!( + get_builtin_migration_feature_index(&compute_budget::id()), + BuiltinMigrationFeatureIndex::BuiltinNoMigrationFeature, + )); + let feature_index = get_builtin_migration_feature_index(&solana_stake_program::id()); + assert!(matches!( + feature_index, + BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(_) + )); + let BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(feature_index) = + feature_index + else { + panic!("expect migrating builtin") + }; + assert_eq!( + get_migration_feature_id(feature_index), + &feature_set::migrate_stake_program_to_core_bpf::id() + ); + let feature_index = get_builtin_migration_feature_index(&solana_config_program::id()); + assert!(matches!( + feature_index, + BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(_) + )); + let BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(feature_index) = + feature_index + else { + panic!("expect migrating builtin") + }; + assert_eq!( + get_migration_feature_id(feature_index), + &feature_set::migrate_config_program_to_core_bpf::id() + ); + let feature_index = + get_builtin_migration_feature_index(&address_lookup_table::program::id()); + assert!(matches!( + feature_index, + BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(_) + )); + let BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature(feature_index) = + feature_index + else { + panic!("expect migrating builtin") + }; + assert_eq!( + get_migration_feature_id(feature_index), + &feature_set::migrate_address_lookup_table_program_to_core_bpf::id() + ); + } + + #[test] + #[should_panic(expected = "valid index of MIGRATING_BUILTINS_COSTS")] + fn test_get_migration_feature_id_invalid_index() { + let _ = get_migration_feature_id(MIGRATING_BUILTINS_COSTS.len() + 1); + } } diff --git a/runtime-transaction/Cargo.toml b/runtime-transaction/Cargo.toml index 9ccb325593da89..c7a1eac29c06ef 100644 --- a/runtime-transaction/Cargo.toml +++ b/runtime-transaction/Cargo.toml @@ -27,11 +27,15 @@ name = "solana_runtime_transaction" bincode = { workspace = true } criterion = { workspace = true } rand = { workspace = true } +solana-builtins-default-costs = { workspace = true, features = ["dev-context-only-utils"] } solana-program = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +[features] +dev-context-only-utils = ["solana-builtins-default-costs/dev-context-only-utils"] + [[bench]] name = "process_compute_budget_instructions" harness = false diff --git a/runtime-transaction/src/builtin_programs_filter.rs b/runtime-transaction/src/builtin_programs_filter.rs index fc935a023cfbf8..705f155e15f8a3 100644 --- a/runtime-transaction/src/builtin_programs_filter.rs +++ b/runtime-transaction/src/builtin_programs_filter.rs @@ -1,6 +1,8 @@ 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_builtins_default_costs::{ + get_builtin_migration_feature_index, BuiltinMigrationFeatureIndex, MAYBE_BUILTIN_KEY, + }, solana_sdk::pubkey::Pubkey, }; @@ -8,6 +10,12 @@ use { pub(crate) enum ProgramKind { NotBuiltin, Builtin, + // Builtin program maybe in process of being migrated to core bpf, + // if core_bpf_migration_feature is activated, then the migration has + // completed and it should no longer be considered as builtin + MigratingBuiltin { + core_bpf_migration_feature_index: usize, + }, } pub(crate) struct BuiltinProgramsFilter { @@ -38,17 +46,24 @@ impl BuiltinProgramsFilter { return ProgramKind::NotBuiltin; } - if is_builtin_program(program_id) { - ProgramKind::Builtin - } else { - ProgramKind::NotBuiltin + match get_builtin_migration_feature_index(program_id) { + BuiltinMigrationFeatureIndex::NotBuiltin => ProgramKind::NotBuiltin, + BuiltinMigrationFeatureIndex::BuiltinNoMigrationFeature => ProgramKind::Builtin, + BuiltinMigrationFeatureIndex::BuiltinWithMigrationFeature( + core_bpf_migration_feature_index, + ) => ProgramKind::MigratingBuiltin { + core_bpf_migration_feature_index, + }, } } } #[cfg(test)] mod test { - use super::*; + use { + super::*, solana_builtins_default_costs::get_migration_feature_position, + solana_sdk::feature_set, + }; const DUMMY_PROGRAM_ID: &str = "dummmy1111111111111111111111111111111111111"; @@ -90,6 +105,32 @@ mod test { test_store.get_program_kind(index, &solana_sdk::compute_budget::id()), ProgramKind::Builtin, ); + + // migrating builtins + for (migrating_builtin_pubkey, migration_feature_id) in [ + ( + solana_sdk::stake::program::id(), + feature_set::migrate_stake_program_to_core_bpf::id(), + ), + ( + solana_sdk::config::program::id(), + feature_set::migrate_config_program_to_core_bpf::id(), + ), + ( + solana_sdk::address_lookup_table::program::id(), + feature_set::migrate_address_lookup_table_program_to_core_bpf::id(), + ), + ] { + index += 1; + assert_eq!( + test_store.get_program_kind(index, &migrating_builtin_pubkey), + ProgramKind::MigratingBuiltin { + core_bpf_migration_feature_index: get_migration_feature_position( + &migration_feature_id + ), + } + ); + } } #[test] diff --git a/runtime-transaction/src/compute_budget_instruction_details.rs b/runtime-transaction/src/compute_budget_instruction_details.rs index aedc55b07d58e5..53ea2ea5c125dd 100644 --- a/runtime-transaction/src/compute_budget_instruction_details.rs +++ b/runtime-transaction/src/compute_budget_instruction_details.rs @@ -3,6 +3,7 @@ use { builtin_programs_filter::{BuiltinProgramsFilter, ProgramKind}, compute_budget_program_id_filter::ComputeBudgetProgramIdFilter, }, + solana_builtins_default_costs::{get_migration_feature_id, MIGRATING_BUILTINS_COSTS}, solana_compute_budget::compute_budget_limits::*, solana_sdk::{ borsh1::try_from_slice_unchecked, @@ -18,6 +19,25 @@ use { }; #[cfg_attr(test, derive(Eq, PartialEq))] +#[cfg_attr(feature = "dev-context-only-utils", derive(Clone))] +#[derive(Debug)] +struct MigrationBuiltinFeatureCounter { + // The vector of counters, matching the size of the static vector MIGRATION_FEATURE_IDS, + // each counter representing the number of times its corresponding feature ID is + // referenced in this transaction. + migrating_builtin: [u16; MIGRATING_BUILTINS_COSTS.len()], +} + +impl Default for MigrationBuiltinFeatureCounter { + fn default() -> Self { + Self { + migrating_builtin: [0; MIGRATING_BUILTINS_COSTS.len()], + } + } +} + +#[cfg_attr(test, derive(Eq, PartialEq))] +#[cfg_attr(feature = "dev-context-only-utils", derive(Clone))] #[derive(Default, Debug)] pub(crate) struct ComputeBudgetInstructionDetails { // compute-budget instruction details: @@ -28,8 +48,9 @@ pub(crate) struct ComputeBudgetInstructionDetails { 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_migratable_builtin_instructions: u16, num_non_builtin_instructions: u16, + migrating_builtin_feature_counters: MigrationBuiltinFeatureCounter, } impl ComputeBudgetInstructionDetails { @@ -60,7 +81,8 @@ impl ComputeBudgetInstructionDetails { 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, + compute_budget_instruction_details + .num_non_migratable_builtin_instructions, 1 ); } @@ -70,6 +92,20 @@ impl ComputeBudgetInstructionDetails { 1 ); } + ProgramKind::MigratingBuiltin { + core_bpf_migration_feature_index, + } => { + saturating_add_assign!( + *compute_budget_instruction_details + .migrating_builtin_feature_counters + .migrating_builtin + .get_mut(core_bpf_migration_feature_index) + .expect( + "migrating feature index within range of MIGRATION_FEATURE_IDS" + ), + 1 + ); + } } } } @@ -174,10 +210,26 @@ impl ComputeBudgetInstructionDetails { 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) + // evaluate if any builtin has migrated with feature_set + let (num_migrated, num_not_migrated) = self + .migrating_builtin_feature_counters + .migrating_builtin + .iter() + .enumerate() + .fold((0, 0), |(migrated, not_migrated), (index, count)| { + if *count > 0 && feature_set.is_active(get_migration_feature_id(index)) { + (migrated + count, not_migrated) + } else { + (migrated, not_migrated + count) + } + }); + + u32::from(self.num_non_migratable_builtin_instructions) + .saturating_add(u32::from(num_not_migrated)) .saturating_mul(MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT) .saturating_add( u32::from(self.num_non_builtin_instructions) + .saturating_add(u32::from(num_migrated)) .saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), ) } else { @@ -191,6 +243,7 @@ impl ComputeBudgetInstructionDetails { mod test { use { super::*, + solana_builtins_default_costs::get_migration_feature_position, solana_sdk::{ instruction::Instruction, message::Message, @@ -220,7 +273,7 @@ mod test { let expected_details = Ok(ComputeBudgetInstructionDetails { requested_heap_size: Some((1, 40 * 1024)), num_non_compute_budget_instructions: 2, - num_builtin_instructions: 1, + num_non_migratable_builtin_instructions: 1, num_non_builtin_instructions: 2, ..ComputeBudgetInstructionDetails::default() }); @@ -278,7 +331,7 @@ mod test { 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_migratable_builtin_instructions: 1, num_non_builtin_instructions: 2, ..ComputeBudgetInstructionDetails::default() }); @@ -308,7 +361,7 @@ mod test { 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_migratable_builtin_instructions: 1, num_non_builtin_instructions: 2, ..ComputeBudgetInstructionDetails::default() }); @@ -335,7 +388,7 @@ mod test { let mut feature_set = FeatureSet::default(); let ComputeBudgetInstructionDetails { num_non_compute_budget_instructions, - num_builtin_instructions, + num_non_migratable_builtin_instructions, num_non_builtin_instructions, .. } = *instruction_details; @@ -345,7 +398,8 @@ mod test { 0, ); u32::from(num_non_builtin_instructions) * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT - + u32::from(num_builtin_instructions) * MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT + + u32::from(num_non_migratable_builtin_instructions) + * MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT } else { u32::from(num_non_compute_budget_instructions) * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT }; @@ -369,7 +423,7 @@ mod test { // 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_migratable_builtin_instructions: 1, num_non_builtin_instructions: 3, ..ComputeBudgetInstructionDetails::default() }; @@ -520,4 +574,87 @@ mod test { ); } } + + #[test] + fn test_builtin_program_migration() { + let tx = build_sanitized_transaction(&[ + Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![]), + solana_sdk::stake::instruction::delegate_stake( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + ), + ]); + let feature_id_index = + get_migration_feature_position(&feature_set::migrate_stake_program_to_core_bpf::id()); + let mut expected_details = ComputeBudgetInstructionDetails { + num_non_compute_budget_instructions: 2, + num_non_builtin_instructions: 1, + ..ComputeBudgetInstructionDetails::default() + }; + expected_details + .migrating_builtin_feature_counters + .migrating_builtin[feature_id_index] = 1; + let expected_details = Ok(expected_details); + let details = + ComputeBudgetInstructionDetails::try_from(SVMMessage::program_instructions_iter(&tx)); + assert_eq!(details, expected_details); + let details = details.unwrap(); + + // reserve_minimal_cus_for_builtin_instructions: false; + // migrate_stake_program_to_core_bpf: false; + // expect: 1 bpf ix, 1 non-compute-budget builtin, cu-limit = 2 * 200K + let mut feature_set = FeatureSet::default(); + let cu_limits = details.sanitize_and_convert_to_compute_budget_limits(&feature_set); + assert_eq!( + cu_limits, + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT * 2, + ..ComputeBudgetLimits::default() + }) + ); + + // reserve_minimal_cus_for_builtin_instructions: true; + // migrate_stake_program_to_core_bpf: false; + // expect: 1 bpf ix, 1 non-compute-budget builtin, cu-limit = 200K + 3K + feature_set.activate( + &feature_set::reserve_minimal_cus_for_builtin_instructions::id(), + 0, + ); + let cu_limits = details.sanitize_and_convert_to_compute_budget_limits(&feature_set); + assert_eq!( + cu_limits, + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT + + MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT, + ..ComputeBudgetLimits::default() + }) + ); + + // reserve_minimal_cus_for_builtin_instructions: true; + // migrate_stake_program_to_core_bpf: true; + // expect: 2 bpf ix, cu-limit = 2 * 200K + feature_set.activate(&feature_set::migrate_stake_program_to_core_bpf::id(), 0); + let cu_limits = details.sanitize_and_convert_to_compute_budget_limits(&feature_set); + assert_eq!( + cu_limits, + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT * 2, + ..ComputeBudgetLimits::default() + }) + ); + + // reserve_minimal_cus_for_builtin_instructions: false; + // migrate_stake_program_to_core_bpf: false; + // expect: 1 bpf ix, 1 non-compute-budget builtin, cu-limit = 2 * 200K + feature_set.deactivate(&feature_set::reserve_minimal_cus_for_builtin_instructions::id()); + let cu_limits = details.sanitize_and_convert_to_compute_budget_limits(&feature_set); + assert_eq!( + cu_limits, + Ok(ComputeBudgetLimits { + compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT * 2, + ..ComputeBudgetLimits::default() + }) + ); + } }