From 8a879faac7b3a312808faff61f79f297862cab22 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Thu, 21 Nov 2019 12:05:31 -0800 Subject: [PATCH] add genesis stake placeholders (#6969) * add investor stake placeholders fixups fixups review comments, fixups make more data-looky for easier management rent may be zero rework with more tables, derived keys fixups rebase-fix fixups fixups * genesis is now too big to boot in 10 seconds --- Cargo.lock | 1 + ci/run-sanity.sh | 2 +- cli/src/cli.rs | 5 +- cli/src/stake.rs | 13 +- genesis/Cargo.toml | 1 + genesis/src/address_generator.rs | 32 ++ genesis/src/genesis_accounts.rs | 659 +++++++++++++++++++++++- genesis/src/main.rs | 85 +-- genesis/src/stakes.rs | 265 ++++++++++ genesis/src/unlocks.rs | 210 ++++++++ programs/stake/src/config.rs | 21 +- programs/stake/src/lib.rs | 12 +- programs/stake/src/rewards_pools.rs | 4 +- programs/stake/src/stake_instruction.rs | 4 +- programs/stake/src/stake_state.rs | 94 +++- programs/storage/src/rewards_pools.rs | 3 +- runtime/src/bank.rs | 44 +- sdk/src/epoch_schedule.rs | 2 +- sdk/src/instruction_processor_utils.rs | 2 +- sdk/src/timing.rs | 42 +- 20 files changed, 1379 insertions(+), 122 deletions(-) create mode 100644 genesis/src/address_generator.rs create mode 100644 genesis/src/stakes.rs create mode 100644 genesis/src/unlocks.rs diff --git a/Cargo.lock b/Cargo.lock index d168c32b5..22a8ad63b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3438,6 +3438,7 @@ version = "0.21.0" dependencies = [ "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ci/run-sanity.sh b/ci/run-sanity.sh index 2d7823785..f83b8d641 100755 --- a/ci/run-sanity.sh +++ b/ci/run-sanity.sh @@ -8,7 +8,7 @@ rm -f config/run/init-completed timeout 15 ./run.sh & pid=$! -attempts=10 +attempts=20 while [[ ! -f config/run/init-completed ]]; do sleep 1 if ((--attempts == 0)); then diff --git a/cli/src/cli.rs b/cli/src/cli.rs index ba208851e..92d88df1b 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1862,7 +1862,10 @@ mod tests { stake_account: bob_keypair.into(), staker: None, withdrawer: None, - lockup: Lockup { slot: 0, custodian }, + lockup: Lockup { + epoch: 0, + custodian, + }, lamports: 1234, }; let signature = process_command(&config); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 008889ba6..43ebfad4a 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -271,7 +271,7 @@ impl StakeSubCommands for App<'_, '_> { pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result { let stake_account = keypair_of(matches, "stake_account").unwrap(); - let slot = value_of(&matches, "lockup").unwrap_or(0); + let epoch = value_of(&matches, "lockup").unwrap_or(0); let custodian = pubkey_of(matches, "custodian").unwrap_or_default(); let staker = pubkey_of(matches, "authorized_staker"); let withdrawer = pubkey_of(matches, "authorized_withdrawer"); @@ -282,7 +282,7 @@ pub fn parse_stake_create_account(matches: &ArgMatches<'_>) -> Result Self { + Self { + base_pubkey: *base_pubkey, + base_name: base_name.to_string(), + nth: 0, + } + } + pub fn nth(&self, nth: usize) -> Pubkey { + Pubkey::new( + hashv(&[ + self.base_pubkey.as_ref(), + format!("{}:{}", self.base_name, nth).as_bytes(), + ]) + .as_ref(), + ) + } + pub fn next(&mut self) -> Pubkey { + let nth = self.nth; + self.nth += 1; + self.nth(nth) + } +} diff --git a/genesis/src/genesis_accounts.rs b/genesis/src/genesis_accounts.rs index f2ca925c8..2e21055ec 100644 --- a/genesis/src/genesis_accounts.rs +++ b/genesis/src/genesis_accounts.rs @@ -1,7 +1,636 @@ -use solana_sdk::{account::Account, pubkey::Pubkey}; +use crate::{ + stakes::{create_and_add_stakes, StakerInfo}, + unlocks::UnlockInfo, +}; +use solana_sdk::{genesis_config::GenesisConfig, native_token::sol_to_lamports}; -pub(crate) fn create_genesis_accounts() -> Vec<(Pubkey, Account)> { - vec![] +// 30 "month" schedule is 1/5th at 6 months +// 1/24 at each 1/12 of a year thereafter +const BATCH_ONE_UNLOCK_INFO: UnlockInfo = UnlockInfo { + cliff_fraction: 0.2, + cliff_years: 0.5, + unlocks: 24, + unlock_years: 1.0 / 12.0, +}; + +// 1st batch +const BATCH_ONE_STAKER_INFOS: &[StakerInfo] = &[ + StakerInfo { + name: "diligent bridge", + staker: "ab22196afde08a090a3721eb20e3e1ea84d36e14d1a3f0815b236b300d9d33ef", + withdrawer: "a2a7ae9098f862f4b3ba7d102d174de5e84a560444c39c035f3eeecce442eadc", + sol: 6_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "four wish", + staker: "6a56514c29f6b1de4d46164621d6bd25b337a711f569f9283c1143c7e8fb546e", + withdrawer: "b420af728f58d9f269d6e07fbbaecf6ed6535e5348538e3f39f2710351f2b940", + sol: 10_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "simple friends", + staker: "ddf2e4c81eafae2d68ac99171b066c87bddb168d6b7c07333cd951f36640163d", + withdrawer: "312fa06ccf1b671b26404a34136161ed2aba9e66f248441b4fddb5c592fde560", + sol: 1_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "noxious leather", + staker: "0cbf98cd35ceff84ca72b752c32cc3eeee4f765ca1bef1140927ebf5c6e74339", + withdrawer: "467e06fa25a9e06824eedc926ce431947ed99c728bed36be54561354c1330959", + sol: 6_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "worthless direction", + staker: "ef1562bf9edfd0f5e62530cce4244e8de544a3a30075a2cd5c9074edfbcbe78a", + withdrawer: "2ab26abb9d8131a30a4a63446125cf961ece4b926c31cce0eb84da4eac3f836e", + sol: 12_500_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "historical company", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 322_850.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "callous money", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 5_927_155.25, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "outstanding jump", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "feeble toes", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 750_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "disillusioned deer", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "unwritten songs", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "overt dime", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 500_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "slow committee", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "curvy twig", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "gamy scissors", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "mushy key", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "marked silver", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "free sock", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "tremendous meeting", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "panoramic cloth", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "normal kick", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_500_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "unbecoming observation", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "cut beginner", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "alcoholic button", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 625_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "old-fashioned clover", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 750_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "expensive underwear", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_500_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "like dust", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 5_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "rapid straw", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 5_850_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "windy trousers", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_579_350.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "dramatic veil", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 3_611_110.50, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "incandescent skin", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 3_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "spiky love", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 3_250_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, +]; + +// 30 "month" schedule is 1/5th at 6 months +// 1/24 at each 1/12 of a year thereafter +const BATCH_TWO_UNLOCK_INFO: UnlockInfo = UnlockInfo { + cliff_fraction: 0.2, + cliff_years: 0.5, + unlocks: 24, + unlock_years: 1.0 / 12.0, +}; +const BATCH_TWO_STAKER_INFOS: &[StakerInfo] = &[ + // 2nd batch + StakerInfo { + name: "macabre note", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "alcoholic letter", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "heady trucks", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "ten support", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "foregoing middle", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 800_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "ludicrous destruction", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "numberless wheel", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "short powder", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "cut name", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "six fly", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "mindless pickle", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 100_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "marked rabbit", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 38_741.36, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "jagged doctor", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 711_258.64, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "truthful pollution", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_587_300.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "unkempt activity", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_222_220.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "ritzy view", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 40_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "remarkable plant", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 300_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "busy value", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 100_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "imperfect slave", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 222_065.84, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "uneven drawer", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 400_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "far behavior", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 4_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "abaft memory", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 400_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "poor glove", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "strange iron", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "nonstop rail", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_000_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "milky bait", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 400_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "wandering start", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_200_000.0, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, +]; +// 30 "month" schedule is 1/5th at 6 months +// 1/24 at each 1/12 of a year thereafter +pub const BATCH_THREE_UNLOCK_INFO: UnlockInfo = UnlockInfo { + cliff_fraction: 0.2, + cliff_years: 0.5, + unlocks: 24, + unlock_years: 1.0 / 12.0, +}; +pub const BATCH_THREE_STAKER_INFOS: &[StakerInfo] = &[ + // 3rd batch + StakerInfo { + name: "dusty dress", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_212_121.21, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "godly bed", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 151_515.15, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "innocent property", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 227_272.73, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "responsible bikes", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 3_030_303.03, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "learned market", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 3_030_303.03, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "jumpy school", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 303_030.30, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "sticky houses", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_515_151.52, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "bustling basketball", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 1_515_152.52, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "ordinary dad", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 606_060.61, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "absurd bat", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 90_909.09, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "cloudy ocean", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 67_945.45, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "black-and-white fold", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 757_575.76, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "stale part", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 45_454.55, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "available health", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 2_797_575.76, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "afraid visitor", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 481_818.18, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "arrogant front", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 151_515.15, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "juvenile zinc", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 151_515.15, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "disturbed box", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 303_030.30, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "disagreeable skate", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 454_545.45, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "miscreant sidewalk", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 75_757.58, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + StakerInfo { + name: "shy play", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: 303_030.30, + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, +]; + +fn add_stakes( + genesis_config: &mut GenesisConfig, + staker_infos: &[StakerInfo], + unlock_info: &UnlockInfo, + granularity: u64, +) -> u64 { + staker_infos + .iter() + .map(|staker_info| { + create_and_add_stakes(genesis_config, staker_info, unlock_info, granularity) + }) + .sum::() +} + +pub(crate) fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 { + add_stakes( + genesis_config, + &BATCH_ONE_STAKER_INFOS, + &BATCH_ONE_UNLOCK_INFO, + sol_to_lamports(1_000_000.0), + ) + add_stakes( + genesis_config, + &BATCH_TWO_STAKER_INFOS, + &BATCH_TWO_UNLOCK_INFO, + sol_to_lamports(1_000_000.0), + ) + add_stakes( + genesis_config, + &BATCH_THREE_STAKER_INFOS, + &BATCH_THREE_UNLOCK_INFO, + sol_to_lamports(1_000_000.0), + ) } #[cfg(test)] @@ -9,7 +638,27 @@ mod tests { use super::*; #[test] - fn test_create_genesis_accounts() { - assert_eq!(create_genesis_accounts(), vec![]); + fn test_add_genesis_accounts() { + let mut genesis_config = GenesisConfig::default(); + + let issued_lamports = add_genesis_accounts(&mut genesis_config); + + let lamports = genesis_config + .accounts + .iter() + .map(|(_, account)| account.lamports) + .sum::(); + + assert_eq!(issued_lamports, lamports); + + genesis_config + .accounts + .sort_by(|(ka, _), (kb, _)| ka.cmp(kb)); + + let len = genesis_config.accounts.len(); + genesis_config + .accounts + .dedup_by(|(ka, _), (kb, _)| ka == kb); + assert_eq!(genesis_config.accounts.len(), len); } } diff --git a/genesis/src/main.rs b/genesis/src/main.rs index eb00b0f21..1961f069c 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -1,19 +1,22 @@ //! A command-line executable for generating the chain's genesis config. +mod address_generator; mod genesis_accounts; +mod stakes; +mod unlocks; -use crate::genesis_accounts::create_genesis_accounts; +use crate::genesis_accounts::add_genesis_accounts; use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches}; use solana_clap_utils::input_parsers::pubkey_of; use solana_genesis::Base64Account; -use solana_ledger::blocktree::create_new_ledger; -use solana_ledger::poh::compute_hashes_per_tick; +use solana_ledger::{blocktree::create_new_ledger, poh::compute_hashes_per_tick}; use solana_sdk::{ account::Account, clock, epoch_schedule::EpochSchedule, fee_calculator::FeeCalculator, genesis_config::{GenesisConfig, OperatingMode}, + native_token::lamports_to_sol, native_token::sol_to_lamports, poh_config::PohConfig, pubkey::Pubkey, @@ -50,7 +53,8 @@ fn pubkey_from_str(key_str: &str) -> Result> { }) } -pub fn add_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> io::Result<()> { +pub fn load_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> io::Result { + let mut lamports = 0; let accounts_file = File::open(file.to_string())?; let genesis_accounts: HashMap = @@ -82,11 +86,11 @@ pub fn add_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> i })?; } account.executable = account_details.executable; - + lamports += account.lamports; genesis_config.add_account(pubkey, account); } - Ok(()) + Ok(lamports) } #[allow(clippy::cognitive_complexity)] @@ -322,15 +326,15 @@ fn main() -> Result<(), Box> { let bootstrap_storage_pubkey = pubkey_of(&matches, "bootstrap_storage_pubkey_file"); let faucet_pubkey = pubkey_of(&matches, "faucet_pubkey_file"); - let bootstrap_leader_vote_account = - vote_state::create_account(&bootstrap_vote_pubkey, &bootstrap_leader_pubkey, 0, 1); - let rent = Rent { lamports_per_byte_year: value_t_or_exit!(matches, "lamports_per_byte_year", u64), exemption_threshold: value_t_or_exit!(matches, "rent_exemption_threshold", f64), burn_percent: value_t_or_exit!(matches, "rent_burn_percentage", u8), }; + let bootstrap_leader_vote_account = + vote_state::create_account(&bootstrap_vote_pubkey, &bootstrap_leader_pubkey, 0, 1); + let bootstrap_leader_stake_account = stake_state::create_account( &bootstrap_leader_pubkey, &bootstrap_vote_pubkey, @@ -358,14 +362,6 @@ fn main() -> Result<(), Box> { )); } - if let Some(faucet_pubkey) = faucet_pubkey { - accounts.push(( - faucet_pubkey, - Account::new(faucet_lamports.unwrap(), 0, &system_program::id()), - )); - } - accounts.append(&mut create_genesis_accounts()); - let ticks_per_slot = value_t_or_exit!(matches, "ticks_per_slot", u64); let fee_calculator = FeeCalculator::new( @@ -388,7 +384,6 @@ fn main() -> Result<(), Box> { OperatingMode::Development => { let hashes_per_tick = compute_hashes_per_tick(poh_config.target_tick_duration, 1_000_000); - println!("Hashes per tick: {}", hashes_per_tick); poh_config.hashes_per_tick = Some(hashes_per_tick); } OperatingMode::SoftLaunch => { @@ -413,14 +408,11 @@ fn main() -> Result<(), Box> { } }; let epoch_schedule = EpochSchedule::new(slots_per_epoch); - println!( - "Genesis mode: {:?} hashes per tick: {:?} slots_per_epoch: {}", - operating_mode, poh_config.hashes_per_tick, slots_per_epoch - ); let native_instruction_processors = solana_genesis_programs::get_programs(operating_mode, 0).unwrap(); let inflation = solana_genesis_programs::get_inflation(operating_mode, 0).unwrap(); + let mut genesis_config = GenesisConfig { accounts, native_instruction_processors, @@ -434,17 +426,46 @@ fn main() -> Result<(), Box> { ..GenesisConfig::default() }; - if let Some(files) = matches.values_of("primordial_accounts_file") { - for file in files { - add_genesis_accounts(file, &mut genesis_config)?; - } + if let Some(faucet_pubkey) = faucet_pubkey { + genesis_config.add_account( + faucet_pubkey, + Account::new(faucet_lamports.unwrap(), 0, &system_program::id()), + ); } // add genesis stuff from storage and stake solana_storage_program::rewards_pools::add_genesis_accounts(&mut genesis_config); solana_stake_program::add_genesis_accounts(&mut genesis_config); + if let Some(files) = matches.values_of("primordial_accounts_file") { + for file in files { + load_genesis_accounts(file, &mut genesis_config)?; + } + } + + add_genesis_accounts(&mut genesis_config); + create_new_ledger(&ledger_path, &genesis_config)?; + + println!( + "Genesis mode: {:?} hashes per tick: {:?} slots_per_epoch: {} capitalization: {}SOL in {} accounts", + operating_mode, + genesis_config.poh_config.hashes_per_tick, + slots_per_epoch, + lamports_to_sol( + genesis_config + .accounts + .iter() + .map(|(pubkey, account)| { + if account.lamports == 0 { + panic!("{:?}", (pubkey, account)); + } + account.lamports + }) + .sum::()), + genesis_config.accounts.len() + ); + Ok(()) } @@ -462,7 +483,7 @@ mod tests { #[test] fn test_append_primordial_accounts_to_genesis() { // Test invalid file returns error - assert!(add_genesis_accounts("unknownfile", &mut GenesisConfig::default()).is_err()); + assert!(load_genesis_accounts("unknownfile", &mut GenesisConfig::default()).is_err()); let mut genesis_config = GenesisConfig::default(); @@ -500,7 +521,7 @@ mod tests { let mut file = File::create(path).unwrap(); file.write_all(&serialized.into_bytes()).unwrap(); - add_genesis_accounts( + load_genesis_accounts( "test_append_primordial_accounts_to_genesis.yml", &mut genesis_config, ) @@ -572,7 +593,7 @@ mod tests { let mut file = File::create(path).unwrap(); file.write_all(&serialized.into_bytes()).unwrap(); - add_genesis_accounts( + load_genesis_accounts( "test_append_primordial_accounts_to_genesis.yml", &mut genesis_config, ) @@ -672,7 +693,7 @@ mod tests { let mut file = File::create(path).unwrap(); file.write_all(&serialized.into_bytes()).unwrap(); - add_genesis_accounts( + load_genesis_accounts( "test_append_primordial_accounts_to_genesis.yml", &mut genesis_config, ) @@ -806,7 +827,7 @@ mod tests { file.write_all(yaml_string_pubkey.as_bytes()).unwrap(); let mut genesis_config = GenesisConfig::default(); - add_genesis_accounts(path.to_str().unwrap(), &mut genesis_config).expect("genesis"); + load_genesis_accounts(path.to_str().unwrap(), &mut genesis_config).expect("genesis"); remove_file(path).unwrap(); assert_eq!(genesis_config.accounts.len(), 4); @@ -834,7 +855,7 @@ mod tests { file.write_all(yaml_string_keypair.as_bytes()).unwrap(); let mut genesis_config = GenesisConfig::default(); - add_genesis_accounts(path.to_str().unwrap(), &mut genesis_config).expect("genesis"); + load_genesis_accounts(path.to_str().unwrap(), &mut genesis_config).expect("genesis"); remove_file(path).unwrap(); assert_eq!(genesis_config.accounts.len(), 3); diff --git a/genesis/src/stakes.rs b/genesis/src/stakes.rs new file mode 100644 index 000000000..73841ccc8 --- /dev/null +++ b/genesis/src/stakes.rs @@ -0,0 +1,265 @@ +//! stakes generator +use crate::{ + address_generator::AddressGenerator, + unlocks::{UnlockInfo, Unlocks}, +}; +use solana_sdk::{ + account::Account, clock::Slot, genesis_config::GenesisConfig, native_token::sol_to_lamports, + 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, +}; + +#[derive(Debug)] +pub struct StakerInfo { + pub name: &'static str, + pub staker: &'static str, + pub withdrawer: &'static str, + pub sol: f64, + pub custodian: &'static str, +} + +// lamports required to run staking operations for one year +// 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 +fn calculate_staker_lamports(genesis_config: &GenesisConfig) -> u64 { + genesis_config.rent.minimum_balance(0).max(1) + + genesis_config.fee_calculator.max_lamports_per_signature + * genesis_config.epoch_schedule.get_epoch(years_as_slots( + 1.0, + &genesis_config.poh_config.target_tick_duration, + genesis_config.ticks_per_slot, + ) as Slot) +} + +/// create stake accounts for lamports with at most stake_granularity in each +/// account +pub fn create_and_add_stakes( + genesis_config: &mut GenesisConfig, + // information about this staker for this group of stakes + staker_info: &StakerInfo, + // description of how the stakes' lockups will expire + unlock_info: &UnlockInfo, + // the largest each stake account should be, in lamports + granularity: u64, +) -> u64 { + let authorized = Authorized { + staker: Pubkey::new(&hex::decode(staker_info.staker).expect("hex")), + withdrawer: Pubkey::new(&hex::decode(staker_info.withdrawer).expect("hex")), + }; + let custodian = Pubkey::new(&hex::decode(staker_info.custodian).expect("hex")); + + let total_lamports = sol_to_lamports(staker_info.sol); + + let staker_lamports = calculate_staker_lamports(genesis_config); + let staker_account = ( + authorized.staker, + Account::new(staker_lamports, 0, &system_program::id()), + ); + + let stakes_lamports = if !genesis_config.accounts.contains(&staker_account) { + genesis_config.accounts.push(staker_account); + + total_lamports - staker_lamports + } else { + total_lamports + }; + + // the staker account needs to be rent exempt *and* carry enough + // lamports to cover TX fees (delegation) for one year + // as we support one re-delegation per epoch + let unlocks = Unlocks::new( + unlock_info.cliff_fraction, + unlock_info.cliff_years, + unlock_info.unlocks, + unlock_info.unlock_years, + &genesis_config.epoch_schedule, + &genesis_config.poh_config.target_tick_duration, + genesis_config.ticks_per_slot, + ); + + let mut address_generator = AddressGenerator::new(&authorized.staker, staker_info.name); + + let stake_rent_exempt_reserve = get_stake_rent_exempt_reserve(&genesis_config.rent); + + for unlock in unlocks { + let lamports = unlock.amount(stakes_lamports); + + let (granularity, remainder) = if granularity < lamports { + (granularity, lamports % granularity) + } else { + (lamports, 0) + }; + + let lockup = Lockup { + epoch: unlock.epoch, + custodian, + }; + for _ in 0..(lamports / granularity).saturating_sub(1) { + genesis_config.add_account( + address_generator.next(), + create_lockup_stake_account( + &authorized, + &lockup, + &genesis_config.rent, + granularity, + ), + ); + } + if remainder <= stake_rent_exempt_reserve { + genesis_config.add_account( + address_generator.next(), + create_lockup_stake_account( + &authorized, + &lockup, + &genesis_config.rent, + granularity + remainder, + ), + ); + } else { + genesis_config.add_account( + address_generator.next(), + create_lockup_stake_account( + &authorized, + &lockup, + &genesis_config.rent, + granularity, + ), + ); + genesis_config.add_account( + address_generator.next(), + create_lockup_stake_account(&authorized, &lockup, &genesis_config.rent, remainder), + ); + } + } + total_lamports +} + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::{native_token::lamports_to_sol, rent::Rent}; + + fn create_and_check_stakes( + genesis_config: &mut GenesisConfig, + staker_info: &StakerInfo, + unlock_info: &UnlockInfo, + total_lamports: u64, + granularity: u64, + len: usize, + ) { + assert!( + total_lamports + == create_and_add_stakes(genesis_config, staker_info, unlock_info, granularity) + ); + + 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 <= granularity + || account.lamports - granularity + < get_stake_rent_exempt_reserve(&genesis_config.rent))); + } + + #[test] + fn test_create_stakes() { + // 2 unlocks + + let rent = Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }; + + let reserve = get_stake_rent_exempt_reserve(&rent); + + // verify that a small remainder ends up in the last stake + let granularity = reserve; + let total_lamports = reserve + reserve * 2 + 1; + create_and_check_stakes( + &mut GenesisConfig { + rent, + ..GenesisConfig::default() + }, + &StakerInfo { + name: "fun", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: lamports_to_sol(total_lamports), + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + &UnlockInfo { + cliff_fraction: 0.5, + cliff_years: 0.5, + unlocks: 1, + unlock_years: 0.5, + }, + total_lamports, + granularity, + 2 + 1, + ); + + // huge granularity doesn't blow up + let granularity = std::u64::MAX; + let total_lamports = reserve + reserve * 2 + 1; + create_and_check_stakes( + &mut GenesisConfig { + rent, + ..GenesisConfig::default() + }, + &StakerInfo { + name: "fun", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: lamports_to_sol(total_lamports), + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + &UnlockInfo { + cliff_fraction: 0.5, + cliff_years: 0.5, + unlocks: 1, + unlock_years: 0.5, + }, + total_lamports, + granularity, + 2 + 1, + ); + + // exactly reserve as a remainder + let granularity = reserve * 3; + let total_lamports = reserve + (granularity + reserve) * 2; + create_and_check_stakes( + &mut GenesisConfig { + rent, + ..GenesisConfig::default() + }, + &StakerInfo { + name: "fun", + staker: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + withdrawer: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + sol: lamports_to_sol(total_lamports), + custodian: "0000000000000000000000000000000000000000000000000000000000000000", + }, + &UnlockInfo { + cliff_fraction: 0.5, + cliff_years: 0.5, + unlocks: 1, + unlock_years: 0.5, + }, + total_lamports, + granularity, + 4 + 1, + ); + } +} diff --git a/genesis/src/unlocks.rs b/genesis/src/unlocks.rs new file mode 100644 index 000000000..026a8e4e9 --- /dev/null +++ b/genesis/src/unlocks.rs @@ -0,0 +1,210 @@ +//! lockups generator +use solana_sdk::{clock::Epoch, epoch_schedule::EpochSchedule, timing::years_as_slots}; +use std::time::Duration; + +#[derive(Debug)] +pub struct UnlockInfo { + pub cliff_fraction: f64, + pub cliff_years: f64, + pub unlocks: usize, + pub unlock_years: f64, +} + +#[derive(Debug, Default, Clone)] +pub struct Unlocks { + /// where in iteration over unlocks, loop var + i: usize, + /// number of unlocks after the first cliff + unlocks: usize, + /// fraction unlocked as of last event + prev_fraction: f64, + + /// first cliff + /// fraction of unlocked at first cliff + cliff_fraction: f64, + /// time of cliff, in epochs, 0-based + cliff_epoch: Epoch, + + /// post cliff + /// fraction unlocked at each post-cliff unlock + unlock_fraction: f64, + /// time between each post-cliff unlock, in Epochs + unlock_epochs: Epoch, +} + +impl Unlocks { + pub fn new( + cliff_fraction: f64, // first cliff fraction + cliff_year: f64, // first cliff time, starting from genesis, in years + unlocks: usize, // number of follow-on unlocks + unlock_years: f64, // years between each following unlock + epoch_schedule: &EpochSchedule, + tick_duration: &Duration, + ticks_per_slot: u64, + ) -> Self { + // convert cliff year to a slot height, as the cliff_year is considered from genesis + let cliff_slot = years_as_slots(cliff_year, tick_duration, ticks_per_slot) as u64; + + // get the first cliff epoch from that slot height + let cliff_epoch = epoch_schedule.get_epoch(cliff_slot); + + // assumes that the first cliff is after any epoch warmup and that follow-on + // epochs are uniform in length + let first_unlock_slot = + years_as_slots(cliff_year + unlock_years, tick_duration, ticks_per_slot) as u64; + let unlock_epochs = epoch_schedule.get_epoch(first_unlock_slot) - cliff_epoch; + + Self::from_epochs(cliff_fraction, cliff_epoch, unlocks, unlock_epochs) + } + + pub fn from_epochs( + cliff_fraction: f64, // first cliff fraction + cliff_epoch: Epoch, // first cliff epoch + unlocks: usize, // number of follow-on unlocks + unlock_epochs: Epoch, // epochs between each following unlock + ) -> Self { + let unlock_fraction = if unlocks != 0 { + (1.0 - cliff_fraction) / unlocks as f64 + } else { + 0.0 + }; + + Self { + prev_fraction: 0.0, + i: 0, + unlocks, + cliff_fraction, + cliff_epoch, + unlock_fraction, + unlock_epochs, + } + } +} + +impl Iterator for Unlocks { + type Item = Unlock; + + fn next(&mut self) -> Option { + let i = self.i; + if i == 0 { + self.i += 1; + self.prev_fraction = self.cliff_fraction; + + Some(Unlock { + prev_fraction: 0.0, + fraction: self.cliff_fraction, + epoch: self.cliff_epoch, + }) + } else if i <= self.unlocks { + self.i += 1; + + let prev_fraction = self.prev_fraction; + // move forward, tortured-looking math comes from wanting to reach 1.0 by the last + // unlock + self.prev_fraction = 1.0 - (self.unlocks - i) as f64 * self.unlock_fraction; + + Some(Unlock { + prev_fraction, + fraction: self.prev_fraction, + epoch: self.cliff_epoch + i as u64 * self.unlock_epochs, + }) + } else { + None + } + } +} + +/// describes an unlock event +#[derive(Debug, Default)] +pub struct Unlock { + /// the epoch height at which this unlock occurs + pub epoch: Epoch, + /// the fraction that was unlocked last iteration + pub prev_fraction: f64, + /// the fraction unlocked this iteration + pub fraction: f64, +} + +impl Unlock { + /// the number of lamports unlocked at this event + #[allow(clippy::float_cmp)] + pub fn amount(&self, total: u64) -> u64 { + if self.fraction == 1.0 { + total - (self.prev_fraction * total as f64) as u64 + } else { + (self.fraction * total as f64) as u64 - (self.prev_fraction * total as f64) as u64 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_make_lockups() { + // this number just a random val + let total_lamports: u64 = 1725987234408923; + + // expected config + const EPOCHS_PER_MONTH: Epoch = 2; + + assert_eq!( + Unlocks::from_epochs(0.20, 6 * EPOCHS_PER_MONTH, 24, EPOCHS_PER_MONTH) + .map(|unlock| unlock.amount(total_lamports)) + .sum::(), + total_lamports + ); + + // one tick/sec + let tick_duration = Duration::new(1, 0); + // one tick per slot + let ticks_per_slot = 1; + // two-week epochs at one second per slot + let epoch_schedule = EpochSchedule::custom(14 * 24 * 60 * 60, 0, false); + assert_eq!( + // 30 "month" schedule is 1/5th at 6 months + // 1/24 at each 1/12 of a year thereafter + Unlocks::new( + 0.20, + 0.5, + 24, + 1.0 / 12.0, + &epoch_schedule, + &tick_duration, + ticks_per_slot, + ) + .map(|unlock| { + if unlock.prev_fraction == 0.0 { + assert_eq!(unlock.epoch, 13); // 26 weeks is 1/2 year, first cliff + } else if unlock.prev_fraction == 0.2 { + assert_eq!(unlock.epoch, 15); // subsequent unlocks are separated by 2 weeks + } + unlock.amount(total_lamports) + }) + .sum::(), + total_lamports + ); + assert_eq!( + Unlocks::new( + 0.20, + 1.5, // start 1.5 years after genesis + 24, + 1.0 / 12.0, + &epoch_schedule, + &tick_duration, + ticks_per_slot, + ) + .map(|unlock| { + if unlock.prev_fraction == 0.0 { + assert_eq!(unlock.epoch, 26 + 13); // 26 weeks is 1/2 year, first cliff is 1.5 years + } else if unlock.prev_fraction == 0.2 { + assert_eq!(unlock.epoch, 26 + 15); // subsequent unlocks are separated by 2 weeks + } + unlock.amount(total_lamports) + }) + .sum::(), + total_lamports + ); + } +} diff --git a/programs/stake/src/config.rs b/programs/stake/src/config.rs index 9dd95b2c2..23c85db04 100644 --- a/programs/stake/src/config.rs +++ b/programs/stake/src/config.rs @@ -5,8 +5,8 @@ use serde_derive::{Deserialize, Serialize}; use solana_config_program::{create_config_account, get_config_data, ConfigState}; use solana_sdk::{ account::{Account, KeyedAccount}, + genesis_config::GenesisConfig, instruction::InstructionError, - pubkey::Pubkey, }; // stake config ID @@ -57,8 +57,15 @@ impl ConfigState for Config { } } -pub fn create_genesis_account() -> (Pubkey, Account) { - (id(), create_config_account(vec![], &Config::default(), 100)) +pub fn add_genesis_account(genesis_config: &mut GenesisConfig) -> u64 { + let mut account = create_config_account(vec![], &Config::default(), 0); + let lamports = genesis_config.rent.minimum_balance(account.data.len()); + + account.lamports = lamports.max(1); + + genesis_config.add_account(id(), account); + + lamports } pub fn create_account(lamports: u64, config: &Config) -> Account { @@ -75,19 +82,15 @@ pub fn from_keyed_account(account: &KeyedAccount) -> Result u64 { + for (pubkey, account) in rewards_pools::create_genesis_accounts() { genesis_config.add_rewards_pool(pubkey, account); } - let (pubkey, account) = create_genesis_account(); - genesis_config.add_account(pubkey, account); + config::add_genesis_account(genesis_config) } diff --git a/programs/stake/src/rewards_pools.rs b/programs/stake/src/rewards_pools.rs index e86eb9416..047cf922e 100644 --- a/programs/stake/src/rewards_pools.rs +++ b/programs/stake/src/rewards_pools.rs @@ -31,7 +31,7 @@ pub fn random_id() -> Pubkey { Pubkey::new(id.as_ref()) } -pub fn create_rewards_accounts() -> Vec<(Pubkey, Account)> { +pub fn create_genesis_accounts() -> Vec<(Pubkey, Account)> { let mut accounts = Vec::with_capacity(NUM_REWARDS_POOLS); let mut pubkey = id(); @@ -51,7 +51,7 @@ mod tests { #[test] fn test() { - let accounts = create_rewards_accounts(); + let accounts = create_genesis_accounts(); for _i in 0..NUM_REWARDS_POOLS { let id = random_id(); diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 425b62f70..3587e9acd 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -365,7 +365,7 @@ mod tests { } 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()) + config::create_account(0, &config::Config::default()) } else if sysvar::rent::check_id(&meta.pubkey) { sysvar::rent::create_account(1, &Rent::default()) } else { @@ -588,7 +588,7 @@ mod tests { KeyedAccount::new( &config::id(), false, - &mut config::create_account(1, &config::Config::default()) + &mut config::create_account(0, &config::Config::default()) ), ], &serialize(&StakeInstruction::DelegateStake).unwrap(), diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index a964b2494..5f80599ad 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -72,9 +72,9 @@ pub enum StakeAuthorize { #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] pub struct Lockup { - /// slot height at which this stake will allow withdrawal, unless + /// epoch at which this stake will allow withdrawal, unless /// to the custodian - pub slot: Slot, + pub epoch: Epoch, /// custodian account, the only account to which this stake will honor a /// withdrawal before lockup expires. After lockup expires, custodian /// is irrelevant @@ -94,16 +94,6 @@ pub struct Meta { pub lockup: Lockup, } -impl Meta { - pub fn auto(authorized: &Pubkey) -> Self { - Self { - authorized: Authorized::auto(authorized), - rent_exempt_reserve: Rent::default().minimum_balance(std::mem::size_of::()), - ..Meta::default() - } - } -} - #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] pub struct Stake { /// most recently delegated vote account pubkey @@ -715,7 +705,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { // verify that lockup has expired or that the withdrawal is going back // to the custodian - if lockup.slot > clock.slot && lockup.custodian != *to.unsigned_key() { + if lockup.epoch > clock.epoch && lockup.custodian != *to.unsigned_key() { return Err(StakeError::LockupInForce.into()); } @@ -764,7 +754,34 @@ where } } -// utility function, used by Bank, tests, genesis +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, + lockup: &Lockup, + rent: &Rent, + lamports: u64, +) -> Account { + let mut stake_account = Account::new(lamports, std::mem::size_of::(), &id()); + + let rent_exempt_reserve = rent.minimum_balance(stake_account.data.len()); + assert!(lamports >= rent_exempt_reserve); + + stake_account + .set_state(&StakeState::Initialized(Meta { + authorized: *authorized, + lockup: *lockup, + rent_exempt_reserve, + })) + .expect("set_state"); + + stake_account +} + +// utility function, used by Bank, tests, genesis for bootstrap pub fn create_account( authorized: &Pubkey, voter_pubkey: &Pubkey, @@ -775,19 +792,18 @@ pub fn create_account( let mut stake_account = Account::new(lamports, std::mem::size_of::(), &id()); let vote_state = VoteState::from(vote_account).expect("vote_state"); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); + + let rent_exempt_reserve = rent.minimum_balance(stake_account.data.len()); + stake_account .set_state(&StakeState::Stake( Meta { + authorized: Authorized::auto(authorized), rent_exempt_reserve, - authorized: Authorized { - staker: *authorized, - withdrawer: *authorized, - }, - lockup: Lockup::default(), + ..Meta::default() }, Stake::new( - lamports - rent_exempt_reserve, // underflow is an error, assert!(lamports> rent_exempt_reserve); + lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve); voter_pubkey, &vote_state, std::u64::MAX, @@ -806,6 +822,15 @@ mod tests { use solana_sdk::{account::Account, pubkey::Pubkey, system_program}; use solana_vote_program::vote_state; + impl Meta { + pub fn auto(authorized: &Pubkey) -> Self { + Self { + authorized: Authorized::auto(authorized), + ..Meta::default() + } + } + } + #[test] fn test_stake_state_stake_from_fail() { let mut stake_account = Account::new(0, std::mem::size_of::(), &id()); @@ -1324,7 +1349,10 @@ mod tests { assert_eq!( stake_keyed_account.initialize( &Authorized::auto(&stake_pubkey), - &Lockup { slot: 1, custodian }, + &Lockup { + epoch: 1, + custodian + }, &Rent::default(), ), Ok(()) @@ -1333,8 +1361,14 @@ mod tests { assert_eq!( StakeState::from(&stake_keyed_account.account).unwrap(), StakeState::Initialized(Meta { - lockup: Lockup { slot: 1, custodian }, - ..Meta::auto(&stake_pubkey) + lockup: Lockup { + epoch: 1, + custodian + }, + ..Meta { + authorized: Authorized::auto(&stake_pubkey), + ..Meta::default() + } }) ); @@ -1467,7 +1501,10 @@ mod tests { stake_keyed_account .initialize( &Authorized::auto(&stake_pubkey), - &Lockup { slot: 0, custodian }, + &Lockup { + epoch: 0, + custodian, + }, &Rent::default(), ) .unwrap(); @@ -1666,7 +1703,10 @@ mod tests { let mut stake_account = Account::new_data_with_space( total_lamports, &StakeState::Initialized(Meta { - lockup: Lockup { slot: 1, custodian }, + lockup: Lockup { + epoch: 1, + custodian, + }, ..Meta::auto(&stake_pubkey) }), std::mem::size_of::(), @@ -1715,7 +1755,7 @@ mod tests { // lockup has expired let mut to_keyed_account = KeyedAccount::new(&to, false, &mut to_account); - clock.slot += 1; + clock.epoch += 1; assert_eq!( stake_keyed_account.withdraw( total_lamports, diff --git a/programs/storage/src/rewards_pools.rs b/programs/storage/src/rewards_pools.rs index 6d2ad599e..3c55d53fb 100644 --- a/programs/storage/src/rewards_pools.rs +++ b/programs/storage/src/rewards_pools.rs @@ -20,13 +20,14 @@ solana_sdk::solana_name_id!(ID, "StorageMiningPoo111111111111111111111111111"); // to cut down on collisions for redemptions, we make multiple accounts pub const NUM_REWARDS_POOLS: usize = 32; -pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) { +pub fn add_genesis_accounts(genesis_config: &mut GenesisConfig) -> u64 { let mut pubkey = id(); for _i in 0..NUM_REWARDS_POOLS { genesis_config.add_rewards_pool(pubkey, create_rewards_pool()); pubkey = Pubkey::new(hash(pubkey.as_ref()).as_ref()); } + 0 // didn't consume any lamports } pub fn random_id() -> Pubkey { diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 91594c84f..8463b53a7 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -41,7 +41,7 @@ use solana_sdk::{ slot_hashes::SlotHashes, system_transaction, sysvar::{self, Sysvar}, - timing::duration_as_ns, + timing::years_as_slots, transaction::{Result, Transaction, TransactionError}, }; use std::{ @@ -699,12 +699,11 @@ impl Bank { self.ticks_per_slot = genesis_config.ticks_per_slot; self.slots_per_segment = genesis_config.slots_per_segment; self.max_tick_height = (self.slot + 1) * self.ticks_per_slot; - // ticks/year = seconds/year ... - self.slots_per_year = SECONDS_PER_YEAR - // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s - *(1_000_000_000.0 / duration_as_ns(&genesis_config.poh_config.target_tick_duration) as f64) - // / ticks/slot - / self.ticks_per_slot as f64; + self.slots_per_year = years_as_slots( + 1.0, + &genesis_config.poh_config.target_tick_duration, + self.ticks_per_slot, + ); self.epoch_schedule = genesis_config.epoch_schedule; @@ -1813,23 +1812,24 @@ mod tests { let bank = Bank::new_from_parent( &root_bank, &Pubkey::default(), - 2 * (SECONDS_PER_YEAR - // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s - *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) - // / ticks/slot - / genesis_block.ticks_per_slot as f64) as u64, + years_as_slots( + 2.0, + &genesis_block.poh_config.target_tick_duration, + genesis_block.ticks_per_slot, + ) as u64, ); let root_bank_2 = Arc::new(Bank::new(&genesis_block)); let bank_with_success_txs = Bank::new_from_parent( &root_bank_2, &Pubkey::default(), - 2 * (SECONDS_PER_YEAR - // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s - *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) - // / ticks/slot - / genesis_block.ticks_per_slot as f64) as u64, + years_as_slots( + 2.0, + &genesis_block.poh_config.target_tick_duration, + genesis_block.ticks_per_slot, + ) as u64, ); + assert_eq!(bank.last_blockhash(), genesis_block.hash()); // Initialize credit-debit and credit only accounts @@ -2017,11 +2017,11 @@ mod tests { let mut bank = Bank::new_from_parent( root_bank, &Pubkey::default(), - 2 * (SECONDS_PER_YEAR - // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s - *(1_000_000_000.0 / duration_as_ns(&genesis_block.poh_config.target_tick_duration) as f64) - // / ticks/slot - / genesis_block.ticks_per_slot as f64) as u64, + years_as_slots( + 2.0, + &genesis_block.poh_config.target_tick_duration, + genesis_block.ticks_per_slot, + ) as u64, ); bank.rent_collector.slots_per_year = 421_812.0; bank.add_instruction_processor(mock_program_id, mock_process_instruction); diff --git a/sdk/src/epoch_schedule.rs b/sdk/src/epoch_schedule.rs index 4c44370f8..514ef19a5 100644 --- a/sdk/src/epoch_schedule.rs +++ b/sdk/src/epoch_schedule.rs @@ -45,7 +45,7 @@ impl EpochSchedule { pub fn new(slots_per_epoch: u64) -> Self { Self::custom(slots_per_epoch, slots_per_epoch, true) } - pub fn custom(slots_per_epoch: Epoch, leader_schedule_slot_offset: u64, warmup: bool) -> Self { + pub fn custom(slots_per_epoch: u64, leader_schedule_slot_offset: u64, warmup: bool) -> Self { assert!(slots_per_epoch >= MINIMUM_SLOTS_PER_EPOCH as u64); let (first_normal_epoch, first_normal_slot) = if warmup { let next_power_of_two = slots_per_epoch.next_power_of_two(); diff --git a/sdk/src/instruction_processor_utils.rs b/sdk/src/instruction_processor_utils.rs index 07bb180b0..07d240348 100644 --- a/sdk/src/instruction_processor_utils.rs +++ b/sdk/src/instruction_processor_utils.rs @@ -66,7 +66,7 @@ macro_rules! solana_entrypoint( /// ``` #[macro_export] macro_rules! declare_program( - ($id:ident, $bs58:expr, $name:ident, $entrypoint:ident) => ( + ($id:ident, $bs58:expr, $name:ident, $entrypoint:expr) => ( $crate::solana_name_id!($id, $bs58); #[macro_export] diff --git a/sdk/src/timing.rs b/sdk/src/timing.rs index be0c87dcc..448faadd6 100644 --- a/sdk/src/timing.rs +++ b/sdk/src/timing.rs @@ -1,6 +1,5 @@ //! The `timing` module provides std::time utility functions. -use std::time::Duration; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub fn duration_as_ns(d: &Duration) -> u64 { d.as_secs() * 1_000_000_000 + u64::from(d.subsec_nanos()) @@ -24,3 +23,42 @@ pub fn timestamp() -> u64 { .expect("create timestamp in timing"); duration_as_ms(&now) } + +pub const SECONDS_PER_YEAR: f64 = (365.242_199 * 24.0 * 60.0 * 60.0); + +/// from years to slots +pub fn years_as_slots(years: f64, tick_duration: &Duration, ticks_per_slot: u64) -> f64 { + // slots is years * slots/year + years * + // slots/year is seconds/year ... + SECONDS_PER_YEAR + // * (ns/s)/(ns/tick) / ticks/slot = 1/s/1/tick = ticks/s + *(1_000_000_000.0 / duration_as_ns(tick_duration) as f64) + // / ticks/slot + / ticks_per_slot as f64 +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_years_as_slots() { + let tick_duration = Duration::from_millis(1000 / 160); + + // interestingly large numbers with 160 ticks/second + assert_eq!(years_as_slots(0.0, &tick_duration, 4) as u64, 0); + assert_eq!( + years_as_slots(1.0 / 12f64, &tick_duration, 4) as u64, + 109_572_659 + ); + assert_eq!(years_as_slots(1.0, &tick_duration, 4) as u64, 1_314_871_916); + + let tick_duration = Duration::from_millis(1000); + // one second in years with one tick per second + one tick per slot + assert_eq!( + years_as_slots(1.0 / SECONDS_PER_YEAR, &tick_duration, 1), + 1.0 + ); + } +}