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; #[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
417fn 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
777fn 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}