1use {
2 crate::{
3 cli::{
4 CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
5 log_instruction_custom_error, log_instruction_custom_error_to_str,
6 },
7 spend_utils::{SpendAmount, resolve_spend_tx_and_check_account_balance},
8 },
9 agave_feature_set::FEATURE_NAMES,
10 clap::{App, AppSettings, Arg, ArgMatches, SubCommand, value_t_or_exit},
11 console::style,
12 serde::{Deserialize, Serialize},
13 solana_account::Account,
14 solana_clap_utils::{
15 compute_budget::ComputeUnitLimit, fee_payer::*, hidden_unless_forced, input_parsers::*,
16 input_validators::*, keypair::*,
17 },
18 solana_cli_output::{QuietDisplay, VerboseDisplay, cli_version::CliVersion},
19 solana_clock::{Epoch, Slot},
20 solana_cluster_type::ClusterType,
21 solana_epoch_schedule::EpochSchedule,
22 solana_feature_gate_interface::{
23 Feature, activate_with_lamports, error::FeatureGateError, from_account,
24 instruction::revoke_pending_activation,
25 },
26 solana_message::Message,
27 solana_pubkey::Pubkey,
28 solana_remote_wallet::remote_wallet::RemoteWalletManager,
29 solana_rpc_client::nonblocking::rpc_client::RpcClient,
30 solana_rpc_client_api::{
31 client_error::Error as ClientError, request::MAX_MULTIPLE_ACCOUNTS,
32 response::RpcVoteAccountInfo,
33 },
34 solana_system_interface::error::SystemError,
35 solana_transaction::Transaction,
36 std::{cmp::Ordering, collections::HashMap, fmt, rc::Rc, str::FromStr},
37};
38
39const DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS: Slot = 15_000_000; #[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
417async fn check_rpc_genesis_hash(
419 cluster_type: &ClusterType,
420 rpc_client: &RpcClient,
421) -> Result<(), Box<dyn std::error::Error>> {
422 if let Some(genesis_hash) = cluster_type.get_genesis_hash() {
423 let rpc_genesis_hash = rpc_client.get_genesis_hash().await?;
424 if rpc_genesis_hash != genesis_hash {
425 return Err(format!(
426 "The genesis hash for the specified cluster {cluster_type:?} does not match the \
427 genesis hash reported by the specified RPC. Cluster genesis hash: \
428 {genesis_hash}, RPC reported genesis hash: {rpc_genesis_hash}"
429 )
430 .into());
431 }
432 }
433 Ok(())
434}
435
436pub trait FeatureSubCommands {
437 fn feature_subcommands(self) -> Self;
438}
439
440impl FeatureSubCommands for App<'_, '_> {
441 fn feature_subcommands(self) -> Self {
442 self.subcommand(
443 SubCommand::with_name("feature")
444 .about("Runtime feature management")
445 .setting(AppSettings::SubcommandRequiredElseHelp)
446 .subcommand(
447 SubCommand::with_name("status")
448 .about("Query runtime feature status")
449 .arg(
450 Arg::with_name("features")
451 .value_name("ADDRESS")
452 .validator(is_valid_pubkey)
453 .index(1)
454 .multiple(true)
455 .help("Feature status to query [default: all known features]"),
456 )
457 .arg(
458 Arg::with_name("display_all")
459 .long("display-all")
460 .help("display all features regardless of age"),
461 ),
462 )
463 .subcommand(
464 SubCommand::with_name("activate")
465 .about("Activate a runtime feature")
466 .arg(
467 Arg::with_name("feature")
468 .value_name("FEATURE_KEYPAIR")
469 .validator(is_valid_signer)
470 .index(1)
471 .required(true)
472 .help("The signer for the feature to activate"),
473 )
474 .arg(
475 Arg::with_name("cluster")
476 .value_name("CLUSTER")
477 .possible_values(&ClusterType::STRINGS)
478 .required(true)
479 .help("The cluster to activate the feature on"),
480 )
481 .arg(
482 Arg::with_name("force")
483 .long("yolo")
484 .hidden(hidden_unless_forced())
485 .multiple(true)
486 .help("Override activation sanity checks. Don't use this flag"),
487 )
488 .arg(fee_payer_arg()),
489 )
490 .subcommand(
491 SubCommand::with_name("revoke")
492 .about("Revoke a pending runtime feature")
493 .arg(
494 Arg::with_name("feature")
495 .value_name("FEATURE_KEYPAIR")
496 .validator(is_valid_signer)
497 .index(1)
498 .required(true)
499 .help("The signer for the feature to revoke"),
500 )
501 .arg(
502 Arg::with_name("cluster")
503 .value_name("CLUSTER")
504 .possible_values(&ClusterType::STRINGS)
505 .required(true)
506 .help("The cluster to revoke the feature on"),
507 )
508 .arg(fee_payer_arg()),
509 ),
510 )
511 }
512}
513
514fn known_feature(feature: &Pubkey) -> Result<(), CliError> {
515 if FEATURE_NAMES.contains_key(feature) {
516 Ok(())
517 } else {
518 Err(CliError::BadParameter(format!(
519 "Unknown feature: {feature}"
520 )))
521 }
522}
523
524pub fn parse_feature_subcommand(
525 matches: &ArgMatches<'_>,
526 default_signer: &DefaultSigner,
527 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
528) -> Result<CliCommandInfo, CliError> {
529 let response = match matches.subcommand() {
530 ("activate", Some(matches)) => {
531 let cluster = value_t_or_exit!(matches, "cluster", ClusterType);
532 let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
533 let (fee_payer, fee_payer_pubkey) =
534 signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
535
536 let force = match matches.occurrences_of("force") {
537 2 => ForceActivation::Yes,
538 1 => ForceActivation::Almost,
539 _ => ForceActivation::No,
540 };
541
542 let signer_info = default_signer.generate_unique_signers(
543 vec![fee_payer, feature_signer],
544 matches,
545 wallet_manager,
546 )?;
547
548 let feature = feature.unwrap();
549
550 known_feature(&feature)?;
551
552 CliCommandInfo {
553 command: CliCommand::Feature(FeatureCliCommand::Activate {
554 feature,
555 cluster,
556 force,
557 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
558 }),
559 signers: signer_info.signers,
560 }
561 }
562 ("revoke", Some(matches)) => {
563 let cluster = value_t_or_exit!(matches, "cluster", ClusterType);
564 let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
565 let (fee_payer, fee_payer_pubkey) =
566 signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
567
568 let signer_info = default_signer.generate_unique_signers(
569 vec![fee_payer, feature_signer],
570 matches,
571 wallet_manager,
572 )?;
573
574 let feature = feature.unwrap();
575
576 known_feature(&feature)?;
577
578 CliCommandInfo {
579 command: CliCommand::Feature(FeatureCliCommand::Revoke {
580 feature,
581 cluster,
582 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
583 }),
584 signers: signer_info.signers,
585 }
586 }
587 ("status", Some(matches)) => {
588 let mut features = if let Some(features) = pubkeys_of(matches, "features") {
589 for feature in &features {
590 known_feature(feature)?;
591 }
592 features
593 } else {
594 FEATURE_NAMES.keys().cloned().collect()
595 };
596 let display_all =
597 matches.is_present("display_all") || features.len() < FEATURE_NAMES.len();
598 features.sort();
599 CliCommandInfo::without_signers(CliCommand::Feature(FeatureCliCommand::Status {
600 features,
601 display_all,
602 }))
603 }
604 _ => unreachable!(),
605 };
606 Ok(response)
607}
608
609pub async fn process_feature_subcommand(
610 rpc_client: &RpcClient,
611 config: &CliConfig<'_>,
612 feature_subcommand: &FeatureCliCommand,
613) -> ProcessResult {
614 match feature_subcommand {
615 FeatureCliCommand::Status {
616 features,
617 display_all,
618 } => process_status(rpc_client, config, features, *display_all).await,
619 FeatureCliCommand::Activate {
620 feature,
621 cluster,
622 force,
623 fee_payer,
624 } => process_activate(rpc_client, config, *feature, *cluster, *force, *fee_payer).await,
625 FeatureCliCommand::Revoke {
626 feature,
627 cluster,
628 fee_payer,
629 } => process_revoke(rpc_client, config, *feature, *cluster, *fee_payer).await,
630 }
631}
632
633#[derive(Debug, Default)]
634struct FeatureSetStatsEntry {
635 stake_percent: f64,
636 rpc_nodes_percent: f32,
637 software_versions: Vec<CliVersion>,
638}
639
640#[derive(Debug, Default, Clone, Copy)]
641struct ClusterInfoStatsEntry {
642 stake_percent: f64,
643 rpc_percent: f32,
644}
645
646struct ClusterInfoStats {
647 stats_map: HashMap<(u32, CliVersion), ClusterInfoStatsEntry>,
648}
649
650impl ClusterInfoStats {
651 fn aggregate_by_feature_set(&self) -> HashMap<u32, FeatureSetStatsEntry> {
652 let mut feature_set_map = HashMap::<u32, FeatureSetStatsEntry>::new();
653 for ((feature_set, software_version), stats_entry) in &self.stats_map {
654 let map_entry = feature_set_map.entry(*feature_set).or_default();
655 map_entry.rpc_nodes_percent += stats_entry.rpc_percent;
656 map_entry.stake_percent += stats_entry.stake_percent;
657 map_entry.software_versions.push(software_version.clone());
658 }
659 for stats_entry in feature_set_map.values_mut() {
660 stats_entry
661 .software_versions
662 .sort_by(|l, r| l.cmp(r).reverse());
663 }
664 feature_set_map
665 }
666
667 fn aggregate_by_software_version(&self) -> HashMap<CliVersion, ClusterInfoStatsEntry> {
668 let mut software_version_map = HashMap::<CliVersion, ClusterInfoStatsEntry>::new();
669 for ((_feature_set, software_version), stats_entry) in &self.stats_map {
670 let map_entry = software_version_map
671 .entry(software_version.clone())
672 .or_default();
673 map_entry.rpc_percent += stats_entry.rpc_percent;
674 map_entry.stake_percent += stats_entry.stake_percent;
675 }
676 software_version_map
677 }
678}
679
680async fn cluster_info_stats(rpc_client: &RpcClient) -> Result<ClusterInfoStats, ClientError> {
681 #[derive(Default)]
682 struct StatsEntry {
683 stake_lamports: u64,
684 rpc_nodes_count: u32,
685 }
686
687 let cluster_info_list = rpc_client
688 .get_cluster_nodes()
689 .await?
690 .into_iter()
691 .map(|contact_info| {
692 (
693 contact_info.pubkey,
694 contact_info.feature_set,
695 contact_info.rpc.is_some(),
696 contact_info
697 .version
698 .and_then(|v| CliVersion::from_str(&v).ok())
699 .unwrap_or_else(CliVersion::unknown_version),
700 )
701 })
702 .collect::<Vec<_>>();
703
704 let vote_accounts = rpc_client.get_vote_accounts().await?;
705
706 let mut total_active_stake: u64 = vote_accounts
707 .delinquent
708 .iter()
709 .map(|vote_account| vote_account.activated_stake)
710 .sum();
711
712 let vote_stakes = vote_accounts
713 .current
714 .into_iter()
715 .map(
716 |RpcVoteAccountInfo {
717 node_pubkey,
718 activated_stake,
719 ..
720 }| {
721 total_active_stake = total_active_stake.saturating_add(activated_stake);
722 (node_pubkey.clone(), activated_stake)
723 },
724 )
725 .collect::<HashMap<_, _>>();
726
727 let mut cluster_info_stats: HashMap<(u32, CliVersion), StatsEntry> = HashMap::new();
728 let mut total_rpc_nodes: u64 = 0;
729 for (node_id, feature_set, is_rpc, version) in cluster_info_list {
730 let feature_set = feature_set.unwrap_or(0);
731 let StatsEntry {
732 stake_lamports,
733 rpc_nodes_count,
734 } = cluster_info_stats
735 .entry((feature_set, version))
736 .or_default();
737
738 if let Some(vote_stake) = vote_stakes.get(&node_id) {
739 *stake_lamports = stake_lamports.saturating_add(*vote_stake);
740 }
741
742 if is_rpc {
743 *rpc_nodes_count = rpc_nodes_count.saturating_add(1);
744 total_rpc_nodes = total_rpc_nodes.saturating_add(1);
745 }
746 }
747
748 Ok(ClusterInfoStats {
749 stats_map: cluster_info_stats
750 .into_iter()
751 .filter_map(
752 |(
753 cluster_config,
754 StatsEntry {
755 stake_lamports,
756 rpc_nodes_count,
757 },
758 )| {
759 let stake_percent = (stake_lamports as f64 / total_active_stake as f64) * 100.;
760 let rpc_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.;
761 if stake_percent >= 0.001 || rpc_percent >= 0.001 {
762 Some((
763 cluster_config,
764 ClusterInfoStatsEntry {
765 stake_percent,
766 rpc_percent,
767 },
768 ))
769 } else {
770 None
771 }
772 },
773 )
774 .collect(),
775 })
776}
777
778async fn feature_activation_allowed(
780 rpc_client: &RpcClient,
781 quiet: bool,
782) -> Result<
783 (
784 bool,
785 Option<CliClusterFeatureSets>,
786 Option<CliClusterSoftwareVersions>,
787 ),
788 ClientError,
789> {
790 let cluster_info_stats = cluster_info_stats(rpc_client).await?;
791 let feature_set_stats = cluster_info_stats.aggregate_by_feature_set();
792
793 let tool_version = solana_version::Version::default();
794 let tool_feature_set = tool_version.feature_set();
795 let tool_software_version = CliVersion::from(tool_version.as_semver_version());
796 let (stake_allowed, rpc_allowed) = feature_set_stats
797 .get(&tool_feature_set)
798 .map(
799 |FeatureSetStatsEntry {
800 stake_percent,
801 rpc_nodes_percent,
802 ..
803 }| (*stake_percent >= 95., *rpc_nodes_percent >= 95.),
804 )
805 .unwrap_or_default();
806
807 let cluster_software_versions = if quiet {
808 None
809 } else {
810 let mut software_versions: Vec<_> = cluster_info_stats
811 .aggregate_by_software_version()
812 .into_iter()
813 .map(|(software_version, stats)| CliSoftwareVersionStats {
814 software_version,
815 stake_percent: stats.stake_percent,
816 rpc_percent: stats.rpc_percent,
817 })
818 .collect();
819 software_versions.sort_by(|l, r| l.software_version.cmp(&r.software_version).reverse());
820 Some(CliClusterSoftwareVersions {
821 software_versions,
822 tool_software_version,
823 })
824 };
825
826 let cluster_feature_sets = if quiet {
827 None
828 } else {
829 let mut feature_sets: Vec<_> = feature_set_stats
830 .into_iter()
831 .map(|(feature_set, stats_entry)| CliFeatureSetStats {
832 feature_set,
833 software_versions: stats_entry.software_versions,
834 rpc_percent: stats_entry.rpc_nodes_percent,
835 stake_percent: stats_entry.stake_percent,
836 })
837 .collect();
838
839 feature_sets.sort_by(|l, r| {
840 match l.software_versions[0]
841 .cmp(&r.software_versions[0])
842 .reverse()
843 {
844 Ordering::Equal => {
845 match l
846 .stake_percent
847 .partial_cmp(&r.stake_percent)
848 .unwrap()
849 .reverse()
850 {
851 Ordering::Equal => {
852 l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
853 }
854 o => o,
855 }
856 }
857 o => o,
858 }
859 });
860 Some(CliClusterFeatureSets {
861 tool_feature_set,
862 feature_sets,
863 stake_allowed,
864 rpc_allowed,
865 })
866 };
867
868 Ok((
869 stake_allowed && rpc_allowed,
870 cluster_feature_sets,
871 cluster_software_versions,
872 ))
873}
874
875pub(super) fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
876 from_account(&account).map(|feature| match feature.activated_at {
877 None => CliFeatureStatus::Pending,
878 Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
879 })
880}
881
882async fn get_feature_status(
883 rpc_client: &RpcClient,
884 feature_id: &Pubkey,
885) -> Result<Option<CliFeatureStatus>, Box<dyn std::error::Error>> {
886 rpc_client
887 .get_account(feature_id)
888 .await
889 .map(status_from_account)
890 .map_err(|e| e.into())
891}
892
893pub async fn get_feature_is_active(
894 rpc_client: &RpcClient,
895 feature_id: &Pubkey,
896) -> Result<bool, Box<dyn std::error::Error>> {
897 get_feature_status(rpc_client, feature_id)
898 .await
899 .map(|status| matches!(status, Some(CliFeatureStatus::Active(_))))
900}
901
902pub async fn get_feature_activation_epoch(
903 rpc_client: &RpcClient,
904 feature_id: &Pubkey,
905) -> Result<Option<Epoch>, ClientError> {
906 let activation_slot = rpc_client
907 .get_account(feature_id)
908 .await
909 .ok()
910 .and_then(|account| from_account(&account))
911 .and_then(|feature| feature.activated_at);
912 let epoch_schedule = rpc_client.get_epoch_schedule().await?;
913 Ok(activation_slot.map(|slot| epoch_schedule.get_epoch(slot)))
914}
915
916async fn process_status(
917 rpc_client: &RpcClient,
918 config: &CliConfig<'_>,
919 feature_ids: &[Pubkey],
920 display_all: bool,
921) -> ProcessResult {
922 let current_slot = rpc_client.get_slot().await?;
923 let filter = if !display_all {
924 current_slot.checked_sub(DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS)
925 } else {
926 None
927 };
928 let mut inactive = false;
929 let mut features = vec![];
930 for feature_ids in feature_ids.chunks(MAX_MULTIPLE_ACCOUNTS) {
931 let mut feature_chunk = rpc_client
932 .get_multiple_accounts(feature_ids)
933 .await?
934 .into_iter()
935 .zip(feature_ids)
936 .map(|(account, feature_id)| {
937 let feature_name = FEATURE_NAMES.get(feature_id).unwrap();
938 account
939 .and_then(status_from_account)
940 .map(|feature_status| CliFeature {
941 id: feature_id.to_string(),
942 description: feature_name.to_string(),
943 status: feature_status,
944 })
945 .unwrap_or_else(|| {
946 inactive = true;
947 CliFeature {
948 id: feature_id.to_string(),
949 description: feature_name.to_string(),
950 status: CliFeatureStatus::Inactive,
951 }
952 })
953 })
954 .filter(|feature| match (filter, &feature.status) {
955 (Some(min_activation), CliFeatureStatus::Active(activation)) => {
956 activation > &min_activation
957 }
958 _ => true,
959 })
960 .collect::<Vec<_>>();
961 features.append(&mut feature_chunk);
962 }
963
964 features.sort_unstable();
965
966 let (feature_activation_allowed, cluster_feature_sets, cluster_software_versions) =
967 feature_activation_allowed(rpc_client, features.len() <= 1).await?;
968 let epoch_schedule = rpc_client.get_epoch_schedule().await?;
969 let feature_set = CliFeatures {
970 features,
971 current_slot,
972 epoch_schedule,
973 feature_activation_allowed,
974 cluster_feature_sets,
975 cluster_software_versions,
976 inactive,
977 };
978 Ok(config.output_format.formatted_string(&feature_set))
979}
980
981async fn process_activate(
982 rpc_client: &RpcClient,
983 config: &CliConfig<'_>,
984 feature_id: Pubkey,
985 cluster: ClusterType,
986 force: ForceActivation,
987 fee_payer: SignerIndex,
988) -> ProcessResult {
989 check_rpc_genesis_hash(&cluster, rpc_client).await?;
990
991 let fee_payer = config.signers[fee_payer];
992 let account = rpc_client
993 .get_multiple_accounts(&[feature_id])
994 .await?
995 .into_iter()
996 .next()
997 .unwrap();
998
999 if let Some(account) = account {
1000 if from_account(&account).is_some() {
1001 return Err(format!("{feature_id} has already been activated").into());
1002 }
1003 }
1004
1005 if !feature_activation_allowed(rpc_client, false).await?.0 {
1006 match force {
1007 ForceActivation::Almost => {
1008 return Err(
1009 "Add force argument once more to override the sanity check to force feature \
1010 activation "
1011 .into(),
1012 );
1013 }
1014 ForceActivation::Yes => println!("FEATURE ACTIVATION FORCED"),
1015 ForceActivation::No => {
1016 return Err("Feature activation is not allowed at this time".into());
1017 }
1018 }
1019 }
1020
1021 let rent = rpc_client
1022 .get_minimum_balance_for_rent_exemption(Feature::size_of())
1023 .await?;
1024
1025 let blockhash = rpc_client.get_latest_blockhash().await?;
1026 let (message, _) = resolve_spend_tx_and_check_account_balance(
1027 rpc_client,
1028 false,
1029 SpendAmount::Some(rent),
1030 &blockhash,
1031 &fee_payer.pubkey(),
1032 ComputeUnitLimit::Default,
1033 |lamports| {
1034 Message::new(
1035 &activate_with_lamports(&feature_id, &fee_payer.pubkey(), lamports),
1036 Some(&fee_payer.pubkey()),
1037 )
1038 },
1039 config.commitment,
1040 )
1041 .await?;
1042 let mut transaction = Transaction::new_unsigned(message);
1043 transaction.try_sign(&config.signers, blockhash)?;
1044
1045 println!(
1046 "Activating {} ({})",
1047 FEATURE_NAMES.get(&feature_id).unwrap(),
1048 feature_id
1049 );
1050 let result = rpc_client
1051 .send_and_confirm_transaction_with_spinner_and_config(
1052 &transaction,
1053 config.commitment,
1054 config.send_transaction_config,
1055 )
1056 .await;
1057 log_instruction_custom_error::<SystemError>(result, config)
1058}
1059
1060async fn process_revoke(
1061 rpc_client: &RpcClient,
1062 config: &CliConfig<'_>,
1063 feature_id: Pubkey,
1064 cluster: ClusterType,
1065 fee_payer: SignerIndex,
1066) -> ProcessResult {
1067 check_rpc_genesis_hash(&cluster, rpc_client).await?;
1068
1069 let fee_payer = config.signers[fee_payer];
1070 let account = rpc_client.get_account(&feature_id).await.ok();
1071
1072 match account.and_then(status_from_account) {
1073 Some(CliFeatureStatus::Pending) => (),
1074 Some(CliFeatureStatus::Active(..)) => {
1075 return Err(format!("{feature_id} has already been fully activated").into());
1076 }
1077 Some(CliFeatureStatus::Inactive) | None => {
1078 return Err(format!("{feature_id} has not been submitted for activation").into());
1079 }
1080 }
1081
1082 let blockhash = rpc_client.get_latest_blockhash().await?;
1083 let (message, _) = resolve_spend_tx_and_check_account_balance(
1084 rpc_client,
1085 false,
1086 SpendAmount::Some(0),
1087 &blockhash,
1088 &fee_payer.pubkey(),
1089 ComputeUnitLimit::Default,
1090 |_lamports| {
1091 Message::new(
1092 &[revoke_pending_activation(&feature_id)],
1093 Some(&fee_payer.pubkey()),
1094 )
1095 },
1096 config.commitment,
1097 )
1098 .await?;
1099 let mut transaction = Transaction::new_unsigned(message);
1100 transaction.try_sign(&config.signers, blockhash)?;
1101
1102 println!(
1103 "Revoking {} ({})",
1104 FEATURE_NAMES.get(&feature_id).unwrap(),
1105 feature_id
1106 );
1107 let result = rpc_client
1108 .send_and_confirm_transaction_with_spinner_and_config(
1109 &transaction,
1110 config.commitment,
1111 config.send_transaction_config,
1112 )
1113 .await;
1114 log_instruction_custom_error_to_str::<FeatureGateError>(result, config)
1115}