Skip to main content

solana_cli/
feature.rs

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