Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.0: Fix reserve minimal compute units for builtins (backport of #3799) #3930

Open
wants to merge 1 commit into
base: v2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions builtins-default-costs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Pubkey>,
}

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<Pubkey, BuiltinCost> = [
(
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<u64> {
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()
);
}
}
106 changes: 106 additions & 0 deletions compute-budget/src/compute_budget_limits.rs
Original file line number Diff line number Diff line change
@@ -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<ComputeBudgetLimits> 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);
}
}
Loading
Loading