diff --git a/genesis/README.md b/genesis/README.md new file mode 100644 index 00000000000000..66bab5e36671c3 --- /dev/null +++ b/genesis/README.md @@ -0,0 +1,76 @@ +# Genesis + +## There are a few ways to bake validator accounts and delegated stake into genesis: +### 1) Through bootstrap validator cli args: +``` +--bootstrap-validator +--bootstrap-validator-lamports +--bootstrap-validator-stake-lamports +``` +Note: you can pass in `--bootstrap-validator ...` multiple times but the lamports associated with `--bootstrap-validator-lamports` and `--bootstrap-validator-stake-lamports` will apply to all `--bootstrap-validator` arguments. +For example: +``` +cargo run --bin solana-genesis -- + --bootstrap-validator + --bootstrap-validator + ... + --bootstrap-validator + --bootstrap-validator-stake-lamports 10000000000 + --bootstrap-validator 100000000000 +``` +All validator accounts will receive the same number of stake and account lamports + +### 2) Through the primordial accounts file flag: +The primordial accounts file can be used to add accounts of any type to genesis. A user can define all account data and metadata. `data` field must be BASE64 encoded. +``` +--primordial-accounts-file +``` +The primordial accounts file has the following format: +``` +--- +: + balance: + owner: + data: + executable: false +: + balance: + owner: + data: + executable: true +... +: + balance: + owner: + data: + executable: true +``` +The `data` portion of the yaml file holds BASE64 encoded data about the account, which can be vote or stake account information. + +### 3) Through the validator accounts file flag: +The main goal with the validator accounts file is to: +- Bake validator stakes into genesis with different stake and account distributions +- Remove the overhead of forcing the user to serialize and deserialize validator stake and vote account state, as required by a primordial accounts file. +``` +--validator-accounts-file +``` +The validator accounts file has the following format: +``` +validator_accounts: +- balance_lamports: + stake_lamports: + identity_account: + vote_account: + stake_account: +- balance_lamports: + stake_lamports: + identity_account: + vote_account: + stake_account: +... +- balance_lamports: + stake_lamports: + identity_account: + vote_account: + stake_account: +``` \ No newline at end of file diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 5faf788d0f4f8a..ef54f1660ffb4d 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -14,3 +14,19 @@ pub struct Base64Account { pub data: String, pub executable: bool, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct ValidatorAccountsFile { + pub validator_accounts: Vec, +} + +/// Info needed to create a staked validator account, +/// including relevant balances and vote- and stake-account addresses +#[derive(Serialize, Deserialize, Debug)] +pub struct StakedValidatorAccountInfo { + pub balance_lamports: u64, + pub stake_lamports: u64, + pub identity_account: String, + pub vote_account: String, + pub stake_account: String, +} diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 46b0ceadd77614..1fa1c28f24ea90 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -16,7 +16,10 @@ use { }, }, solana_entry::poh::compute_hashes_per_tick, - solana_genesis::{genesis_accounts::add_genesis_accounts, Base64Account}, + solana_genesis::{ + genesis_accounts::add_genesis_accounts, Base64Account, StakedValidatorAccountInfo, + ValidatorAccountsFile, + }, solana_ledger::{blockstore::create_new_ledger, blockstore_options::LedgerColumnOptions}, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, @@ -49,6 +52,7 @@ use { io::{self, Read}, path::PathBuf, process, + slice::Iter, str::FromStr, time::Duration, }, @@ -112,6 +116,63 @@ pub fn load_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> Ok(lamports) } +pub fn load_validator_accounts( + file: &str, + commission: u8, + rent: &Rent, + genesis_config: &mut GenesisConfig, +) -> io::Result<()> { + let accounts_file = File::open(file)?; + let validator_genesis_accounts: Vec = + serde_yaml::from_reader::<_, ValidatorAccountsFile>(accounts_file) + .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{err:?}")))? + .validator_accounts; + + for account_details in validator_genesis_accounts { + let pubkeys = [ + pubkey_from_str(account_details.identity_account.as_str()).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!( + "Invalid pubkey/keypair {}: {:?}", + account_details.identity_account, err + ), + ) + })?, + pubkey_from_str(account_details.vote_account.as_str()).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!( + "Invalid pubkey/keypair {}: {:?}", + account_details.vote_account, err + ), + ) + })?, + pubkey_from_str(account_details.stake_account.as_str()).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!( + "Invalid pubkey/keypair {}: {:?}", + account_details.stake_account, err + ), + ) + })?, + ]; + + add_validator_accounts( + genesis_config, + &mut pubkeys.iter(), + account_details.balance_lamports, + account_details.stake_lamports, + commission, + rent, + None, + )?; + } + + Ok(()) +} + fn check_rpc_genesis_hash( cluster_type: &ClusterType, rpc_client: &RpcClient, @@ -176,6 +237,68 @@ fn features_to_deactivate_for_cluster( Ok(features_to_deactivate) } +fn add_validator_accounts( + genesis_config: &mut GenesisConfig, + pubkeys_iter: &mut Iter, + lamports: u64, + stake_lamports: u64, + commission: u8, + rent: &Rent, + authorized_pubkey: Option<&Pubkey>, +) -> io::Result<()> { + rent_exempt_check( + stake_lamports, + rent.minimum_balance(StakeStateV2::size_of()), + )?; + + loop { + let Some(identity_pubkey) = pubkeys_iter.next() else { + break; + }; + let vote_pubkey = pubkeys_iter.next().unwrap(); + let stake_pubkey = pubkeys_iter.next().unwrap(); + + genesis_config.add_account( + *identity_pubkey, + AccountSharedData::new(lamports, 0, &system_program::id()), + ); + + let vote_account = vote_state::create_account_with_authorized( + identity_pubkey, + identity_pubkey, + identity_pubkey, + commission, + VoteState::get_rent_exempt_reserve(rent).max(1), + ); + + genesis_config.add_account( + *stake_pubkey, + stake_state::create_account( + authorized_pubkey.unwrap_or(identity_pubkey), + vote_pubkey, + &vote_account, + rent, + stake_lamports, + ), + ); + genesis_config.add_account(*vote_pubkey, vote_account); + } + Ok(()) +} + +fn rent_exempt_check(stake_lamports: u64, exempt: u64) -> io::Result<()> { + if stake_lamports < exempt { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "error: insufficient validator stake lamports: {stake_lamports} for rent exemption, requires {exempt}" + ), + )) + } else { + Ok(()) + } +} + #[allow(clippy::cognitive_complexity)] fn main() -> Result<(), Box> { let default_faucet_pubkey = solana_cli_config::Config::default().keypair_path; @@ -422,6 +545,14 @@ fn main() -> Result<(), Box> { .multiple(true) .help("The location of pubkey for primordial accounts and balance"), ) + .arg( + Arg::with_name("validator_accounts_file") + .long("validator-accounts-file") + .value_name("FILENAME") + .takes_value(true) + .multiple(true) + .help("The location of a file containing a list of identity, vote, and stake pubkeys and balances for validator accounts to bake into genesis") + ) .arg( Arg::with_name("cluster_type") .long("cluster-type") @@ -501,21 +632,6 @@ fn main() -> Result<(), Box> { burn_percent: value_t_or_exit!(matches, "rent_burn_percentage", u8), }; - fn rent_exempt_check(matches: &ArgMatches<'_>, name: &str, exempt: u64) -> io::Result { - let lamports = value_t_or_exit!(matches, name, u64); - - if lamports < exempt { - Err(io::Error::new( - io::ErrorKind::Other, - format!( - "error: insufficient {name}: {lamports} for rent exemption, requires {exempt}" - ), - )) - } else { - Ok(lamports) - } - } - let bootstrap_validator_pubkeys = pubkeys_of(&matches, "bootstrap_validator").unwrap(); assert_eq!(bootstrap_validator_pubkeys.len() % 3, 0); @@ -533,11 +649,8 @@ fn main() -> Result<(), Box> { let bootstrap_validator_lamports = value_t_or_exit!(matches, "bootstrap_validator_lamports", u64); - let bootstrap_validator_stake_lamports = rent_exempt_check( - &matches, - "bootstrap_validator_stake_lamports", - rent.minimum_balance(StakeStateV2::size_of()), - )?; + let bootstrap_validator_stake_lamports = + value_t_or_exit!(matches, "bootstrap_validator_stake_lamports", u64); let bootstrap_stake_authorized_pubkey = pubkey_of(&matches, "bootstrap_stake_authorized_pubkey"); @@ -627,43 +740,17 @@ fn main() -> Result<(), Box> { } let commission = value_t_or_exit!(matches, "vote_commission_percentage", u8); - - let mut bootstrap_validator_pubkeys_iter = bootstrap_validator_pubkeys.iter(); - loop { - let Some(identity_pubkey) = bootstrap_validator_pubkeys_iter.next() else { - break; - }; - let vote_pubkey = bootstrap_validator_pubkeys_iter.next().unwrap(); - let stake_pubkey = bootstrap_validator_pubkeys_iter.next().unwrap(); - - genesis_config.add_account( - *identity_pubkey, - AccountSharedData::new(bootstrap_validator_lamports, 0, &system_program::id()), - ); - - let vote_account = vote_state::create_account_with_authorized( - identity_pubkey, - identity_pubkey, - identity_pubkey, - commission, - VoteState::get_rent_exempt_reserve(&genesis_config.rent).max(1), - ); - - genesis_config.add_account( - *stake_pubkey, - stake_state::create_account( - bootstrap_stake_authorized_pubkey - .as_ref() - .unwrap_or(identity_pubkey), - vote_pubkey, - &vote_account, - &genesis_config.rent, - bootstrap_validator_stake_lamports, - ), - ); - - genesis_config.add_account(*vote_pubkey, vote_account); - } + let rent = genesis_config.rent.clone(); + + add_validator_accounts( + &mut genesis_config, + &mut bootstrap_validator_pubkeys.iter(), + bootstrap_validator_lamports, + bootstrap_validator_stake_lamports, + commission, + &rent, + bootstrap_stake_authorized_pubkey.as_ref(), + )?; if let Some(creation_time) = unix_timestamp_from_rfc3339_datetime(&matches, "creation_time") { genesis_config.creation_time = creation_time; @@ -691,6 +778,12 @@ fn main() -> Result<(), Box> { } } + if let Some(files) = matches.values_of("validator_accounts_file") { + for file in files { + load_validator_accounts(file, commission, &rent, &mut genesis_config)?; + } + } + let max_genesis_archive_unpacked_size = value_t_or_exit!(matches, "max_genesis_archive_unpacked_size", u64); @@ -810,7 +903,7 @@ fn main() -> Result<(), Box> { mod tests { use { super::*, - solana_sdk::genesis_config::GenesisConfig, + solana_sdk::{borsh1, genesis_config::GenesisConfig, stake}, std::{collections::HashMap, fs::remove_file, io::Write, path::Path}, }; @@ -1150,4 +1243,124 @@ mod tests { assert_eq!(genesis_config.accounts.len(), 3); } + + #[test] + fn test_append_validator_accounts_to_genesis() { + // Test invalid file returns error + assert!(load_validator_accounts( + "unknownfile", + 100, + &Rent::default(), + &mut GenesisConfig::default() + ) + .is_err()); + + let mut genesis_config = GenesisConfig::default(); + + let validator_accounts = vec![ + StakedValidatorAccountInfo { + identity_account: solana_sdk::pubkey::new_rand().to_string(), + vote_account: solana_sdk::pubkey::new_rand().to_string(), + stake_account: solana_sdk::pubkey::new_rand().to_string(), + balance_lamports: 100000000000, + stake_lamports: 10000000000, + }, + StakedValidatorAccountInfo { + identity_account: solana_sdk::pubkey::new_rand().to_string(), + vote_account: solana_sdk::pubkey::new_rand().to_string(), + stake_account: solana_sdk::pubkey::new_rand().to_string(), + balance_lamports: 200000000000, + stake_lamports: 20000000000, + }, + StakedValidatorAccountInfo { + identity_account: solana_sdk::pubkey::new_rand().to_string(), + vote_account: solana_sdk::pubkey::new_rand().to_string(), + stake_account: solana_sdk::pubkey::new_rand().to_string(), + balance_lamports: 300000000000, + stake_lamports: 30000000000, + }, + ]; + + let serialized = serde_yaml::to_string(&validator_accounts).unwrap(); + + // write accounts to file + let path = Path::new("test_append_validator_accounts_to_genesis.yml"); + let mut file = File::create(path).unwrap(); + file.write_all(b"validator_accounts:\n").unwrap(); + file.write_all(serialized.as_bytes()).unwrap(); + + load_validator_accounts( + "test_append_validator_accounts_to_genesis.yml", + 100, + &Rent::default(), + &mut genesis_config, + ) + .expect("Failed to load validator accounts"); + + remove_file(path).unwrap(); + + let accounts_per_validator = 3; + let expected_accounts_len = validator_accounts.len() * accounts_per_validator; + { + assert_eq!(genesis_config.accounts.len(), expected_accounts_len); + + // test account data matches + for b64_account in validator_accounts.iter() { + // check identity + let identity_pk = b64_account.identity_account.parse().unwrap(); + assert_eq!( + system_program::id(), + genesis_config.accounts[&identity_pk].owner + ); + assert_eq!( + b64_account.balance_lamports, + genesis_config.accounts[&identity_pk].lamports + ); + + // check vote account + let vote_pk = b64_account.vote_account.parse().unwrap(); + let vote_data = genesis_config.accounts[&vote_pk].data.clone(); + let vote_state = VoteState::deserialize(&vote_data).unwrap(); + assert_eq!(vote_state.node_pubkey, identity_pk); + assert_eq!(vote_state.authorized_withdrawer, identity_pk); + let authorized_voters = vote_state.authorized_voters(); + assert_eq!(authorized_voters.first().unwrap().1, &identity_pk); + + // check stake account + let stake_pk = b64_account.stake_account.parse().unwrap(); + assert_eq!( + b64_account.stake_lamports, + genesis_config.accounts[&stake_pk].lamports + ); + + let stake_data = genesis_config.accounts[&stake_pk].data.clone(); + let stake_state = + borsh1::try_from_slice_unchecked::(&stake_data).unwrap(); + assert!( + matches!(stake_state, StakeStateV2::Stake(_, _, _)), + "Expected StakeStateV2::Stake variant" + ); + + if let StakeStateV2::Stake(meta, stake, stake_flags) = stake_state { + assert_eq!(meta.authorized.staker, identity_pk); + assert_eq!(meta.authorized.withdrawer, identity_pk); + + assert_eq!(stake.delegation.voter_pubkey, vote_pk); + let stake_account = AccountSharedData::new( + b64_account.stake_lamports, + StakeStateV2::size_of(), + &solana_stake_program::id(), + ); + let rent_exempt_reserve = + &Rent::default().minimum_balance(stake_account.data().len()); + assert_eq!( + stake.delegation.stake, + b64_account.stake_lamports - rent_exempt_reserve + ); + + assert_eq!(stake_flags, stake::stake_flags::StakeFlags::empty()); + } + } + } + } }