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