From df1c4733415897acb562727bdcb0b23738b4e6db Mon Sep 17 00:00:00 2001 From: Sagar Dhawan Date: Wed, 26 Jun 2019 10:40:03 -0700 Subject: [PATCH] Add storage point tracking and tie in storage rewards to economics (#4824) * Add storage point tracking and tie in storage rewards to epochs and economics * Prevent validators from updating their validations for a segment * Fix test * Retain syscall scoping for readability * Update Credits to own epoch tracking --- core/src/replicator.rs | 7 +- programs/storage_api/src/storage_contract.rs | 182 +++++++++++------- .../storage_api/src/storage_instruction.rs | 13 +- programs/storage_api/src/storage_processor.rs | 34 +++- .../tests/storage_processor.rs | 36 ++-- runtime/src/bank.rs | 23 ++- runtime/src/storage_utils.rs | 122 +++++++++++- sdk/src/inflation.rs | 18 +- sdk/src/syscall/rewards.rs | 6 +- 9 files changed, 318 insertions(+), 123 deletions(-) diff --git a/core/src/replicator.rs b/core/src/replicator.rs index d2c05fe3a6..c827708d2f 100644 --- a/core/src/replicator.rs +++ b/core/src/replicator.rs @@ -338,11 +338,8 @@ impl Replicator { let client = crate::gossip_service::get_client(&nodes); if let Ok(Some(account)) = client.get_account(&self.storage_keypair.pubkey()) { - if let Ok(StorageContract::ReplicatorStorage { - reward_validations, .. - }) = account.state() - { - if !reward_validations.is_empty() { + if let Ok(StorageContract::ReplicatorStorage { validations, .. }) = account.state() { + if !validations.is_empty() { let ix = storage_instruction::claim_reward( &self.keypair.pubkey(), &self.storage_keypair.pubkey(), diff --git a/programs/storage_api/src/storage_contract.rs b/programs/storage_api/src/storage_contract.rs index da0813d088..4b122de073 100644 --- a/programs/storage_api/src/storage_contract.rs +++ b/programs/storage_api/src/storage_contract.rs @@ -9,14 +9,33 @@ use solana_sdk::hash::Hash; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signature; +use solana_sdk::syscall; use std::collections::BTreeMap; -pub const VALIDATOR_REWARD: u64 = 200; -pub const REPLICATOR_REWARD: u64 = 200; -// Todo Tune this for actual use cases when replicators are feature complete +// Todo Tune this for actual use cases when PoRep is feature complete pub const STORAGE_ACCOUNT_SPACE: u64 = 1024 * 8; pub const MAX_PROOFS_PER_SEGMENT: usize = 80; +#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Credits { + // current epoch + epoch: u64, + // currently pending credits + pub current_epoch: u64, + // credits ready to be claimed + pub redeemable: u64, +} + +impl Credits { + pub fn update_epoch(&mut self, current_epoch: u64) { + if self.epoch != current_epoch { + self.epoch = current_epoch; + self.redeemable += self.current_epoch; + self.current_epoch = 0; + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, FromPrimitive)] pub enum StorageError { InvalidSegment, @@ -64,11 +83,12 @@ pub enum StorageContract { // Most recently advertised blockhash hash: Hash, // Lockouts and Rewards are per segment per replicator. It needs to remain this way until - // the challenge stage is added. Once challenges are in rewards can just be a number + // the challenge stage is added. lockout_validations: BTreeMap>>, - // lamports that are ready to be claimed - pending_lamports: u64, + // Used to keep track of ongoing credits + credits: Credits, }, + ReplicatorStorage { owner: Pubkey, // TODO what to do about duplicate proofs across segments? - Check the blockhashes @@ -76,7 +96,9 @@ pub enum StorageContract { proofs: BTreeMap>, // Map of Rewards per segment, in a BTreeMap based on the validator account that verified // the proof. This can be used for challenge stage when its added - reward_validations: BTreeMap>>, + validations: BTreeMap>>, + // Used to keep track of ongoing credits + credits: Credits, }, RewardsPool, @@ -92,7 +114,7 @@ pub fn create_validator_storage_account(owner: Pubkey, lamports: u64) -> Account slot: 0, hash: Hash::default(), lockout_validations: BTreeMap::new(), - pending_lamports: 0, + credits: Credits::default(), }) .expect("set_state"); @@ -115,7 +137,8 @@ impl<'a> StorageAccount<'a> { *storage_contract = StorageContract::ReplicatorStorage { owner, proofs: BTreeMap::new(), - reward_validations: BTreeMap::new(), + validations: BTreeMap::new(), + credits: Credits::default(), }; self.account.set_state(storage_contract) } else { @@ -131,7 +154,7 @@ impl<'a> StorageAccount<'a> { slot: 0, hash: Hash::default(), lockout_validations: BTreeMap::new(), - pending_lamports: 0, + credits: Credits::default(), }; self.account.set_state(storage_contract) } else { @@ -145,16 +168,17 @@ impl<'a> StorageAccount<'a> { segment_index: usize, signature: Signature, blockhash: Hash, - current_slot: u64, + current: syscall::current::Current, ) -> Result<(), InstructionError> { let mut storage_contract = &mut self.account.state()?; if let StorageContract::ReplicatorStorage { proofs, - reward_validations, + validations, + credits, .. } = &mut storage_contract { - let current_segment = get_segment_from_slot(current_slot); + let current_segment = get_segment_from_slot(current.slot); // clean up the account // TODO check for time correctness - storage seems to run at a delay of about 3 @@ -163,7 +187,7 @@ impl<'a> StorageAccount<'a> { .filter(|(segment, _)| **segment >= current_segment.saturating_sub(5)) .map(|(segment, proofs)| (*segment, proofs.clone())) .collect(); - *reward_validations = reward_validations + *validations = validations .iter() .filter(|(segment, _)| **segment >= current_segment.saturating_sub(10)) .map(|(segment, rewards)| (*segment, rewards.clone())) @@ -207,6 +231,7 @@ impl<'a> StorageAccount<'a> { StorageError::ProofLimitReached as u32, )); } + credits.update_epoch(current.epoch); segment_proofs.push(proof); self.account.set_state(storage_contract) } else { @@ -218,18 +243,18 @@ impl<'a> StorageAccount<'a> { &mut self, hash: Hash, slot: u64, - current_slot: u64, + current: syscall::current::Current, ) -> Result<(), InstructionError> { let mut storage_contract = &mut self.account.state()?; if let StorageContract::ValidatorStorage { slot: state_slot, hash: state_hash, lockout_validations, - pending_lamports, + credits, .. } = &mut storage_contract { - let current_segment = get_segment_from_slot(current_slot); + let current_segment = get_segment_from_slot(current.slot); let original_segment = get_segment_from_slot(*state_slot); let segment = get_segment_from_slot(slot); debug!( @@ -245,20 +270,11 @@ impl<'a> StorageAccount<'a> { *state_slot = slot; *state_hash = hash; - // storage epoch updated, move the lockout_validations to pending_lamports - let num_validations = count_valid_proofs( - &lockout_validations - .iter() - .flat_map(|(_segment, proofs)| { - proofs - .iter() - .flat_map(|(_, proof)| proof.clone()) - .collect::>() - }) - .collect::>(), - ); + // storage epoch updated, move the lockout_validations to credits + let (_num_valid, total_validations) = count_valid_proofs(&lockout_validations); lockout_validations.clear(); - *pending_lamports += VALIDATOR_REWARD * num_validations; + credits.update_epoch(current.epoch); + credits.current_epoch += total_validations; self.account.set_state(storage_contract) } else { Err(InstructionError::InvalidArgument)? @@ -268,6 +284,7 @@ impl<'a> StorageAccount<'a> { pub fn proof_validation( &mut self, me: &Pubkey, + current: syscall::current::Current, segment: u64, proofs_per_account: Vec>, replicator_accounts: &mut [StorageAccount], @@ -331,7 +348,14 @@ impl<'a> StorageAccount<'a> { .into_iter() .zip(accounts.into_iter()) .filter_map(|(checked_proofs, account)| { - if store_validation_result(me, account, segment_index, &checked_proofs).is_ok() + if store_validation_result( + me, + ¤t, + account, + segment_index, + &checked_proofs, + ) + .is_ok() { Some((account.id, checked_proofs)) } else { @@ -359,13 +383,15 @@ impl<'a> StorageAccount<'a> { pub fn claim_storage_reward( &mut self, rewards_pool: &mut KeyedAccount, + current: syscall::current::Current, + rewards: syscall::rewards::Rewards, owner: &mut StorageAccount, ) -> Result<(), InstructionError> { let mut storage_contract = &mut self.account.state()?; if let StorageContract::ValidatorStorage { owner: account_owner, - pending_lamports, + credits, .. } = &mut storage_contract { @@ -375,21 +401,24 @@ impl<'a> StorageAccount<'a> { ))? } - let pending = *pending_lamports; + credits.update_epoch(current.epoch); + let pending = (credits.redeemable as f64 * rewards.storage_point_value) as u64; if rewards_pool.account.lamports < pending { - println!("reward pool has {}", rewards_pool.account.lamports); Err(InstructionError::CustomError( StorageError::RewardPoolDepleted as u32, ))? } - rewards_pool.account.lamports -= pending; - owner.account.lamports += pending; - //clear pending_lamports - *pending_lamports = 0; + if pending >= 1 { + rewards_pool.account.lamports -= pending; + owner.account.lamports += pending; + //clear credits + credits.redeemable = 0; + } self.account.set_state(storage_contract) } else if let StorageContract::ReplicatorStorage { owner: account_owner, - reward_validations, + validations, + credits, .. } = &mut storage_contract { @@ -398,22 +427,21 @@ impl<'a> StorageAccount<'a> { StorageError::InvalidOwner as u32, ))? } - - let checked_proofs = reward_validations - .iter() - .flat_map(|(_, proofs)| { - proofs - .iter() - .flat_map(|(_, proofs)| proofs.clone()) - .collect::>() - }) - .collect::>(); - reward_validations.clear(); - let total_proofs = checked_proofs.len() as u64; - let num_validations = count_valid_proofs(&checked_proofs); - let reward = num_validations * REPLICATOR_REWARD * (num_validations / total_proofs); - rewards_pool.account.lamports -= reward; - owner.account.lamports += reward; + credits.update_epoch(current.epoch); + let (num_validations, _total_proofs) = count_valid_proofs(&validations); + credits.current_epoch += num_validations; + validations.clear(); + let reward = (credits.redeemable as f64 * rewards.storage_point_value) as u64; + if rewards_pool.account.lamports < reward { + Err(InstructionError::CustomError( + StorageError::RewardPoolDepleted as u32, + ))? + } + if reward >= 1 { + rewards_pool.account.lamports -= reward; + owner.account.lamports += reward; + credits.redeemable = 0; + } self.account.set_state(storage_contract) } else { Err(InstructionError::InvalidArgument)? @@ -428,6 +456,7 @@ pub fn create_rewards_pool() -> Account { /// Store the result of a proof validation into the replicator account fn store_validation_result( me: &Pubkey, + current: &syscall::current::Current, storage_account: &mut StorageAccount, segment: usize, proof_mask: &[ProofStatus], @@ -436,7 +465,8 @@ fn store_validation_result( match &mut storage_contract { StorageContract::ReplicatorStorage { proofs, - reward_validations, + validations, + credits, .. } => { if !proofs.contains_key(&segment) { @@ -447,24 +477,39 @@ fn store_validation_result( return Err(InstructionError::InvalidAccountData); } - reward_validations - .entry(segment) - .or_default() - .insert(*me, proof_mask.to_vec()); + let (recorded_validations, _) = count_valid_proofs(&validations); + let entry = validations.entry(segment).or_default(); + if !entry.contains_key(me) { + entry.insert(*me, proof_mask.to_vec()); + } + let (total_validations, _) = count_valid_proofs(&validations); + credits.update_epoch(current.epoch); + credits.current_epoch += total_validations - recorded_validations; } _ => return Err(InstructionError::InvalidAccountData), } storage_account.account.set_state(&storage_contract) } -fn count_valid_proofs(proofs: &[ProofStatus]) -> u64 { +fn count_valid_proofs( + validations: &BTreeMap>>, +) -> (u64, u64) { + let proofs = validations + .iter() + .flat_map(|(_, proofs)| { + proofs + .iter() + .flat_map(|(_, proofs)| proofs) + .collect::>() + }) + .collect::>(); let mut num = 0; - for proof in proofs { + for proof in proofs.iter() { if let ProofStatus::Valid = proof { num += 1; } } - num + (num, proofs.len() as u64) } #[cfg(test)] @@ -493,7 +538,7 @@ mod tests { slot: 0, hash: Hash::default(), lockout_validations: BTreeMap::new(), - pending_lamports: 0, + credits: Credits::default(), }; storage_account.account.set_state(&contract).unwrap(); if let StorageContract::ReplicatorStorage { .. } = contract { @@ -502,7 +547,8 @@ mod tests { contract = StorageContract::ReplicatorStorage { owner: Pubkey::default(), proofs: BTreeMap::new(), - reward_validations: BTreeMap::new(), + validations: BTreeMap::new(), + credits: Credits::default(), }; storage_account.account.set_state(&contract).unwrap(); if let StorageContract::ValidatorStorage { .. } = contract { @@ -530,6 +576,7 @@ mod tests { // account has no space store_validation_result( &Pubkey::default(), + &syscall::current::Current::default(), &mut account, segment_index, &vec![ProofStatus::default(); 1], @@ -547,7 +594,8 @@ mod tests { *storage_contract = StorageContract::ReplicatorStorage { owner: Pubkey::default(), proofs, - reward_validations: BTreeMap::new(), + validations: BTreeMap::new(), + credits: Credits::default(), }; }; account.account.set_state(storage_contract).unwrap(); @@ -555,6 +603,7 @@ mod tests { // proof is valid store_validation_result( &Pubkey::default(), + &syscall::current::Current::default(), &mut account, segment_index, &vec![ProofStatus::Valid], @@ -564,6 +613,7 @@ mod tests { // proof failed verification but we should still be able to store it store_validation_result( &Pubkey::default(), + &syscall::current::Current::default(), &mut account, segment_index, &vec![ProofStatus::NotValid], diff --git a/programs/storage_api/src/storage_instruction.rs b/programs/storage_api/src/storage_instruction.rs index d2b09471d3..b3bd0eaad3 100644 --- a/programs/storage_api/src/storage_instruction.rs +++ b/programs/storage_api/src/storage_instruction.rs @@ -5,7 +5,7 @@ use solana_sdk::hash::Hash; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signature; -use solana_sdk::syscall::current; +use solana_sdk::syscall::{current, rewards}; use solana_sdk::system_instruction; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -35,8 +35,10 @@ pub enum StorageInstruction { /// /// Expects 1 Account: /// 0 - Storage account with credits to redeem - /// 1 - MiningPool account to redeem credits from + /// 1 - Current Syscall to figure out the current epoch /// 2 - Replicator account to credit - this account *must* be the owner + /// 3 - MiningPool account to redeem credits from + /// 4 - Rewards Syscall to figure out point values ClaimStorageReward, ProofValidation { /// The segment during which this proof was generated @@ -168,7 +170,10 @@ pub fn proof_validation( segment: u64, checked_proofs: Vec<(Pubkey, Vec)>, ) -> Instruction { - let mut account_metas = vec![AccountMeta::new(*storage_pubkey, true)]; + let mut account_metas = vec![ + AccountMeta::new(*storage_pubkey, true), + AccountMeta::new(current::id(), false), + ]; let mut proofs = vec![]; checked_proofs.into_iter().for_each(|(id, p)| { proofs.push(p); @@ -182,6 +187,8 @@ pub fn claim_reward(owner_pubkey: &Pubkey, storage_pubkey: &Pubkey) -> Instructi let storage_instruction = StorageInstruction::ClaimStorageReward; let account_metas = vec![ AccountMeta::new(*storage_pubkey, false), + AccountMeta::new(current::id(), false), + AccountMeta::new(rewards::id(), false), AccountMeta::new(rewards_pools::random_id(), false), AccountMeta::new(*owner_pubkey, false), ]; diff --git a/programs/storage_api/src/storage_processor.rs b/programs/storage_api/src/storage_processor.rs index 002f05c990..e680df4f46 100644 --- a/programs/storage_api/src/storage_processor.rs +++ b/programs/storage_api/src/storage_processor.rs @@ -6,7 +6,7 @@ use crate::storage_instruction::StorageInstruction; use solana_sdk::account::KeyedAccount; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::syscall::current::Current; +use solana_sdk::syscall; pub fn process_instruction( _program_id: &Pubkey, @@ -42,13 +42,13 @@ pub fn process_instruction( // This instruction must be signed by `me` Err(InstructionError::InvalidArgument)?; } - let current = Current::from(&rest[0].account).unwrap(); + let current = syscall::current::from_keyed_account(&rest[0])?; storage_account.submit_mining_proof( sha_state, segment_index, signature, blockhash, - current.slot, + current, ) } StorageInstruction::AdvertiseStorageRecentBlockhash { hash, slot } => { @@ -56,31 +56,47 @@ pub fn process_instruction( // This instruction must be signed by `me` Err(InstructionError::InvalidArgument)?; } - let current = Current::from(&rest[0].account).unwrap(); - storage_account.advertise_storage_recent_blockhash(hash, slot, current.slot) + let current = syscall::current::from_keyed_account(&rest[0])?; + storage_account.advertise_storage_recent_blockhash(hash, slot, current) } StorageInstruction::ClaimStorageReward => { - if rest.len() != 2 { + if rest.len() != 4 { Err(InstructionError::InvalidArgument)?; } - let (mining_pool, owner) = rest.split_at_mut(1); + let (current, rest) = rest.split_at_mut(1); + let (rewards, rest) = rest.split_at_mut(1); + let (rewards_pools, owner) = rest.split_at_mut(1); + + let rewards = syscall::rewards::from_keyed_account(&rewards[0])?; + let current = syscall::current::from_keyed_account(¤t[0])?; let mut owner = StorageAccount::new(*owner[0].unsigned_key(), &mut owner[0].account); - storage_account.claim_storage_reward(&mut mining_pool[0], &mut owner) + storage_account.claim_storage_reward( + &mut rewards_pools[0], + current, + rewards, + &mut owner, + ) } StorageInstruction::ProofValidation { segment, proofs } => { + if rest.is_empty() { + Err(InstructionError::InvalidArgument)?; + } + + let (current, rest) = rest.split_at_mut(1); if me_unsigned || rest.is_empty() { // This instruction must be signed by `me` and `rest` cannot be empty Err(InstructionError::InvalidArgument)?; } let me_id = storage_account.id; + let current = syscall::current::from_keyed_account(¤t[0])?; let mut rest: Vec<_> = rest .iter_mut() .map(|keyed_account| { StorageAccount::new(*keyed_account.unsigned_key(), &mut keyed_account.account) }) .collect(); - storage_account.proof_validation(&me_id, segment, proofs, &mut rest) + storage_account.proof_validation(&me_id, current, segment, proofs, &mut rest) } } } diff --git a/programs/storage_program/tests/storage_processor.rs b/programs/storage_program/tests/storage_processor.rs index 2adb572b93..3166a6a23e 100644 --- a/programs/storage_program/tests/storage_processor.rs +++ b/programs/storage_program/tests/storage_processor.rs @@ -12,14 +12,13 @@ use solana_sdk::instruction::{Instruction, InstructionError}; use solana_sdk::message::Message; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; -use solana_sdk::syscall::current; use solana_sdk::syscall::current::Current; +use solana_sdk::syscall::rewards::Rewards; +use solana_sdk::syscall::{current, rewards}; use solana_sdk::system_instruction; use solana_sdk::timing::DEFAULT_TICKS_PER_SLOT; use solana_storage_api::storage_contract::StorageAccount; -use solana_storage_api::storage_contract::{ - ProofStatus, StorageContract, REPLICATOR_REWARD, STORAGE_ACCOUNT_SPACE, VALIDATOR_REWARD, -}; +use solana_storage_api::storage_contract::{ProofStatus, StorageContract, STORAGE_ACCOUNT_SPACE}; use solana_storage_api::storage_instruction; use solana_storage_api::storage_processor::process_instruction; use solana_storage_api::SLOTS_PER_SEGMENT; @@ -153,12 +152,12 @@ fn test_storage_tx() { #[test] fn test_serialize_overflow() { let pubkey = Pubkey::new_rand(); - let tick_pubkey = Pubkey::new_rand(); + let current_id = current::id(); let mut keyed_accounts = Vec::new(); let mut user_account = Account::default(); let mut current_account = current::create_account(1, 0, 0, 0); keyed_accounts.push(KeyedAccount::new(&pubkey, true, &mut user_account)); - keyed_accounts.push(KeyedAccount::new(&tick_pubkey, false, &mut current_account)); + keyed_accounts.push(KeyedAccount::new(¤t_id, false, &mut current_account)); let ix = storage_instruction::advertise_recent_blockhash( &pubkey, @@ -267,7 +266,7 @@ fn test_validate_mining() { mut genesis_block, mint_keypair, .. - } = create_genesis_block(100_000); + } = create_genesis_block(100_000_000_000); genesis_block .native_instruction_processors .push(solana_storage_program::solana_storage_program!()); @@ -402,6 +401,17 @@ fn test_validate_mining() { assert_eq!(bank_client.get_balance(&validator_storage_id).unwrap(), 10); + let bank = Arc::new(Bank::new_from_parent( + &bank, + &Pubkey::default(), + bank.slot() + bank.epoch_schedule().slots_per_epoch, + )); + let bank_client = BankClient::new_shared(&bank); + + let rewards = bank + .get_account(&rewards::id()) + .map(|account| Rewards::from(&account).unwrap()) + .unwrap(); let message = Message::new_with_payer( vec![storage_instruction::claim_reward( &owner_pubkey, @@ -412,7 +422,7 @@ fn test_validate_mining() { assert_matches!(bank_client.send_message(&[&mint_keypair], message), Ok(_)); assert_eq!( bank_client.get_balance(&owner_pubkey).unwrap(), - 1 + (VALIDATOR_REWARD * 10) + 1 + ((rewards.storage_point_value * 10_f64) as u64) ); // tick the bank into the next storage epoch so that rewards can be claimed @@ -432,12 +442,11 @@ fn test_validate_mining() { )], Some(&mint_pubkey), ); - assert_matches!(bank_client.send_message(&[&mint_keypair], message), Ok(_)); - assert_eq!( bank_client.get_balance(&owner_pubkey).unwrap(), - 1 + (VALIDATOR_REWARD * 10) + (REPLICATOR_REWARD * 5) + 1 + ((rewards.storage_point_value * 10_f64) as u64) + + (rewards.storage_point_value * 5_f64) as u64 ); let message = Message::new_with_payer( @@ -448,10 +457,11 @@ fn test_validate_mining() { Some(&mint_pubkey), ); assert_matches!(bank_client.send_message(&[&mint_keypair], message), Ok(_)); - assert_eq!( bank_client.get_balance(&owner_pubkey).unwrap(), - 1 + (VALIDATOR_REWARD * 10) + (REPLICATOR_REWARD * 10) + 1 + (rewards.storage_point_value * 10_f64) as u64 + + (rewards.storage_point_value * 5_f64) as u64 + + (rewards.storage_point_value * 5_f64) as u64 ); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5f1b4d720f..c08a3c27b8 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -269,8 +269,6 @@ impl Default for BlockhashQueue { } } -pub const DUMMY_REPLICATOR_POINTS: u64 = 100; - impl Bank { pub fn new(genesis_block: &GenesisBlock) -> Self { Self::new_with_paths(&genesis_block, None) @@ -463,22 +461,21 @@ impl Bank { let validator_points = self.stakes.write().unwrap().claim_points(); - let replicator_rewards = - self.inflation.replicator(year) * self.capitalization() as f64 * period; + let storage_rewards = self.inflation.storage(year) * self.capitalization() as f64 * period; - let replicator_points = DUMMY_REPLICATOR_POINTS; // TODO: real value for points earned last epoch + let storage_points = self.storage_accounts.write().unwrap().claim_points(); self.store_account( &rewards::id(), &rewards::create_account( 1, validator_rewards / validator_points as f64, - replicator_rewards / replicator_points as f64, + storage_rewards / storage_points as f64, ), ); self.capitalization.fetch_add( - (validator_rewards + replicator_rewards) as usize, + (validator_rewards + storage_rewards) as usize, Ordering::Relaxed, ); } @@ -1471,8 +1468,13 @@ mod tests { let ((vote_id, mut vote_account), stake) = crate::stakes::tests::create_staked_node_accounts(1_0000); - // set up stakes and vote accounts + let ((validator_id, validator_account), (replicator_id, replicator_account)) = + crate::storage_utils::tests::create_storage_accounts_with_credits(100); + + // set up stakes,vote, and storage accounts bank.store_account(&stake.0, &stake.1); + bank.store_account(&validator_id, &validator_account); + bank.store_account(&replicator_id, &replicator_account); // generate some rewards let mut vote_state = VoteState::from(&vote_account).unwrap(); @@ -1484,6 +1486,7 @@ mod tests { bank.store_account(&vote_id, &vote_account); let validator_points = bank.stakes.read().unwrap().points(); + let storage_points = bank.storage_accounts.read().unwrap().points(); // put a child bank in epoch 1, which calls update_rewards()... let bank1 = Bank::new_from_parent( @@ -1504,8 +1507,8 @@ mod tests { assert!( ((rewards.validator_point_value * validator_points as f64 - + rewards.replicator_point_value * DUMMY_REPLICATOR_POINTS as f64) - // TODO: need replicator points per-epoch - inflation as f64) + + rewards.storage_point_value * storage_points as f64) + - inflation as f64) .abs() < 1.0 // rounding, truncating ); diff --git a/runtime/src/storage_utils.rs b/runtime/src/storage_utils.rs index 9253a66b97..f02189c7f2 100644 --- a/runtime/src/storage_utils.rs +++ b/runtime/src/storage_utils.rs @@ -7,11 +7,15 @@ use std::collections::{HashMap, HashSet}; #[derive(Default, Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct StorageAccounts { - /// validator storage accounts + /// validator storage accounts and their credits validator_accounts: HashSet, - /// replicator storage accounts + /// replicator storage accounts and their credits replicator_accounts: HashSet, + + /// unclaimed points. + // 1 point == 1 storage account credit + points: HashMap, } pub fn is_storage(account: &Account) -> bool { @@ -21,21 +25,35 @@ pub fn is_storage(account: &Account) -> bool { impl StorageAccounts { pub fn store(&mut self, pubkey: &Pubkey, account: &Account) { if let Ok(storage_state) = account.state() { - if let StorageContract::ReplicatorStorage { .. } = storage_state { + if let StorageContract::ReplicatorStorage { credits, .. } = storage_state { if account.lamports == 0 { self.replicator_accounts.remove(pubkey); } else { self.replicator_accounts.insert(*pubkey); + self.points.insert(*pubkey, credits.current_epoch); } - } else if let StorageContract::ValidatorStorage { .. } = storage_state { + } else if let StorageContract::ValidatorStorage { credits, .. } = storage_state { if account.lamports == 0 { self.validator_accounts.remove(pubkey); } else { self.validator_accounts.insert(*pubkey); + self.points.insert(*pubkey, credits.current_epoch); } } }; } + + /// currently unclaimed points + pub fn points(&self) -> u64 { + self.points.values().sum() + } + + /// "claims" points, resets points to 0 + pub fn claim_points(&mut self) -> u64 { + let points = self.points(); + self.points.clear(); + points + } } pub fn validator_accounts(bank: &Bank) -> HashMap { @@ -61,13 +79,14 @@ pub fn replicator_accounts(bank: &Bank) -> HashMap { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::bank_client::BankClient; use solana_sdk::client::SyncClient; use solana_sdk::genesis_block::create_genesis_block; use solana_sdk::message::Message; use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_storage_api::storage_contract::{StorageAccount, STORAGE_ACCOUNT_SPACE}; use solana_storage_api::{storage_instruction, storage_processor}; use std::sync::Arc; @@ -113,4 +132,97 @@ mod tests { assert_eq!(validator_accounts(bank.as_ref()).len(), 1); assert_eq!(replicator_accounts(bank.as_ref()).len(), 1); } + + #[test] + fn test_points() { + // note: storage_points == storage_credits + let credits = 42; + let mut storage_accounts = StorageAccounts::default(); + assert_eq!(storage_accounts.points(), 0); + assert_eq!(storage_accounts.claim_points(), 0); + + // create random validator and replicator accounts with `credits` + let ((validator_pubkey, validator_account), (replicator_pubkey, replicator_account)) = + create_storage_accounts_with_credits(credits); + + storage_accounts.store(&validator_pubkey, &validator_account); + storage_accounts.store(&replicator_pubkey, &replicator_account); + // check that 2x credits worth of points are available + assert_eq!(storage_accounts.points(), credits * 2); + + let ((validator_pubkey, validator_account), (replicator_pubkey, mut replicator_account)) = + create_storage_accounts_with_credits(credits); + + storage_accounts.store(&validator_pubkey, &validator_account); + storage_accounts.store(&replicator_pubkey, &replicator_account); + // check that 4x credits worth of points are available + assert_eq!(storage_accounts.points(), credits * 2 * 2); + + storage_accounts.store(&validator_pubkey, &validator_account); + storage_accounts.store(&replicator_pubkey, &replicator_account); + // check that storing again has no effect + assert_eq!(storage_accounts.points(), credits * 2 * 2); + + let storage_contract = &mut replicator_account.state().unwrap(); + if let StorageContract::ReplicatorStorage { + credits: account_credits, + .. + } = storage_contract + { + account_credits.current_epoch += 1; + } + replicator_account.set_state(storage_contract).unwrap(); + storage_accounts.store(&replicator_pubkey, &replicator_account); + + // check that incremental store increases credits + assert_eq!(storage_accounts.points(), credits * 2 * 2 + 1); + + assert_eq!(storage_accounts.claim_points(), credits * 2 * 2 + 1); + // check that once redeemed, the points are gone + assert_eq!(storage_accounts.claim_points(), 0); + } + + pub fn create_storage_accounts_with_credits( + credits: u64, + ) -> ((Pubkey, Account), (Pubkey, Account)) { + let validator_pubkey = Pubkey::new_rand(); + let replicator_pubkey = Pubkey::new_rand(); + + let mut validator_account = + Account::new(1, STORAGE_ACCOUNT_SPACE as usize, &solana_storage_api::id()); + let mut validator = StorageAccount::new(validator_pubkey, &mut validator_account); + validator + .initialize_validator_storage(validator_pubkey) + .unwrap(); + let storage_contract = &mut validator_account.state().unwrap(); + if let StorageContract::ValidatorStorage { + credits: account_credits, + .. + } = storage_contract + { + account_credits.current_epoch = credits; + } + validator_account.set_state(storage_contract).unwrap(); + + let mut replicator_account = + Account::new(1, STORAGE_ACCOUNT_SPACE as usize, &solana_storage_api::id()); + let mut replicator = StorageAccount::new(replicator_pubkey, &mut replicator_account); + replicator + .initialize_replicator_storage(replicator_pubkey) + .unwrap(); + let storage_contract = &mut replicator_account.state().unwrap(); + if let StorageContract::ReplicatorStorage { + credits: account_credits, + .. + } = storage_contract + { + account_credits.current_epoch = credits; + } + replicator_account.set_state(storage_contract).unwrap(); + + ( + (validator_pubkey, validator_account), + (replicator_pubkey, replicator_account), + ) + } } diff --git a/sdk/src/inflation.rs b/sdk/src/inflation.rs index 0bcd766d5e..5d538e0c2b 100644 --- a/sdk/src/inflation.rs +++ b/sdk/src/inflation.rs @@ -22,8 +22,8 @@ pub struct Inflation { /// Duration of grant pool inflation, in years pub grant_term: f64, - /// Percentage of total inflation allocated to replicator rewards - pub replicator: f64, + /// Percentage of total inflation allocated to storage rewards + pub storage: f64, } const DEFAULT_INITIAL: f64 = 0.15; @@ -32,7 +32,7 @@ const DEFAULT_TAPER: f64 = 0.15; const DEFAULT_FOUNDATION: f64 = 0.05; const DEFAULT_GRANT: f64 = 0.05; const DEFAULT_FOUNDATION_GRANT_TERM: f64 = 7.0; -const DEFAULT_REPLICATOR: f64 = 0.10; +const DEFAULT_STORAGE: f64 = 0.10; impl Default for Inflation { fn default() -> Self { @@ -44,7 +44,7 @@ impl Default for Inflation { foundation_term: DEFAULT_FOUNDATION_GRANT_TERM, grant: DEFAULT_GRANT, grant_term: DEFAULT_FOUNDATION_GRANT_TERM, - replicator: DEFAULT_REPLICATOR, + storage: DEFAULT_STORAGE, } } } @@ -63,12 +63,12 @@ impl Inflation { /// portion of total that goes to validators pub fn validator(&self, year: f64) -> f64 { - self.total(year) - self.replicator(year) - self.grant(year) - self.foundation(year) + self.total(year) - self.storage(year) - self.grant(year) - self.foundation(year) } - /// portion of total that goes to replicators - pub fn replicator(&self, year: f64) -> f64 { - self.total(year) * self.replicator + /// portion of total that goes to storage mining + pub fn storage(&self, year: f64) -> f64 { + self.total(year) * self.storage } /// portion of total that goes to grant pools @@ -105,7 +105,7 @@ mod tests { assert_eq!( total, inflation.validator(*year) - + inflation.replicator(*year) + + inflation.storage(*year) + inflation.grant(*year) + inflation.foundation(*year) ); diff --git a/sdk/src/syscall/rewards.rs b/sdk/src/syscall/rewards.rs index 8aaf393c3f..a8299ccefb 100644 --- a/sdk/src/syscall/rewards.rs +++ b/sdk/src/syscall/rewards.rs @@ -16,7 +16,7 @@ crate::solana_name_id!(ID, "Sysca11Rewards11111111111111111111111111111"); #[derive(Serialize, Deserialize, Debug, Default, PartialEq)] pub struct Rewards { pub validator_point_value: f64, - pub replicator_point_value: f64, + pub storage_point_value: f64, } impl Rewards { @@ -34,13 +34,13 @@ impl Rewards { pub fn create_account( lamports: u64, validator_point_value: f64, - replicator_point_value: f64, + storage_point_value: f64, ) -> Account { Account::new_data( lamports, &Rewards { validator_point_value, - replicator_point_value, + storage_point_value, }, &syscall::id(), )