From 93259f0bae814c8051a5c8bd2869daf584a825db Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Mon, 21 Sep 2020 14:03:35 -0700 Subject: [PATCH] Runtime feature activation framework --- genesis/src/main.rs | 1 + runtime/src/bank.rs | 53 +++++++++++++++++++++++++++++++ runtime/src/feature.rs | 55 +++++++++++++++++++++++++++++++++ runtime/src/feature_set.rs | 60 ++++++++++++++++++++++++++++++++++++ runtime/src/genesis_utils.rs | 22 ++++++++++++- runtime/src/lib.rs | 2 ++ 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 runtime/src/feature.rs create mode 100644 runtime/src/feature_set.rs 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/runtime/src/bank.rs b/runtime/src/bank.rs index 51c762de07..37b3fd21b9 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -13,6 +13,8 @@ use crate::{ builtins::get_builtins, epoch_stakes::{EpochStakes, NodeVoteAccounts}, instruction_recorder::InstructionRecorder, + feature::Feature, + feature_set::{FeatureSet}, log_collector::LogCollector, message_processor::{Executors, MessageProcessor}, nonce_utils, @@ -526,6 +528,8 @@ pub struct Bank { cached_executors: Arc>, transaction_debug_keys: Option>>, + + pub feature_set: Arc, } impl Default for BlockhashQueue { @@ -649,6 +653,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!( @@ -753,6 +758,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); @@ -3458,6 +3464,11 @@ impl Bank { // 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) { + let new_feature_activations = self.compute_active_feature_set(); + for feature_id in new_feature_activations { + info!("New feature activated: {}", feature_id); + } + self.ensure_builtins(init_finish_or_warp); self.reinvoke_entered_epoch_callback(initiate_callback); self.recheck_cross_program_support(); @@ -3466,6 +3477,48 @@ impl Bank { self.ensure_no_storage_rewards_pool(); } + // 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) -> 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 { + if let Some(mut account) = self.get_account(feature_id) { + if let Some(mut feature) = Feature::from_account(&account) { + match feature.activated_at { + None => { + // 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); + active.insert(*feature_id); + continue; + } + Some(activation_slot) => { + if slot >= activation_slot { + // Feature is already active + active.insert(*feature_id); + continue; + } + } + } + } + } + inactive.insert(*feature_id); + } + + self.feature_set = Arc::new(FeatureSet { + id: self.feature_set.id, + active, + inactive, + }); + newly_activated + } + fn ensure_builtins(&mut self, init_or_warp: bool) { for (program, start_epoch) in get_builtins(self.cluster_type()) { let should_populate = init_or_warp && self.epoch() >= start_epoch diff --git a/runtime/src/feature.rs b/runtime/src/feature.rs new file mode 100644 index 0000000000..c283459524 --- /dev/null +++ b/runtime/src/feature.rs @@ -0,0 +1,55 @@ +use solana_sdk::{ + account::{Account, KeyedAccount}, + account_info::AccountInfo, + clock::Slot, + instruction::InstructionError, + program_error::ProgramError, +}; + +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, Serialize, Deserialize)] +pub struct Feature { + pub activated_at: Option, +} + +impl Feature { + pub fn size_of() -> usize { + bincode::serialized_size(&Self::default()).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 from_account_info(account_info: &AccountInfo) -> Result { + bincode::deserialize(&account_info.data.borrow()).map_err(|_| ProgramError::InvalidArgument) + } + pub fn to_account_info(&self, account_info: &mut AccountInfo) -> Option<()> { + bincode::serialize_into(&mut account_info.data.borrow_mut()[..], self).ok() + } + pub fn from_keyed_account(keyed_account: &KeyedAccount) -> Result { + Self::from_account(&*keyed_account.try_account_ref()?) + .ok_or(InstructionError::InvalidArgument) + } + 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..6a101d9a74 --- /dev/null +++ b/runtime/src/feature_set.rs @@ -0,0 +1,60 @@ +use solana_sdk::{ + hash::{Hash, Hasher}, + pubkey::Pubkey, +}; +use std::collections::HashSet; + +/// The `FeatureSet` struct tracks the set of available and currently active runtime features +#[derive(AbiExample)] +pub struct FeatureSet { + /// Unique identifier of this feature set + pub id: Hash, + + /// Features that are currently active + pub active: HashSet, + + /// Features that are currently inactive + pub inactive: HashSet, +} + +impl FeatureSet { + pub fn active(&self, feature_id: &Pubkey) -> bool { + self.active.contains(feature_id) + } +} + +impl Default for FeatureSet { + // By default all features are disabled + fn default() -> Self { + let features: [Pubkey; 0] = []; + + Self { + id: { + let mut hasher = Hasher::default(); + for feature in features.iter() { + hasher.hash(feature.as_ref()); + } + hasher.result() + }, + active: HashSet::new(), + inactive: features.iter().cloned().collect(), + } + } +} + +impl FeatureSet { + // New `FeatureSet` with all features enabled + pub fn new_enabled() -> Self { + let default = Self::default(); + + Self { + id: default.id, + active: default + .active + .intersection(&default.inactive) + .cloned() + .collect::>(), + inactive: HashSet::new(), + } + } +} diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index badb51feae..b2638c2bba 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) { + // Activate all features at genesis in development mode + if genesis_config.cluster_type == ClusterType::Development { + let feature_set = FeatureSet::new_enabled(); + + for feature_id in feature_set.active { + let feature = Feature { + activated_at: Some(0), + }; + + genesis_config.accounts.insert( + feature_id, + feature.create_account(genesis_config.rent.minimum_balance(Feature::size_of())), + ); + } + } +} + 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;