Skip to main content

surfpool_types/
features.rs

1use std::str::FromStr;
2
3use agave_feature_set::FEATURE_NAMES;
4use serde::{Deserialize, Serialize};
5use solana_pubkey::Pubkey;
6
7/// Returns the pubkey for a known feature name (kebab-case or snake_case).
8///
9/// This covers the set of features previously exposed as CLI names.
10fn lookup_feature_by_name(name: &str) -> Option<Pubkey> {
11    use agave_feature_set::*;
12
13    // Normalize: accept both kebab-case and snake_case by converting underscores to hyphens
14    let normalized = name.replace('_', "-");
15    let s = normalized.as_str();
16
17    let pubkey = match s {
18        "move-precompile-verification-to-svm" => move_precompile_verification_to_svm::id(),
19        "enable-bpf-loader-set-authority-checked-ix" => {
20            enable_bpf_loader_set_authority_checked_ix::id()
21        }
22        "enable-loader-v4" => enable_loader_v4::id(),
23        "deplete-cu-meter-on-vm-failure" => deplete_cu_meter_on_vm_failure::id(),
24        "abort-on-invalid-curve" => abort_on_invalid_curve::id(),
25        "blake3-syscall-enabled" => blake3_syscall_enabled::id(),
26        "curve25519-syscall-enabled" => curve25519_syscall_enabled::id(),
27        "disable-deploy-of-alloc-free-syscall" => disable_deploy_of_alloc_free_syscall::id(),
28        "disable-fees-sysvar" => disable_fees_sysvar::id(),
29        "disable-sbpf-v0-execution" => disable_sbpf_v0_execution::id(),
30        "enable-alt-bn128-compression-syscall" => enable_alt_bn128_compression_syscall::id(),
31        "enable-alt-bn128-syscall" => enable_alt_bn128_syscall::id(),
32        "enable-big-mod-exp-syscall" => enable_big_mod_exp_syscall::id(),
33        "enable-get-epoch-stake-syscall" => enable_get_epoch_stake_syscall::id(),
34        "enable-poseidon-syscall" => enable_poseidon_syscall::id(),
35        "enable-sbpf-v1-deployment-and-execution" => enable_sbpf_v1_deployment_and_execution::id(),
36        "enable-sbpf-v2-deployment-and-execution" => enable_sbpf_v2_deployment_and_execution::id(),
37        "enable-sbpf-v3-deployment-and-execution" => enable_sbpf_v3_deployment_and_execution::id(),
38        "get-sysvar-syscall-enabled" => get_sysvar_syscall_enabled::id(),
39        "last-restart-slot-sysvar" => last_restart_slot_sysvar::id(),
40        "reenable-sbpf-v0-execution" => reenable_sbpf_v0_execution::id(),
41        "remaining-compute-units-syscall-enabled" => remaining_compute_units_syscall_enabled::id(),
42        "remove-bpf-loader-incorrect-program-id" => remove_bpf_loader_incorrect_program_id::id(),
43        "move-stake-and-move-lamports-ixs" => move_stake_and_move_lamports_ixs::id(),
44        "stake-raise-minimum-delegation-to-1-sol" => stake_raise_minimum_delegation_to_1_sol::id(),
45        "deprecate-legacy-vote-ixs" => deprecate_legacy_vote_ixs::id(),
46        "mask-out-rent-epoch-in-vm-serialization" => mask_out_rent_epoch_in_vm_serialization::id(),
47        "simplify-alt-bn128-syscall-error-codes" => simplify_alt_bn128_syscall_error_codes::id(),
48        "fix-alt-bn128-multiplication-input-length" => {
49            fix_alt_bn128_multiplication_input_length::id()
50        }
51        "increase-tx-account-lock-limit" => increase_tx_account_lock_limit::id(),
52        "enable-extend-program-checked" => enable_extend_program_checked::id(),
53        "formalize-loaded-transaction-data-size" => formalize_loaded_transaction_data_size::id(),
54        "disable-zk-elgamal-proof-program" => disable_zk_elgamal_proof_program::id(),
55        "reenable-zk-elgamal-proof-program" => reenable_zk_elgamal_proof_program::id(),
56        "raise-cpi-nesting-limit-to-8" => raise_cpi_nesting_limit_to_8::id(),
57        "account-data-direct-mapping" => account_data_direct_mapping::id(),
58        "provide-instruction-data-offset-in-vm-r2" => {
59            provide_instruction_data_offset_in_vm_r2::id()
60        }
61        "increase-cpi-account-info-limit" => increase_cpi_account_info_limit::id(),
62        "vote-state-v4" => vote_state_v4::id(),
63        "poseidon-enforce-padding" => poseidon_enforce_padding::id(),
64        "fix-alt-bn128-pairing-length-check" => fix_alt_bn128_pairing_length_check::id(),
65        // "lift-cpi-caller-restriction" not available in agave-feature-set 3.1.x
66        "remove-accounts-executable-flag-checks" => remove_accounts_executable_flag_checks::id(),
67        "loosen-cpi-size-restriction" => loosen_cpi_size_restriction::id(),
68        "disable-rent-fees-collection" => disable_rent_fees_collection::id(),
69        "deprecate-rent-exemption-threshold" => deprecate_rent_exemption_threshold::id(),
70        "replace-spl-token-with-p-token" => replace_spl_token_with_p_token::id(),
71        _ => return None,
72    };
73
74    Some(pubkey)
75}
76
77/// Parse a feature from either a name (kebab-case or snake_case) or a base58 pubkey string,
78/// validating it against known agave feature gates.
79pub fn parse_feature_pubkey(s: &str) -> Result<Pubkey, String> {
80    // 1. Try name lookup (supports both kebab-case and snake_case)
81    if let Some(pubkey) = lookup_feature_by_name(s) {
82        return Ok(pubkey);
83    }
84
85    // 2. Try base58 pubkey parse
86    let pubkey = Pubkey::from_str(s).map_err(|_| {
87        format!(
88            "Invalid feature pubkey: '{}'. Expected a base58-encoded pubkey of a known agave feature gate.",
89            s
90        )
91    })?;
92
93    if !FEATURE_NAMES.contains_key(&pubkey) {
94        let mut available: Vec<_> = FEATURE_NAMES
95            .iter()
96            .map(|(k, name)| format!("  {} ({})", k, name))
97            .collect();
98        available.sort();
99        return Err(format!(
100            "Available features:\n{}\n\nUnknown feature: '{}'. Not a known agave feature gate. Available features listed above.",
101            available.join("\n"),
102            pubkey,
103        ));
104    }
105
106    Ok(pubkey)
107}
108
109/// Overrides applied on top of LiteSVM's mainnet-beta feature baseline.
110///
111/// surfpool's SVM is constructed with the mainnet-beta feature set already
112/// active (see [`litesvm::LiteSVM::mainnet_feature_set`]). This struct
113/// expresses the user's deltas relative to that baseline:
114///
115/// * features listed in [`enable`](Self::enable) are activated on top of the
116///   mainnet baseline (typically features that have not yet shipped to
117///   mainnet);
118/// * features listed in [`disable`](Self::disable) are deactivated from the
119///   mainnet baseline (typically to reproduce older program behavior).
120///
121/// The default value is an empty override set, meaning "run with exactly the
122/// mainnet baseline."
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct SvmFeatureConfig {
125    /// Features to activate on top of the mainnet baseline.
126    pub enable: Vec<Pubkey>,
127    /// Features to deactivate from the mainnet baseline.
128    pub disable: Vec<Pubkey>,
129}
130
131impl SvmFeatureConfig {
132    /// Creates an empty override set (i.e. "mainnet baseline, no overrides").
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Adds a feature to enable on top of the mainnet baseline.
138    pub fn enable(mut self, feature: Pubkey) -> Self {
139        if !self.enable.contains(&feature) {
140            self.enable.push(feature);
141        }
142        // Remove from disable if present
143        self.disable.retain(|f| f != &feature);
144        self
145    }
146
147    /// Adds a feature to disable from the mainnet baseline.
148    pub fn disable(mut self, feature: Pubkey) -> Self {
149        if !self.disable.contains(&feature) {
150            self.disable.push(feature);
151        }
152        // Remove from enable if present
153        self.enable.retain(|f| f != &feature);
154        self
155    }
156
157    /// Checks if a feature should be enabled based on this configuration.
158    /// Returns None if not explicitly configured (use default).
159    pub fn is_enabled(&self, feature: &Pubkey) -> Option<bool> {
160        if self.enable.contains(feature) {
161            Some(true)
162        } else if self.disable.contains(feature) {
163            Some(false)
164        } else {
165            None
166        }
167    }
168
169    /// Returns a config with every known agave feature gate explicitly enabled.
170    ///
171    /// Mirrors the CLI's `--features-all` flag.
172    pub fn all_features_enabled() -> Self {
173        let mut cfg = Self::default();
174        for pubkey in FEATURE_NAMES.keys() {
175            cfg = cfg.enable(*pubkey);
176        }
177        cfg
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use agave_feature_set::*;
184
185    use super::*;
186
187    // ==================== parse_feature_pubkey tests ====================
188
189    #[test]
190    fn test_parse_feature_pubkey_valid() {
191        let pubkey = parse_feature_pubkey(&enable_loader_v4::id().to_string()).unwrap();
192        assert_eq!(pubkey, enable_loader_v4::id());
193    }
194
195    #[test]
196    fn test_parse_feature_name_snake_case() {
197        let pubkey = parse_feature_pubkey("enable_loader_v4").unwrap();
198        assert_eq!(pubkey, enable_loader_v4::id());
199    }
200
201    #[test]
202    fn test_parse_feature_name_kebab_case() {
203        let pubkey = parse_feature_pubkey("enable-loader-v4").unwrap();
204        assert_eq!(pubkey, enable_loader_v4::id());
205    }
206
207    #[test]
208    fn test_parse_feature_name_prefers_name_over_pubkey() {
209        // Verify that a known feature name resolves correctly
210        let pubkey = parse_feature_pubkey("blake3_syscall_enabled").unwrap();
211        assert_eq!(pubkey, blake3_syscall_enabled::id());
212    }
213
214    #[test]
215    fn test_parse_feature_unknown_name() {
216        let err = parse_feature_pubkey("nonexistent-feature-name").unwrap_err();
217        assert!(err.contains("Invalid feature"));
218        assert!(err.contains("nonexistent-feature-name"));
219    }
220
221    #[test]
222    fn test_parse_feature_pubkey_unknown_pubkey() {
223        // System program is not a feature gate
224        let err = parse_feature_pubkey("11111111111111111111111111111111").unwrap_err();
225        assert!(err.contains("Not a known agave feature gate"));
226    }
227
228    // ==================== SvmFeatureConfig basic tests ====================
229
230    #[test]
231    fn test_feature_config_new_is_empty() {
232        let config = SvmFeatureConfig::new();
233        assert!(config.enable.is_empty());
234        assert!(config.disable.is_empty());
235    }
236
237    #[test]
238    fn test_feature_config_default_is_empty() {
239        let config = SvmFeatureConfig::default();
240        assert!(config.enable.is_empty());
241        assert!(config.disable.is_empty());
242    }
243
244    #[test]
245    fn test_feature_config_enable() {
246        let config = SvmFeatureConfig::new().enable(enable_loader_v4::id());
247        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
248        assert_eq!(config.enable.len(), 1);
249        assert!(config.disable.is_empty());
250    }
251
252    #[test]
253    fn test_feature_config_disable() {
254        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
255        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
256        assert!(config.enable.is_empty());
257        assert_eq!(config.disable.len(), 1);
258    }
259
260    #[test]
261    fn test_feature_config_is_enabled_not_configured() {
262        let config = SvmFeatureConfig::new();
263        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), None);
264    }
265
266    // ==================== SvmFeatureConfig complex scenarios ====================
267
268    #[test]
269    fn test_feature_config_enable_then_disable() {
270        let config = SvmFeatureConfig::new()
271            .enable(enable_loader_v4::id())
272            .disable(enable_loader_v4::id());
273
274        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(false));
275        assert!(config.enable.is_empty());
276        assert_eq!(config.disable.len(), 1);
277    }
278
279    #[test]
280    fn test_feature_config_disable_then_enable() {
281        let config = SvmFeatureConfig::new()
282            .disable(enable_loader_v4::id())
283            .enable(enable_loader_v4::id());
284
285        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
286        assert_eq!(config.enable.len(), 1);
287        assert!(config.disable.is_empty());
288    }
289
290    #[test]
291    fn test_feature_config_enable_idempotent() {
292        let config = SvmFeatureConfig::new()
293            .enable(enable_loader_v4::id())
294            .enable(enable_loader_v4::id());
295
296        assert_eq!(config.enable.len(), 1);
297    }
298
299    #[test]
300    fn test_feature_config_disable_idempotent() {
301        let config = SvmFeatureConfig::new()
302            .disable(enable_loader_v4::id())
303            .disable(enable_loader_v4::id());
304
305        assert_eq!(config.disable.len(), 1);
306    }
307
308    #[test]
309    fn test_feature_config_multiple_features() {
310        let config = SvmFeatureConfig::new()
311            .enable(enable_loader_v4::id())
312            .enable(blake3_syscall_enabled::id())
313            .disable(disable_fees_sysvar::id())
314            .disable(disable_sbpf_v0_execution::id());
315
316        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
317        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), Some(true));
318        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
319        assert_eq!(
320            config.is_enabled(&disable_sbpf_v0_execution::id()),
321            Some(false)
322        );
323        assert_eq!(config.enable.len(), 2);
324        assert_eq!(config.disable.len(), 2);
325    }
326
327    // ==================== Serialization tests ====================
328
329    #[test]
330    fn test_feature_config_serde_roundtrip() {
331        let config = SvmFeatureConfig::new()
332            .enable(enable_loader_v4::id())
333            .disable(disable_fees_sysvar::id());
334
335        let json = serde_json::to_string(&config).unwrap();
336        let parsed: SvmFeatureConfig = serde_json::from_str(&json).unwrap();
337
338        assert_eq!(config, parsed);
339    }
340
341    // ==================== Edge cases ====================
342
343    #[test]
344    fn test_feature_config_clone() {
345        let config = SvmFeatureConfig::new()
346            .enable(enable_loader_v4::id())
347            .disable(disable_fees_sysvar::id());
348
349        let cloned = config.clone();
350        assert_eq!(config, cloned);
351    }
352}