From 88ea9506528b2d9ffa0e2a9f05746c8a45951adf Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Thu, 15 Aug 2019 14:35:48 -0700 Subject: [PATCH] add stake_api config account (#5531) --- Cargo.lock | 1 + genesis/src/main.rs | 4 +- programs/stake_api/Cargo.toml | 1 + programs/stake_api/src/config.rs | 89 +++++++++++++++++++ programs/stake_api/src/lib.rs | 10 +++ programs/stake_api/src/rewards_pools.rs | 46 +++++----- programs/stake_api/src/stake_instruction.rs | 24 +++++- programs/stake_api/src/stake_state.rs | 94 +++++++++++++++------ runtime/src/genesis_utils.rs | 2 +- 9 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 programs/stake_api/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 1cf5d0650..af522e99a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,6 +3725,7 @@ dependencies = [ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-config-api 0.18.0-pre1", "solana-logger 0.18.0-pre1", "solana-metrics 0.18.0-pre1", "solana-sdk 0.18.0-pre1", diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 29cbe15db..2dda64bcc 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -331,9 +331,9 @@ fn main() -> Result<(), Box> { builder = append_primordial_accounts(file, AccountFileFormat::Keypair, builder)?; } - // add the reward pools + // add genesis stuff from storage and stake builder = solana_storage_api::rewards_pools::genesis(builder); - builder = solana_stake_api::rewards_pools::genesis(builder); + builder = solana_stake_api::genesis(builder); create_new_ledger(&ledger_path, &builder.build())?; Ok(()) diff --git a/programs/stake_api/Cargo.toml b/programs/stake_api/Cargo.toml index 44435bede..803eef979 100644 --- a/programs/stake_api/Cargo.toml +++ b/programs/stake_api/Cargo.toml @@ -18,6 +18,7 @@ solana-logger = { path = "../../logger", version = "0.18.0-pre1" } solana-metrics = { path = "../../metrics", version = "0.18.0-pre1" } solana-sdk = { path = "../../sdk", version = "0.18.0-pre1" } solana-vote-api = { path = "../vote_api", version = "0.18.0-pre1" } +solana-config-api = { path = "../config_api", version = "0.18.0-pre1" } [lib] crate-type = ["lib"] diff --git a/programs/stake_api/src/config.rs b/programs/stake_api/src/config.rs new file mode 100644 index 000000000..4639503af --- /dev/null +++ b/programs/stake_api/src/config.rs @@ -0,0 +1,89 @@ +//! config for staking +//! carries variables that the stake program cares about +use bincode::{deserialize, serialized_size}; +use serde_derive::{Deserialize, Serialize}; +use solana_config_api::{create_config_account, get_config_data, ConfigState}; +use solana_sdk::{ + account::{Account, KeyedAccount}, + instruction::InstructionError, + pubkey::Pubkey, +}; + +// stake config ID +const ID: [u8; 32] = [ + 6, 161, 216, 23, 165, 2, 5, 11, 104, 7, 145, 230, 206, 109, 184, 142, 30, 91, 113, 80, 246, 31, + 198, 121, 10, 78, 180, 209, 0, 0, 0, 0, +]; + +solana_sdk::solana_name_id!(ID, "StakeConfig11111111111111111111111111111111"); + +// means that no more tha +pub const DEFAULT_WARMUP_RATE: f64 = 0.15; +pub const DEFAULT_COOLDOWN_RATE: f64 = 0.15; +pub const DEFAULT_SLASH_PENALTY: u8 = ((5 * std::u8::MAX as usize) / 100) as u8; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct Config { + pub warmup_rate: f64, + pub cooldown_rate: f64, + pub slash_penalty: u8, +} + +impl Config { + pub fn from(account: &Account) -> Option { + get_config_data(&account.data) + .ok() + .and_then(|data| deserialize(data).ok()) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + warmup_rate: DEFAULT_WARMUP_RATE, + cooldown_rate: DEFAULT_COOLDOWN_RATE, + slash_penalty: DEFAULT_SLASH_PENALTY, + } + } +} + +impl ConfigState for Config { + fn max_space() -> u64 { + serialized_size(&Config::default()).unwrap() + } +} + +pub fn genesis() -> (Pubkey, Account) { + (id(), create_config_account(vec![], &Config::default(), 100)) +} + +pub fn create_account(lamports: u64, config: &Config) -> Account { + create_config_account(vec![], config, lamports) +} + +pub fn from_keyed_account(account: &KeyedAccount) -> Result { + if !check_id(account.unsigned_key()) { + return Err(InstructionError::InvalidArgument); + } + Config::from(account.account).ok_or(InstructionError::InvalidArgument) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let mut account = create_account(1, &Config::default()); + assert_eq!(Config::from(&account), Some(Config::default())); + assert_eq!( + from_keyed_account(&KeyedAccount::new(&Pubkey::default(), false, &mut account)), + Err(InstructionError::InvalidArgument) + ); + let (pubkey, mut account) = genesis(); + assert_eq!( + from_keyed_account(&KeyedAccount::new(&pubkey, false, &mut account)), + Ok(Config::default()) + ); + } +} diff --git a/programs/stake_api/src/lib.rs b/programs/stake_api/src/lib.rs index 30af545ca..69b30f6e7 100644 --- a/programs/stake_api/src/lib.rs +++ b/programs/stake_api/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod rewards_pools; pub mod stake_instruction; pub mod stake_state; @@ -11,3 +12,12 @@ solana_sdk::solana_name_id!( STAKE_PROGRAM_ID, "Stake11111111111111111111111111111111111111" ); + +use solana_sdk::genesis_block::Builder; + +pub fn genesis(mut builder: Builder) -> Builder { + for (pubkey, account) in crate::rewards_pools::genesis().iter() { + builder = builder.rewards_pool(*pubkey, account.clone()); + } + builder.accounts(&[crate::config::genesis()]) +} diff --git a/programs/stake_api/src/rewards_pools.rs b/programs/stake_api/src/rewards_pools.rs index a87f2df54..20d447dcc 100644 --- a/programs/stake_api/src/rewards_pools.rs +++ b/programs/stake_api/src/rewards_pools.rs @@ -2,12 +2,13 @@ //! * initialize genesis with rewards pools //! * keep track of rewards //! * own mining pools - -use crate::stake_state::create_rewards_pool; +use crate::stake_state::StakeState; use rand::{thread_rng, Rng}; -use solana_sdk::genesis_block::Builder; -use solana_sdk::hash::{hash, Hash}; -use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + hash::{hash, Hash}, + pubkey::Pubkey, +}; // base rewards pool ID const ID: [u8; 32] = [ @@ -20,16 +21,6 @@ solana_sdk::solana_name_id!(ID, "StakeRewards1111111111111111111111111111111"); // to cut down on collisions for redemptions, we make multiple accounts pub const NUM_REWARDS_POOLS: usize = 256; -pub fn genesis(mut builder: Builder) -> Builder { - let mut pubkey = id(); - - for _i in 0..NUM_REWARDS_POOLS { - builder = builder.rewards_pool(pubkey, create_rewards_pool()); - pubkey = Pubkey::new(hash(pubkey.as_ref()).as_ref()); - } - builder -} - pub fn random_id() -> Pubkey { let mut id = Hash::new(&ID); @@ -40,24 +31,31 @@ pub fn random_id() -> Pubkey { Pubkey::new(id.as_ref()) } +pub fn genesis() -> Vec<(Pubkey, Account)> { + let mut accounts = Vec::with_capacity(NUM_REWARDS_POOLS); + let mut pubkey = id(); + + for _i in 0..NUM_REWARDS_POOLS { + accounts.push(( + pubkey, + Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap(), + )); + pubkey = Pubkey::new(hash(pubkey.as_ref()).as_ref()); + } + accounts +} + #[cfg(test)] mod tests { use super::*; - use solana_sdk::genesis_block::Builder; #[test] fn test() { - let builder = Builder::new(); - - let genesis_block = genesis(builder).build(); + let accounts = genesis(); for _i in 0..NUM_REWARDS_POOLS { let id = random_id(); - assert!(genesis_block - .rewards_pools - .iter() - .position(|x| x.0 == id) - .is_some()); + assert!(accounts.iter().position(|x| x.0 == id).is_some()); } } } diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 4f392e452..d9fef6482 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -1,5 +1,7 @@ -use crate::id; -use crate::stake_state::{StakeAccount, StakeState}; +use crate::{ + config, id, + stake_state::{StakeAccount, StakeState}, +}; use bincode::deserialize; use log::*; use serde_derive::{Deserialize, Serialize}; @@ -18,6 +20,7 @@ pub enum StakeInstruction { /// 0 - Uninitialized StakeAccount to be delegated <= must have this signature /// 1 - VoteAccount to which this Stake will be delegated /// 2 - Clock sysvar Account that carries clock bank epoch + /// 3 - Config Account that carries stake config /// /// The u64 is the portion of the Stake account balance to be activated, /// must be less than StakeAccount.lamports @@ -96,6 +99,7 @@ pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, stake: u64) - AccountMeta::new(*stake_pubkey, true), AccountMeta::new_credit_only(*vote_pubkey, false), AccountMeta::new_credit_only(sysvar::clock::id(), false), + AccountMeta::new_credit_only(crate::config::id(), false), ]; Instruction::new(id(), &StakeInstruction::DelegateStake(stake), account_metas) } @@ -139,12 +143,17 @@ pub fn process_instruction( // TODO: data-driven unpack and dispatch of KeyedAccounts match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { StakeInstruction::DelegateStake(stake) => { - if rest.len() != 2 { + if rest.len() != 3 { Err(InstructionError::InvalidInstructionData)?; } let vote = &rest[0]; - me.delegate_stake(vote, stake, &sysvar::clock::from_keyed_account(&rest[1])?) + me.delegate_stake( + vote, + stake, + &sysvar::clock::from_keyed_account(&rest[1])?, + &config::from_keyed_account(&rest[2])?, + ) } StakeInstruction::RedeemVoteCredits => { if rest.len() != 4 { @@ -206,6 +215,8 @@ mod tests { sysvar::rewards::create_account(1, 0.0, 0.0) } else if sysvar::stake_history::check_id(&meta.pubkey) { sysvar::stake_history::create_account(1, &StakeHistory::default()) + } else if config::check_id(&meta.pubkey) { + config::create_account(1, &config::Config::default()) } else { Account::default() } @@ -299,6 +310,11 @@ mod tests { false, &mut sysvar::clock::create_account(1, 0, 0, 0, 0) ), + KeyedAccount::new( + &config::id(), + false, + &mut config::create_account(1, &config::Config::default()) + ), ], &serialize(&StakeInstruction::DelegateStake(0)).unwrap(), ), diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 0ad72af7d..3594c21cf 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -3,7 +3,7 @@ //! * keep track of rewards //! * own mining pools -use crate::id; +use crate::{config::Config, id}; use serde_derive::{Deserialize, Serialize}; use solana_sdk::{ account::{Account, KeyedAccount}, @@ -56,10 +56,9 @@ pub struct Stake { pub stake: u64, // stake amount activated pub activated: Epoch, // epoch the stake was activated, std::Epoch::MAX if is a bootstrap stake pub deactivated: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated + pub config: Config, } -pub const STAKE_WARMUP_RATE: f64 = 0.15; - impl Default for Stake { fn default() -> Self { Self { @@ -68,6 +67,7 @@ impl Default for Stake { stake: 0, activated: 0, deactivated: std::u64::MAX, + config: Config::default(), } } } @@ -109,7 +109,7 @@ impl Stake { // portion of activating stake in this epoch I'm entitled to effective_stake += - (weight * entry.effective as f64 * STAKE_WARMUP_RATE) as u64; + (weight * entry.effective as f64 * self.config.warmup_rate) as u64; if effective_stake >= self.stake { effective_stake = self.stake; @@ -210,12 +210,19 @@ impl Stake { } } - fn new(stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, activated: Epoch) -> Self { + fn new( + stake: u64, + voter_pubkey: &Pubkey, + vote_state: &VoteState, + activated: Epoch, + config: &Config, + ) -> Self { Self { stake, activated, voter_pubkey: *voter_pubkey, credits_observed: vote_state.credits(), + config: *config, ..Stake::default() } } @@ -231,6 +238,7 @@ pub trait StakeAccount { vote_account: &KeyedAccount, stake: u64, clock: &sysvar::clock::Clock, + config: &Config, ) -> Result<(), InstructionError>; fn deactivate_stake( &mut self, @@ -259,6 +267,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { vote_account: &KeyedAccount, new_stake: u64, clock: &sysvar::clock::Clock, + config: &Config, ) -> Result<(), InstructionError> { if self.signer_key().is_none() { return Err(InstructionError::MissingRequiredSignature); @@ -274,6 +283,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { vote_account.unsigned_key(), &vote_account.state()?, clock.epoch, + config, ); self.set_state(&StakeState::Stake(stake)) @@ -381,10 +391,6 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } } -//find_min<'a, I>(vals: I) -> Option<&'a u32> -//where -// I: Iterator, - // utility function, used by runtime::Stakes, tests pub fn new_stake_history_entry<'a, I>( epoch: Epoch, @@ -429,11 +435,6 @@ pub fn create_stake_account( stake_account } -// utility function, used by Bank, tests, genesis -pub fn create_rewards_pool() -> Account { - Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -506,14 +507,19 @@ mod tests { } assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, 0, &clock), + stake_keyed_account.delegate_stake(&vote_keyed_account, 0, &clock, &Config::default()), Err(InstructionError::MissingRequiredSignature) ); // signed keyed account let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert!(stake_keyed_account - .delegate_stake(&vote_keyed_account, stake_lamports, &clock) + .delegate_stake( + &vote_keyed_account, + stake_lamports, + &clock, + &Config::default() + ) .is_ok()); // verify that delegate_stake() looks right, compare against hand-rolled @@ -526,19 +532,30 @@ mod tests { stake: stake_lamports, activated: clock.epoch, deactivated: std::u64::MAX, + config: Config::default() }) ); // verify that delegate_stake can't be called twice StakeState::default() // signed keyed account assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, stake_lamports, &clock), + stake_keyed_account.delegate_stake( + &vote_keyed_account, + stake_lamports, + &clock, + &Config::default() + ), Err(InstructionError::InvalidAccountData) ); // verify can only stake up to account lamports let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, stake_lamports + 1, &clock), + stake_keyed_account.delegate_stake( + &vote_keyed_account, + stake_lamports + 1, + &clock, + &Config::default() + ), Err(InstructionError::InsufficientFunds) ); @@ -546,7 +563,7 @@ mod tests { stake_keyed_account.set_state(&stake_state).unwrap(); assert!(stake_keyed_account - .delegate_stake(&vote_keyed_account, 0, &clock) + .delegate_stake(&vote_keyed_account, 0, &clock, &Config::default()) .is_err()); } @@ -608,7 +625,9 @@ mod tests { break; } assert!(epoch < epochs); // should have warmed everything up by this time - assert!(delta as f64 / prev_total_effective_stake as f64 <= STAKE_WARMUP_RATE); + assert!( + delta as f64 / prev_total_effective_stake as f64 <= Config::default().warmup_rate + ); prev_total_effective_stake = total_effective_stake; } } @@ -651,7 +670,12 @@ mod tests { let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, stake_lamports, &clock), + stake_keyed_account.delegate_stake( + &vote_keyed_account, + stake_lamports, + &clock, + &Config::default() + ), Ok(()) ); @@ -723,7 +747,12 @@ mod tests { let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, stake_lamports, &clock), + stake_keyed_account.delegate_stake( + &vote_keyed_account, + stake_lamports, + &clock, + &Config::default() + ), Ok(()) ); @@ -810,7 +839,12 @@ mod tests { let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, stake_lamports, &future), + stake_keyed_account.delegate_stake( + &vote_keyed_account, + stake_lamports, + &future, + &Config::default() + ), Ok(()) ); @@ -944,7 +978,12 @@ mod tests { rewards.validator_point_value = 100.0; let rewards_pool_pubkey = Pubkey::new_rand(); - let mut rewards_pool_account = create_rewards_pool(); + let mut rewards_pool_account = Account::new_data( + std::u64::MAX, + &StakeState::RewardsPool, + &crate::rewards_pools::id(), + ) + .unwrap(); let mut rewards_pool_keyed_account = KeyedAccount::new(&rewards_pool_pubkey, false, &mut rewards_pool_account); @@ -972,7 +1011,12 @@ mod tests { // delegate the stake assert!(stake_keyed_account - .delegate_stake(&vote_keyed_account, stake_lamports, &clock) + .delegate_stake( + &vote_keyed_account, + stake_lamports, + &clock, + &Config::default() + ) .is_ok()); let stake_history = create_stake_history_from_stakes( diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 9bbe1a07e..24a5d555b 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -74,7 +74,7 @@ pub fn create_genesis_block_with_leader( ]) .fee_calculator(FeeCalculator::new(0)); // most tests don't want fees - builder = solana_stake_api::rewards_pools::genesis(builder); + builder = solana_stake_api::genesis(builder); builder = solana_storage_api::rewards_pools::genesis(builder); GenesisBlockInfo {