From 369f37a0a471576afd7905c68ea580236ad62b48 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Tue, 3 Dec 2019 20:44:02 -0800 Subject: [PATCH] genesis validators (#7235) * genesis validators * slp1 nodes get 500SOL * no commission --- genesis/src/genesis_accounts.rs | 18 ++- genesis/src/lib.rs | 1 + genesis/src/main.rs | 1 + genesis/src/stakes.rs | 23 ++-- genesis/src/validators.rs | 209 ++++++++++++++++++++++++++++++ programs/stake/src/stake_state.rs | 8 +- programs/vote/src/vote_state.rs | 5 + 7 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 genesis/src/validators.rs diff --git a/genesis/src/genesis_accounts.rs b/genesis/src/genesis_accounts.rs index 559819383f..7beed2dea4 100644 --- a/genesis/src/genesis_accounts.rs +++ b/genesis/src/genesis_accounts.rs @@ -1,6 +1,7 @@ use crate::{ stakes::{create_and_add_stakes, StakerInfo}, unlocks::UnlockInfo, + validators::{create_and_add_validator, ValidatorInfo}, }; use solana_sdk::{genesis_config::GenesisConfig, native_token::sol_to_lamports}; @@ -614,6 +615,21 @@ fn add_stakes( .sum::() } +pub const VALIDATOR_INFOS: &[ValidatorInfo] = &[ValidatorInfo { + name: "aurel@ethereum.ro", + node: "GeZ5PrJi9muVCJiJAaFBNGoCEdxGEqTp7L2BmT2WTTy1", + vote: "7ZdRx2EBYoRuPfyeoNbuHodMUXcAQRcC37MUw3kP6akn", + node_sol: 500.0, + commission: 0, +}]; + +fn add_validators(genesis_config: &mut GenesisConfig, validator_infos: &[ValidatorInfo]) -> u64 { + validator_infos + .iter() + .map(|validator_info| create_and_add_validator(genesis_config, validator_info)) + .sum::() +} + pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 { add_stakes( genesis_config, @@ -630,7 +646,7 @@ pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 { &BATCH_THREE_STAKER_INFOS, &BATCH_THREE_UNLOCK_INFO, sol_to_lamports(1_000_000.0), - ) + ) + add_validators(genesis_config, &VALIDATOR_INFOS) } #[cfg(test)] diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 19eab13672..d8ba02ca32 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -2,6 +2,7 @@ pub mod address_generator; pub mod genesis_accounts; pub mod stakes; pub mod unlocks; +pub mod validators; use serde::{Deserialize, Serialize}; diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 8684cc1519..e678c0e1d1 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -4,6 +4,7 @@ mod address_generator; mod genesis_accounts; mod stakes; mod unlocks; +mod validators; use crate::genesis_accounts::add_genesis_accounts; use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches}; diff --git a/genesis/src/stakes.rs b/genesis/src/stakes.rs index bcbec1e2ab..1934e03928 100644 --- a/genesis/src/stakes.rs +++ b/genesis/src/stakes.rs @@ -8,7 +8,7 @@ use solana_sdk::{ pubkey::Pubkey, system_program, timing::years_as_slots, }; use solana_stake_program::stake_state::{ - create_lockup_stake_account, get_stake_rent_exempt_reserve, Authorized, Lockup, + create_lockup_stake_account, Authorized, Lockup, StakeState, }; #[derive(Debug)] @@ -52,7 +52,8 @@ pub fn create_and_add_stakes( let total_lamports = sol_to_lamports(staker_info.sol); - let staker_rent_reserve = get_stake_rent_exempt_reserve(&genesis_config.rent).max(1); + // staker is a system account + let staker_rent_reserve = genesis_config.rent.minimum_balance(0).max(1); let staker_fees = calculate_staker_fees(genesis_config, 1.0); let mut stakes_lamports = total_lamports - staker_fees; @@ -61,6 +62,7 @@ pub fn create_and_add_stakes( // the staker account needs to be rent exempt *and* carry enough // lamports to cover TX fees (delegation) for one year, // and we support one delegation per epoch + // a single staker may administer any number of accounts genesis_config .accounts .entry(authorized.staker) @@ -85,7 +87,7 @@ pub fn create_and_add_stakes( let mut address_generator = AddressGenerator::new(&authorized.staker, staker_info.name); - let stake_rent_exempt_reserve = get_stake_rent_exempt_reserve(&genesis_config.rent); + let stake_rent_reserve = StakeState::get_rent_exempt_reserve(&genesis_config.rent); for unlock in unlocks { let lamports = unlock.amount(stakes_lamports); @@ -111,7 +113,7 @@ pub fn create_and_add_stakes( ), ); } - if remainder <= stake_rent_exempt_reserve { + if remainder <= stake_rent_reserve { genesis_config.add_account( address_generator.next(), create_lockup_stake_account( @@ -171,7 +173,7 @@ mod tests { .iter() .all(|(_pubkey, account)| account.lamports <= granularity || account.lamports - granularity - <= get_stake_rent_exempt_reserve(&genesis_config.rent))); + <= StakeState::get_rent_exempt_reserve(&genesis_config.rent))); } #[test] @@ -184,11 +186,12 @@ mod tests { ..Rent::default() }; - let reserve = get_stake_rent_exempt_reserve(&rent); + let reserve = StakeState::get_rent_exempt_reserve(&rent); + let staker_reserve = rent.minimum_balance(0); // verify that a small remainder ends up in the last stake let granularity = reserve; - let total_lamports = reserve + reserve * 2 + 1; + let total_lamports = staker_reserve + reserve * 2 + 1; create_and_check_stakes( &mut GenesisConfig { rent, @@ -214,7 +217,7 @@ mod tests { // huge granularity doesn't blow up let granularity = std::u64::MAX; - let total_lamports = reserve + reserve * 2 + 1; + let total_lamports = staker_reserve + reserve * 2 + 1; create_and_check_stakes( &mut GenesisConfig { rent, @@ -240,7 +243,7 @@ mod tests { // exactly reserve as a remainder, reserve gets folded in let granularity = reserve * 3; - let total_lamports = reserve + (granularity + reserve) * 2; + let total_lamports = staker_reserve + (granularity + reserve) * 2; create_and_check_stakes( &mut GenesisConfig { rent, @@ -265,7 +268,7 @@ mod tests { ); // exactly reserve + 1 as a remainder, reserve + 1 gets its own stake let granularity = reserve * 3; - let total_lamports = reserve + (granularity + reserve + 1) * 2; + let total_lamports = staker_reserve + (granularity + reserve + 1) * 2; create_and_check_stakes( &mut GenesisConfig { rent, diff --git a/genesis/src/validators.rs b/genesis/src/validators.rs new file mode 100644 index 0000000000..8e9840d84a --- /dev/null +++ b/genesis/src/validators.rs @@ -0,0 +1,209 @@ +//! validators generator +use solana_sdk::{ + account::Account, genesis_config::GenesisConfig, native_token::sol_to_lamports, pubkey::Pubkey, + system_program, timing::years_as_slots, +}; +use solana_vote_program::vote_state::{self, VoteState}; + +#[derive(Debug)] +pub struct ValidatorInfo { + pub name: &'static str, + pub node: &'static str, + pub node_sol: f64, + pub vote: &'static str, + pub commission: u8, +} + +// the node's account needs carry enough +// lamports to cover TX fees for voting for one year, +// validators can vote once per slot +fn calculate_voting_fees(genesis_config: &GenesisConfig, years: f64) -> u64 { + genesis_config.fee_calculator.max_lamports_per_signature + * years_as_slots( + years, + &genesis_config.poh_config.target_tick_duration, + genesis_config.ticks_per_slot, + ) as u64 +} + +/// create and add vote and node id accounts for a validator +pub fn create_and_add_validator( + genesis_config: &mut GenesisConfig, + // information about this validator + validator_info: &ValidatorInfo, +) -> u64 { + let node: Pubkey = validator_info.node.parse().expect("invalide node"); + let vote: Pubkey = validator_info.vote.parse().expect("invalide vote"); + let node_lamports = sol_to_lamports(validator_info.node_sol); + + // node is the system account from which votes will be issued + let node_rent_reserve = genesis_config.rent.minimum_balance(0).max(1); + let node_voting_fees = calculate_voting_fees(genesis_config, 1.0); + + let vote_rent_reserve = VoteState::get_rent_exempt_reserve(&genesis_config.rent).max(1); + + let mut total_lamports = node_voting_fees + vote_rent_reserve + node_lamports; + + genesis_config + .accounts + .entry(node) + .or_insert_with(|| { + total_lamports += node_rent_reserve; + Account::new(node_rent_reserve, 0, &system_program::id()) + }) + .lamports += node_voting_fees + node_lamports; + + assert!( + genesis_config.accounts.get(&vote).is_none(), + "{} is already in genesis", + vote + ); + + genesis_config.add_account( + vote, + vote_state::create_account(&vote, &node, validator_info.commission, vote_rent_reserve), + ); + + total_lamports +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::rent::Rent; + + fn create_and_check_validators( + genesis_config: &mut GenesisConfig, + validator_infos: &[ValidatorInfo], + total_lamports: u64, + len: usize, + ) { + assert_eq!( + validator_infos + .iter() + .map(|validator_info| create_and_add_validator(genesis_config, validator_info)) + .sum::(), + total_lamports + ); + assert_eq!(genesis_config.accounts.len(), len); + assert_eq!( + genesis_config + .accounts + .iter() + .map(|(_pubkey, account)| account.lamports) + .sum::(), + total_lamports, + ); + assert!(genesis_config + .accounts + .iter() + .all(|(_pubkey, account)| account.lamports + >= genesis_config.rent.minimum_balance(0).max(1))); + } + + #[test] + fn test_create_one_validator() { + let rent = Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }; + let mut genesis_config = GenesisConfig { + rent, + ..GenesisConfig::default() + }; + + let total_lamports = VoteState::get_rent_exempt_reserve(&rent) + + calculate_voting_fees(&genesis_config, 1.0) + + rent.minimum_balance(0); + + create_and_check_validators( + &mut genesis_config, + &[ValidatorInfo { + name: "fun", + node: "AiTDdNHW2vNtHt7PqWMHx3B8cMPRDNgc7kMiLPJM25QC", // random pubkeys + node_sol: 0.0, + vote: "77TQYZTHodhnxJcSuVjUvx8GYRCkykPyHtmFTFLjj1Rc", + commission: 50, + }], + total_lamports, + 2, + ); + } + + #[test] + fn test_create_one_validator_two_votes() { + let rent = Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }; + let mut genesis_config = GenesisConfig { + rent, + ..GenesisConfig::default() + }; + let total_lamports = VoteState::get_rent_exempt_reserve(&rent) * 2 + + calculate_voting_fees(&genesis_config, 1.0) * 2 // two vote accounts + + rent.minimum_balance(0) // one node account + + sol_to_lamports(1.0); // 2nd vote account ask has SOL + + // weird case, just wanted to verify that the duplicated node account gets double fees + create_and_check_validators( + &mut genesis_config, + &[ + ValidatorInfo { + name: "fun", + node: "3VTm54dw8w6jTTsPH4BfoV5vo6mF985JAMtNDRYcaGFc", // random pubkeys + node_sol: 0.0, + vote: "GTKWbUoLw3Bv7Ld92crhyXcEk9zUu3VEKfzeuWJZdnfW", + commission: 50, + }, + ValidatorInfo { + name: "unfun", + node: "3VTm54dw8w6jTTsPH4BfoV5vo6mF985JAMtNDRYcaGFc", // random pubkeys, same node + node_sol: 1.0, + vote: "8XrFPRULg98kSm535kFaLV4GMnK5JQSuAymyrCHXsUcy", + commission: 50, + }, + ], + total_lamports, + 3, + ); + } + + #[test] + #[should_panic] + fn test_vote_collision() { + let rent = Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }; + let mut genesis_config = GenesisConfig { + rent, + ..GenesisConfig::default() + }; + + create_and_check_validators( + &mut genesis_config, + &[ + ValidatorInfo { + name: "fun", + node: "3VTm54dw8w6jTTsPH4BfoV5vo6mF985JAMtNDRYcaGFc", // random pubkeys + node_sol: 0.0, + vote: "GTKWbUoLw3Bv7Ld92crhyXcEk9zUu3VEKfzeuWJZdnfW", + commission: 50, + }, + ValidatorInfo { + name: "unfun", + node: "3VTm54dw8w6jTTsPH4BfoV5vo6mF985JAMtNDRYcaGFc", // random pubkeys, same node + node_sol: 0.0, + vote: "GTKWbUoLw3Bv7Ld92crhyXcEk9zUu3VEKfzeuWJZdnfW", // duplicate vote, bad juju + commission: 50, + }, + ], + 0, + 0, + ); + } +} diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index fe8d42f6b1..b4d23c71b7 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -36,6 +36,10 @@ impl Default for StakeState { } impl StakeState { + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { + rent.minimum_balance(std::mem::size_of::()) + } + // utility function, used by Stakes, tests pub fn from(account: &Account) -> Option { account.state().ok() @@ -801,10 +805,6 @@ where } } -pub fn get_stake_rent_exempt_reserve(rent: &Rent) -> u64 { - rent.minimum_balance(std::mem::size_of::()) -} - // genesis investor accounts pub fn create_lockup_stake_account( authorized: &Authorized, diff --git a/programs/vote/src/vote_state.rs b/programs/vote/src/vote_state.rs index 80aa4e3e0d..d3e00a2fd9 100644 --- a/programs/vote/src/vote_state.rs +++ b/programs/vote/src/vote_state.rs @@ -12,6 +12,7 @@ use solana_sdk::{ hash::Hash, instruction::InstructionError, pubkey::Pubkey, + rent::Rent, slot_hashes::SlotHash, sysvar::clock::Clock, }; @@ -121,6 +122,10 @@ impl VoteState { } } + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { + rent.minimum_balance(VoteState::size_of()) + } + pub fn size_of() -> usize { // Upper limit on the size of the Vote State. Equal to // size_of(VoteState) when votes.len() is MAX_LOCKOUT_HISTORY