From bc3aa53e029c4f86cda0f11a38c91b20e2e7ea8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2020 17:49:53 +0000 Subject: [PATCH] Runtime feature activation framework (bp #12376) (#12497) * Runtime feature activation framework (cherry picked from commit 93259f0bae814c8051a5c8bd2869daf584a825db) # Conflicts: # runtime/src/bank.rs * Add feature set identifier to gossiped version information (cherry picked from commit 35f5f9fc7b6b712a67f6d81135a839a02c165a00) # Conflicts: # Cargo.lock # version/Cargo.toml * Port instructions sysvar and secp256k1 program activation to FeatureSet (cherry picked from commit c10da16d7bf0677d91e2e218a73a7bf33afbfe9b) # Conflicts: # runtime/src/bank.rs # runtime/src/message_processor.rs * Add feature management commands (cherry picked from commit 93ed0ab2bbdf5c769131622bfc38bddde1b904ce) # Conflicts: # Cargo.lock # cli/Cargo.toml * Make test_process_rest_api less fragile (cherry picked from commit 7526bb96f3461c652d5136ce35ee82b8d56eec36) * Remove id field (cherry picked from commit cc6ba1e13106525b8f1dd25aadeb0626d3786e1d) * FeatureSet test (cherry picked from commit 92406cf9a048b07cdef9b2439344d1596e32bcd9) * cargo fmt (cherry picked from commit 199940d68323ec9a6d9284c9ba57c68d4334c91d) * cli review feedback (cherry picked from commit 3a2b8c5e5b5cccb058bc32abbb1e51d01d20c3e8) * Rename active() to is_active() (cherry picked from commit e39fac9f0197bdcb93ef222ba155edfdd89406b7) * Resolve merge conflicts * Remove continues from compute_active_feature_set() Co-authored-by: Michael Vines --- Cargo.lock | 2 + cli/Cargo.toml | 1 + cli/src/checks.rs | 2 +- cli/src/cli.rs | 11 +- cli/src/cluster_query.rs | 11 +- cli/src/feature.rs | 273 +++++++++++++++++++++++++ cli/src/lib.rs | 1 + client/src/rpc_response.rs | 5 + core/src/banking_stage.rs | 43 ++-- core/src/cluster_info.rs | 27 ++- core/src/crds_value.rs | 42 +++- core/src/rpc.rs | 21 +- core/src/rpc_service.rs | 6 +- core/src/transaction_status_service.rs | 5 +- genesis/src/main.rs | 1 + ledger/src/blockstore_processor.rs | 3 +- ledger/src/entry.rs | 25 +-- poh-bench/src/main.rs | 4 +- runtime/src/accounts.rs | 35 ++-- runtime/src/bank.rs | 145 +++++++++++-- runtime/src/builtins.rs | 55 ++--- runtime/src/feature.rs | 42 ++++ runtime/src/feature_set.rs | 69 +++++++ runtime/src/genesis_utils.rs | 22 +- runtime/src/lib.rs | 2 + runtime/src/message_processor.rs | 37 ++-- sdk/src/fee_calculator.rs | 70 +++---- sdk/src/secp256k1.rs | 22 -- sdk/src/sysvar/instructions.rs | 10 - stake-o-matic/src/main.rs | 2 +- version/Cargo.toml | 1 + version/src/lib.rs | 46 ++++- 32 files changed, 808 insertions(+), 233 deletions(-) create mode 100644 cli/src/feature.rs create mode 100644 runtime/src/feature.rs create mode 100644 runtime/src/feature_set.rs diff --git a/Cargo.lock b/Cargo.lock index 3015934ebb..b0ac0b02d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3572,6 +3572,7 @@ dependencies = [ "solana-logger 1.3.14", "solana-net-utils", "solana-remote-wallet", + "solana-runtime", "solana-sdk 1.3.14", "solana-stake-program", "solana-transaction-status", @@ -4711,6 +4712,7 @@ dependencies = [ "serde", "serde_derive", "solana-logger 1.3.14", + "solana-runtime", "solana-sdk 1.3.14", "solana-sdk-macro-frozen-abi 1.3.14", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c60d4b0fa0..74243d6ade 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -38,6 +38,7 @@ solana-faucet = { path = "../faucet", version = "1.3.14" } solana-logger = { path = "../logger", version = "1.3.14" } solana-net-utils = { path = "../net-utils", version = "1.3.14" } solana-remote-wallet = { path = "../remote-wallet", version = "1.3.14" } +solana-runtime = { path = "../runtime", version = "1.3.14" } solana-sdk = { path = "../sdk", version = "1.3.14" } solana-stake-program = { path = "../programs/stake", version = "1.3.14" } solana-transaction-status = { path = "../transaction-status", version = "1.3.14" } diff --git a/cli/src/checks.rs b/cli/src/checks.rs index e84add1f28..f1c6514878 100644 --- a/cli/src/checks.rs +++ b/cli/src/checks.rs @@ -67,7 +67,7 @@ pub fn check_account_for_multiple_fees_with_commitment( pub fn calculate_fee(fee_calculator: &FeeCalculator, messages: &[&Message]) -> u64 { messages .iter() - .map(|message| fee_calculator.calculate_fee(message, None)) + .map(|message| fee_calculator.calculate_fee(message)) .sum() } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 3125c91427..8eb0d57944 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,5 +1,6 @@ use crate::{ - checks::*, cluster_query::*, nonce::*, spend_utils::*, stake::*, validator_info::*, vote::*, + checks::*, cluster_query::*, feature::*, nonce::*, spend_utils::*, stake::*, validator_info::*, + vote::*, }; use chrono::prelude::*; use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -107,6 +108,7 @@ pub enum CliCommand { seed: String, program_id: Pubkey, }, + Feature(FeatureCliCommand), Fees, FirstAvailableBlock, GetBlock { @@ -554,6 +556,9 @@ pub fn parse_command( ("create-address-with-seed", Some(matches)) => { parse_create_address_with_seed(matches, default_signer, wallet_manager) } + ("feature", Some(matches)) => { + parse_feature_subcommand(matches, default_signer, wallet_manager) + } ("fees", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::Fees, signers: vec![], @@ -1709,6 +1714,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { program_id, } => process_create_address_with_seed(config, from_pubkey.as_ref(), &seed, &program_id), CliCommand::Fees => process_fees(&rpc_client, config), + CliCommand::Feature(feature_subcommand) => { + process_feature_subcommand(&rpc_client, config, feature_subcommand) + } CliCommand::FirstAvailableBlock => process_first_available_block(&rpc_client), CliCommand::GetBlock { slot } => process_get_block(&rpc_client, config, *slot), CliCommand::GetBlockTime { slot } => process_get_block_time(&rpc_client, config, *slot), @@ -2357,6 +2365,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' ), ) .cluster_query_subcommands() + .feature_subcommands() .nonce_subcommands() .stake_subcommands() .subcommand( diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 4b036406a6..1e04780130 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -17,7 +17,7 @@ use solana_client::{ pubsub_client::PubsubClient, rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient}, rpc_config::{RpcLargestAccountsConfig, RpcLargestAccountsFilter}, - rpc_response::{RpcVersionInfo, SlotInfo}, + rpc_response::SlotInfo, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ @@ -1314,12 +1314,9 @@ pub fn process_show_validators( for contact_info in rpc_client.get_cluster_nodes()? { node_version.insert( contact_info.pubkey, - RpcVersionInfo { - solana_core: contact_info - .version - .unwrap_or_else(|| unknown_version.clone()), - } - .to_string(), + contact_info + .version + .unwrap_or_else(|| unknown_version.clone()), ); } diff --git a/cli/src/feature.rs b/cli/src/feature.rs new file mode 100644 index 0000000000..4626c14958 --- /dev/null +++ b/cli/src/feature.rs @@ -0,0 +1,273 @@ +use crate::{ + cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, + spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, +}; +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use console::style; +use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*}; +use solana_client::{client_error::ClientError, rpc_client::RpcClient}; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; +use solana_runtime::{ + feature::{self, Feature}, + feature_set::FEATURE_NAMES, +}; +use solana_sdk::{message::Message, pubkey::Pubkey, system_instruction, transaction::Transaction}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum FeatureCliCommand { + Status { features: Vec }, + Activate { feature: Pubkey }, +} + +pub trait FeatureSubCommands { + fn feature_subcommands(self) -> Self; +} + +impl FeatureSubCommands for App<'_, '_> { + fn feature_subcommands(self) -> Self { + self.subcommand( + SubCommand::with_name("feature") + .about("Runtime feature management") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("status") + .about("Query runtime feature status") + .arg( + Arg::with_name("features") + .value_name("ADDRESS") + .validator(is_valid_pubkey) + .index(1) + .multiple(true) + .help("Feature status to query [default: all known features]"), + ), + ) + .subcommand( + SubCommand::with_name("activate") + .about("Activate a runtime feature") + .arg( + Arg::with_name("feature") + .value_name("FEATURE_KEYPAIR") + .validator(is_valid_signer) + .index(1) + .required(true) + .help("The signer for the feature to activate"), + ), + ), + ) + } +} + +fn known_feature(feature: &Pubkey) -> Result<(), CliError> { + if FEATURE_NAMES.contains_key(feature) { + Ok(()) + } else { + Err(CliError::BadParameter(format!( + "Unknown feature: {}", + feature + ))) + } +} + +pub fn parse_feature_subcommand( + matches: &ArgMatches<'_>, + default_signer: &DefaultSigner, + wallet_manager: &mut Option>, +) -> Result { + let response = match matches.subcommand() { + ("activate", Some(matches)) => { + let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?; + let mut signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; + signers.push(feature_signer.unwrap()); + let feature = feature.unwrap(); + + known_feature(&feature)?; + + CliCommandInfo { + command: CliCommand::Feature(FeatureCliCommand::Activate { feature }), + signers, + } + } + ("status", Some(matches)) => { + let mut features = if let Some(features) = pubkeys_of(matches, "features") { + for feature in &features { + known_feature(feature)?; + } + features + } else { + FEATURE_NAMES.keys().cloned().collect() + }; + features.sort(); + CliCommandInfo { + command: CliCommand::Feature(FeatureCliCommand::Status { features }), + signers: vec![], + } + } + _ => unreachable!(), + }; + Ok(response) +} + +pub fn process_feature_subcommand( + rpc_client: &RpcClient, + config: &CliConfig, + feature_subcommand: &FeatureCliCommand, +) -> ProcessResult { + match feature_subcommand { + FeatureCliCommand::Status { features } => process_status(rpc_client, features), + FeatureCliCommand::Activate { feature } => process_activate(rpc_client, config, *feature), + } +} + +// Feature activation is only allowed when 95% of the active stake is on the current feature set +fn feature_activation_allowed(rpc_client: &RpcClient) -> Result { + let my_feature_set = solana_version::Version::default().feature_set; + + let feature_set_map = rpc_client + .get_cluster_nodes()? + .into_iter() + .map(|contact_info| (contact_info.pubkey, contact_info.feature_set)) + .collect::>(); + + let vote_accounts = rpc_client.get_vote_accounts()?; + + let total_active_stake: u64 = vote_accounts + .current + .iter() + .chain(vote_accounts.delinquent.iter()) + .map(|vote_account| vote_account.activated_stake) + .sum(); + + let total_compatible_stake: u64 = vote_accounts + .current + .iter() + .map(|vote_account| { + if Some(&Some(my_feature_set)) == feature_set_map.get(&vote_account.node_pubkey) { + vote_account.activated_stake + } else { + 0 + } + }) + .sum(); + + Ok(total_compatible_stake * 100 / total_active_stake >= 95) +} + +fn process_status(rpc_client: &RpcClient, feature_ids: &[Pubkey]) -> ProcessResult { + if feature_ids.len() > 1 { + println!( + "{}", + style(format!( + "{:<44} {:<40} {}", + "Feature", "Description", "Status" + )) + .bold() + ); + } + + let mut inactive = false; + for (i, account) in rpc_client + .get_multiple_accounts(feature_ids)? + .into_iter() + .enumerate() + { + let feature_id = &feature_ids[i]; + let feature_name = FEATURE_NAMES.get(feature_id).unwrap(); + if let Some(account) = account { + if let Some(feature) = Feature::from_account(&account) { + match feature.activated_at { + None => println!( + "{:<44} {:<40} {}", + feature_id, + feature_name, + style("activation pending").yellow() + ), + Some(activation_slot) => { + println!( + "{:<44} {:<40} {}", + feature_id, + feature_name, + style(format!("active since slot {}", activation_slot)).green() + ); + } + } + continue; + } + } + inactive = true; + println!( + "{:<44} {:<40} {}", + feature_id, + feature_name, + style("inactive").red() + ); + } + + if inactive && !feature_activation_allowed(rpc_client)? { + println!( + "{}", + style("\nFeature activation is not allowed at this time") + .bold() + .red() + ); + } + Ok("".to_string()) +} + +fn process_activate( + rpc_client: &RpcClient, + config: &CliConfig, + feature_id: Pubkey, +) -> ProcessResult { + let account = rpc_client + .get_multiple_accounts(&[feature_id])? + .into_iter() + .next() + .unwrap(); + if let Some(account) = account { + if Feature::from_account(&account).is_some() { + return Err(format!("{} has already been activated", feature_id).into()); + } + } + + if !feature_activation_allowed(rpc_client)? { + return Err("Feature activation is not allowed at this time".into()); + } + + let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?; + + let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let (message, _) = resolve_spend_tx_and_check_account_balance( + rpc_client, + false, + SpendAmount::Some(rent), + &fee_calculator, + &config.signers[0].pubkey(), + |lamports| { + Message::new( + &[ + system_instruction::transfer( + &config.signers[0].pubkey(), + &feature_id, + lamports, + ), + system_instruction::allocate(&feature_id, Feature::size_of() as u64), + system_instruction::assign(&feature_id, &feature::id()), + ], + Some(&config.signers[0].pubkey()), + ) + }, + config.commitment, + )?; + let mut transaction = Transaction::new_unsigned(message); + transaction.try_sign(&config.signers, blockhash)?; + + println!( + "Activating {} ({})", + FEATURE_NAMES.get(&feature_id).unwrap(), + feature_id + ); + rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?; + Ok("".to_string()) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 0dfbbe841a..30f36b999a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -23,6 +23,7 @@ extern crate serde_derive; pub mod checks; pub mod cli; pub mod cluster_query; +pub mod feature; pub mod nonce; pub mod spend_utils; pub mod stake; diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index b98bc57a1f..94ef19c449 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -121,6 +121,7 @@ pub enum ReceivedSignatureResult { } #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] pub struct RpcContactInfo { /// Pubkey of the node as a base-58 string pub pubkey: String, @@ -132,6 +133,8 @@ pub struct RpcContactInfo { pub rpc: Option, /// Software version pub version: Option, + /// First 4 bytes of the FeatureSet identifier + pub feature_set: Option, } /// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot @@ -142,6 +145,8 @@ pub type RpcLeaderSchedule = HashMap>; pub struct RpcVersionInfo { /// The current version of solana-core pub solana_core: String, + /// first 4 bytes of the FeatureSet identifier + pub feature_set: Option, } impl fmt::Debug for RpcVersionInfo { diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 0714b648c6..dfbdadf04f 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -30,10 +30,9 @@ use solana_runtime::{ }; use solana_sdk::{ clock::{ - Epoch, Slot, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, MAX_TRANSACTION_FORWARDING_DELAY, + Slot, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, MAX_TRANSACTION_FORWARDING_DELAY, MAX_TRANSACTION_FORWARDING_DELAY_GPU, }, - genesis_config::ClusterType, poh_config::PohConfig, pubkey::Pubkey, timing::{duration_as_ms, timestamp}, @@ -737,8 +736,7 @@ impl BankingStage { fn transactions_from_packets( msgs: &Packets, transaction_indexes: &[usize], - cluster_type: ClusterType, - epoch: Epoch, + secp256k1_program_enabled: bool, ) -> (Vec, Vec) { let packets = Packets::new( transaction_indexes @@ -748,25 +746,24 @@ impl BankingStage { ); let transactions = Self::deserialize_transactions(&packets); - let maybe_secp_verified_transactions: Vec<_> = - if solana_sdk::secp256k1::is_enabled(cluster_type, epoch) { - transactions - .into_iter() - .map(|tx| { - if let Some(tx) = tx { - if tx.verify_precompiles().is_ok() { - Some(tx) - } else { - None - } + let maybe_secp_verified_transactions: Vec<_> = if secp256k1_program_enabled { + transactions + .into_iter() + .map(|tx| { + if let Some(tx) = tx { + if tx.verify_precompiles().is_ok() { + Some(tx) } else { None } - }) - .collect() - } else { - transactions - }; + } else { + None + } + }) + .collect() + } else { + transactions + }; Self::filter_transaction_indexes(maybe_secp_verified_transactions, &transaction_indexes) } @@ -820,8 +817,7 @@ impl BankingStage { let (transactions, transaction_to_packet_indexes) = Self::transactions_from_packets( msgs, &packet_indexes, - bank.cluster_type(), - bank.epoch(), + bank.secp256k1_program_enabled(), ); debug!( "bank: {} filtered transactions {}", @@ -874,8 +870,7 @@ impl BankingStage { let (transactions, transaction_to_packet_indexes) = Self::transactions_from_packets( msgs, &transaction_indexes, - bank.cluster_type(), - bank.epoch(), + bank.secp256k1_program_enabled(), ); let tx_count = transaction_to_packet_indexes.len(); diff --git a/core/src/cluster_info.rs b/core/src/cluster_info.rs index 08c43bdb73..925aef0a30 100644 --- a/core/src/cluster_info.rs +++ b/core/src/cluster_info.rs @@ -359,7 +359,7 @@ pub fn make_accounts_hashes_message( } // TODO These messages should go through the gpu pipeline for spam filtering -#[frozen_abi(digest = "CnN1gW2K2TRydGc84eYnQJwdTADPjQf6LJLZ4RP1QeoH")] +#[frozen_abi(digest = "3ZHQscZ9SgxKh45idzHv3hiagyyPRtDgeySmJn171PTi")] #[derive(Serialize, Deserialize, Debug, AbiEnumVisitor, AbiExample)] #[allow(clippy::large_enum_variant)] enum Protocol { @@ -573,7 +573,7 @@ impl ClusterInfo { } let ip_addr = node.gossip.ip(); Some(format!( - "{:15} {:2}| {:5} | {:44} |{:^15}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", + "{:15} {:2}| {:5} | {:44} |{:^9}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", if ContactInfo::is_valid_address(&node.gossip) { ip_addr.to_string() } else { @@ -605,8 +605,8 @@ impl ClusterInfo { format!( "IP Address |Age(ms)| Node identifier \ - | Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\ - ------------------+-------+----------------------------------------------+---------------+\ + | Version |Gossip| TPU |TPUfwd| TVU |TVUfwd|Repair|ServeR| RPC |PubSub|ShredVer\n\ + ------------------+-------+----------------------------------------------+---------+\ ------+------+------+------+------+------+------+------+------+--------\n\ {}\ Nodes: {}{}{}", @@ -894,7 +894,8 @@ impl ClusterInfo { } pub fn get_node_version(&self, pubkey: &Pubkey) -> Option { - self.gossip + let version = self + .gossip .read() .unwrap() .crds @@ -902,7 +903,21 @@ impl ClusterInfo { .get(&CrdsValueLabel::Version(*pubkey)) .map(|x| x.value.version()) .flatten() - .map(|version| version.version.clone()) + .map(|version| version.version.clone()); + + if version.is_none() { + self.gossip + .read() + .unwrap() + .crds + .table + .get(&CrdsValueLabel::LegacyVersion(*pubkey)) + .map(|x| x.value.legacy_version()) + .flatten() + .map(|version| version.version.clone().into()) + } else { + version + } } /// all validators that have a valid rpc port regardless of `shred_version`. diff --git a/core/src/crds_value.rs b/core/src/crds_value.rs index 52fb8ebaaf..de925bcffb 100644 --- a/core/src/crds_value.rs +++ b/core/src/crds_value.rs @@ -75,6 +75,7 @@ pub enum CrdsData { SnapshotHashes(SnapshotHash), AccountsHashes(SnapshotHash), EpochSlots(EpochSlotsIndex, EpochSlots), + LegacyVersion(LegacyVersion), Version(Version), } @@ -102,6 +103,7 @@ impl Sanitize for CrdsData { } val.sanitize() } + CrdsData::LegacyVersion(version) => version.sanitize(), CrdsData::Version(version) => version.sanitize(), } } @@ -208,6 +210,23 @@ impl Vote { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] +pub struct LegacyVersion { + pub from: Pubkey, + pub wallclock: u64, + pub version: solana_version::LegacyVersion, +} + +impl Sanitize for LegacyVersion { + fn sanitize(&self) -> Result<(), SanitizeError> { + if self.wallclock >= MAX_WALLCLOCK { + return Err(SanitizeError::ValueOutOfBounds); + } + self.from.sanitize()?; + self.version.sanitize() + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] pub struct Version { pub from: Pubkey, @@ -245,6 +264,7 @@ pub enum CrdsValueLabel { SnapshotHashes(Pubkey), EpochSlots(EpochSlotsIndex, Pubkey), AccountsHashes(Pubkey), + LegacyVersion(Pubkey), Version(Pubkey), } @@ -257,6 +277,7 @@ impl fmt::Display for CrdsValueLabel { CrdsValueLabel::SnapshotHashes(_) => write!(f, "SnapshotHash({})", self.pubkey()), CrdsValueLabel::EpochSlots(ix, _) => write!(f, "EpochSlots({}, {})", ix, self.pubkey()), CrdsValueLabel::AccountsHashes(_) => write!(f, "AccountsHashes({})", self.pubkey()), + CrdsValueLabel::LegacyVersion(_) => write!(f, "LegacyVersion({})", self.pubkey()), CrdsValueLabel::Version(_) => write!(f, "Version({})", self.pubkey()), } } @@ -271,6 +292,7 @@ impl CrdsValueLabel { CrdsValueLabel::SnapshotHashes(p) => *p, CrdsValueLabel::EpochSlots(_, p) => *p, CrdsValueLabel::AccountsHashes(p) => *p, + CrdsValueLabel::LegacyVersion(p) => *p, CrdsValueLabel::Version(p) => *p, } } @@ -300,6 +322,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(hash) => hash.wallclock, CrdsData::AccountsHashes(hash) => hash.wallclock, CrdsData::EpochSlots(_, p) => p.wallclock, + CrdsData::LegacyVersion(version) => version.wallclock, CrdsData::Version(version) => version.wallclock, } } @@ -311,6 +334,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(hash) => hash.from, CrdsData::AccountsHashes(hash) => hash.from, CrdsData::EpochSlots(_, p) => p.from, + CrdsData::LegacyVersion(version) => version.from, CrdsData::Version(version) => version.from, } } @@ -322,6 +346,7 @@ impl CrdsValue { CrdsData::SnapshotHashes(_) => CrdsValueLabel::SnapshotHashes(self.pubkey()), CrdsData::AccountsHashes(_) => CrdsValueLabel::AccountsHashes(self.pubkey()), CrdsData::EpochSlots(ix, _) => CrdsValueLabel::EpochSlots(*ix, self.pubkey()), + CrdsData::LegacyVersion(_) => CrdsValueLabel::LegacyVersion(self.pubkey()), CrdsData::Version(_) => CrdsValueLabel::Version(self.pubkey()), } } @@ -373,6 +398,13 @@ impl CrdsValue { } } + pub fn legacy_version(&self) -> Option<&LegacyVersion> { + match &self.data { + CrdsData::LegacyVersion(legacy_version) => Some(legacy_version), + _ => None, + } + } + pub fn version(&self) -> Option<&Version> { match &self.data { CrdsData::Version(version) => Some(version), @@ -387,6 +419,7 @@ impl CrdsValue { CrdsValueLabel::LowestSlot(*key), CrdsValueLabel::SnapshotHashes(*key), CrdsValueLabel::AccountsHashes(*key), + CrdsValueLabel::LegacyVersion(*key), CrdsValueLabel::Version(*key), ]; labels.extend((0..MAX_VOTES).map(|ix| CrdsValueLabel::Vote(ix, *key))); @@ -438,7 +471,7 @@ mod test { #[test] fn test_labels() { - let mut hits = [false; 5 + MAX_VOTES as usize + MAX_EPOCH_SLOTS as usize]; + let mut hits = [false; 6 + MAX_VOTES as usize + MAX_EPOCH_SLOTS as usize]; // this method should cover all the possible labels for v in &CrdsValue::record_labels(&Pubkey::default()) { match v { @@ -446,10 +479,11 @@ mod test { CrdsValueLabel::LowestSlot(_) => hits[1] = true, CrdsValueLabel::SnapshotHashes(_) => hits[2] = true, CrdsValueLabel::AccountsHashes(_) => hits[3] = true, - CrdsValueLabel::Version(_) => hits[4] = true, - CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 5] = true, + CrdsValueLabel::LegacyVersion(_) => hits[4] = true, + CrdsValueLabel::Version(_) => hits[5] = true, + CrdsValueLabel::Vote(ix, _) => hits[*ix as usize + 6] = true, CrdsValueLabel::EpochSlots(ix, _) => { - hits[*ix as usize + MAX_VOTES as usize + 5] = true + hits[*ix as usize + MAX_VOTES as usize + 6] = true } } } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 763465b57b..722843e136 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1950,14 +1950,19 @@ impl RpcSol for RpcSolImpl { if my_shred_version == contact_info.shred_version && ContactInfo::is_valid_address(&contact_info.gossip) { + let (version, feature_set) = + if let Some(version) = cluster_info.get_node_version(&contact_info.id) { + (Some(version.to_string()), Some(version.feature_set)) + } else { + (None, None) + }; Some(RpcContactInfo { pubkey: contact_info.id.to_string(), gossip: Some(contact_info.gossip), tpu: valid_address_or_none(&contact_info.tpu), rpc: valid_address_or_none(&contact_info.rpc), - version: cluster_info - .get_node_version(&contact_info.id) - .map(|v| v.to_string()), + version, + feature_set, }) } else { None // Exclude spy nodes @@ -2304,8 +2309,10 @@ impl RpcSol for RpcSolImpl { fn get_version(&self, _: Self::Metadata) -> Result { debug!("get_version rpc request received"); + let version = solana_version::Version::default(); Ok(RpcVersionInfo { - solana_core: solana_version::Version::default().to_string(), + solana_core: version.to_string(), + feature_set: Some(version.feature_set), }) } @@ -2853,7 +2860,7 @@ pub mod tests { .expect("actual response deserialization"); let expected = format!( - r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null}}],"id":1}}"#, + r#"{{"jsonrpc":"2.0","result":[{{"pubkey": "{}", "gossip": "127.0.0.1:1235", "tpu": "127.0.0.1:1234", "rpc": "127.0.0.1:{}", "version": null, "featureSet": null}}],"id":1}}"#, leader_pubkey, rpc_port::DEFAULT_RPC_PORT ); @@ -4404,10 +4411,12 @@ pub mod tests { let req = r#"{"jsonrpc":"2.0","id":1,"method":"getVersion"}"#; let res = io.handle_request_sync(&req, meta); + let version = solana_version::Version::default(); let expected = json!({ "jsonrpc": "2.0", "result": { - "solana-core": solana_version::version!().to_string() + "solana-core": version.to_string(), + "feature-set": version.feature_set, }, "id": 1 }); diff --git a/core/src/rpc_service.rs b/core/src/rpc_service.rs index d4b24db7ca..c9fa919a81 100644 --- a/core/src/rpc_service.rs +++ b/core/src/rpc_service.rs @@ -480,11 +480,7 @@ mod tests { assert_eq!(None, process_rest(&bank_forks, "not-a-supported-rest-api")); assert_eq!( - Some("0.000010127".to_string()), - process_rest(&bank_forks, "/v0/circulating-supply") - ); - assert_eq!( - Some("0.000010127".to_string()), + process_rest(&bank_forks, "/v0/circulating-supply"), process_rest(&bank_forks, "/v0/total-supply") ); } diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index 108f482f71..b1ce0b38b8 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -80,10 +80,7 @@ impl TransactionStatusService { _ => bank.get_fee_calculator(&transaction.message().recent_blockhash), } .expect("FeeCalculator must exist"); - let fee = fee_calculator.calculate_fee( - transaction.message(), - solana_sdk::secp256k1::get_fee_config(bank.cluster_type(), bank.epoch()), - ); + let fee = fee_calculator.calculate_fee(transaction.message()); let (writable_keys, readonly_keys) = transaction.message.get_account_keys_by_lock_type(); diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 6b1b900b43..5f27f42135 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -526,6 +526,7 @@ fn main() -> Result<(), Box> { } solana_stake_program::add_genesis_accounts(&mut genesis_config); + solana_runtime::genesis_utils::add_feature_accounts(&mut genesis_config); if let Some(files) = matches.values_of("primordial_accounts_file") { for file in files { diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 72c11c2cdb..d9e18e703c 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -681,8 +681,7 @@ pub fn confirm_slot( let entry_state = entries.start_verify( &progress.last_entry, recyclers.clone(), - bank.cluster_type(), - bank.epoch(), + bank.secp256k1_program_enabled(), ); if entry_state.status() == EntryVerificationStatus::Failure { warn!("Ledger proof of history failed at slot: {}", slot); diff --git a/ledger/src/entry.rs b/ledger/src/entry.rs index c181d6c5f8..40d247642c 100644 --- a/ledger/src/entry.rs +++ b/ledger/src/entry.rs @@ -17,8 +17,6 @@ use solana_perf::cuda_runtime::PinnedVec; use solana_perf::perf_libs; use solana_perf::recycler::Recycler; use solana_rayon_threadlimit::get_thread_count; -use solana_sdk::clock::Epoch; -use solana_sdk::genesis_config::ClusterType; use solana_sdk::hash::Hash; use solana_sdk::timing; use solana_sdk::transaction::Transaction; @@ -329,8 +327,7 @@ pub trait EntrySlice { &self, start_hash: &Hash, recyclers: VerifyRecyclers, - cluster_type: ClusterType, - epoch: Epoch, + secp256k1_program_enabled: bool, ) -> EntryVerificationState; fn verify(&self, start_hash: &Hash) -> bool; /// Checks that each entry tick has the correct number of hashes. Entry slices do not @@ -339,18 +336,13 @@ pub trait EntrySlice { fn verify_tick_hash_count(&self, tick_hash_count: &mut u64, hashes_per_tick: u64) -> bool; /// Counts tick entries fn tick_count(&self) -> u64; - fn verify_transaction_signatures(&self, cluster_type: ClusterType, epoch: Epoch) -> bool; + fn verify_transaction_signatures(&self, secp256k1_program_enabled: bool) -> bool; } impl EntrySlice for [Entry] { fn verify(&self, start_hash: &Hash) -> bool { - self.start_verify( - start_hash, - VerifyRecyclers::default(), - ClusterType::Development, - 0, - ) - .finish_verify(self) + self.start_verify(start_hash, VerifyRecyclers::default(), true) + .finish_verify(self) } fn verify_cpu_generic(&self, start_hash: &Hash) -> EntryVerificationState { @@ -497,14 +489,14 @@ impl EntrySlice for [Entry] { } } - fn verify_transaction_signatures(&self, cluster_type: ClusterType, epoch: Epoch) -> bool { + fn verify_transaction_signatures(&self, secp256k1_program_enabled: bool) -> bool { PAR_THREAD_POOL.with(|thread_pool| { thread_pool.borrow().install(|| { self.par_iter().all(|e| { e.transactions.par_iter().all(|transaction| { let sig_verify = transaction.verify().is_ok(); if sig_verify - && solana_sdk::secp256k1::is_enabled(cluster_type, epoch) + && secp256k1_program_enabled && transaction.verify_precompiles().is_err() { return false; @@ -520,11 +512,10 @@ impl EntrySlice for [Entry] { &self, start_hash: &Hash, recyclers: VerifyRecyclers, - cluster_type: ClusterType, - epoch: Epoch, + secp256k1_program_enabled: bool, ) -> EntryVerificationState { let start = Instant::now(); - let res = self.verify_transaction_signatures(cluster_type, epoch); + let res = self.verify_transaction_signatures(secp256k1_program_enabled); let transaction_duration_us = timing::duration_as_us(&start.elapsed()); if !res { return EntryVerificationState { diff --git a/poh-bench/src/main.rs b/poh-bench/src/main.rs index f462a5f22d..9e4662ab61 100644 --- a/poh-bench/src/main.rs +++ b/poh-bench/src/main.rs @@ -2,7 +2,7 @@ use clap::{crate_description, crate_name, value_t, App, Arg}; use solana_ledger::entry::{self, create_ticks, init_poh, EntrySlice, VerifyRecyclers}; use solana_measure::measure::Measure; use solana_perf::perf_libs; -use solana_sdk::{genesis_config::ClusterType, hash::hash}; +use solana_sdk::hash::hash; fn main() { solana_logger::setup(); @@ -118,7 +118,7 @@ fn main() { let recyclers = VerifyRecyclers::default(); for _ in 0..iterations { assert!(ticks[..num_entries] - .start_verify(&start_hash, recyclers.clone(), ClusterType::Development, 0) + .start_verify(&start_hash, recyclers.clone(), true) .finish_verify(&ticks[..num_entries])); } time.stop(); diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 2e74af0aa4..191fe78ed9 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -6,6 +6,7 @@ use crate::{ append_vec::StoredAccount, bank::{HashAgeKind, TransactionProcessResult}, blockhash_queue::BlockhashQueue, + feature_set::{self, FeatureSet}, nonce_utils, rent_collector::RentCollector, system_instruction_processor::{get_system_account_kind, SystemAccountKind}, @@ -17,7 +18,7 @@ use rayon::slice::ParallelSliceMut; use solana_sdk::{ account::Account, clock::{Epoch, Slot}, - fee_calculator::FeeCalculator, + fee_calculator::{FeeCalculator, FeeConfig}, genesis_config::ClusterType, hash::Hash, message::Message, @@ -72,11 +73,10 @@ pub enum AccountAddressFilter { impl Accounts { pub fn new(paths: Vec, cluster_type: &ClusterType) -> Self { Self { - slot: 0, - epoch: 0, accounts_db: Arc::new(AccountsDB::new(paths, cluster_type)), account_locks: Mutex::new(HashSet::new()), readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + ..Self::default() } } @@ -94,11 +94,10 @@ impl Accounts { pub(crate) fn new_empty(accounts_db: AccountsDB) -> Self { Self { - slot: 0, - epoch: 0, accounts_db: Arc::new(accounts_db), account_locks: Mutex::new(HashSet::new()), readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + ..Self::default() } } @@ -133,6 +132,7 @@ impl Accounts { fee: u64, error_counters: &mut ErrorCounters, rent_collector: &RentCollector, + feature_set: &FeatureSet, ) -> Result<(TransactionAccounts, TransactionRent)> { // Copy all the accounts let message = tx.message(); @@ -150,10 +150,8 @@ impl Accounts { payer_index = Some(i); } - if solana_sdk::sysvar::instructions::is_enabled( - self.epoch, - self.accounts_db.cluster_type.unwrap(), - ) && solana_sdk::sysvar::instructions::check_id(key) + if solana_sdk::sysvar::instructions::check_id(key) + && feature_set.is_active(&feature_set::instructions_sysvar_enabled::id()) { if message.is_writable(i) { return Err(TransactionError::InvalidAccountIndex); @@ -300,11 +298,17 @@ impl Accounts { hash_queue: &BlockhashQueue, error_counters: &mut ErrorCounters, rent_collector: &RentCollector, + feature_set: &FeatureSet, ) -> Vec<(Result, Option)> { //PERF: hold the lock to scan for the references, but not to clone the accounts //TODO: two locks usually leads to deadlocks, should this be one structure? let accounts_index = self.accounts_db.accounts_index.read().unwrap(); let storage = self.accounts_db.storage.read().unwrap(); + + let fee_config = FeeConfig { + secp256k1_program_enabled: feature_set + .is_active(&feature_set::secp256k1_program_enabled::id()), + }; OrderedIterator::new(txs, txs_iteration_order) .zip(lock_results.into_iter()) .map(|etx| match etx { @@ -318,13 +322,7 @@ impl Accounts { .cloned(), }; let fee = if let Some(fee_calculator) = fee_calculator { - fee_calculator.calculate_fee( - tx.message(), - solana_sdk::secp256k1::get_fee_config( - self.accounts_db.cluster_type.unwrap(), - self.epoch, - ), - ) + fee_calculator.calculate_fee_with_config(tx.message(), &fee_config) } else { return (Err(TransactionError::BlockhashNotFound), hash_age_kind); }; @@ -337,6 +335,7 @@ impl Accounts { fee, error_counters, rent_collector, + feature_set, ); let (accounts, rents) = match load_res { Ok((a, r)) => (a, r), @@ -888,6 +887,7 @@ mod tests { &hash_queue, error_counters, rent_collector, + &FeatureSet::default(), ) } @@ -1024,7 +1024,7 @@ mod tests { ); let fee_calculator = FeeCalculator::new(10); - assert_eq!(fee_calculator.calculate_fee(tx.message(), None), 10); + assert_eq!(fee_calculator.calculate_fee(tx.message()), 10); let loaded_accounts = load_accounts_with_fee(tx, &accounts, &fee_calculator, &mut error_counters); @@ -1832,6 +1832,7 @@ mod tests { &hash_queue, &mut error_counters, &rent_collector, + &FeatureSet::default(), ) } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index abf4279ec7..15834eeace 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -10,8 +10,10 @@ use crate::{ accounts_db::{ErrorCounters, SnapshotStorages}, accounts_index::Ancestors, blockhash_queue::BlockhashQueue, - builtins::get_builtins, + builtins::*, epoch_stakes::{EpochStakes, NodeVoteAccounts}, + feature::Feature, + feature_set::{self, FeatureSet}, instruction_recorder::InstructionRecorder, log_collector::LogCollector, message_processor::{Executors, MessageProcessor}, @@ -41,7 +43,7 @@ use solana_sdk::{ }, epoch_info::EpochInfo, epoch_schedule::EpochSchedule, - fee_calculator::{FeeCalculator, FeeRateGovernor}, + fee_calculator::{FeeCalculator, FeeConfig, FeeRateGovernor}, genesis_config::{ClusterType, GenesisConfig}, hard_forks::HardForks, hash::{extend_and_hash, hashv, Hash}, @@ -526,6 +528,8 @@ pub struct Bank { cached_executors: Arc>, transaction_debug_keys: Option>>, + + pub feature_set: Arc, } impl Default for BlockhashQueue { @@ -651,6 +655,7 @@ impl Bank { rewards_pool_pubkeys: parent.rewards_pool_pubkeys.clone(), cached_executors: parent.cached_executors.clone(), transaction_debug_keys: parent.transaction_debug_keys.clone(), + feature_set: parent.feature_set.clone(), }; datapoint_info!( @@ -756,6 +761,7 @@ impl Bank { rewards_pool_pubkeys: new(), cached_executors: Arc::new(RwLock::new(CachedExecutors::new(MAX_CACHED_EXECUTORS))), transaction_debug_keys: debug_keys, + feature_set: new(), }; bank.finish_init(genesis_config); @@ -1607,6 +1613,7 @@ impl Bank { &self.blockhash_queue.read().unwrap(), error_counters, &self.rent_collector, + &self.feature_set, ) } fn check_age( @@ -2045,8 +2052,7 @@ impl Bank { log_collector.clone(), executors.clone(), instruction_recorders.as_deref(), - self.cluster_type(), - self.epoch(), + &self.feature_set, ); Self::compile_recorded_instructions( @@ -2127,6 +2133,11 @@ impl Bank { ) -> Vec> { let hash_queue = self.blockhash_queue.read().unwrap(); let mut fees = 0; + + let fee_config = FeeConfig { + secp256k1_program_enabled: self.secp256k1_program_enabled(), + }; + let results = OrderedIterator::new(txs, iteration_order) .zip(executed.iter()) .map(|((_, tx), (res, hash_age_kind))| { @@ -2143,10 +2154,7 @@ impl Bank { }; let fee_calculator = fee_calculator.ok_or(TransactionError::BlockhashNotFound)?; - let fee = fee_calculator.calculate_fee( - tx.message(), - solana_sdk::secp256k1::get_fee_config(self.cluster_type(), self.epoch()), - ); + let fee = fee_calculator.calculate_fee_with_config(tx.message(), &fee_config); let message = tx.message(); match *res { @@ -3486,10 +3494,16 @@ impl Bank { consumed_budget.saturating_sub(budget_recovery_delta) } + pub fn secp256k1_program_enabled(&self) -> bool { + self.feature_set + .is_active(&feature_set::secp256k1_program_enabled::id()) + } + // This is called from snapshot restore AND for each epoch boundary // The entire code path herein must be idempotent fn apply_feature_activations(&mut self, init_finish_or_warp: bool, initiate_callback: bool) { - self.ensure_builtins(init_finish_or_warp); + let new_feature_activations = self.compute_active_feature_set(!init_finish_or_warp); + self.ensure_builtins(init_finish_or_warp, &new_feature_activations); self.reinvoke_entered_epoch_callback(initiate_callback); self.recheck_cross_program_support(); self.recheck_compute_budget(); @@ -3497,14 +3511,66 @@ impl Bank { self.ensure_no_storage_rewards_pool(); } - fn ensure_builtins(&mut self, init_or_warp: bool) { - for (program, start_epoch) in get_builtins(self.cluster_type()) { + // Compute the active feature set based on the current bank state, and return the set of newly activated features + fn compute_active_feature_set(&mut self, allow_new_activations: bool) -> HashSet { + let mut active = self.feature_set.active.clone(); + let mut inactive = HashSet::new(); + let mut newly_activated = HashSet::new(); + let slot = self.slot(); + + for feature_id in &self.feature_set.inactive { + let mut activated = false; + if let Some(mut account) = self.get_account(feature_id) { + if let Some(mut feature) = Feature::from_account(&account) { + match feature.activated_at { + None => { + if allow_new_activations { + // Feature has been requested, activate it now + feature.activated_at = Some(slot); + if feature.to_account(&mut account).is_some() { + self.store_account(feature_id, &account); + } + newly_activated.insert(*feature_id); + activated = true; + info!("Feature {} activated at slot {}", feature_id, slot); + } + } + Some(activation_slot) => { + if slot >= activation_slot { + // Feature is already active + activated = true; + } + } + } + } + } + if activated { + active.insert(*feature_id); + } else { + inactive.insert(*feature_id); + } + } + + self.feature_set = Arc::new(FeatureSet { active, inactive }); + newly_activated + } + + fn ensure_builtins(&mut self, init_or_warp: bool, new_feature_activations: &HashSet) { + for (program, start_epoch) in get_cluster_builtins(self.cluster_type()) { let should_populate = init_or_warp && self.epoch() >= start_epoch || !init_or_warp && self.epoch() == start_epoch; if should_populate { self.add_builtin(&program.name, program.id, program.entrypoint); } } + + for (program, feature) in get_feature_builtins() { + let should_populate = init_or_warp && self.feature_set.is_active(&feature) + || !init_or_warp && new_feature_activations.contains(&feature); + if should_populate { + self.add_builtin(&program.name, program.id, program.entrypoint); + } + } } fn reinvoke_entered_epoch_callback(&mut self, initiate: bool) { @@ -8508,7 +8574,7 @@ mod tests { .collect::>(); consumed_budgets.sort(); // consumed_budgets represents the count of alive accounts in the three slots 0,1,2 - assert_eq!(consumed_budgets, vec![0, 1, 10]); + assert_eq!(consumed_budgets, vec![0, 1, 9]); } #[test] @@ -8973,4 +9039,59 @@ mod tests { assert!(executors.borrow().executors.contains_key(&key3)); assert!(executors.borrow().executors.contains_key(&key4)); } + + #[test] + fn test_compute_active_feature_set() { + let (genesis_config, _mint_keypair) = create_genesis_config(100_000); + let bank0 = Arc::new(Bank::new(&genesis_config)); + let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + + let test_feature = "TestFeature11111111111111111111111111111111" + .parse::() + .unwrap(); + let mut feature_set = FeatureSet::default(); + feature_set.inactive.insert(test_feature); + bank.feature_set = Arc::new(feature_set.clone()); + + let new_activations = bank.compute_active_feature_set(true); + assert!(new_activations.is_empty()); + assert!(!bank.feature_set.is_active(&test_feature)); + + // Depositing into the `test_feature` account should do nothing + bank.deposit(&test_feature, 42); + let new_activations = bank.compute_active_feature_set(true); + assert!(new_activations.is_empty()); + assert!(!bank.feature_set.is_active(&test_feature)); + + // Request `test_feature` activation + let feature = Feature::default(); + assert_eq!(feature.activated_at, None); + bank.store_account(&test_feature, &feature.create_account(42)); + + // Run `compute_active_feature_set` disallowing new activations + let new_activations = bank.compute_active_feature_set(false); + assert!(new_activations.is_empty()); + assert!(!bank.feature_set.is_active(&test_feature)); + let feature = Feature::from_account(&bank.get_account(&test_feature).expect("get_account")) + .expect("from_account"); + assert_eq!(feature.activated_at, None); + + // Run `compute_active_feature_set` allowing new activations + let new_activations = bank.compute_active_feature_set(true); + assert_eq!(new_activations.len(), 1); + assert!(bank.feature_set.is_active(&test_feature)); + let feature = Feature::from_account(&bank.get_account(&test_feature).expect("get_account")) + .expect("from_account"); + assert_eq!(feature.activated_at, Some(1)); + + // Reset the bank's feature set + bank.feature_set = Arc::new(feature_set); + assert!(!bank.feature_set.is_active(&test_feature)); + + // Running `compute_active_feature_set` will not cause new activations, but + // `test_feature` is now be active + let new_activations = bank.compute_active_feature_set(true); + assert!(new_activations.is_empty()); + assert!(bank.feature_set.is_active(&test_feature)); + } } diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index d53a69307a..b7ac385d46 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -1,18 +1,21 @@ use crate::{ bank::{Builtin, Entrypoint}, - system_instruction_processor, + feature_set, system_instruction_processor, }; use solana_sdk::{ clock::{Epoch, GENESIS_EPOCH}, genesis_config::ClusterType, + pubkey::Pubkey, system_program, }; use log::*; -/// The entire set of available builtin programs that should be active at the given cluster_type -pub fn get_builtins(cluster_type: ClusterType) -> Vec<(Builtin, Epoch)> { - trace!("get_builtins: {:?}", cluster_type); +/// Builtin programs that should be active for the given cluster_type +/// +/// Old style. Use `get_feature_builtins()` instead +pub fn get_cluster_builtins(cluster_type: ClusterType) -> Vec<(Builtin, Epoch)> { + trace!("get_cluster_builtins: {:?}", cluster_type); let mut builtins = vec![]; builtins.extend( @@ -46,8 +49,8 @@ pub fn get_builtins(cluster_type: ClusterType) -> Vec<(Builtin, Epoch)> { // repurpose Testnet for test_get_builtins because the Development is overloaded... #[cfg(test)] if cluster_type == ClusterType::Testnet { + use solana_sdk::account::KeyedAccount; use solana_sdk::instruction::InstructionError; - use solana_sdk::{account::KeyedAccount, pubkey::Pubkey}; use std::str::FromStr; fn mock_ix_processor( _pubkey: &Pubkey, @@ -57,35 +60,33 @@ pub fn get_builtins(cluster_type: ClusterType) -> Vec<(Builtin, Epoch)> { Err(InstructionError::Custom(42)) } let program_id = Pubkey::from_str("7saCc6X5a2syoYANA5oUUnPZLcLMfKoSjiDhFU5fbpoK").unwrap(); - builtins.extend(vec![( + builtins.push(( Builtin::new("mock", program_id, Entrypoint::Program(mock_ix_processor)), 2, - )]); + )); } - let secp256k1_builtin = Builtin::new( - "secp256k1_program", - solana_sdk::secp256k1_program::id(), - Entrypoint::Program(solana_secp256k1_program::process_instruction), - ); - let secp_epoch = solana_sdk::secp256k1::is_enabled_epoch(cluster_type); - builtins.push((secp256k1_builtin, secp_epoch)); - builtins } +/// Builtin programs that are activated dynamically by feature +pub fn get_feature_builtins() -> Vec<(Builtin, Pubkey)> { + vec![( + Builtin::new( + "secp256k1_program", + solana_sdk::secp256k1_program::id(), + Entrypoint::Program(solana_secp256k1_program::process_instruction), + ), + feature_set::secp256k1_program_enabled::id(), + )] +} + #[cfg(test)] mod tests { use super::*; use crate::bank::Bank; - use solana_sdk::{ - genesis_config::{create_genesis_config, ClusterType}, - pubkey::Pubkey, - }; - - use std::collections::HashSet; - use std::str::FromStr; - use std::sync::Arc; + use solana_sdk::genesis_config::create_genesis_config; + use std::{collections::HashSet, str::FromStr, sync::Arc}; fn do_test_uniqueness(builtins: Vec<(Builtin, Epoch)>) { let mut unique_ids = HashSet::new(); @@ -101,10 +102,10 @@ mod tests { #[test] fn test_uniqueness() { - do_test_uniqueness(get_builtins(ClusterType::Development)); - do_test_uniqueness(get_builtins(ClusterType::Devnet)); - do_test_uniqueness(get_builtins(ClusterType::Testnet)); - do_test_uniqueness(get_builtins(ClusterType::MainnetBeta)); + do_test_uniqueness(get_cluster_builtins(ClusterType::Development)); + do_test_uniqueness(get_cluster_builtins(ClusterType::Devnet)); + do_test_uniqueness(get_cluster_builtins(ClusterType::Testnet)); + do_test_uniqueness(get_cluster_builtins(ClusterType::MainnetBeta)); } #[test] diff --git a/runtime/src/feature.rs b/runtime/src/feature.rs new file mode 100644 index 0000000000..202b693c69 --- /dev/null +++ b/runtime/src/feature.rs @@ -0,0 +1,42 @@ +use solana_sdk::{account::Account, clock::Slot}; + +solana_sdk::declare_id!("Feature111111111111111111111111111111111111"); + +/// The `Feature` struct is the on-chain representation of a runtime feature. +/// +/// Feature activation is accomplished by: +/// 1. Activation is requested by the feature authority, who issues a transaction to create the +/// feature account. The newly created feature account will have the value of +/// `Feature::default()` +/// 2. When the next epoch is entered the runtime will check for new activation requests and +/// active them. When this occurs, the activation slot is recorded in the feature account +/// +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct Feature { + pub activated_at: Option, +} + +impl Feature { + pub fn size_of() -> usize { + bincode::serialized_size(&Self { + activated_at: Some(Slot::MAX), + }) + .unwrap() as usize + } + pub fn from_account(account: &Account) -> Option { + if account.owner != id() { + None + } else { + bincode::deserialize(&account.data).ok() + } + } + pub fn to_account(&self, account: &mut Account) -> Option<()> { + bincode::serialize_into(&mut account.data[..], self).ok() + } + pub fn create_account(&self, lamports: u64) -> Account { + let data_len = Self::size_of().max(bincode::serialized_size(self).unwrap() as usize); + let mut account = Account::new(lamports, data_len, &id()); + self.to_account(&mut account).unwrap(); + account + } +} diff --git a/runtime/src/feature_set.rs b/runtime/src/feature_set.rs new file mode 100644 index 0000000000..6d17f2ddbc --- /dev/null +++ b/runtime/src/feature_set.rs @@ -0,0 +1,69 @@ +use lazy_static::lazy_static; +use solana_sdk::{ + hash::{Hash, Hasher}, + pubkey::Pubkey, +}; +use std::collections::{HashMap, HashSet}; + +pub mod instructions_sysvar_enabled { + solana_sdk::declare_id!("EnvhHCLvg55P7PDtbvR1NwuTuAeodqpusV3MR5QEK8gs"); +} + +pub mod secp256k1_program_enabled { + solana_sdk::declare_id!("E3PHP7w8kB7np3CTQ1qQ2tW3KCtjRSXBQgW9vM2mWv2Y"); +} + +lazy_static! { + /// Map of feature identifiers to user-visible description + pub static ref FEATURE_NAMES: HashMap = [ + (instructions_sysvar_enabled::id(), "instructions sysvar"), + (secp256k1_program_enabled::id(), "secp256k1 program") + /*************** ADD NEW FEATURES HERE ***************/ + ] + .iter() + .cloned() + .collect(); + + /// Unique identifier of the current software's feature set + pub static ref ID: Hash = { + let mut hasher = Hasher::default(); + let mut feature_ids = FEATURE_NAMES.keys().collect::>(); + feature_ids.sort(); + for feature in feature_ids { + hasher.hash(feature.as_ref()); + } + hasher.result() + }; +} + +/// `FeatureSet` holds the set of currently active/inactive runtime features +#[derive(AbiExample, Clone)] +pub struct FeatureSet { + pub active: HashSet, + pub inactive: HashSet, +} + +impl FeatureSet { + pub fn is_active(&self, feature_id: &Pubkey) -> bool { + self.active.contains(feature_id) + } +} + +impl Default for FeatureSet { + fn default() -> Self { + // All features disabled + Self { + active: HashSet::new(), + inactive: FEATURE_NAMES.keys().cloned().collect(), + } + } +} + +impl FeatureSet { + pub fn enabled() -> Self { + Self { + active: FEATURE_NAMES.keys().cloned().collect(), + inactive: HashSet::new(), + } + } +} diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index badb51feae..4b52a08b8e 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -1,7 +1,8 @@ +use crate::{feature::Feature, feature_set::FeatureSet}; use solana_sdk::{ account::Account, fee_calculator::FeeRateGovernor, - genesis_config::GenesisConfig, + genesis_config::{ClusterType, GenesisConfig}, pubkey::Pubkey, rent::Rent, signature::{Keypair, Signer}, @@ -107,6 +108,24 @@ pub fn create_genesis_config_with_leader( ) } +pub fn add_feature_accounts(genesis_config: &mut GenesisConfig) { + if genesis_config.cluster_type == ClusterType::Development { + // Activate all features at genesis in development mode + for feature_id in FeatureSet::default().inactive { + let feature = Feature { + activated_at: Some(0), + }; + genesis_config.accounts.insert( + feature_id, + feature.create_account(std::cmp::max( + genesis_config.rent.minimum_balance(Feature::size_of()), + 1, + )), + ); + } + } +} + pub fn create_genesis_config_with_leader_ex( mint_lamports: u64, bootstrap_validator_pubkey: &Pubkey, @@ -164,6 +183,7 @@ pub fn create_genesis_config_with_leader_ex( }; solana_stake_program::add_genesis_accounts(&mut genesis_config); + add_feature_accounts(&mut genesis_config); GenesisConfigInfo { genesis_config, diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 47104635e3..137f1d44af 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,6 +12,8 @@ pub mod bloom; pub mod builtins; pub mod commitment; pub mod epoch_stakes; +pub mod feature; +pub mod feature_set; pub mod genesis_utils; pub mod hardened_unpack; pub mod instruction_recorder; diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 8dd67e1e3a..43a48d8fba 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -1,6 +1,9 @@ use crate::{ - instruction_recorder::InstructionRecorder, log_collector::LogCollector, - native_loader::NativeLoader, rent_collector::RentCollector, + feature_set::{self, FeatureSet}, + instruction_recorder::InstructionRecorder, + log_collector::LogCollector, + native_loader::NativeLoader, + rent_collector::RentCollector, }; use log::*; use serde::{Deserialize, Serialize}; @@ -11,7 +14,6 @@ use solana_sdk::{ ComputeBudget, ComputeMeter, ErasedProcessInstruction, ErasedProcessInstructionWithContext, Executor, InvokeContext, Logger, ProcessInstruction, ProcessInstructionWithContext, }, - genesis_config::ClusterType, instruction::{CompiledInstruction, Instruction, InstructionError}, message::Message, native_loader, @@ -679,12 +681,11 @@ impl MessageProcessor { executors: Rc>, instruction_recorder: Option, instruction_index: usize, - cluster_type: ClusterType, - epoch: Epoch, + feature_set: &FeatureSet, ) -> Result<(), InstructionError> { // Fixup the special instructions key if present // before the account pre-values are taken care of - if solana_sdk::sysvar::instructions::is_enabled(epoch, cluster_type) { + if feature_set.is_active(&feature_set::instructions_sysvar_enabled::id()) { for (i, key) in message.account_keys.iter().enumerate() { if solana_sdk::sysvar::instructions::check_id(key) { let mut mut_account_ref = accounts[i].borrow_mut(); @@ -736,8 +737,7 @@ impl MessageProcessor { log_collector: Option>, executors: Rc>, instruction_recorders: Option<&[InstructionRecorder]>, - cluster_type: ClusterType, - epoch: Epoch, + feature_set: &FeatureSet, ) -> Result<(), TransactionError> { for (instruction_index, instruction) in message.instructions.iter().enumerate() { let instruction_recorder = instruction_recorders @@ -753,8 +753,7 @@ impl MessageProcessor { executors.clone(), instruction_recorder, instruction_index, - cluster_type, - epoch, + feature_set, ) .map_err(|err| TransactionError::InstructionError(instruction_index as u8, err))?; } @@ -1349,8 +1348,7 @@ mod tests { None, executors.clone(), None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!(result, Ok(())); assert_eq!(accounts[0].borrow().lamports, 100); @@ -1373,8 +1371,7 @@ mod tests { None, executors.clone(), None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!( result, @@ -1401,8 +1398,7 @@ mod tests { None, executors, None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!( result, @@ -1512,8 +1508,7 @@ mod tests { None, executors.clone(), None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!( result, @@ -1540,8 +1535,7 @@ mod tests { None, executors.clone(), None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!(result, Ok(())); @@ -1565,8 +1559,7 @@ mod tests { None, executors, None, - ClusterType::Development, - 0, + &FeatureSet::default(), ); assert_eq!(result, Ok(())); assert_eq!(accounts[0].borrow().lamports, 80); diff --git a/sdk/src/fee_calculator.rs b/sdk/src/fee_calculator.rs index 8210d8439b..5abc497099 100644 --- a/sdk/src/fee_calculator.rs +++ b/sdk/src/fee_calculator.rs @@ -19,9 +19,16 @@ impl Default for FeeCalculator { } } -#[derive(Clone)] pub struct FeeConfig { - pub is_secp256k1_enabled: bool, + pub secp256k1_program_enabled: bool, +} + +impl Default for FeeConfig { + fn default() -> Self { + Self { + secp256k1_program_enabled: true, + } + } } impl FeeCalculator { @@ -31,27 +38,27 @@ impl FeeCalculator { } } - // extra_config: None == everything enabled - pub fn calculate_fee(&self, message: &Message, extra_config: Option) -> u64 { - let is_secp256k1_enabled = match extra_config { - Some(config) => config.is_secp256k1_enabled, - None => true, - }; - let mut num_secp_signatures: u64 = 0; - if is_secp256k1_enabled { + pub fn calculate_fee(&self, message: &Message) -> u64 { + self.calculate_fee_with_config(message, &FeeConfig::default()) + } + + pub fn calculate_fee_with_config(&self, message: &Message, fee_config: &FeeConfig) -> u64 { + let mut num_secp256k1_signatures: u64 = 0; + if fee_config.secp256k1_program_enabled { for instruction in &message.instructions { let program_index = instruction.program_id_index as usize; // Transaction may not be sanitized here if program_index < message.account_keys.len() { let id = message.account_keys[program_index]; if secp256k1_program::check_id(&id) && !instruction.data.is_empty() { - num_secp_signatures += instruction.data[0] as u64; + num_secp256k1_signatures += instruction.data[0] as u64; } } } } + self.lamports_per_signature - * (u64::from(message.header.num_required_signatures) + num_secp_signatures) + * (u64::from(message.header.num_required_signatures) + num_secp256k1_signatures) } } @@ -182,9 +189,7 @@ impl FeeRateGovernor { /// create a FeeCalculator based on current cluster signature throughput pub fn create_fee_calculator(&self) -> FeeCalculator { - FeeCalculator { - lamports_per_signature: self.lamports_per_signature, - } + FeeCalculator::new(self.lamports_per_signature) } } @@ -207,34 +212,25 @@ mod tests { #[test] fn test_fee_calculator_calculate_fee() { - let fee_config = Some(FeeConfig { - is_secp256k1_enabled: true, - }); // Default: no fee. let message = Message::default(); - assert_eq!( - FeeCalculator::default().calculate_fee(&message, fee_config.clone()), - 0 - ); + assert_eq!(FeeCalculator::default().calculate_fee(&message), 0); // No signature, no fee. - assert_eq!(FeeCalculator::new(1).calculate_fee(&message, fee_config), 0); + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 0); - let fee_config = Some(FeeConfig { - is_secp256k1_enabled: false, - }); // One signature, a fee. let pubkey0 = Pubkey::new(&[0; 32]); let pubkey1 = Pubkey::new(&[1; 32]); let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); let message = Message::new(&[ix0], Some(&pubkey0)); - assert_eq!(FeeCalculator::new(2).calculate_fee(&message, fee_config), 2); + assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 2); // Two signatures, double the fee. let ix0 = system_instruction::transfer(&pubkey0, &pubkey1, 1); let ix1 = system_instruction::transfer(&pubkey1, &pubkey0, 1); let message = Message::new(&[ix0, ix1], Some(&pubkey0)); - assert_eq!(FeeCalculator::new(2).calculate_fee(&message, None), 4); + assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 4); } #[test] @@ -262,21 +258,21 @@ mod tests { ], Some(&pubkey0), ); - let fee_config = Some(FeeConfig { - is_secp256k1_enabled: true, - }); + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 2); assert_eq!( - FeeCalculator::new(1).calculate_fee(&message, fee_config.clone()), - 2 + FeeCalculator::new(1).calculate_fee_with_config( + &message, + &FeeConfig { + secp256k1_program_enabled: false + } + ), + 1 ); secp_instruction.data = vec![0]; secp_instruction2.data = vec![10]; let message = Message::new(&[ix0, secp_instruction, secp_instruction2], Some(&pubkey0)); - assert_eq!( - FeeCalculator::new(1).calculate_fee(&message, fee_config), - 11 - ); + assert_eq!(FeeCalculator::new(1).calculate_fee(&message), 11); } #[test] diff --git a/sdk/src/secp256k1.rs b/sdk/src/secp256k1.rs index 6eddcc7a8e..4a62f5897c 100644 --- a/sdk/src/secp256k1.rs +++ b/sdk/src/secp256k1.rs @@ -1,28 +1,6 @@ -use crate::clock::{Epoch, GENESIS_EPOCH}; -use crate::fee_calculator::FeeConfig; -use crate::genesis_config::ClusterType; use digest::Digest; use serde_derive::{Deserialize, Serialize}; -pub fn get_fee_config(cluster_type: ClusterType, epoch: Epoch) -> Option { - Some(FeeConfig { - is_secp256k1_enabled: is_enabled(cluster_type, epoch), - }) -} - -pub fn is_enabled_epoch(cluster_type: ClusterType) -> Epoch { - match cluster_type { - ClusterType::Development => GENESIS_EPOCH, - ClusterType::Testnet => u64::MAX, - ClusterType::MainnetBeta => u64::MAX, - ClusterType::Devnet => u64::MAX, - } -} - -pub fn is_enabled(cluster_type: ClusterType, epoch: Epoch) -> bool { - epoch >= is_enabled_epoch(cluster_type) -} - #[derive(Debug)] pub enum Secp256k1Error { InvalidSignature, diff --git a/sdk/src/sysvar/instructions.rs b/sdk/src/sysvar/instructions.rs index e68499f845..e4efc75cf3 100644 --- a/sdk/src/sysvar/instructions.rs +++ b/sdk/src/sysvar/instructions.rs @@ -11,16 +11,6 @@ crate::declare_sysvar_id!("Sysvar1nstructions1111111111111111111111111", Instruc impl Sysvar for Instructions {} -#[cfg(not(feature = "program"))] -use crate::clock::Epoch; -#[cfg(not(feature = "program"))] -use crate::genesis_config::ClusterType; - -#[cfg(not(feature = "program"))] -pub fn is_enabled(_epoch: Epoch, cluster_type: ClusterType) -> bool { - cluster_type == ClusterType::Development -} - pub fn load_current_index(data: &[u8]) -> u16 { let mut instr_fixed_data = [0u8; 2]; let len = data.len(); diff --git a/stake-o-matic/src/main.rs b/stake-o-matic/src/main.rs index 96cb7c10c4..3aa3bdda57 100644 --- a/stake-o-matic/src/main.rs +++ b/stake-o-matic/src/main.rs @@ -430,7 +430,7 @@ fn transact( info!("{} transactions to send", transactions.len()); let required_fee = transactions.iter().fold(0, |fee, (transaction, _)| { - fee + fee_calculator.calculate_fee(&transaction.message, None) + fee + fee_calculator.calculate_fee(&transaction.message) }); info!("Required fee: {} SOL", lamports_to_sol(required_fee)); if required_fee > authorized_staker_balance { diff --git a/version/Cargo.toml b/version/Cargo.toml index b7daa82bca..3e85ab5573 100644 --- a/version/Cargo.toml +++ b/version/Cargo.toml @@ -14,6 +14,7 @@ serde = "1.0.112" serde_derive = "1.0.103" solana-logger = { path = "../logger", version = "1.3.14" } solana-sdk = { path = "../sdk", version = "1.3.14" } +solana-runtime = { path = "../runtime", version = "1.3.14" } solana-sdk-macro-frozen-abi = { path = "../sdk/macro-frozen-abi", version = "1.3.14" } [lib] diff --git a/version/src/lib.rs b/version/src/lib.rs index 7e74c60579..6b21555de5 100644 --- a/version/src/lib.rs +++ b/version/src/lib.rs @@ -3,19 +3,42 @@ extern crate serde_derive; use serde_derive::{Deserialize, Serialize}; use solana_sdk::sanitize::Sanitize; -use std::fmt; - +use std::{convert::TryInto, fmt}; #[macro_use] extern crate solana_sdk_macro_frozen_abi; +// Older version structure used earlier 1.3.x releases #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, AbiExample)] -pub struct Version { +pub struct LegacyVersion { major: u16, minor: u16, patch: u16, commit: Option, // first 4 bytes of the sha1 commit hash } +impl Sanitize for LegacyVersion {} + +#[derive(Serialize, Deserialize, Clone, PartialEq, AbiExample)] +pub struct Version { + pub major: u16, + pub minor: u16, + pub patch: u16, + pub commit: Option, // first 4 bytes of the sha1 commit hash + pub feature_set: u32, // first 4 bytes of the FeatureSet identifier +} + +impl From for Version { + fn from(legacy_version: LegacyVersion) -> Self { + Self { + major: legacy_version.major, + minor: legacy_version.minor, + patch: legacy_version.patch, + commit: legacy_version.commit, + feature_set: 0, + } + } +} + fn compute_commit(sha1: Option<&'static str>) -> Option { let sha1 = sha1?; if sha1.len() < 8 { @@ -27,27 +50,40 @@ fn compute_commit(sha1: Option<&'static str>) -> Option { impl Default for Version { fn default() -> Self { + let feature_set = u32::from_le_bytes( + solana_runtime::feature_set::ID.as_ref()[..4] + .try_into() + .unwrap(), + ); Self { major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), commit: compute_commit(option_env!("CI_COMMIT")), + feature_set, } } } impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch,) + } +} + +impl fmt::Debug for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{}.{}.{} {}", + "{}.{}.{} (src:{}; feat:{})", self.major, self.minor, self.patch, match self.commit { None => "devbuild".to_string(), Some(commit) => format!("{:08x}", commit), - } + }, + self.feature_set, ) } }