From fd66b0f7f07e8f00f678e44c19325d61b09811ca Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+tao-stones@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:55:02 -0500 Subject: [PATCH] Accounting migrating builtin programs default Compute Unit Limit with feature status (#3975) * Accounting migrating builtin programs default Compute Unit Limit with its feature gate status * Declare Non/migrating buiiltins in const array, eleminates heap allocation (Vec<>) per transaction * updates for review commients add explicit positional information to migrating builtin feature obj update developer notes, added static_assertion to validate no new items are added, add enum type * use enum to separately define migrating and not-migrating builtins * rename for clarity (cherry picked from commit 9379fbcba4808df8ad4bc8cc5cf058f4bad069fa) --- Cargo.lock | 1 + builtins-default-costs/Cargo.toml | 2 + builtins-default-costs/src/lib.rs | 360 ++++++++++++++++-- compute-budget-instruction/Cargo.toml | 43 +++ .../src/builtin_programs_filter.rs | 148 +++++++ .../src/compute_budget_instruction_details.rs | 155 +++++++- 6 files changed, 677 insertions(+), 32 deletions(-) create mode 100644 compute-budget-instruction/Cargo.toml create mode 100644 compute-budget-instruction/src/builtin_programs_filter.rs diff --git a/Cargo.lock b/Cargo.lock index 58e5938ed0f090..45532002cc8f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6131,6 +6131,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 a484a5100cc26a..d6606439af7688 100644 --- a/builtins-default-costs/src/lib.rs +++ b/builtins-default-costs/src/lib.rs @@ -9,7 +9,70 @@ use { }, }; -// Number of compute units for each built-in programs +#[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, 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 MIGRATING_BUILTINS_COSTS. +#[derive(Clone)] +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! { /// Number of compute units for each built-in programs /// @@ -20,27 +83,111 @@ 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. - pub static ref BUILTIN_INSTRUCTION_COSTS: AHashMap = [ - (solana_stake_program::id(), solana_stake_program::stake_instruction::DEFAULT_COMPUTE_UNITS), - (solana_config_program::id(), solana_config_program::config_processor::DEFAULT_COMPUTE_UNITS), - (solana_vote_program::id(), solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS), - (solana_system_program::id(), solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS), - (compute_budget::id(), solana_compute_budget_program::DEFAULT_COMPUTE_UNITS), - (address_lookup_table::program::id(), solana_address_lookup_table_program::processor::DEFAULT_COMPUTE_UNITS), - (bpf_loader_upgradeable::id(), solana_bpf_loader_program::UPGRADEABLE_LOADER_COMPUTE_UNITS), - (bpf_loader_deprecated::id(), solana_bpf_loader_program::DEPRECATED_LOADER_COMPUTE_UNITS), - (bpf_loader::id(), solana_bpf_loader_program::DEFAULT_LOADER_COMPUTE_UNITS), - (loader_v4::id(), solana_loader_v4_program::DEFAULT_COMPUTE_UNITS), - // Note: These are precompile, run directly in bank during sanitizing; - (secp256k1_program::id(), 0), - (ed25519_program::id(), 0), - // DO NOT ADD MORE ENTRIES TO THIS MAP - ] - .iter() - .cloned() - .collect(); + 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)] = &[ + ( + stake::id(), + BuiltinCost::Migrating(MigratingBuiltinCost { + native_cost: solana_stake_program::stake_instruction::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: feature_set::migrate_stake_program_to_core_bpf::id(), + position: 0, + }), + ), + ( + config::id(), + BuiltinCost::Migrating(MigratingBuiltinCost { + native_cost: solana_config_program::config_processor::DEFAULT_COMPUTE_UNITS, + core_bpf_migration_feature: feature_set::migrate_config_program_to_core_bpf::id(), + position: 1, + }), + ), + ( + address_lookup_table::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)] = &[ + ( + vote::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, + }), + ), + ( + system_program::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS, + }), + ), + ( + compute_budget::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_compute_budget_program::DEFAULT_COMPUTE_UNITS, + }), + ), + ( + bpf_loader_upgradeable::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_bpf_loader_program::UPGRADEABLE_LOADER_COMPUTE_UNITS, + }), + ), + ( + bpf_loader_deprecated::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_bpf_loader_program::DEPRECATED_LOADER_COMPUTE_UNITS, + }), + ), + ( + bpf_loader::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_bpf_loader_program::DEFAULT_LOADER_COMPUTE_UNITS, + }), + ), + ( + loader_v4::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { + native_cost: solana_loader_v4_program::DEFAULT_COMPUTE_UNITS, + }), + ), + // Note: These are precompile, run directly in bank during sanitizing; + ( + secp256k1_program::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: 0 }), + ), + ( + ed25519_program::id(), + BuiltinCost::NotMigrating(NotMigratingBuiltinCost { native_cost: 0 }), + ), +]; + 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; @@ -55,7 +202,174 @@ lazy_static! { }; } -#[inline] -pub fn is_builtin_program(program_id: &Pubkey) -> bool { - BUILTIN_INSTRUCTION_COSTS.contains_key(program_id) +pub fn get_builtin_instruction_cost<'a>( + program_id: &'a Pubkey, + feature_set: &'a FeatureSet, +) -> Option { + BUILTIN_INSTRUCTION_COSTS + .get(program_id) + .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); + +/// 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 + 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(&stake::id(), &FeatureSet::default()) + ); + + // None if migration is planned and activated, in which case, it's no longer builtin + assert!(get_builtin_instruction_cost(&stake::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() + ); + } + + #[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(&stake::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(&config::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::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/compute-budget-instruction/Cargo.toml b/compute-budget-instruction/Cargo.toml new file mode 100644 index 00000000000000..8e5bdd0b1c9bf7 --- /dev/null +++ b/compute-budget-instruction/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "solana-compute-budget-instruction" +description = "Solana Compute Budget Instruction" +documentation = "https://docs.rs/solana-compute-budget-instruction" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +log = { workspace = true } +solana-builtins-default-costs = { workspace = true } +solana-compute-budget = { workspace = true } +solana-pubkey = { workspace = true } +solana-sdk = { workspace = true } +solana-svm-transaction = { workspace = true } +thiserror = { workspace = true } + +[lib] +crate-type = ["lib"] +name = "solana_compute_budget_instruction" + +[dev-dependencies] +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 = [] + +[[bench]] +name = "process_compute_budget_instructions" +harness = false + +[lints] +workspace = true diff --git a/compute-budget-instruction/src/builtin_programs_filter.rs b/compute-budget-instruction/src/builtin_programs_filter.rs new file mode 100644 index 00000000000000..1525dd1f2cfc61 --- /dev/null +++ b/compute-budget-instruction/src/builtin_programs_filter.rs @@ -0,0 +1,148 @@ +use { + solana_builtins_default_costs::{ + get_builtin_migration_feature_index, BuiltinMigrationFeatureIndex, MAYBE_BUILTIN_KEY, + }, + solana_sdk::{packet::PACKET_DATA_SIZE, pubkey::Pubkey}, +}; + +// The maximum number of pubkeys that a packet can contain. +pub const FILTER_SIZE: u8 = (PACKET_DATA_SIZE / core::mem::size_of::()) as u8; + +#[derive(Clone, Copy, Debug, PartialEq)] +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 { + // 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; + } + + 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::*, solana_builtins_default_costs::get_migration_feature_position, + solana_sdk::feature_set, + }; + + 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, + ); + + // 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] + #[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 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() + }) + ); + } }