solana_cli/
feature.rs

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; // ~90days
41
42#[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
418/// Check an RPC's reported genesis hash against the ClusterType's known genesis hash
419fn 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
778// Feature activation is only allowed when 95% of the active stake is on the current feature set
779fn 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}