1use std::str::FromStr;
2
3use agave_feature_set::FEATURE_NAMES;
4use serde::{Deserialize, Serialize};
5use solana_pubkey::Pubkey;
6
7fn lookup_feature_by_name(name: &str) -> Option<Pubkey> {
11 use agave_feature_set::*;
12
13 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 "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
77pub fn parse_feature_pubkey(s: &str) -> Result<Pubkey, String> {
80 if let Some(pubkey) = lookup_feature_by_name(s) {
82 return Ok(pubkey);
83 }
84
85 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct SvmFeatureConfig {
125 pub enable: Vec<Pubkey>,
127 pub disable: Vec<Pubkey>,
129}
130
131impl SvmFeatureConfig {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn enable(mut self, feature: Pubkey) -> Self {
139 if !self.enable.contains(&feature) {
140 self.enable.push(feature);
141 }
142 self.disable.retain(|f| f != &feature);
144 self
145 }
146
147 pub fn disable(mut self, feature: Pubkey) -> Self {
149 if !self.disable.contains(&feature) {
150 self.disable.push(feature);
151 }
152 self.enable.retain(|f| f != &feature);
154 self
155 }
156
157 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 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 #[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 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 let err = parse_feature_pubkey("11111111111111111111111111111111").unwrap_err();
225 assert!(err.contains("Not a known agave feature gate"));
226 }
227
228 #[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 #[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 #[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 #[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}