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/// Overrides applied on top of LiteSVM's mainnet-beta feature baseline.
111///
112/// surfpool's SVM is constructed with the mainnet-beta feature set already
113/// active (see [`litesvm::LiteSVM::mainnet_feature_set`]). This struct
114/// expresses the user's deltas relative to that baseline:
115///
116/// * features listed in [`enable`](Self::enable) are activated on top of the
117///   mainnet baseline (typically features that have not yet shipped to
118///   mainnet);
119/// * features listed in [`disable`](Self::disable) are deactivated from the
120///   mainnet baseline (typically to reproduce older program behavior).
121///
122/// The default value is an empty override set, meaning "run with exactly the
123/// mainnet baseline."
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub struct SvmFeatureConfig {
126    /// Features to activate on top of the mainnet baseline.
127    pub enable: Vec<Pubkey>,
128    /// Features to deactivate from the mainnet baseline.
129    pub disable: Vec<Pubkey>,
130}
131
132impl SvmFeatureConfig {
133    /// Creates an empty override set (i.e. "mainnet baseline, no overrides").
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Adds a feature to enable on top of the mainnet baseline.
139    pub fn enable(mut self, feature: Pubkey) -> Self {
140        if !self.enable.contains(&feature) {
141            self.enable.push(feature);
142        }
143        // Remove from disable if present
144        self.disable.retain(|f| f != &feature);
145        self
146    }
147
148    /// Adds a feature to disable from the mainnet baseline.
149    pub fn disable(mut self, feature: Pubkey) -> Self {
150        if !self.disable.contains(&feature) {
151            self.disable.push(feature);
152        }
153        // Remove from enable if present
154        self.enable.retain(|f| f != &feature);
155        self
156    }
157
158    /// Checks if a feature should be enabled based on this configuration.
159    /// Returns None if not explicitly configured (use default).
160    pub fn is_enabled(&self, feature: &Pubkey) -> Option<bool> {
161        if self.enable.contains(feature) {
162            Some(true)
163        } else if self.disable.contains(feature) {
164            Some(false)
165        } else {
166            None
167        }
168    }
169
170    /// Returns a config with every known agave feature gate explicitly enabled.
171    ///
172    /// Mirrors the CLI's `--features-all` flag.
173    pub fn all_features_enabled() -> Self {
174        let mut cfg = Self::default();
175        for pubkey in FEATURE_NAMES.keys() {
176            cfg = cfg.enable(*pubkey);
177        }
178        cfg
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use agave_feature_set::*;
185
186    use super::*;
187
188    // ==================== parse_feature_pubkey tests ====================
189
190    #[test]
191    fn test_parse_feature_pubkey_valid() {
192        let pubkey = parse_feature_pubkey(&enable_loader_v4::id().to_string()).unwrap();
193        assert_eq!(pubkey, enable_loader_v4::id());
194    }
195
196    #[test]
197    fn test_parse_feature_name_snake_case() {
198        let pubkey = parse_feature_pubkey("enable_loader_v4").unwrap();
199        assert_eq!(pubkey, enable_loader_v4::id());
200    }
201
202    #[test]
203    fn test_parse_feature_name_kebab_case() {
204        let pubkey = parse_feature_pubkey("enable-loader-v4").unwrap();
205        assert_eq!(pubkey, enable_loader_v4::id());
206    }
207
208    #[test]
209    fn test_parse_feature_name_prefers_name_over_pubkey() {
210        // Verify that a known feature name resolves correctly
211        let pubkey = parse_feature_pubkey("blake3_syscall_enabled").unwrap();
212        assert_eq!(pubkey, blake3_syscall_enabled::id());
213    }
214
215    #[test]
216    fn test_parse_feature_unknown_name() {
217        let err = parse_feature_pubkey("nonexistent-feature-name").unwrap_err();
218        assert!(err.contains("Invalid feature"));
219        assert!(err.contains("nonexistent-feature-name"));
220    }
221
222    #[test]
223    fn test_parse_feature_pubkey_unknown_pubkey() {
224        // System program is not a feature gate
225        let err = parse_feature_pubkey("11111111111111111111111111111111").unwrap_err();
226        assert!(err.contains("Not a known agave feature gate"));
227    }
228
229    // ==================== SvmFeatureConfig basic tests ====================
230
231    #[test]
232    fn test_feature_config_new_is_empty() {
233        let config = SvmFeatureConfig::new();
234        assert!(config.enable.is_empty());
235        assert!(config.disable.is_empty());
236    }
237
238    #[test]
239    fn test_feature_config_default_is_empty() {
240        let config = SvmFeatureConfig::default();
241        assert!(config.enable.is_empty());
242        assert!(config.disable.is_empty());
243    }
244
245    #[test]
246    fn test_feature_config_enable() {
247        let config = SvmFeatureConfig::new().enable(enable_loader_v4::id());
248        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
249        assert_eq!(config.enable.len(), 1);
250        assert!(config.disable.is_empty());
251    }
252
253    #[test]
254    fn test_feature_config_disable() {
255        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
256        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
257        assert!(config.enable.is_empty());
258        assert_eq!(config.disable.len(), 1);
259    }
260
261    #[test]
262    fn test_feature_config_is_enabled_not_configured() {
263        let config = SvmFeatureConfig::new();
264        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), None);
265    }
266
267    // ==================== SvmFeatureConfig complex scenarios ====================
268
269    #[test]
270    fn test_feature_config_enable_then_disable() {
271        let config = SvmFeatureConfig::new()
272            .enable(enable_loader_v4::id())
273            .disable(enable_loader_v4::id());
274
275        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(false));
276        assert!(config.enable.is_empty());
277        assert_eq!(config.disable.len(), 1);
278    }
279
280    #[test]
281    fn test_feature_config_disable_then_enable() {
282        let config = SvmFeatureConfig::new()
283            .disable(enable_loader_v4::id())
284            .enable(enable_loader_v4::id());
285
286        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
287        assert_eq!(config.enable.len(), 1);
288        assert!(config.disable.is_empty());
289    }
290
291    #[test]
292    fn test_feature_config_enable_idempotent() {
293        let config = SvmFeatureConfig::new()
294            .enable(enable_loader_v4::id())
295            .enable(enable_loader_v4::id());
296
297        assert_eq!(config.enable.len(), 1);
298    }
299
300    #[test]
301    fn test_feature_config_disable_idempotent() {
302        let config = SvmFeatureConfig::new()
303            .disable(enable_loader_v4::id())
304            .disable(enable_loader_v4::id());
305
306        assert_eq!(config.disable.len(), 1);
307    }
308
309    #[test]
310    fn test_feature_config_multiple_features() {
311        let config = SvmFeatureConfig::new()
312            .enable(enable_loader_v4::id())
313            .enable(blake3_syscall_enabled::id())
314            .disable(disable_fees_sysvar::id())
315            .disable(disable_sbpf_v0_execution::id());
316
317        assert_eq!(config.is_enabled(&enable_loader_v4::id()), Some(true));
318        assert_eq!(config.is_enabled(&blake3_syscall_enabled::id()), Some(true));
319        assert_eq!(config.is_enabled(&disable_fees_sysvar::id()), Some(false));
320        assert_eq!(
321            config.is_enabled(&disable_sbpf_v0_execution::id()),
322            Some(false)
323        );
324        assert_eq!(config.enable.len(), 2);
325        assert_eq!(config.disable.len(), 2);
326    }
327
328    // ==================== Serialization tests ====================
329
330    #[test]
331    fn test_feature_config_serde_roundtrip() {
332        let config = SvmFeatureConfig::new()
333            .enable(enable_loader_v4::id())
334            .disable(disable_fees_sysvar::id());
335
336        let json = serde_json::to_string(&config).unwrap();
337        let parsed: SvmFeatureConfig = serde_json::from_str(&json).unwrap();
338
339        assert_eq!(config, parsed);
340    }
341
342    // ==================== Edge cases ====================
343
344    #[test]
345    fn test_feature_config_clone() {
346        let config = SvmFeatureConfig::new()
347            .enable(enable_loader_v4::id())
348            .disable(disable_fees_sysvar::id());
349
350        let cloned = config.clone();
351        assert_eq!(config, cloned);
352    }
353}