solana_cli/
feature.rs

1use {
2    crate::{
3        cli::{
4            log_instruction_custom_error, log_instruction_custom_error_to_str, CliCommand,
5            CliCommandInfo, CliConfig, CliError, ProcessResult,
6        },
7        spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
8    },
9    agave_feature_set::FEATURE_NAMES,
10    clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
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::{cli_version::CliVersion, QuietDisplay, VerboseDisplay},
19    solana_clock::{Epoch, Slot},
20    solana_cluster_type::ClusterType,
21    solana_epoch_schedule::EpochSchedule,
22    solana_feature_gate_interface::{
23        activate_with_lamports, error::FeatureGateError, from_account,
24        instruction::revoke_pending_activation, Feature,
25    },
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_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
418fn 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()?;
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 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),
619        FeatureCliCommand::Activate {
620            feature,
621            cluster,
622            force,
623            fee_payer,
624        } => process_activate(rpc_client, config, *feature, *cluster, *force, *fee_payer),
625        FeatureCliCommand::Revoke {
626            feature,
627            cluster,
628            fee_payer,
629        } => process_revoke(rpc_client, config, *feature, *cluster, *fee_payer),
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
680fn 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        .into_iter()
690        .map(|contact_info| {
691            (
692                contact_info.pubkey,
693                contact_info.feature_set,
694                contact_info.rpc.is_some(),
695                contact_info
696                    .version
697                    .and_then(|v| CliVersion::from_str(&v).ok())
698                    .unwrap_or_else(CliVersion::unknown_version),
699            )
700        })
701        .collect::<Vec<_>>();
702
703    let vote_accounts = rpc_client.get_vote_accounts()?;
704
705    let mut total_active_stake: u64 = vote_accounts
706        .delinquent
707        .iter()
708        .map(|vote_account| vote_account.activated_stake)
709        .sum();
710
711    let vote_stakes = vote_accounts
712        .current
713        .into_iter()
714        .map(
715            |RpcVoteAccountInfo {
716                 node_pubkey,
717                 activated_stake,
718                 ..
719             }| {
720                total_active_stake = total_active_stake.saturating_add(activated_stake);
721                (node_pubkey.clone(), activated_stake)
722            },
723        )
724        .collect::<HashMap<_, _>>();
725
726    let mut cluster_info_stats: HashMap<(u32, CliVersion), StatsEntry> = HashMap::new();
727    let mut total_rpc_nodes: u64 = 0;
728    for (node_id, feature_set, is_rpc, version) in cluster_info_list {
729        let feature_set = feature_set.unwrap_or(0);
730        let StatsEntry {
731            stake_lamports,
732            rpc_nodes_count,
733        } = cluster_info_stats
734            .entry((feature_set, version))
735            .or_default();
736
737        if let Some(vote_stake) = vote_stakes.get(&node_id) {
738            *stake_lamports = stake_lamports.saturating_add(*vote_stake);
739        }
740
741        if is_rpc {
742            *rpc_nodes_count = rpc_nodes_count.saturating_add(1);
743            total_rpc_nodes = total_rpc_nodes.saturating_add(1);
744        }
745    }
746
747    Ok(ClusterInfoStats {
748        stats_map: cluster_info_stats
749            .into_iter()
750            .filter_map(
751                |(
752                    cluster_config,
753                    StatsEntry {
754                        stake_lamports,
755                        rpc_nodes_count,
756                    },
757                )| {
758                    let stake_percent = (stake_lamports as f64 / total_active_stake as f64) * 100.;
759                    let rpc_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.;
760                    if stake_percent >= 0.001 || rpc_percent >= 0.001 {
761                        Some((
762                            cluster_config,
763                            ClusterInfoStatsEntry {
764                                stake_percent,
765                                rpc_percent,
766                            },
767                        ))
768                    } else {
769                        None
770                    }
771                },
772            )
773            .collect(),
774    })
775}
776
777// Feature activation is only allowed when 95% of the active stake is on the current feature set
778fn feature_activation_allowed(
779    rpc_client: &RpcClient,
780    quiet: bool,
781) -> Result<
782    (
783        bool,
784        Option<CliClusterFeatureSets>,
785        Option<CliClusterSoftwareVersions>,
786    ),
787    ClientError,
788> {
789    let cluster_info_stats = cluster_info_stats(rpc_client)?;
790    let feature_set_stats = cluster_info_stats.aggregate_by_feature_set();
791
792    let tool_version = solana_version::Version::default();
793    let tool_feature_set = tool_version.feature_set;
794    let tool_software_version = CliVersion::from(semver::Version::new(
795        tool_version.major as u64,
796        tool_version.minor as u64,
797        tool_version.patch as u64,
798    ));
799    let (stake_allowed, rpc_allowed) = feature_set_stats
800        .get(&tool_feature_set)
801        .map(
802            |FeatureSetStatsEntry {
803                 stake_percent,
804                 rpc_nodes_percent,
805                 ..
806             }| (*stake_percent >= 95., *rpc_nodes_percent >= 95.),
807        )
808        .unwrap_or_default();
809
810    let cluster_software_versions = if quiet {
811        None
812    } else {
813        let mut software_versions: Vec<_> = cluster_info_stats
814            .aggregate_by_software_version()
815            .into_iter()
816            .map(|(software_version, stats)| CliSoftwareVersionStats {
817                software_version,
818                stake_percent: stats.stake_percent,
819                rpc_percent: stats.rpc_percent,
820            })
821            .collect();
822        software_versions.sort_by(|l, r| l.software_version.cmp(&r.software_version).reverse());
823        Some(CliClusterSoftwareVersions {
824            software_versions,
825            tool_software_version,
826        })
827    };
828
829    let cluster_feature_sets = if quiet {
830        None
831    } else {
832        let mut feature_sets: Vec<_> = feature_set_stats
833            .into_iter()
834            .map(|(feature_set, stats_entry)| CliFeatureSetStats {
835                feature_set,
836                software_versions: stats_entry.software_versions,
837                rpc_percent: stats_entry.rpc_nodes_percent,
838                stake_percent: stats_entry.stake_percent,
839            })
840            .collect();
841
842        feature_sets.sort_by(|l, r| {
843            match l.software_versions[0]
844                .cmp(&r.software_versions[0])
845                .reverse()
846            {
847                Ordering::Equal => {
848                    match l
849                        .stake_percent
850                        .partial_cmp(&r.stake_percent)
851                        .unwrap()
852                        .reverse()
853                    {
854                        Ordering::Equal => {
855                            l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
856                        }
857                        o => o,
858                    }
859                }
860                o => o,
861            }
862        });
863        Some(CliClusterFeatureSets {
864            tool_feature_set,
865            feature_sets,
866            stake_allowed,
867            rpc_allowed,
868        })
869    };
870
871    Ok((
872        stake_allowed && rpc_allowed,
873        cluster_feature_sets,
874        cluster_software_versions,
875    ))
876}
877
878pub(super) fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
879    from_account(&account).map(|feature| match feature.activated_at {
880        None => CliFeatureStatus::Pending,
881        Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
882    })
883}
884
885fn get_feature_status(
886    rpc_client: &RpcClient,
887    feature_id: &Pubkey,
888) -> Result<Option<CliFeatureStatus>, Box<dyn std::error::Error>> {
889    rpc_client
890        .get_account(feature_id)
891        .map(status_from_account)
892        .map_err(|e| e.into())
893}
894
895pub 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        .map(|status| matches!(status, Some(CliFeatureStatus::Active(_))))
901}
902
903pub fn get_feature_activation_epoch(
904    rpc_client: &RpcClient,
905    feature_id: &Pubkey,
906) -> Result<Option<Epoch>, ClientError> {
907    rpc_client
908        .get_feature_activation_slot(feature_id)
909        .and_then(|activation_slot: Option<Slot>| {
910            rpc_client
911                .get_epoch_schedule()
912                .map(|epoch_schedule| (activation_slot, epoch_schedule))
913        })
914        .map(|(activation_slot, epoch_schedule)| {
915            activation_slot.map(|slot| epoch_schedule.get_epoch(slot))
916        })
917}
918
919fn process_status(
920    rpc_client: &RpcClient,
921    config: &CliConfig,
922    feature_ids: &[Pubkey],
923    display_all: bool,
924) -> ProcessResult {
925    let current_slot = rpc_client.get_slot()?;
926    let filter = if !display_all {
927        current_slot.checked_sub(DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS)
928    } else {
929        None
930    };
931    let mut inactive = false;
932    let mut features = vec![];
933    for feature_ids in feature_ids.chunks(MAX_MULTIPLE_ACCOUNTS) {
934        let mut feature_chunk = rpc_client
935            .get_multiple_accounts(feature_ids)?
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)?;
970    let epoch_schedule = rpc_client.get_epoch_schedule()?;
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
983fn 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)?;
992
993    let fee_payer = config.signers[fee_payer];
994    let account = rpc_client
995        .get_multiple_accounts(&[feature_id])?
996        .into_iter()
997        .next()
998        .unwrap();
999
1000    if let Some(account) = account {
1001        if from_account(&account).is_some() {
1002            return Err(format!("{feature_id} has already been activated").into());
1003        }
1004    }
1005
1006    if !feature_activation_allowed(rpc_client, false)?.0 {
1007        match force {
1008            ForceActivation::Almost => {
1009                return Err(
1010                    "Add force argument once more to override the sanity check to force feature \
1011                     activation "
1012                        .into(),
1013                )
1014            }
1015            ForceActivation::Yes => println!("FEATURE ACTIVATION FORCED"),
1016            ForceActivation::No => {
1017                return Err("Feature activation is not allowed at this time".into())
1018            }
1019        }
1020    }
1021
1022    let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?;
1023
1024    let blockhash = rpc_client.get_latest_blockhash()?;
1025    let (message, _) = resolve_spend_tx_and_check_account_balance(
1026        rpc_client,
1027        false,
1028        SpendAmount::Some(rent),
1029        &blockhash,
1030        &fee_payer.pubkey(),
1031        ComputeUnitLimit::Default,
1032        |lamports| {
1033            Message::new(
1034                &activate_with_lamports(&feature_id, &fee_payer.pubkey(), lamports),
1035                Some(&fee_payer.pubkey()),
1036            )
1037        },
1038        config.commitment,
1039    )?;
1040    let mut transaction = Transaction::new_unsigned(message);
1041    transaction.try_sign(&config.signers, blockhash)?;
1042
1043    println!(
1044        "Activating {} ({})",
1045        FEATURE_NAMES.get(&feature_id).unwrap(),
1046        feature_id
1047    );
1048    let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
1049        &transaction,
1050        config.commitment,
1051        config.send_transaction_config,
1052    );
1053    log_instruction_custom_error::<SystemError>(result, config)
1054}
1055
1056fn process_revoke(
1057    rpc_client: &RpcClient,
1058    config: &CliConfig,
1059    feature_id: Pubkey,
1060    cluster: ClusterType,
1061    fee_payer: SignerIndex,
1062) -> ProcessResult {
1063    check_rpc_genesis_hash(&cluster, rpc_client)?;
1064
1065    let fee_payer = config.signers[fee_payer];
1066    let account = rpc_client.get_account(&feature_id).ok();
1067
1068    match account.and_then(status_from_account) {
1069        Some(CliFeatureStatus::Pending) => (),
1070        Some(CliFeatureStatus::Active(..)) => {
1071            return Err(format!("{feature_id} has already been fully activated").into());
1072        }
1073        Some(CliFeatureStatus::Inactive) | None => {
1074            return Err(format!("{feature_id} has not been submitted for activation").into());
1075        }
1076    }
1077
1078    let blockhash = rpc_client.get_latest_blockhash()?;
1079    let (message, _) = resolve_spend_tx_and_check_account_balance(
1080        rpc_client,
1081        false,
1082        SpendAmount::Some(0),
1083        &blockhash,
1084        &fee_payer.pubkey(),
1085        ComputeUnitLimit::Default,
1086        |_lamports| {
1087            Message::new(
1088                &[revoke_pending_activation(&feature_id)],
1089                Some(&fee_payer.pubkey()),
1090            )
1091        },
1092        config.commitment,
1093    )?;
1094    let mut transaction = Transaction::new_unsigned(message);
1095    transaction.try_sign(&config.signers, blockhash)?;
1096
1097    println!(
1098        "Revoking {} ({})",
1099        FEATURE_NAMES.get(&feature_id).unwrap(),
1100        feature_id
1101    );
1102    let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
1103        &transaction,
1104        config.commitment,
1105        config.send_transaction_config,
1106    );
1107    log_instruction_custom_error_to_str::<FeatureGateError>(result, config)
1108}