1use {
2 crate::{
3 cli::{
4 log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError,
5 ProcessResult,
6 },
7 spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
8 },
9 clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
10 console::style,
11 serde::{Deserialize, Serialize},
12 solana_account::Account,
13 solana_clap_utils::{
14 compute_budget::ComputeUnitLimit, fee_payer::*, hidden_unless_forced, input_parsers::*,
15 input_validators::*, keypair::*,
16 },
17 solana_cli_output::{cli_version::CliVersion, QuietDisplay, VerboseDisplay},
18 solana_clock::{Epoch, Slot},
19 solana_cluster_type::ClusterType,
20 solana_epoch_schedule::EpochSchedule,
21 solana_feature_gate_client::{
22 errors::SolanaFeatureGateError, instructions::RevokePendingActivation,
23 },
24 solana_feature_gate_interface::{activate_with_lamports, from_account, Feature},
25 solana_feature_set::FEATURE_NAMES,
26 solana_message::Message,
27 solana_pubkey::Pubkey,
28 solana_remote_wallet::remote_wallet::RemoteWalletManager,
29 solana_rpc_client::rpc_client::RpcClient,
30 solana_rpc_client_api::{
31 client_error::Error as ClientError, request::MAX_MULTIPLE_ACCOUNTS,
32 response::RpcVoteAccountInfo,
33 },
34 solana_sdk_ids::{incinerator, system_program},
35 solana_system_interface::error::SystemError,
36 solana_transaction::Transaction,
37 std::{cmp::Ordering, collections::HashMap, fmt, rc::Rc, str::FromStr},
38};
39
40const DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS: Slot = 15_000_000; #[derive(Copy, Clone, Debug, PartialEq, Eq)]
43pub enum ForceActivation {
44 No,
45 Almost,
46 Yes,
47}
48
49#[derive(Debug, PartialEq, Eq)]
50pub enum FeatureCliCommand {
51 Status {
52 features: Vec<Pubkey>,
53 display_all: bool,
54 },
55 Activate {
56 feature: Pubkey,
57 cluster: ClusterType,
58 force: ForceActivation,
59 fee_payer: SignerIndex,
60 },
61 Revoke {
62 feature: Pubkey,
63 cluster: ClusterType,
64 fee_payer: SignerIndex,
65 },
66}
67
68#[derive(Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "camelCase", tag = "status", content = "sinceSlot")]
70pub enum CliFeatureStatus {
71 Inactive,
72 Pending,
73 Active(Slot),
74}
75
76impl PartialOrd for CliFeatureStatus {
77 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
78 Some(self.cmp(other))
79 }
80}
81
82impl Ord for CliFeatureStatus {
83 fn cmp(&self, other: &Self) -> Ordering {
84 match (self, other) {
85 (Self::Inactive, Self::Inactive) => Ordering::Equal,
86 (Self::Inactive, _) => Ordering::Greater,
87 (_, Self::Inactive) => Ordering::Less,
88 (Self::Pending, Self::Pending) => Ordering::Equal,
89 (Self::Pending, _) => Ordering::Greater,
90 (_, Self::Pending) => Ordering::Less,
91 (Self::Active(self_active_slot), Self::Active(other_active_slot)) => {
92 self_active_slot.cmp(other_active_slot)
93 }
94 }
95 }
96}
97
98#[derive(Serialize, Deserialize, PartialEq, Eq)]
99#[serde(rename_all = "camelCase")]
100pub struct CliFeature {
101 pub id: String,
102 pub description: String,
103 #[serde(flatten)]
104 pub status: CliFeatureStatus,
105}
106
107impl PartialOrd for CliFeature {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109 Some(self.cmp(other))
110 }
111}
112
113impl Ord for CliFeature {
114 fn cmp(&self, other: &Self) -> Ordering {
115 (&self.status, &self.id).cmp(&(&other.status, &other.id))
116 }
117}
118
119#[derive(Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct CliFeatures {
122 pub features: Vec<CliFeature>,
123 #[serde(skip)]
124 pub epoch_schedule: EpochSchedule,
125 #[serde(skip)]
126 pub current_slot: Slot,
127 pub feature_activation_allowed: bool,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub cluster_feature_sets: Option<CliClusterFeatureSets>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub cluster_software_versions: Option<CliClusterSoftwareVersions>,
132 #[serde(skip)]
133 pub inactive: bool,
134}
135
136impl fmt::Display for CliFeatures {
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 if !self.features.is_empty() {
139 writeln!(
140 f,
141 "{}",
142 style(format!(
143 "{:<44} | {:<23} | {} | {}",
144 "Feature", "Status", "Activation Slot", "Description"
145 ))
146 .bold()
147 )?;
148 }
149 for feature in &self.features {
150 writeln!(
151 f,
152 "{:<44} | {:<23} | {:<15} | {}",
153 feature.id,
154 match feature.status {
155 CliFeatureStatus::Inactive => style("inactive".to_string()).red(),
156 CliFeatureStatus::Pending => {
157 let current_epoch = self.epoch_schedule.get_epoch(self.current_slot);
158 style(format!(
159 "pending until epoch {}",
160 current_epoch.saturating_add(1)
161 ))
162 .yellow()
163 }
164 CliFeatureStatus::Active(activation_slot) => {
165 let activation_epoch = self.epoch_schedule.get_epoch(activation_slot);
166 style(format!("active since epoch {activation_epoch}")).green()
167 }
168 },
169 match feature.status {
170 CliFeatureStatus::Active(activation_slot) => activation_slot.to_string(),
171 _ => "NA".to_string(),
172 },
173 feature.description,
174 )?;
175 }
176
177 if let Some(software_versions) = &self.cluster_software_versions {
178 write!(f, "{software_versions}")?;
179 }
180
181 if let Some(feature_sets) = &self.cluster_feature_sets {
182 write!(f, "{feature_sets}")?;
183 }
184
185 if self.inactive && !self.feature_activation_allowed {
186 writeln!(
187 f,
188 "{}",
189 style("\nFeature activation is not allowed at this time")
190 .bold()
191 .red()
192 )?;
193 }
194 Ok(())
195 }
196}
197
198impl QuietDisplay for CliFeatures {}
199impl VerboseDisplay for CliFeatures {}
200
201#[derive(Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct CliClusterFeatureSets {
204 pub tool_feature_set: u32,
205 pub feature_sets: Vec<CliFeatureSetStats>,
206 #[serde(skip)]
207 pub stake_allowed: bool,
208 #[serde(skip)]
209 pub rpc_allowed: bool,
210}
211
212#[derive(Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct CliClusterSoftwareVersions {
215 tool_software_version: CliVersion,
216 software_versions: Vec<CliSoftwareVersionStats>,
217}
218
219impl fmt::Display for CliClusterSoftwareVersions {
220 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221 let software_version_title = "Software Version";
222 let stake_percent_title = "Stake";
223 let rpc_percent_title = "RPC";
224 let mut max_software_version_len = software_version_title.len();
225 let mut max_stake_percent_len = stake_percent_title.len();
226 let mut max_rpc_percent_len = rpc_percent_title.len();
227
228 let software_versions: Vec<_> = self
229 .software_versions
230 .iter()
231 .map(|software_version_stats| {
232 let stake_percent = format!("{:.2}%", software_version_stats.stake_percent);
233 let rpc_percent = format!("{:.2}%", software_version_stats.rpc_percent);
234 let software_version = software_version_stats.software_version.to_string();
235
236 max_software_version_len = max_software_version_len.max(software_version.len());
237 max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
238 max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
239
240 (software_version, stake_percent, rpc_percent)
241 })
242 .collect();
243
244 writeln!(
245 f,
246 "\n\n{}",
247 style(format!(
248 "Tool Software Version: {}",
249 self.tool_software_version
250 ))
251 .bold()
252 )?;
253 writeln!(
254 f,
255 "{}",
256 style(format!(
257 "{software_version_title:<max_software_version_len$} \
258 {stake_percent_title:>max_stake_percent_len$} \
259 {rpc_percent_title:>max_rpc_percent_len$}",
260 ))
261 .bold(),
262 )?;
263 for (software_version, stake_percent, rpc_percent) in software_versions {
264 let me = self.tool_software_version.to_string() == software_version;
265 writeln!(
266 f,
267 "{1:<0$} {3:>2$} {5:>4$} {6}",
268 max_software_version_len,
269 software_version,
270 max_stake_percent_len,
271 stake_percent,
272 max_rpc_percent_len,
273 rpc_percent,
274 if me { "<-- me" } else { "" },
275 )?;
276 }
277 writeln!(f)
278 }
279}
280
281impl fmt::Display for CliClusterFeatureSets {
282 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
283 let mut tool_feature_set_matches_cluster = false;
284
285 let software_versions_title = "Software Version";
286 let feature_set_title = "Feature Set";
287 let stake_percent_title = "Stake";
288 let rpc_percent_title = "RPC";
289 let mut max_software_versions_len = software_versions_title.len();
290 let mut max_feature_set_len = feature_set_title.len();
291 let mut max_stake_percent_len = stake_percent_title.len();
292 let mut max_rpc_percent_len = rpc_percent_title.len();
293
294 let feature_sets: Vec<_> = self
295 .feature_sets
296 .iter()
297 .map(|feature_set_info| {
298 let me = if self.tool_feature_set == feature_set_info.feature_set {
299 tool_feature_set_matches_cluster = true;
300 true
301 } else {
302 false
303 };
304 let software_versions: Vec<_> = feature_set_info
305 .software_versions
306 .iter()
307 .map(ToString::to_string)
308 .collect();
309 let software_versions = software_versions.join(", ");
310 let feature_set = if feature_set_info.feature_set == 0 {
311 "unknown".to_string()
312 } else {
313 feature_set_info.feature_set.to_string()
314 };
315 let stake_percent = format!("{:.2}%", feature_set_info.stake_percent);
316 let rpc_percent = format!("{:.2}%", feature_set_info.rpc_percent);
317
318 max_software_versions_len = max_software_versions_len.max(software_versions.len());
319 max_feature_set_len = max_feature_set_len.max(feature_set.len());
320 max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
321 max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
322
323 (
324 software_versions,
325 feature_set,
326 stake_percent,
327 rpc_percent,
328 me,
329 )
330 })
331 .collect();
332
333 if !tool_feature_set_matches_cluster {
334 writeln!(
335 f,
336 "\n{}",
337 style(
338 "To activate features the tool and cluster feature sets must match, select a \
339 tool version that matches the cluster"
340 )
341 .bold()
342 )?;
343 } else {
344 if !self.stake_allowed {
345 write!(
346 f,
347 "\n{}",
348 style("To activate features the stake must be >= 95%")
349 .bold()
350 .red()
351 )?;
352 }
353 if !self.rpc_allowed {
354 write!(
355 f,
356 "\n{}",
357 style("To activate features the RPC nodes must be >= 95%")
358 .bold()
359 .red()
360 )?;
361 }
362 }
363 writeln!(
364 f,
365 "\n\n{}",
366 style(format!("Tool Feature Set: {}", self.tool_feature_set)).bold()
367 )?;
368 writeln!(
369 f,
370 "{}",
371 style(format!(
372 "{software_versions_title:<max_software_versions_len$} \
373 {feature_set_title:<max_feature_set_len$} \
374 {stake_percent_title:>max_stake_percent_len$} \
375 {rpc_percent_title:>max_rpc_percent_len$}",
376 ))
377 .bold(),
378 )?;
379 for (software_versions, feature_set, stake_percent, rpc_percent, me) in feature_sets {
380 writeln!(
381 f,
382 "{1:<0$} {3:>2$} {5:>4$} {7:>6$} {8}",
383 max_software_versions_len,
384 software_versions,
385 max_feature_set_len,
386 feature_set,
387 max_stake_percent_len,
388 stake_percent,
389 max_rpc_percent_len,
390 rpc_percent,
391 if me { "<-- me" } else { "" },
392 )?;
393 }
394 writeln!(f)
395 }
396}
397
398impl QuietDisplay for CliClusterFeatureSets {}
399impl VerboseDisplay for CliClusterFeatureSets {}
400
401#[derive(Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct CliFeatureSetStats {
404 software_versions: Vec<CliVersion>,
405 feature_set: u32,
406 stake_percent: f64,
407 rpc_percent: f32,
408}
409
410#[derive(Serialize, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct CliSoftwareVersionStats {
413 software_version: CliVersion,
414 stake_percent: f64,
415 rpc_percent: f32,
416}
417
418fn check_rpc_genesis_hash(
420 cluster_type: &ClusterType,
421 rpc_client: &RpcClient,
422) -> Result<(), Box<dyn std::error::Error>> {
423 if let Some(genesis_hash) = cluster_type.get_genesis_hash() {
424 let rpc_genesis_hash = rpc_client.get_genesis_hash()?;
425 if rpc_genesis_hash != genesis_hash {
426 return Err(format!(
427 "The genesis hash for the specified cluster {cluster_type:?} does not match the \
428 genesis hash reported by the specified RPC. Cluster genesis hash: \
429 {genesis_hash}, RPC reported genesis hash: {rpc_genesis_hash}"
430 )
431 .into());
432 }
433 }
434 Ok(())
435}
436
437pub trait FeatureSubCommands {
438 fn feature_subcommands(self) -> Self;
439}
440
441impl FeatureSubCommands for App<'_, '_> {
442 fn feature_subcommands(self) -> Self {
443 self.subcommand(
444 SubCommand::with_name("feature")
445 .about("Runtime feature management")
446 .setting(AppSettings::SubcommandRequiredElseHelp)
447 .subcommand(
448 SubCommand::with_name("status")
449 .about("Query runtime feature status")
450 .arg(
451 Arg::with_name("features")
452 .value_name("ADDRESS")
453 .validator(is_valid_pubkey)
454 .index(1)
455 .multiple(true)
456 .help("Feature status to query [default: all known features]"),
457 )
458 .arg(
459 Arg::with_name("display_all")
460 .long("display-all")
461 .help("display all features regardless of age"),
462 ),
463 )
464 .subcommand(
465 SubCommand::with_name("activate")
466 .about("Activate a runtime feature")
467 .arg(
468 Arg::with_name("feature")
469 .value_name("FEATURE_KEYPAIR")
470 .validator(is_valid_signer)
471 .index(1)
472 .required(true)
473 .help("The signer for the feature to activate"),
474 )
475 .arg(
476 Arg::with_name("cluster")
477 .value_name("CLUSTER")
478 .possible_values(&ClusterType::STRINGS)
479 .required(true)
480 .help("The cluster to activate the feature on"),
481 )
482 .arg(
483 Arg::with_name("force")
484 .long("yolo")
485 .hidden(hidden_unless_forced())
486 .multiple(true)
487 .help("Override activation sanity checks. Don't use this flag"),
488 )
489 .arg(fee_payer_arg()),
490 )
491 .subcommand(
492 SubCommand::with_name("revoke")
493 .about("Revoke a pending runtime feature")
494 .arg(
495 Arg::with_name("feature")
496 .value_name("FEATURE_KEYPAIR")
497 .validator(is_valid_signer)
498 .index(1)
499 .required(true)
500 .help("The signer for the feature to revoke"),
501 )
502 .arg(
503 Arg::with_name("cluster")
504 .value_name("CLUSTER")
505 .possible_values(&ClusterType::STRINGS)
506 .required(true)
507 .help("The cluster to revoke the feature on"),
508 )
509 .arg(fee_payer_arg()),
510 ),
511 )
512 }
513}
514
515fn known_feature(feature: &Pubkey) -> Result<(), CliError> {
516 if FEATURE_NAMES.contains_key(feature) {
517 Ok(())
518 } else {
519 Err(CliError::BadParameter(format!(
520 "Unknown feature: {feature}"
521 )))
522 }
523}
524
525pub fn parse_feature_subcommand(
526 matches: &ArgMatches<'_>,
527 default_signer: &DefaultSigner,
528 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
529) -> Result<CliCommandInfo, CliError> {
530 let response = match matches.subcommand() {
531 ("activate", Some(matches)) => {
532 let cluster = value_t_or_exit!(matches, "cluster", ClusterType);
533 let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
534 let (fee_payer, fee_payer_pubkey) =
535 signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
536
537 let force = match matches.occurrences_of("force") {
538 2 => ForceActivation::Yes,
539 1 => ForceActivation::Almost,
540 _ => ForceActivation::No,
541 };
542
543 let signer_info = default_signer.generate_unique_signers(
544 vec![fee_payer, feature_signer],
545 matches,
546 wallet_manager,
547 )?;
548
549 let feature = feature.unwrap();
550
551 known_feature(&feature)?;
552
553 CliCommandInfo {
554 command: CliCommand::Feature(FeatureCliCommand::Activate {
555 feature,
556 cluster,
557 force,
558 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
559 }),
560 signers: signer_info.signers,
561 }
562 }
563 ("revoke", Some(matches)) => {
564 let cluster = value_t_or_exit!(matches, "cluster", ClusterType);
565 let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
566 let (fee_payer, fee_payer_pubkey) =
567 signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
568
569 let signer_info = default_signer.generate_unique_signers(
570 vec![fee_payer, feature_signer],
571 matches,
572 wallet_manager,
573 )?;
574
575 let feature = feature.unwrap();
576
577 known_feature(&feature)?;
578
579 CliCommandInfo {
580 command: CliCommand::Feature(FeatureCliCommand::Revoke {
581 feature,
582 cluster,
583 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
584 }),
585 signers: signer_info.signers,
586 }
587 }
588 ("status", Some(matches)) => {
589 let mut features = if let Some(features) = pubkeys_of(matches, "features") {
590 for feature in &features {
591 known_feature(feature)?;
592 }
593 features
594 } else {
595 FEATURE_NAMES.keys().cloned().collect()
596 };
597 let display_all =
598 matches.is_present("display_all") || features.len() < FEATURE_NAMES.len();
599 features.sort();
600 CliCommandInfo::without_signers(CliCommand::Feature(FeatureCliCommand::Status {
601 features,
602 display_all,
603 }))
604 }
605 _ => unreachable!(),
606 };
607 Ok(response)
608}
609
610pub fn process_feature_subcommand(
611 rpc_client: &RpcClient,
612 config: &CliConfig,
613 feature_subcommand: &FeatureCliCommand,
614) -> ProcessResult {
615 match feature_subcommand {
616 FeatureCliCommand::Status {
617 features,
618 display_all,
619 } => process_status(rpc_client, config, features, *display_all),
620 FeatureCliCommand::Activate {
621 feature,
622 cluster,
623 force,
624 fee_payer,
625 } => process_activate(rpc_client, config, *feature, *cluster, *force, *fee_payer),
626 FeatureCliCommand::Revoke {
627 feature,
628 cluster,
629 fee_payer,
630 } => process_revoke(rpc_client, config, *feature, *cluster, *fee_payer),
631 }
632}
633
634#[derive(Debug, Default)]
635struct FeatureSetStatsEntry {
636 stake_percent: f64,
637 rpc_nodes_percent: f32,
638 software_versions: Vec<CliVersion>,
639}
640
641#[derive(Debug, Default, Clone, Copy)]
642struct ClusterInfoStatsEntry {
643 stake_percent: f64,
644 rpc_percent: f32,
645}
646
647struct ClusterInfoStats {
648 stats_map: HashMap<(u32, CliVersion), ClusterInfoStatsEntry>,
649}
650
651impl ClusterInfoStats {
652 fn aggregate_by_feature_set(&self) -> HashMap<u32, FeatureSetStatsEntry> {
653 let mut feature_set_map = HashMap::<u32, FeatureSetStatsEntry>::new();
654 for ((feature_set, software_version), stats_entry) in &self.stats_map {
655 let map_entry = feature_set_map.entry(*feature_set).or_default();
656 map_entry.rpc_nodes_percent += stats_entry.rpc_percent;
657 map_entry.stake_percent += stats_entry.stake_percent;
658 map_entry.software_versions.push(software_version.clone());
659 }
660 for stats_entry in feature_set_map.values_mut() {
661 stats_entry
662 .software_versions
663 .sort_by(|l, r| l.cmp(r).reverse());
664 }
665 feature_set_map
666 }
667
668 fn aggregate_by_software_version(&self) -> HashMap<CliVersion, ClusterInfoStatsEntry> {
669 let mut software_version_map = HashMap::<CliVersion, ClusterInfoStatsEntry>::new();
670 for ((_feature_set, software_version), stats_entry) in &self.stats_map {
671 let map_entry = software_version_map
672 .entry(software_version.clone())
673 .or_default();
674 map_entry.rpc_percent += stats_entry.rpc_percent;
675 map_entry.stake_percent += stats_entry.stake_percent;
676 }
677 software_version_map
678 }
679}
680
681fn cluster_info_stats(rpc_client: &RpcClient) -> Result<ClusterInfoStats, ClientError> {
682 #[derive(Default)]
683 struct StatsEntry {
684 stake_lamports: u64,
685 rpc_nodes_count: u32,
686 }
687
688 let cluster_info_list = rpc_client
689 .get_cluster_nodes()?
690 .into_iter()
691 .map(|contact_info| {
692 (
693 contact_info.pubkey,
694 contact_info.feature_set,
695 contact_info.rpc.is_some(),
696 contact_info
697 .version
698 .and_then(|v| CliVersion::from_str(&v).ok())
699 .unwrap_or_else(CliVersion::unknown_version),
700 )
701 })
702 .collect::<Vec<_>>();
703
704 let vote_accounts = rpc_client.get_vote_accounts()?;
705
706 let mut total_active_stake: u64 = vote_accounts
707 .delinquent
708 .iter()
709 .map(|vote_account| vote_account.activated_stake)
710 .sum();
711
712 let vote_stakes = vote_accounts
713 .current
714 .into_iter()
715 .map(
716 |RpcVoteAccountInfo {
717 node_pubkey,
718 activated_stake,
719 ..
720 }| {
721 total_active_stake = total_active_stake.saturating_add(activated_stake);
722 (node_pubkey.clone(), activated_stake)
723 },
724 )
725 .collect::<HashMap<_, _>>();
726
727 let mut cluster_info_stats: HashMap<(u32, CliVersion), StatsEntry> = HashMap::new();
728 let mut total_rpc_nodes: u64 = 0;
729 for (node_id, feature_set, is_rpc, version) in cluster_info_list {
730 let feature_set = feature_set.unwrap_or(0);
731 let StatsEntry {
732 stake_lamports,
733 rpc_nodes_count,
734 } = cluster_info_stats
735 .entry((feature_set, version))
736 .or_default();
737
738 if let Some(vote_stake) = vote_stakes.get(&node_id) {
739 *stake_lamports = stake_lamports.saturating_add(*vote_stake);
740 }
741
742 if is_rpc {
743 *rpc_nodes_count = rpc_nodes_count.saturating_add(1);
744 total_rpc_nodes = total_rpc_nodes.saturating_add(1);
745 }
746 }
747
748 Ok(ClusterInfoStats {
749 stats_map: cluster_info_stats
750 .into_iter()
751 .filter_map(
752 |(
753 cluster_config,
754 StatsEntry {
755 stake_lamports,
756 rpc_nodes_count,
757 },
758 )| {
759 let stake_percent = (stake_lamports as f64 / total_active_stake as f64) * 100.;
760 let rpc_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.;
761 if stake_percent >= 0.001 || rpc_percent >= 0.001 {
762 Some((
763 cluster_config,
764 ClusterInfoStatsEntry {
765 stake_percent,
766 rpc_percent,
767 },
768 ))
769 } else {
770 None
771 }
772 },
773 )
774 .collect(),
775 })
776}
777
778fn feature_activation_allowed(
780 rpc_client: &RpcClient,
781 quiet: bool,
782) -> Result<
783 (
784 bool,
785 Option<CliClusterFeatureSets>,
786 Option<CliClusterSoftwareVersions>,
787 ),
788 ClientError,
789> {
790 let cluster_info_stats = cluster_info_stats(rpc_client)?;
791 let feature_set_stats = cluster_info_stats.aggregate_by_feature_set();
792
793 let tool_version = solana_version::Version::default();
794 let tool_feature_set = tool_version.feature_set;
795 let tool_software_version = CliVersion::from(semver::Version::new(
796 tool_version.major as u64,
797 tool_version.minor as u64,
798 tool_version.patch as u64,
799 ));
800 let (stake_allowed, rpc_allowed) = feature_set_stats
801 .get(&tool_feature_set)
802 .map(
803 |FeatureSetStatsEntry {
804 stake_percent,
805 rpc_nodes_percent,
806 ..
807 }| (*stake_percent >= 95., *rpc_nodes_percent >= 95.),
808 )
809 .unwrap_or_default();
810
811 let cluster_software_versions = if quiet {
812 None
813 } else {
814 let mut software_versions: Vec<_> = cluster_info_stats
815 .aggregate_by_software_version()
816 .into_iter()
817 .map(|(software_version, stats)| CliSoftwareVersionStats {
818 software_version,
819 stake_percent: stats.stake_percent,
820 rpc_percent: stats.rpc_percent,
821 })
822 .collect();
823 software_versions.sort_by(|l, r| l.software_version.cmp(&r.software_version).reverse());
824 Some(CliClusterSoftwareVersions {
825 software_versions,
826 tool_software_version,
827 })
828 };
829
830 let cluster_feature_sets = if quiet {
831 None
832 } else {
833 let mut feature_sets: Vec<_> = feature_set_stats
834 .into_iter()
835 .map(|(feature_set, stats_entry)| CliFeatureSetStats {
836 feature_set,
837 software_versions: stats_entry.software_versions,
838 rpc_percent: stats_entry.rpc_nodes_percent,
839 stake_percent: stats_entry.stake_percent,
840 })
841 .collect();
842
843 feature_sets.sort_by(|l, r| {
844 match l.software_versions[0]
845 .cmp(&r.software_versions[0])
846 .reverse()
847 {
848 Ordering::Equal => {
849 match l
850 .stake_percent
851 .partial_cmp(&r.stake_percent)
852 .unwrap()
853 .reverse()
854 {
855 Ordering::Equal => {
856 l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
857 }
858 o => o,
859 }
860 }
861 o => o,
862 }
863 });
864 Some(CliClusterFeatureSets {
865 tool_feature_set,
866 feature_sets,
867 stake_allowed,
868 rpc_allowed,
869 })
870 };
871
872 Ok((
873 stake_allowed && rpc_allowed,
874 cluster_feature_sets,
875 cluster_software_versions,
876 ))
877}
878
879pub(super) fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
880 from_account(&account).map(|feature| match feature.activated_at {
881 None => CliFeatureStatus::Pending,
882 Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
883 })
884}
885
886fn get_feature_status(
887 rpc_client: &RpcClient,
888 feature_id: &Pubkey,
889) -> Result<Option<CliFeatureStatus>, Box<dyn std::error::Error>> {
890 rpc_client
891 .get_account(feature_id)
892 .map(status_from_account)
893 .map_err(|e| e.into())
894}
895
896pub fn get_feature_is_active(
897 rpc_client: &RpcClient,
898 feature_id: &Pubkey,
899) -> Result<bool, Box<dyn std::error::Error>> {
900 get_feature_status(rpc_client, feature_id)
901 .map(|status| matches!(status, Some(CliFeatureStatus::Active(_))))
902}
903
904pub fn get_feature_activation_epoch(
905 rpc_client: &RpcClient,
906 feature_id: &Pubkey,
907) -> Result<Option<Epoch>, ClientError> {
908 rpc_client
909 .get_feature_activation_slot(feature_id)
910 .and_then(|activation_slot: Option<Slot>| {
911 rpc_client
912 .get_epoch_schedule()
913 .map(|epoch_schedule| (activation_slot, epoch_schedule))
914 })
915 .map(|(activation_slot, epoch_schedule)| {
916 activation_slot.map(|slot| epoch_schedule.get_epoch(slot))
917 })
918}
919
920fn process_status(
921 rpc_client: &RpcClient,
922 config: &CliConfig,
923 feature_ids: &[Pubkey],
924 display_all: bool,
925) -> ProcessResult {
926 let current_slot = rpc_client.get_slot()?;
927 let filter = if !display_all {
928 current_slot.checked_sub(DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS)
929 } else {
930 None
931 };
932 let mut inactive = false;
933 let mut features = vec![];
934 for feature_ids in feature_ids.chunks(MAX_MULTIPLE_ACCOUNTS) {
935 let mut feature_chunk = rpc_client
936 .get_multiple_accounts(feature_ids)?
937 .into_iter()
938 .zip(feature_ids)
939 .map(|(account, feature_id)| {
940 let feature_name = FEATURE_NAMES.get(feature_id).unwrap();
941 account
942 .and_then(status_from_account)
943 .map(|feature_status| CliFeature {
944 id: feature_id.to_string(),
945 description: feature_name.to_string(),
946 status: feature_status,
947 })
948 .unwrap_or_else(|| {
949 inactive = true;
950 CliFeature {
951 id: feature_id.to_string(),
952 description: feature_name.to_string(),
953 status: CliFeatureStatus::Inactive,
954 }
955 })
956 })
957 .filter(|feature| match (filter, &feature.status) {
958 (Some(min_activation), CliFeatureStatus::Active(activation)) => {
959 activation > &min_activation
960 }
961 _ => true,
962 })
963 .collect::<Vec<_>>();
964 features.append(&mut feature_chunk);
965 }
966
967 features.sort_unstable();
968
969 let (feature_activation_allowed, cluster_feature_sets, cluster_software_versions) =
970 feature_activation_allowed(rpc_client, features.len() <= 1)?;
971 let epoch_schedule = rpc_client.get_epoch_schedule()?;
972 let feature_set = CliFeatures {
973 features,
974 current_slot,
975 epoch_schedule,
976 feature_activation_allowed,
977 cluster_feature_sets,
978 cluster_software_versions,
979 inactive,
980 };
981 Ok(config.output_format.formatted_string(&feature_set))
982}
983
984fn process_activate(
985 rpc_client: &RpcClient,
986 config: &CliConfig,
987 feature_id: Pubkey,
988 cluster: ClusterType,
989 force: ForceActivation,
990 fee_payer: SignerIndex,
991) -> ProcessResult {
992 check_rpc_genesis_hash(&cluster, rpc_client)?;
993
994 let fee_payer = config.signers[fee_payer];
995 let account = rpc_client
996 .get_multiple_accounts(&[feature_id])?
997 .into_iter()
998 .next()
999 .unwrap();
1000
1001 if let Some(account) = account {
1002 if from_account(&account).is_some() {
1003 return Err(format!("{feature_id} has already been activated").into());
1004 }
1005 }
1006
1007 if !feature_activation_allowed(rpc_client, false)?.0 {
1008 match force {
1009 ForceActivation::Almost => {
1010 return Err(
1011 "Add force argument once more to override the sanity check to force feature \
1012 activation "
1013 .into(),
1014 )
1015 }
1016 ForceActivation::Yes => println!("FEATURE ACTIVATION FORCED"),
1017 ForceActivation::No => {
1018 return Err("Feature activation is not allowed at this time".into())
1019 }
1020 }
1021 }
1022
1023 let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?;
1024
1025 let blockhash = rpc_client.get_latest_blockhash()?;
1026 let (message, _) = resolve_spend_tx_and_check_account_balance(
1027 rpc_client,
1028 false,
1029 SpendAmount::Some(rent),
1030 &blockhash,
1031 &fee_payer.pubkey(),
1032 ComputeUnitLimit::Default,
1033 |lamports| {
1034 Message::new(
1035 &activate_with_lamports(&feature_id, &fee_payer.pubkey(), lamports),
1036 Some(&fee_payer.pubkey()),
1037 )
1038 },
1039 config.commitment,
1040 )?;
1041 let mut transaction = Transaction::new_unsigned(message);
1042 transaction.try_sign(&config.signers, blockhash)?;
1043
1044 println!(
1045 "Activating {} ({})",
1046 FEATURE_NAMES.get(&feature_id).unwrap(),
1047 feature_id
1048 );
1049 let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
1050 &transaction,
1051 config.commitment,
1052 config.send_transaction_config,
1053 );
1054 log_instruction_custom_error::<SystemError>(result, config)
1055}
1056
1057fn process_revoke(
1058 rpc_client: &RpcClient,
1059 config: &CliConfig,
1060 feature_id: Pubkey,
1061 cluster: ClusterType,
1062 fee_payer: SignerIndex,
1063) -> ProcessResult {
1064 check_rpc_genesis_hash(&cluster, rpc_client)?;
1065
1066 let fee_payer = config.signers[fee_payer];
1067 let account = rpc_client.get_account(&feature_id).ok();
1068
1069 match account.and_then(status_from_account) {
1070 Some(CliFeatureStatus::Pending) => (),
1071 Some(CliFeatureStatus::Active(..)) => {
1072 return Err(format!("{feature_id} has already been fully activated").into());
1073 }
1074 Some(CliFeatureStatus::Inactive) | None => {
1075 return Err(format!("{feature_id} has not been submitted for activation").into());
1076 }
1077 }
1078
1079 let blockhash = rpc_client.get_latest_blockhash()?;
1080 let (message, _) = resolve_spend_tx_and_check_account_balance(
1081 rpc_client,
1082 false,
1083 SpendAmount::Some(0),
1084 &blockhash,
1085 &fee_payer.pubkey(),
1086 ComputeUnitLimit::Default,
1087 |_lamports| {
1088 Message::new(
1089 &[RevokePendingActivation {
1090 feature: feature_id,
1091 incinerator: incinerator::id(),
1092 system_program: system_program::id(),
1093 }
1094 .instruction()],
1095 Some(&fee_payer.pubkey()),
1096 )
1097 },
1098 config.commitment,
1099 )?;
1100 let mut transaction = Transaction::new_unsigned(message);
1101 transaction.try_sign(&config.signers, blockhash)?;
1102
1103 println!(
1104 "Revoking {} ({})",
1105 FEATURE_NAMES.get(&feature_id).unwrap(),
1106 feature_id
1107 );
1108 let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
1109 &transaction,
1110 config.commitment,
1111 config.send_transaction_config,
1112 );
1113 log_instruction_custom_error::<SolanaFeatureGateError>(result, config)
1114}