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