use std::sync::{Arc, OnceLock};
use agave_feature_set::{
FeatureSet, deprecate_rent_exemption_threshold, increase_tx_account_lock_limit,
raise_cpi_nesting_limit_to_8, replace_spl_token_with_p_token,
};
use solana_address::Address;
use solana_compute_budget::compute_budget::ComputeBudget;
use solana_fee_structure::FeeStructure;
use solana_rent::Rent;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum ClusterSpecId {
MainnetBeta = 0,
Testnet = 1,
Devnet = 2,
Next = 3,
}
impl ClusterSpecId {
pub const ALL: [Self; 4] = [Self::MainnetBeta, Self::Testnet, Self::Devnet, Self::Next];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::MainnetBeta => "mainnet-beta",
Self::Testnet => "testnet",
Self::Devnet => "devnet",
Self::Next => "next",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimeLimits {
pub log_bytes_limit: Option<usize>,
}
impl Default for RuntimeLimits {
fn default() -> Self {
Self { log_bytes_limit: Some(10_000) }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClusterInfo {
pub spec_id: ClusterSpecId,
pub name: &'static str,
}
use crate::programs::JitPolicy;
#[derive(Debug, Clone)]
pub struct Version {
pub feature_set: Arc<FeatureSet>,
pub fee_structure: FeeStructure,
pub compute_budget: Option<ComputeBudget>,
pub rent: Rent,
pub limits: RuntimeLimits,
pub cluster: ClusterInfo,
pub sigverify: bool,
pub blockhash_check: bool,
pub compute_diagnostics: bool,
pub jit_policy: JitPolicy,
}
static BASE_VERSIONS: OnceLock<[Version; 4]> = OnceLock::new();
fn build_base_table() -> [Version; 4] {
ClusterSpecId::ALL.map(|spec_id| Version {
feature_set: Arc::new(FeatureSet::all_enabled()),
fee_structure: FeeStructure::default(),
compute_budget: None,
rent: Rent::default(),
limits: RuntimeLimits::default(),
cluster: ClusterInfo { spec_id, name: spec_id.name() },
sigverify: true,
blockhash_check: true,
compute_diagnostics: true,
jit_policy: JitPolicy::DEFAULT,
})
}
impl Version {
#[must_use]
pub fn base(spec_id: ClusterSpecId) -> &'static Self {
&BASE_VERSIONS.get_or_init(build_base_table)[spec_id as usize]
}
#[must_use]
pub const fn with_jit_policy(mut self, policy: JitPolicy) -> Self {
self.jit_policy = policy;
self
}
#[must_use]
pub fn with_feature(mut self, id: &Address, enabled: bool) -> Self {
let mut feature_set = (*self.feature_set).clone();
if enabled {
feature_set.activate(id, 0);
} else {
feature_set.deactivate(id);
}
self.feature_set = Arc::new(feature_set);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FeatureGate {
RaiseCpiNestingLimitTo8,
DeprecateRentExemptionThreshold,
IncreaseTxAccountLockLimit,
ReplaceSplTokenWithPToken,
}
impl FeatureGate {
pub const ALL: &'static [Self] = &[
Self::RaiseCpiNestingLimitTo8,
Self::DeprecateRentExemptionThreshold,
Self::IncreaseTxAccountLockLimit,
Self::ReplaceSplTokenWithPToken,
];
#[must_use]
pub const fn id(self) -> Address {
match self {
Self::RaiseCpiNestingLimitTo8 => raise_cpi_nesting_limit_to_8::ID,
Self::DeprecateRentExemptionThreshold => deprecate_rent_exemption_threshold::ID,
Self::IncreaseTxAccountLockLimit => increase_tx_account_lock_limit::ID,
Self::ReplaceSplTokenWithPToken => replace_spl_token_with_p_token::ID,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TxFeatureFlags {
pub raise_cpi_nesting_limit_to_8: bool,
pub deprecate_rent_exemption_threshold: bool,
pub increase_tx_account_lock_limit: bool,
pub replace_spl_token_with_p_token: bool,
}
#[cfg(test)]
std::thread_local! {
static TX_FEATURE_FLAGS_LOADS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
impl TxFeatureFlags {
#[must_use]
pub fn load(feature_set: &FeatureSet) -> Self {
#[cfg(test)]
TX_FEATURE_FLAGS_LOADS.with(|count| count.set(count.get() + 1));
Self {
raise_cpi_nesting_limit_to_8: feature_set.is_active(&raise_cpi_nesting_limit_to_8::ID),
deprecate_rent_exemption_threshold: feature_set
.is_active(&deprecate_rent_exemption_threshold::ID),
increase_tx_account_lock_limit: feature_set
.is_active(&increase_tx_account_lock_limit::ID),
replace_spl_token_with_p_token: feature_set
.is_active(&replace_spl_token_with_p_token::ID),
}
}
#[must_use]
pub const fn is_enabled(&self, gate: FeatureGate) -> bool {
match gate {
FeatureGate::RaiseCpiNestingLimitTo8 => self.raise_cpi_nesting_limit_to_8,
FeatureGate::DeprecateRentExemptionThreshold => self.deprecate_rent_exemption_threshold,
FeatureGate::IncreaseTxAccountLockLimit => self.increase_tx_account_lock_limit,
FeatureGate::ReplaceSplTokenWithPToken => self.replace_spl_token_with_p_token,
}
}
#[cfg(test)]
pub(crate) fn load_count() -> usize {
TX_FEATURE_FLAGS_LOADS.with(|count| count.get())
}
#[cfg(test)]
pub(crate) fn reset_load_count() {
TX_FEATURE_FLAGS_LOADS.with(|count| count.set(0));
}
}
#[cfg(test)]
mod tests {
use std::ptr;
use agave_feature_set::{FeatureSet, raise_cpi_nesting_limit_to_8};
use solana_fee_structure::{FeeBin, FeeStructure};
use solana_rent::{DEFAULT_LAMPORTS_PER_BYTE, Rent};
use super::*;
use crate::SvmTypesHost;
fn takes_static(_: &'static Version) {}
#[test]
fn cluster_spec_id_is_a_stable_u32() {
assert_eq!(size_of::<ClusterSpecId>(), 4);
assert_eq!(ClusterSpecId::MainnetBeta as u32, 0);
assert_eq!(ClusterSpecId::Testnet as u32, 1);
assert_eq!(ClusterSpecId::Devnet as u32, 2);
assert_eq!(ClusterSpecId::Next as u32, 3);
assert_eq!(ClusterSpecId::ALL.len(), 4);
}
#[test]
fn cluster_names_are_distinct_and_match_spec() {
for spec_id in ClusterSpecId::ALL {
assert_eq!(Version::base(spec_id).cluster.spec_id, spec_id);
assert_eq!(Version::base(spec_id).cluster.name, spec_id.name());
}
let names: Vec<&str> = ClusterSpecId::ALL.iter().map(|id| id.name()).collect();
let mut deduped = names.clone();
deduped.sort_unstable();
deduped.dedup();
assert_eq!(deduped.len(), names.len(), "cluster names must be distinct");
}
#[test]
fn base_reproduces_exact_fee_values_for_every_profile() {
let expected = FeeStructure {
lamports_per_signature: 5_000,
lamports_per_write_lock: 0,
compute_fee_bins: vec![FeeBin { limit: 1_400_000, fee: 0 }],
};
assert_eq!(FeeStructure::default(), expected);
for spec_id in ClusterSpecId::ALL {
let version = Version::base(spec_id);
takes_static(version);
assert_eq!(version.fee_structure, expected);
assert_eq!(version.fee_structure.lamports_per_signature, 5_000);
}
}
#[test]
fn base_reproduces_exact_rent_values_for_every_profile() {
let expected = Rent::default();
assert_eq!(expected.lamports_per_byte, DEFAULT_LAMPORTS_PER_BYTE);
assert_eq!(expected.lamports_per_byte, 6_960);
for data_len in [0usize, 82, 165, 355] {
let expected_balance = expected.minimum_balance(data_len);
for spec_id in ClusterSpecId::ALL {
let version = Version::base(spec_id);
assert_eq!(version.rent, expected);
assert_eq!(version.rent.minimum_balance(data_len), expected_balance);
}
}
}
#[test]
fn base_enables_every_feature_for_every_profile() {
let all_enabled = FeatureSet::all_enabled();
assert!(!all_enabled.active().is_empty());
for spec_id in ClusterSpecId::ALL {
let version = Version::base(spec_id);
assert_eq!(version.feature_set.as_ref(), &all_enabled);
}
}
#[test]
fn base_carries_current_runtime_defaults_for_every_profile() {
for spec_id in ClusterSpecId::ALL {
let version = Version::base(spec_id);
assert_eq!(version.compute_budget, None);
assert_eq!(version.limits.log_bytes_limit, Some(10_000));
assert!(version.sigverify);
assert!(version.blockhash_check);
assert!(version.compute_diagnostics);
assert_eq!(version.jit_policy, crate::JitPolicy::Eager);
}
}
#[test]
fn base_returns_the_same_pointer_on_repeat_calls() {
for spec_id in ClusterSpecId::ALL {
assert!(ptr::eq(Version::base(spec_id), Version::base(spec_id)));
}
}
#[test]
fn with_feature_flips_exactly_one_gate_in_both_directions() {
let probe = raise_cpi_nesting_limit_to_8::ID;
let control = agave_feature_set::increase_tx_account_lock_limit::id();
assert!(Version::base(ClusterSpecId::MainnetBeta).feature_set.is_active(&probe));
assert!(Version::base(ClusterSpecId::MainnetBeta).feature_set.is_active(&control));
let disabled =
Version::base(ClusterSpecId::MainnetBeta).clone().with_feature(&probe, false);
assert!(!disabled.feature_set.is_active(&probe));
assert!(disabled.feature_set.is_active(&control));
assert!(Version::base(ClusterSpecId::MainnetBeta).feature_set.is_active(&probe));
let re_enabled = disabled.with_feature(&probe, true);
assert!(re_enabled.feature_set.is_active(&probe));
assert!(re_enabled.feature_set.is_active(&control));
assert_eq!(re_enabled.feature_set.as_ref(), &FeatureSet::all_enabled());
}
#[test]
fn with_feature_accepts_agave_feature_ids_directly() {
let id: Address = agave_feature_set::deprecate_rent_exemption_threshold::id();
let version = Version::base(ClusterSpecId::Testnet).clone().with_feature(&id, false);
assert!(!version.feature_set.is_active(&id));
}
#[test]
fn cluster_spec_id_satisfies_the_deferred_spec_id_bound() {
fn takes_spec_id<T: SvmTypesHost<SpecId = ClusterSpecId>>() {}
takes_spec_id::<crate::BaseSvmTypes>();
fn into_spec_id<T: Into<ClusterSpecId>>(id: T) -> ClusterSpecId {
id.into()
}
assert_eq!(into_spec_id(ClusterSpecId::Devnet), ClusterSpecId::Devnet);
}
use super::{FeatureGate, TxFeatureFlags};
#[test]
fn feature_gate_all_covers_every_hoisted_flag() {
assert_eq!(FeatureGate::ALL.len(), 4, "one entry per TxFeatureFlags field");
for gate in FeatureGate::ALL {
let _ = *gate;
}
}
#[test]
fn tx_feature_flags_match_feature_set_for_every_gate() {
for gate in FeatureGate::ALL {
let all_on = FeatureSet::all_enabled();
assert_eq!(
TxFeatureFlags::load(&all_on).is_enabled(*gate),
all_on.is_active(&gate.id())
);
assert!(TxFeatureFlags::load(&all_on).is_enabled(*gate));
let all_off = FeatureSet::default();
assert_eq!(
TxFeatureFlags::load(&all_off).is_enabled(*gate),
all_off.is_active(&gate.id())
);
assert!(!TxFeatureFlags::load(&all_off).is_enabled(*gate));
let single_off =
Version::base(ClusterSpecId::MainnetBeta).clone().with_feature(&gate.id(), false);
assert_eq!(
TxFeatureFlags::load(&single_off.feature_set).is_enabled(*gate),
single_off.feature_set.is_active(&gate.id())
);
assert!(!TxFeatureFlags::load(&single_off.feature_set).is_enabled(*gate));
for other in FeatureGate::ALL {
if *other != *gate {
assert!(TxFeatureFlags::load(&single_off.feature_set).is_enabled(*other));
}
}
let mut single_on = FeatureSet::default();
single_on.activate(&gate.id(), 0);
assert!(TxFeatureFlags::load(&single_on).is_enabled(*gate));
for other in FeatureGate::ALL {
if *other != *gate {
assert!(!TxFeatureFlags::load(&single_on).is_enabled(*other));
}
}
}
}
#[test]
fn tx_feature_flags_load_once_per_transaction_regardless_of_size() {
use solana_keypair::Keypair;
use solana_message::Message;
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::Transaction;
let mut svm = crate::Hpsvm::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000_000).expect("setup airdrop should succeed");
let blockhash = svm.latest_blockhash();
TxFeatureFlags::reset_load_count();
let recipient = Address::new_unique();
let one_ix = Transaction::new(
&[&payer],
Message::new(&[transfer(&payer.pubkey(), &recipient, 42)], Some(&payer.pubkey())),
blockhash,
);
svm.send_transaction(one_ix).expect("1-instruction transfer should succeed");
assert_eq!(TxFeatureFlags::load_count(), 1, "exactly one flag load per transaction");
TxFeatureFlags::reset_load_count();
let five_ixs: Vec<_> =
(0..5).map(|_| transfer(&payer.pubkey(), &Address::new_unique(), 42)).collect();
let five_ix =
Transaction::new(&[&payer], Message::new(&five_ixs, Some(&payer.pubkey())), blockhash);
svm.send_transaction(five_ix).expect("5-instruction transfer should succeed");
assert_eq!(
TxFeatureFlags::load_count(),
1,
"flag loads must not grow with instruction count"
);
}
}