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        "stricter-abi-and-runtime-constraints" => stricter_abi_and_runtime_constraints::id(),
20        "enable-bpf-loader-set-authority-checked-ix" => {
21            enable_bpf_loader_set_authority_checked_ix::id()
22        }
23        "enable-loader-v4" => enable_loader_v4::id(),
24        "deplete-cu-meter-on-vm-failure" => deplete_cu_meter_on_vm_failure::id(),
25        "abort-on-invalid-curve" => abort_on_invalid_curve::id(),
26        "blake3-syscall-enabled" => blake3_syscall_enabled::id(),
27        "curve25519-syscall-enabled" => curve25519_syscall_enabled::id(),
28        "disable-deploy-of-alloc-free-syscall" => disable_deploy_of_alloc_free_syscall::id(),
29        "disable-fees-sysvar" => disable_fees_sysvar::id(),
30        "disable-sbpf-v0-execution" => disable_sbpf_v0_execution::id(),
31        "enable-alt-bn128-compression-syscall" => enable_alt_bn128_compression_syscall::id(),
32        "enable-alt-bn128-syscall" => enable_alt_bn128_syscall::id(),
33        "enable-big-mod-exp-syscall" => enable_big_mod_exp_syscall::id(),
34        "enable-get-epoch-stake-syscall" => enable_get_epoch_stake_syscall::id(),
35        "enable-poseidon-syscall" => enable_poseidon_syscall::id(),
36        "enable-sbpf-v1-deployment-and-execution" => enable_sbpf_v1_deployment_and_execution::id(),
37        "enable-sbpf-v2-deployment-and-execution" => enable_sbpf_v2_deployment_and_execution::id(),
38        "enable-sbpf-v3-deployment-and-execution" => enable_sbpf_v3_deployment_and_execution::id(),
39        "get-sysvar-syscall-enabled" => get_sysvar_syscall_enabled::id(),
40        "last-restart-slot-sysvar" => last_restart_slot_sysvar::id(),
41        "reenable-sbpf-v0-execution" => reenable_sbpf_v0_execution::id(),
42        "remaining-compute-units-syscall-enabled" => remaining_compute_units_syscall_enabled::id(),
43        "remove-bpf-loader-incorrect-program-id" => remove_bpf_loader_incorrect_program_id::id(),
44        "move-stake-and-move-lamports-ixs" => move_stake_and_move_lamports_ixs::id(),
45        "stake-raise-minimum-delegation-to-1-sol" => stake_raise_minimum_delegation_to_1_sol::id(),
46        "deprecate-legacy-vote-ixs" => deprecate_legacy_vote_ixs::id(),
47        "mask-out-rent-epoch-in-vm-serialization" => mask_out_rent_epoch_in_vm_serialization::id(),
48        "simplify-alt-bn128-syscall-error-codes" => simplify_alt_bn128_syscall_error_codes::id(),
49        "fix-alt-bn128-multiplication-input-length" => {
50            fix_alt_bn128_multiplication_input_length::id()
51        }
52        "increase-tx-account-lock-limit" => increase_tx_account_lock_limit::id(),
53        "enable-extend-program-checked" => enable_extend_program_checked::id(),
54        "formalize-loaded-transaction-data-size" => formalize_loaded_transaction_data_size::id(),
55        "disable-zk-elgamal-proof-program" => disable_zk_elgamal_proof_program::id(),
56        "reenable-zk-elgamal-proof-program" => reenable_zk_elgamal_proof_program::id(),
57        "raise-cpi-nesting-limit-to-8" => raise_cpi_nesting_limit_to_8::id(),
58        "account-data-direct-mapping" => account_data_direct_mapping::id(),
59        "provide-instruction-data-offset-in-vm-r2" => {
60            provide_instruction_data_offset_in_vm_r2::id()
61        }
62        "increase-cpi-account-info-limit" => increase_cpi_account_info_limit::id(),
63        "vote-state-v4" => vote_state_v4::id(),
64        "poseidon-enforce-padding" => poseidon_enforce_padding::id(),
65        "fix-alt-bn128-pairing-length-check" => fix_alt_bn128_pairing_length_check::id(),
66        // "lift-cpi-caller-restriction" not available in agave-feature-set 3.1.x
67        "remove-accounts-executable-flag-checks" => remove_accounts_executable_flag_checks::id(),
68        "loosen-cpi-size-restriction" => loosen_cpi_size_restriction::id(),
69        "disable-rent-fees-collection" => disable_rent_fees_collection::id(),
70        "deprecate-rent-exemption-threshold" => deprecate_rent_exemption_threshold::id(),
71        "replace-spl-token-with-p-token" => replace_spl_token_with_p_token::id(),
72        _ => return None,
73    };
74
75    Some(pubkey)
76}
77
78/// Parse a feature from either a name (kebab-case or snake_case) or a base58 pubkey string,
79/// validating it against known agave feature gates.
80pub fn parse_feature_pubkey(s: &str) -> Result<Pubkey, String> {
81    // 1. Try name lookup (supports both kebab-case and snake_case)
82    if let Some(pubkey) = lookup_feature_by_name(s) {
83        return Ok(pubkey);
84    }
85
86    // 2. Try base58 pubkey parse
87    let pubkey = Pubkey::from_str(s).map_err(|_| {
88        format!(
89            "Invalid feature pubkey: '{}'. Expected a base58-encoded pubkey of a known agave feature gate.",
90            s
91        )
92    })?;
93
94    if !FEATURE_NAMES.contains_key(&pubkey) {
95        let mut available: Vec<_> = FEATURE_NAMES
96            .iter()
97            .map(|(k, name)| format!("  {} ({})", k, name))
98            .collect();
99        available.sort();
100        return Err(format!(
101            "Available features:\n{}\n\nUnknown feature: '{}'. Not a known agave feature gate. Available features listed above.",
102            pubkey,
103            available.join("\n")
104        ));
105    }
106
107    Ok(pubkey)
108}
109
110/// Configuration for SVM features, specifying which features to enable or disable.
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
112pub struct SvmFeatureConfig {
113    /// Features to explicitly enable (override defaults)
114    pub enable: Vec<Pubkey>,
115    /// Features to explicitly disable (override defaults)
116    pub disable: Vec<Pubkey>,
117}
118
119impl SvmFeatureConfig {
120    /// Creates a new empty feature configuration.
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Returns the default mainnet feature configuration.
126    ///
127    /// This reflects features currently active on Solana mainnet-beta.
128    /// Note: This may need periodic updates as mainnet features change.
129    /// Last updated: 2026-05-05 (queried from mainnet RPC)
130    pub fn default_mainnet_features() -> Self {
131        use agave_feature_set::*;
132
133        // Features that are NOT yet active on mainnet (should be disabled)
134        let disable = vec![
135            // Account data direct mapping not yet on mainnet
136            account_data_direct_mapping::id(),
137            // Blake3 syscall not yet on mainnet
138            blake3_syscall_enabled::id(),
139            // Legacy vote deprecation not yet on mainnet
140            deprecate_legacy_vote_ixs::id(),
141            // SBPF v0 disable not yet on mainnet
142            disable_sbpf_v0_execution::id(),
143            // big_mod_exp syscall not yet on mainnet
144            enable_big_mod_exp_syscall::id(),
145            // Extended program checked not yet on mainnet
146            enable_extend_program_checked::id(),
147            // Loader v4 not yet on mainnet
148            enable_loader_v4::id(),
149            // SBPF v3 not yet on mainnet (v1 and v2 ARE active)
150            enable_sbpf_v3_deployment_and_execution::id(),
151            // Increase tx account lock limit to 128 not yet on mainnet
152            increase_tx_account_lock_limit::id(),
153            // CPI nesting limit raise to 8 not yet on mainnet
154            raise_cpi_nesting_limit_to_8::id(),
155            // SBPF v0 reenable not yet on mainnet
156            reenable_sbpf_v0_execution::id(),
157            // ZK ElGamal reenable not yet on mainnet (disable IS active)
158            reenable_zk_elgamal_proof_program::id(),
159            // remaining_compute_units syscall not yet on mainnet
160            remaining_compute_units_syscall_enabled::id(),
161            // SPL token / p-token replacement not yet on mainnet
162            replace_spl_token_with_p_token::id(),
163            // Stake minimum delegation raise not yet on mainnet
164            stake_raise_minimum_delegation_to_1_sol::id(),
165            // Stricter ABI and runtime constraints not yet on mainnet
166            stricter_abi_and_runtime_constraints::id(),
167        ];
168
169        Self {
170            enable: vec![],
171            disable,
172        }
173    }
174
175    /// Adds a feature to enable.
176    pub fn enable(mut self, feature: Pubkey) -> Self {
177        if !self.enable.contains(&feature) {
178            self.enable.push(feature);
179        }
180        // Remove from disable if present
181        self.disable.retain(|f| f != &feature);
182        self
183    }
184
185    /// Adds a feature to disable.
186    pub fn disable(mut self, feature: Pubkey) -> Self {
187        if !self.disable.contains(&feature) {
188            self.disable.push(feature);
189        }
190        // Remove from enable if present
191        self.enable.retain(|f| f != &feature);
192        self
193    }
194
195    /// Checks if a feature should be enabled based on this configuration.
196    /// Returns None if not explicitly configured (use default).
197    pub fn is_enabled(&self, feature: &Pubkey) -> Option<bool> {
198        if self.enable.contains(feature) {
199            Some(true)
200        } else if self.disable.contains(feature) {
201            Some(false)
202        } else {
203            None
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use agave_feature_set::*;
211
212    use super::*;
213
214    // ==================== parse_feature_pubkey tests ====================
215
216    #[test]
217    fn test_parse_feature_pubkey_valid() {
218        let pubkey = parse_feature_pubkey(&enable_loader_v4::id().to_string()).unwrap();
219        assert_eq!(pubkey, enable_loader_v4::id());
220    }
221
222    #[test]
223    fn test_parse_feature_name_snake_case() {
224        let pubkey = parse_feature_pubkey("enable_loader_v4").unwrap();
225        assert_eq!(pubkey, enable_loader_v4::id());
226    }
227
228    #[test]
229    fn test_parse_feature_name_kebab_case() {
230        let pubkey = parse_feature_pubkey("enable-loader-v4").unwrap();
231        assert_eq!(pubkey, enable_loader_v4::id());
232    }
233
234    #[test]
235    fn test_parse_feature_name_prefers_name_over_pubkey() {
236        // Verify that a known feature name resolves correctly
237        let pubkey = parse_feature_pubkey("blake3_syscall_enabled").unwrap();
238        assert_eq!(pubkey, blake3_syscall_enabled::id());
239    }
240
241    #[test]
242    fn test_parse_feature_unknown_name() {
243        let err = parse_feature_pubkey("nonexistent-feature-name").unwrap_err();
244        assert!(err.contains("Invalid feature"));
245        assert!(err.contains("nonexistent-feature-name"));
246    }
247
248    #[test]
249    fn test_parse_feature_pubkey_unknown_pubkey() {
250        // System program is not a feature gate
251        let err = parse_feature_pubkey("11111111111111111111111111111111").unwrap_err();
252        assert!(err.contains("Not a known agave feature gate"));
253    }
254
255    // ==================== SvmFeatureConfig basic tests ====================
256
257    #[test]
258    fn test_feature_config_new_is_empty() {
259        let config = SvmFeatureConfig::new();
260        assert!(config.enable.is_empty());
261        assert!(config.disable.is_empty());
262    }
263
264    #[test]
265    fn test_feature_config_default_is_empty() {
266        let config = SvmFeatureConfig::default();
267        assert!(config.enable.is_empty());
268        assert!(config.disable.is_empty());
269    }
270
271    #[test]
272    fn test_feature_config_enable() {
273        let config = SvmFeatureConfig::new().enable(enable_loader_v4::id());
274        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
275        assert_eq!(config.enable.len(), 1);
276        assert!(config.disable.is_empty());
277    }
278
279    #[test]
280    fn test_feature_config_disable() {
281        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
282        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
283        assert!(config.enable.is_empty());
284        assert_eq!(config.disable.len(), 1);
285    }
286
287    #[test]
288    fn test_feature_config_is_enabled_not_configured() {
289        let config = SvmFeatureConfig::new();
290        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), None);
291    }
292
293    // ==================== SvmFeatureConfig complex scenarios ====================
294
295    #[test]
296    fn test_feature_config_enable_then_disable() {
297        let config = SvmFeatureConfig::new()
298            .enable(enable_loader_v4::id())
299            .disable(enable_loader_v4::id());
300
301        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(false));
302        assert!(config.enable.is_empty());
303        assert_eq!(config.disable.len(), 1);
304    }
305
306    #[test]
307    fn test_feature_config_disable_then_enable() {
308        let config = SvmFeatureConfig::new()
309            .disable(enable_loader_v4::id())
310            .enable(enable_loader_v4::id());
311
312        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
313        assert_eq!(config.enable.len(), 1);
314        assert!(config.disable.is_empty());
315    }
316
317    #[test]
318    fn test_feature_config_enable_idempotent() {
319        let config = SvmFeatureConfig::new()
320            .enable(enable_loader_v4::id())
321            .enable(enable_loader_v4::id());
322
323        assert_eq!(config.enable.len(), 1);
324    }
325
326    #[test]
327    fn test_feature_config_disable_idempotent() {
328        let config = SvmFeatureConfig::new()
329            .disable(enable_loader_v4::id())
330            .disable(enable_loader_v4::id());
331
332        assert_eq!(config.disable.len(), 1);
333    }
334
335    #[test]
336    fn test_feature_config_multiple_features() {
337        let config = SvmFeatureConfig::new()
338            .enable(enable_loader_v4::id())
339            .enable(blake3_syscall_enabled::id())
340            .disable(disable_fees_sysvar::id())
341            .disable(disable_sbpf_v0_execution::id());
342
343        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
344        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), Some(true));
345        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
346        assert_eq!(
347            config.is_enabled(&disable_sbpf_v0_execution::id()),
348            Some(false)
349        );
350        assert_eq!(config.enable.len(), 2);
351        assert_eq!(config.disable.len(), 2);
352    }
353
354    // ==================== Mainnet defaults tests ====================
355
356    #[test]
357    fn test_mainnet_features_disabled_list() {
358        let config = SvmFeatureConfig::default_mainnet_features();
359
360        assert_eq!(
361            config.is_enabled(&blake3_syscall_enabled::id()),
362            Some(false)
363        );
364        assert_eq!(
365            config.is_enabled(&deprecate_legacy_vote_ixs::id()),
366            Some(false)
367        );
368        assert_eq!(
369            config.is_enabled(&disable_sbpf_v0_execution::id()),
370            Some(false)
371        );
372        assert_eq!(
373            config.is_enabled(&reenable_sbpf_v0_execution::id()),
374            Some(false)
375        );
376        assert_eq!(
377            config.is_enabled(&reenable_zk_elgamal_proof_program::id()),
378            Some(false)
379        );
380        assert_eq!(
381            config.is_enabled(&enable_extend_program_checked::id()),
382            Some(false)
383        );
384        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(false));
385        assert_eq!(
386            config.is_enabled(&enable_sbpf_v3_deployment_and_execution::id()),
387            Some(false)
388        );
389        assert_eq!(
390            config.is_enabled(&raise_cpi_nesting_limit_to_8::id()),
391            Some(false)
392        );
393        assert_eq!(
394            config.is_enabled(&account_data_direct_mapping::id()),
395            Some(false)
396        );
397        assert_eq!(
398            config.is_enabled(&stake_raise_minimum_delegation_to_1_sol::id()),
399            Some(false)
400        );
401    }
402
403    #[test]
404    fn test_mainnet_features_has_no_enables() {
405        let config = SvmFeatureConfig::default_mainnet_features();
406        assert!(config.enable.is_empty());
407    }
408
409    #[test]
410    fn test_mainnet_features_override_with_enable() {
411        let config = SvmFeatureConfig::default_mainnet_features().enable(enable_loader_v4::id());
412
413        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
414        assert_eq!(
415            config.is_enabled(&blake3_syscall_enabled::id()),
416            Some(false)
417        );
418        assert_eq!(
419            config.is_enabled(&enable_extend_program_checked::id()),
420            Some(false)
421        );
422    }
423
424    #[test]
425    fn test_mainnet_features_active_features_not_in_disable() {
426        let config = SvmFeatureConfig::default_mainnet_features();
427
428        // Features that ARE active on mainnet should not be in disable list
429        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), None);
430        assert_eq!(config.is_enabled(&curve25519_syscall_enabled::id()), None);
431        assert_eq!(config.is_enabled(&enable_alt_bn128_syscall::id()), None);
432        assert_eq!(config.is_enabled(&enable_poseidon_syscall::id()), None);
433        assert_eq!(
434            config.is_enabled(&enable_sbpf_v1_deployment_and_execution::id()),
435            None
436        );
437        assert_eq!(
438            config.is_enabled(&enable_sbpf_v2_deployment_and_execution::id()),
439            None
440        );
441        assert_eq!(
442            config.is_enabled(&disable_zk_elgamal_proof_program::id()),
443            None
444        );
445        assert_eq!(config.is_enabled(&vote_state_v4::id()), None);
446        assert_eq!(config.is_enabled(&poseidon_enforce_padding::id()), None);
447        assert_eq!(
448            config.is_enabled(&deprecate_rent_exemption_threshold::id()),
449            None
450        );
451        assert_eq!(
452            config.is_enabled(&move_precompile_verification_to_svm::id()),
453            None
454        );
455        assert_eq!(
456            config.is_enabled(&remove_accounts_executable_flag_checks::id()),
457            None
458        );
459        assert_eq!(config.is_enabled(&loosen_cpi_size_restriction::id()), None);
460    }
461
462    // ==================== Serialization tests ====================
463
464    #[test]
465    fn test_feature_config_serde_roundtrip() {
466        let config = SvmFeatureConfig::new()
467            .enable(enable_loader_v4::id())
468            .disable(disable_fees_sysvar::id());
469
470        let json = serde_json::to_string(&config).unwrap();
471        let parsed: SvmFeatureConfig = serde_json::from_str(&json).unwrap();
472
473        assert_eq!(config, parsed);
474    }
475
476    // ==================== Edge cases ====================
477
478    #[test]
479    fn test_feature_config_clone() {
480        let config = SvmFeatureConfig::new()
481            .enable(enable_loader_v4::id())
482            .disable(disable_fees_sysvar::id());
483
484        let cloned = config.clone();
485        assert_eq!(config, cloned);
486    }
487}