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 "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 "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
78pub fn parse_feature_pubkey(s: &str) -> Result<Pubkey, String> {
81 if let Some(pubkey) = lookup_feature_by_name(s) {
83 return Ok(pubkey);
84 }
85
86 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
112pub struct SvmFeatureConfig {
113 pub enable: Vec<Pubkey>,
115 pub disable: Vec<Pubkey>,
117}
118
119impl SvmFeatureConfig {
120 pub fn new() -> Self {
122 Self::default()
123 }
124
125 pub fn default_mainnet_features() -> Self {
131 use agave_feature_set::*;
132
133 let disable = vec![
135 account_data_direct_mapping::id(),
137 blake3_syscall_enabled::id(),
139 deprecate_legacy_vote_ixs::id(),
141 disable_sbpf_v0_execution::id(),
143 enable_big_mod_exp_syscall::id(),
145 enable_extend_program_checked::id(),
147 enable_loader_v4::id(),
149 enable_sbpf_v3_deployment_and_execution::id(),
151 increase_tx_account_lock_limit::id(),
153 raise_cpi_nesting_limit_to_8::id(),
155 reenable_sbpf_v0_execution::id(),
157 reenable_zk_elgamal_proof_program::id(),
159 remaining_compute_units_syscall_enabled::id(),
161 replace_spl_token_with_p_token::id(),
163 stake_raise_minimum_delegation_to_1_sol::id(),
165 stricter_abi_and_runtime_constraints::id(),
167 ];
168
169 Self {
170 enable: vec![],
171 disable,
172 }
173 }
174
175 pub fn enable(mut self, feature: Pubkey) -> Self {
177 if !self.enable.contains(&feature) {
178 self.enable.push(feature);
179 }
180 self.disable.retain(|f| f != &feature);
182 self
183 }
184
185 pub fn disable(mut self, feature: Pubkey) -> Self {
187 if !self.disable.contains(&feature) {
188 self.disable.push(feature);
189 }
190 self.enable.retain(|f| f != &feature);
192 self
193 }
194
195 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 #[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 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 let err = parse_feature_pubkey("11111111111111111111111111111111").unwrap_err();
252 assert!(err.contains("Not a known agave feature gate"));
253 }
254
255 #[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 #[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 #[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 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 #[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 #[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}