Merge api/program into single units (#7061)
This commit is contained in:
24
programs/storage/Cargo.toml
Normal file
24
programs/storage/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "solana-storage-program"
|
||||
version = "0.21.0"
|
||||
description = "Solana Storage program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.2.0"
|
||||
log = "0.4.8"
|
||||
rand = "0.6.5"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = "1.0.102"
|
||||
serde_derive = "1.0.102"
|
||||
solana-logger = { path = "../../logger", version = "0.21.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "0.21.0" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
name = "solana_storage_program"
|
18
programs/storage/src/lib.rs
Normal file
18
programs/storage/src/lib.rs
Normal file
@ -0,0 +1,18 @@
|
||||
pub mod rewards_pools;
|
||||
pub mod storage_contract;
|
||||
pub mod storage_instruction;
|
||||
pub mod storage_processor;
|
||||
|
||||
use crate::storage_processor::process_instruction;
|
||||
|
||||
const STORAGE_PROGRAM_ID: [u8; 32] = [
|
||||
6, 162, 25, 123, 127, 68, 233, 59, 131, 151, 21, 152, 162, 120, 90, 37, 154, 88, 86, 5, 156,
|
||||
221, 182, 201, 142, 103, 151, 112, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
solana_sdk::declare_program!(
|
||||
STORAGE_PROGRAM_ID,
|
||||
"Storage111111111111111111111111111111111111",
|
||||
solana_storage_program,
|
||||
process_instruction
|
||||
);
|
60
programs/storage/src/rewards_pools.rs
Normal file
60
programs/storage/src/rewards_pools.rs
Normal file
@ -0,0 +1,60 @@
|
||||
//! rewards_pools
|
||||
//! * initialize genesis with rewards pools
|
||||
//! * keep track of rewards
|
||||
//! * own mining pools
|
||||
|
||||
use crate::storage_contract::create_rewards_pool;
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana_sdk::genesis_config::GenesisConfig;
|
||||
use solana_sdk::hash::{hash, Hash};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
// base rewards pool ID
|
||||
const ID: [u8; 32] = [
|
||||
6, 162, 25, 123, 127, 71, 141, 232, 129, 171, 58, 183, 79, 88, 181, 17, 163, 11, 51, 111, 22,
|
||||
123, 67, 115, 5, 131, 109, 161, 16, 0, 0, 0,
|
||||
];
|
||||
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random_id() -> Pubkey {
|
||||
let mut id = Hash::new(&ID);
|
||||
|
||||
for _i in 0..thread_rng().gen_range(0, NUM_REWARDS_POOLS) {
|
||||
id = hash(id.as_ref());
|
||||
}
|
||||
|
||||
Pubkey::new(id.as_ref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let mut genesis_config = GenesisConfig::default();
|
||||
add_genesis_accounts(&mut genesis_config);
|
||||
|
||||
for _i in 0..NUM_REWARDS_POOLS {
|
||||
let id = random_id();
|
||||
assert!(genesis_config
|
||||
.rewards_pools
|
||||
.iter()
|
||||
.position(|x| x.0 == id)
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
}
|
646
programs/storage/src/storage_contract.rs
Normal file
646
programs/storage/src/storage_contract.rs
Normal file
@ -0,0 +1,646 @@
|
||||
use crate::storage_instruction::StorageAccountType;
|
||||
use log::*;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::{
|
||||
account::{Account, KeyedAccount},
|
||||
account_utils::State,
|
||||
clock::Epoch,
|
||||
hash::Hash,
|
||||
instruction::InstructionError,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
sysvar,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// 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: Epoch,
|
||||
// currently pending credits
|
||||
pub current_epoch: Epoch,
|
||||
// credits ready to be claimed
|
||||
pub redeemable: u64,
|
||||
}
|
||||
|
||||
impl Credits {
|
||||
pub fn update_epoch(&mut self, current_epoch: Epoch) {
|
||||
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,
|
||||
InvalidBlockhash,
|
||||
InvalidProofMask,
|
||||
DuplicateProof,
|
||||
RewardPoolDepleted,
|
||||
InvalidOwner,
|
||||
ProofLimitReached,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum ProofStatus {
|
||||
Skipped,
|
||||
Valid,
|
||||
NotValid,
|
||||
}
|
||||
|
||||
impl Default for ProofStatus {
|
||||
fn default() -> Self {
|
||||
ProofStatus::Skipped
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct Proof {
|
||||
/// The encryption key the archiver used (also used to generate offsets)
|
||||
pub signature: Signature,
|
||||
/// A "recent" blockhash used to generate the seed
|
||||
pub blockhash: Hash,
|
||||
/// The resulting sampled state
|
||||
pub sha_state: Hash,
|
||||
/// The segment this proof is for
|
||||
pub segment_index: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum StorageContract {
|
||||
Uninitialized, // Must be first (aka, 0)
|
||||
|
||||
ValidatorStorage {
|
||||
owner: Pubkey,
|
||||
// Most recently advertised segment
|
||||
segment: u64,
|
||||
// Most recently advertised blockhash
|
||||
hash: Hash,
|
||||
// Lockouts and Rewards are per segment per archiver. It needs to remain this way until
|
||||
// the challenge stage is added.
|
||||
lockout_validations: BTreeMap<u64, BTreeMap<Pubkey, Vec<ProofStatus>>>,
|
||||
// Used to keep track of ongoing credits
|
||||
credits: Credits,
|
||||
},
|
||||
|
||||
ArchiverStorage {
|
||||
owner: Pubkey,
|
||||
// TODO what to do about duplicate proofs across segments? - Check the blockhashes
|
||||
// Map of Proofs per segment, in a Vec
|
||||
proofs: BTreeMap<u64, Vec<Proof>>,
|
||||
// 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
|
||||
validations: BTreeMap<u64, BTreeMap<Pubkey, Vec<ProofStatus>>>,
|
||||
// Used to keep track of ongoing credits
|
||||
credits: Credits,
|
||||
},
|
||||
|
||||
RewardsPool,
|
||||
}
|
||||
|
||||
// utility function, used by Bank, tests, genesis
|
||||
pub fn create_validator_storage_account(owner: Pubkey, lamports: u64) -> Account {
|
||||
let mut storage_account = Account::new(lamports, STORAGE_ACCOUNT_SPACE as usize, &crate::id());
|
||||
|
||||
storage_account
|
||||
.set_state(&StorageContract::ValidatorStorage {
|
||||
owner,
|
||||
segment: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
})
|
||||
.expect("set_state");
|
||||
|
||||
storage_account
|
||||
}
|
||||
|
||||
pub struct StorageAccount<'a> {
|
||||
pub(crate) id: Pubkey,
|
||||
account: &'a mut Account,
|
||||
}
|
||||
|
||||
impl<'a> StorageAccount<'a> {
|
||||
pub fn new(id: Pubkey, account: &'a mut Account) -> Self {
|
||||
Self { id, account }
|
||||
}
|
||||
|
||||
pub fn initialize_storage(
|
||||
&mut self,
|
||||
owner: Pubkey,
|
||||
account_type: StorageAccountType,
|
||||
) -> Result<(), InstructionError> {
|
||||
let storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::Uninitialized = storage_contract {
|
||||
*storage_contract = match account_type {
|
||||
StorageAccountType::Archiver => StorageContract::ArchiverStorage {
|
||||
owner,
|
||||
proofs: BTreeMap::new(),
|
||||
validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
},
|
||||
StorageAccountType::Validator => StorageContract::ValidatorStorage {
|
||||
owner,
|
||||
segment: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
},
|
||||
};
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::AccountAlreadyInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit_mining_proof(
|
||||
&mut self,
|
||||
sha_state: Hash,
|
||||
segment_index: u64,
|
||||
signature: Signature,
|
||||
blockhash: Hash,
|
||||
clock: sysvar::clock::Clock,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::ArchiverStorage {
|
||||
proofs,
|
||||
validations,
|
||||
credits,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
let current_segment = clock.segment;
|
||||
|
||||
// clean up the account
|
||||
// TODO check for time correctness - storage seems to run at a delay of about 3
|
||||
*proofs = proofs
|
||||
.iter()
|
||||
.filter(|(segment, _)| **segment >= current_segment.saturating_sub(5))
|
||||
.map(|(segment, proofs)| (*segment, proofs.clone()))
|
||||
.collect();
|
||||
*validations = validations
|
||||
.iter()
|
||||
.filter(|(segment, _)| **segment >= current_segment.saturating_sub(10))
|
||||
.map(|(segment, rewards)| (*segment, rewards.clone()))
|
||||
.collect();
|
||||
|
||||
if segment_index >= current_segment {
|
||||
// attempt to submit proof for unconfirmed segment
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidSegment as u32,
|
||||
));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Mining proof submitted with contract {:?} segment_index: {}",
|
||||
sha_state, segment_index
|
||||
);
|
||||
|
||||
// TODO check that this blockhash is valid and recent
|
||||
// if !is_valid(&blockhash) {
|
||||
// // proof isn't using a recent blockhash
|
||||
// return Err(InstructionError::CustomError(InvalidBlockhash as u32));
|
||||
// }
|
||||
|
||||
let proof = Proof {
|
||||
sha_state,
|
||||
signature,
|
||||
blockhash,
|
||||
segment_index,
|
||||
};
|
||||
// store the proofs in the "current" segment's entry in the hash map.
|
||||
let segment_proofs = proofs.entry(current_segment).or_default();
|
||||
if segment_proofs.contains(&proof) {
|
||||
// do not accept duplicate proofs
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::DuplicateProof as u32,
|
||||
));
|
||||
}
|
||||
if segment_proofs.len() >= MAX_PROOFS_PER_SEGMENT {
|
||||
// do not accept more than MAX_PROOFS_PER_SEGMENT
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::ProofLimitReached as u32,
|
||||
));
|
||||
}
|
||||
credits.update_epoch(clock.epoch);
|
||||
segment_proofs.push(proof);
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advertise_storage_recent_blockhash(
|
||||
&mut self,
|
||||
hash: Hash,
|
||||
segment: u64,
|
||||
clock: sysvar::clock::Clock,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::ValidatorStorage {
|
||||
segment: state_segment,
|
||||
hash: state_hash,
|
||||
lockout_validations,
|
||||
credits,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
debug!("advertise new segment: {} orig: {}", segment, clock.segment);
|
||||
if segment < *state_segment || segment > clock.segment {
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidSegment as u32,
|
||||
));
|
||||
}
|
||||
|
||||
*state_segment = segment;
|
||||
*state_hash = hash;
|
||||
|
||||
// storage epoch updated, move the lockout_validations to credits
|
||||
let (_num_valid, total_validations) = count_valid_proofs(&lockout_validations);
|
||||
lockout_validations.clear();
|
||||
credits.update_epoch(clock.epoch);
|
||||
credits.current_epoch += total_validations;
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn proof_validation(
|
||||
&mut self,
|
||||
me: &Pubkey,
|
||||
clock: sysvar::clock::Clock,
|
||||
segment_index: u64,
|
||||
proofs_per_account: Vec<Vec<ProofStatus>>,
|
||||
archiver_accounts: &mut [StorageAccount],
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
if let StorageContract::ValidatorStorage {
|
||||
segment: state_segment,
|
||||
lockout_validations,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
if segment_index > *state_segment {
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidSegment as u32,
|
||||
));
|
||||
}
|
||||
|
||||
let accounts = archiver_accounts
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, account)| {
|
||||
account.account.state().ok().map(|contract| match contract {
|
||||
StorageContract::ArchiverStorage {
|
||||
proofs: account_proofs,
|
||||
..
|
||||
} => {
|
||||
//TODO do this better
|
||||
if let Some(segment_proofs) =
|
||||
account_proofs.get(&segment_index).cloned()
|
||||
{
|
||||
if proofs_per_account
|
||||
.get(i)
|
||||
.filter(|proofs| proofs.len() == segment_proofs.len())
|
||||
.is_some()
|
||||
{
|
||||
Some(account)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if accounts.len() != proofs_per_account.len() {
|
||||
// don't have all the accounts to validate the proofs_per_account against
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidProofMask as u32,
|
||||
));
|
||||
}
|
||||
|
||||
let stored_proofs: Vec<_> = proofs_per_account
|
||||
.into_iter()
|
||||
.zip(accounts.into_iter())
|
||||
.filter_map(|(checked_proofs, account)| {
|
||||
if store_validation_result(me, &clock, account, segment_index, &checked_proofs)
|
||||
.is_ok()
|
||||
{
|
||||
Some((account.id, checked_proofs))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// allow validators to store successful validations
|
||||
stored_proofs
|
||||
.into_iter()
|
||||
.for_each(|(archiver_account_id, proof_mask)| {
|
||||
lockout_validations
|
||||
.entry(segment_index)
|
||||
.or_default()
|
||||
.insert(archiver_account_id, proof_mask);
|
||||
});
|
||||
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claim_storage_reward(
|
||||
&mut self,
|
||||
rewards_pool: &mut KeyedAccount,
|
||||
clock: sysvar::clock::Clock,
|
||||
rewards: sysvar::rewards::Rewards,
|
||||
owner: &mut StorageAccount,
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = &mut self.account.state()?;
|
||||
|
||||
if let StorageContract::ValidatorStorage {
|
||||
owner: account_owner,
|
||||
credits,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
if owner.id != *account_owner {
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidOwner as u32,
|
||||
));
|
||||
}
|
||||
|
||||
credits.update_epoch(clock.epoch);
|
||||
check_redeemable(credits, rewards.storage_point_value, rewards_pool, owner)?;
|
||||
|
||||
self.account.set_state(storage_contract)
|
||||
} else if let StorageContract::ArchiverStorage {
|
||||
owner: account_owner,
|
||||
validations,
|
||||
credits,
|
||||
..
|
||||
} = &mut storage_contract
|
||||
{
|
||||
if owner.id != *account_owner {
|
||||
return Err(InstructionError::CustomError(
|
||||
StorageError::InvalidOwner as u32,
|
||||
));
|
||||
}
|
||||
credits.update_epoch(clock.epoch);
|
||||
let (num_validations, _total_proofs) = count_valid_proofs(&validations);
|
||||
credits.current_epoch += num_validations;
|
||||
validations.clear();
|
||||
check_redeemable(credits, rewards.storage_point_value, rewards_pool, owner)?;
|
||||
|
||||
self.account.set_state(storage_contract)
|
||||
} else {
|
||||
Err(InstructionError::InvalidArgument)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_redeemable(
|
||||
credits: &mut Credits,
|
||||
storage_point_value: f64,
|
||||
rewards_pool: &mut KeyedAccount,
|
||||
owner: &mut StorageAccount,
|
||||
) -> Result<(), InstructionError> {
|
||||
let rewards = (credits.redeemable as f64 * storage_point_value) as u64;
|
||||
if rewards_pool.account.lamports < rewards {
|
||||
Err(InstructionError::CustomError(
|
||||
StorageError::RewardPoolDepleted as u32,
|
||||
))
|
||||
} else {
|
||||
if rewards >= 1 {
|
||||
rewards_pool.account.lamports -= rewards;
|
||||
owner.account.lamports += rewards;
|
||||
//clear credits
|
||||
credits.redeemable = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_rewards_pool() -> Account {
|
||||
Account::new_data(std::u64::MAX, &StorageContract::RewardsPool, &crate::id()).unwrap()
|
||||
}
|
||||
|
||||
/// Store the result of a proof validation into the archiver account
|
||||
fn store_validation_result(
|
||||
me: &Pubkey,
|
||||
clock: &sysvar::clock::Clock,
|
||||
storage_account: &mut StorageAccount,
|
||||
segment: u64,
|
||||
proof_mask: &[ProofStatus],
|
||||
) -> Result<(), InstructionError> {
|
||||
let mut storage_contract = storage_account.account.state()?;
|
||||
match &mut storage_contract {
|
||||
StorageContract::ArchiverStorage {
|
||||
proofs,
|
||||
validations,
|
||||
credits,
|
||||
..
|
||||
} => {
|
||||
if !proofs.contains_key(&segment) {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
|
||||
if proofs.get(&segment).unwrap().len() != proof_mask.len() {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
|
||||
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(clock.epoch);
|
||||
credits.current_epoch += total_validations - recorded_validations;
|
||||
}
|
||||
_ => return Err(InstructionError::InvalidAccountData),
|
||||
}
|
||||
storage_account.account.set_state(&storage_contract)
|
||||
}
|
||||
|
||||
fn count_valid_proofs(
|
||||
validations: &BTreeMap<u64, BTreeMap<Pubkey, Vec<ProofStatus>>>,
|
||||
) -> (u64, u64) {
|
||||
let proofs = validations
|
||||
.iter()
|
||||
.flat_map(|(_, proofs)| {
|
||||
proofs
|
||||
.iter()
|
||||
.flat_map(|(_, proofs)| proofs)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut num = 0;
|
||||
for proof in proofs.iter() {
|
||||
if let ProofStatus::Valid = proof {
|
||||
num += 1;
|
||||
}
|
||||
}
|
||||
(num, proofs.len() as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{id, rewards_pools};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn test_account_data() {
|
||||
solana_logger::setup();
|
||||
let mut account = Account::default();
|
||||
account.data.resize(STORAGE_ACCOUNT_SPACE as usize, 0);
|
||||
let storage_account = StorageAccount::new(Pubkey::default(), &mut account);
|
||||
// pretend it's a validator op code
|
||||
let mut contract = storage_account.account.state().unwrap();
|
||||
if let StorageContract::ValidatorStorage { .. } = contract {
|
||||
assert!(true)
|
||||
}
|
||||
if let StorageContract::ArchiverStorage { .. } = &mut contract {
|
||||
panic!("Contract should not decode into two types");
|
||||
}
|
||||
|
||||
contract = StorageContract::ValidatorStorage {
|
||||
owner: Pubkey::default(),
|
||||
segment: 0,
|
||||
hash: Hash::default(),
|
||||
lockout_validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
};
|
||||
storage_account.account.set_state(&contract).unwrap();
|
||||
if let StorageContract::ArchiverStorage { .. } = contract {
|
||||
panic!("Wrong contract type");
|
||||
}
|
||||
contract = StorageContract::ArchiverStorage {
|
||||
owner: Pubkey::default(),
|
||||
proofs: BTreeMap::new(),
|
||||
validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
};
|
||||
storage_account.account.set_state(&contract).unwrap();
|
||||
if let StorageContract::ValidatorStorage { .. } = contract {
|
||||
panic!("Wrong contract type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_validation() {
|
||||
let mut account = StorageAccount {
|
||||
id: Pubkey::default(),
|
||||
account: &mut Account {
|
||||
owner: id(),
|
||||
..Account::default()
|
||||
},
|
||||
};
|
||||
let segment_index = 0;
|
||||
let proof = Proof {
|
||||
segment_index,
|
||||
..Proof::default()
|
||||
};
|
||||
|
||||
// account has no space
|
||||
store_validation_result(
|
||||
&Pubkey::default(),
|
||||
&sysvar::clock::Clock::default(),
|
||||
&mut account,
|
||||
segment_index,
|
||||
&vec![ProofStatus::default(); 1],
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
account
|
||||
.account
|
||||
.data
|
||||
.resize(STORAGE_ACCOUNT_SPACE as usize, 0);
|
||||
let storage_contract = &mut account.account.state().unwrap();
|
||||
if let StorageContract::Uninitialized = storage_contract {
|
||||
let mut proofs = BTreeMap::new();
|
||||
proofs.insert(0, vec![proof.clone()]);
|
||||
*storage_contract = StorageContract::ArchiverStorage {
|
||||
owner: Pubkey::default(),
|
||||
proofs,
|
||||
validations: BTreeMap::new(),
|
||||
credits: Credits::default(),
|
||||
};
|
||||
};
|
||||
account.account.set_state(storage_contract).unwrap();
|
||||
|
||||
// proof is valid
|
||||
store_validation_result(
|
||||
&Pubkey::default(),
|
||||
&sysvar::clock::Clock::default(),
|
||||
&mut account,
|
||||
segment_index,
|
||||
&vec![ProofStatus::Valid],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// proof failed verification but we should still be able to store it
|
||||
store_validation_result(
|
||||
&Pubkey::default(),
|
||||
&sysvar::clock::Clock::default(),
|
||||
&mut account,
|
||||
segment_index,
|
||||
&vec![ProofStatus::NotValid],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redeemable() {
|
||||
let mut credits = Credits {
|
||||
epoch: 0,
|
||||
current_epoch: 0,
|
||||
redeemable: 100,
|
||||
};
|
||||
let mut owner_account = Account {
|
||||
lamports: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let mut rewards_pool = create_rewards_pool();
|
||||
let pool_id = rewards_pools::id();
|
||||
let mut keyed_pool_account = KeyedAccount::new(&pool_id, false, &mut rewards_pool);
|
||||
let mut owner = StorageAccount {
|
||||
id: Pubkey::default(),
|
||||
account: &mut owner_account,
|
||||
};
|
||||
|
||||
// check that redeeming from depleted pools fails
|
||||
keyed_pool_account.account.lamports = 0;
|
||||
assert_eq!(
|
||||
check_redeemable(&mut credits, 1.0, &mut keyed_pool_account, &mut owner),
|
||||
Err(InstructionError::CustomError(
|
||||
StorageError::RewardPoolDepleted as u32,
|
||||
))
|
||||
);
|
||||
assert_eq!(owner.account.lamports, 1);
|
||||
|
||||
keyed_pool_account.account.lamports = 200;
|
||||
assert_eq!(
|
||||
check_redeemable(&mut credits, 1.0, &mut keyed_pool_account, &mut owner),
|
||||
Ok(())
|
||||
);
|
||||
// check that the owner's balance increases
|
||||
assert_eq!(owner.account.lamports, 101);
|
||||
}
|
||||
}
|
189
programs/storage/src/storage_instruction.rs
Normal file
189
programs/storage/src/storage_instruction.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use crate::storage_contract::{ProofStatus, STORAGE_ACCOUNT_SPACE};
|
||||
use crate::{id, rewards_pools};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Signature;
|
||||
use solana_sdk::system_instruction;
|
||||
use solana_sdk::sysvar::{clock, rewards};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
pub enum StorageAccountType {
|
||||
Archiver,
|
||||
Validator,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum StorageInstruction {
|
||||
/// Initialize the account as a validator or archiver
|
||||
///
|
||||
/// Expects 1 Account:
|
||||
/// 0 - Account to be initialized
|
||||
InitializeStorage {
|
||||
owner: Pubkey,
|
||||
account_type: StorageAccountType,
|
||||
},
|
||||
|
||||
SubmitMiningProof {
|
||||
sha_state: Hash,
|
||||
segment_index: u64,
|
||||
signature: Signature,
|
||||
blockhash: Hash,
|
||||
},
|
||||
AdvertiseStorageRecentBlockhash {
|
||||
hash: Hash,
|
||||
segment: u64,
|
||||
},
|
||||
/// Redeem storage reward credits
|
||||
///
|
||||
/// Expects 1 Account:
|
||||
/// 0 - Storage account with credits to redeem
|
||||
/// 1 - Clock Syscall to figure out the clock epoch
|
||||
/// 2 - Archiver 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
|
||||
segment: u64,
|
||||
/// A Vec of proof masks per keyed archiver account loaded by the instruction
|
||||
proofs: Vec<Vec<ProofStatus>>,
|
||||
},
|
||||
}
|
||||
|
||||
fn get_ratios() -> (u64, u64) {
|
||||
// max number bytes available for account metas and proofs
|
||||
// The maximum transaction size is == `PACKET_DATA_SIZE` (1232 bytes)
|
||||
// There are approx. 900 bytes left over after the storage instruction is wrapped into
|
||||
// a signed transaction.
|
||||
static MAX_BYTES: u64 = 900;
|
||||
let account_meta_size: u64 =
|
||||
bincode::serialized_size(&AccountMeta::new(Pubkey::new_rand(), false)).unwrap_or(0);
|
||||
let proof_size: u64 = bincode::serialized_size(&ProofStatus::default()).unwrap_or(0);
|
||||
|
||||
// the ratio between account meta size and a single proof status
|
||||
let ratio = (account_meta_size + proof_size - 1) / proof_size;
|
||||
let bytes = (MAX_BYTES + ratio - 1) / ratio;
|
||||
(ratio, bytes)
|
||||
}
|
||||
|
||||
/// Returns how many accounts and their proofs will fit in a single proof validation tx
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `proof_mask_max` - The largest proof mask across all accounts intended for submission
|
||||
///
|
||||
pub fn validation_account_limit(proof_mask_max: usize) -> u64 {
|
||||
let (ratio, bytes) = get_ratios();
|
||||
// account_meta_count * (ratio + proof_mask_max) = bytes
|
||||
bytes / (ratio + proof_mask_max as u64)
|
||||
}
|
||||
|
||||
pub fn proof_mask_limit() -> u64 {
|
||||
let (ratio, bytes) = get_ratios();
|
||||
bytes - ratio
|
||||
}
|
||||
|
||||
pub fn create_storage_account(
|
||||
from_pubkey: &Pubkey,
|
||||
storage_owner: &Pubkey,
|
||||
storage_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
account_type: StorageAccountType,
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::create_account(
|
||||
from_pubkey,
|
||||
storage_pubkey,
|
||||
lamports,
|
||||
STORAGE_ACCOUNT_SPACE,
|
||||
&id(),
|
||||
),
|
||||
Instruction::new(
|
||||
id(),
|
||||
&StorageInstruction::InitializeStorage {
|
||||
owner: *storage_owner,
|
||||
account_type,
|
||||
},
|
||||
vec![AccountMeta::new(*storage_pubkey, false)],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn mining_proof(
|
||||
storage_pubkey: &Pubkey,
|
||||
sha_state: Hash,
|
||||
segment_index: u64,
|
||||
signature: Signature,
|
||||
blockhash: Hash,
|
||||
) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::SubmitMiningProof {
|
||||
sha_state,
|
||||
segment_index,
|
||||
signature,
|
||||
blockhash,
|
||||
};
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*storage_pubkey, true),
|
||||
AccountMeta::new(clock::id(), false),
|
||||
];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn advertise_recent_blockhash(
|
||||
storage_pubkey: &Pubkey,
|
||||
storage_hash: Hash,
|
||||
segment: u64,
|
||||
) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::AdvertiseStorageRecentBlockhash {
|
||||
hash: storage_hash,
|
||||
segment,
|
||||
};
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*storage_pubkey, true),
|
||||
AccountMeta::new(clock::id(), false),
|
||||
];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn proof_validation(
|
||||
storage_pubkey: &Pubkey,
|
||||
segment: u64,
|
||||
checked_proofs: Vec<(Pubkey, Vec<ProofStatus>)>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*storage_pubkey, true),
|
||||
AccountMeta::new(clock::id(), false),
|
||||
];
|
||||
let mut proofs = vec![];
|
||||
checked_proofs.into_iter().for_each(|(id, p)| {
|
||||
proofs.push(p);
|
||||
account_metas.push(AccountMeta::new(id, false))
|
||||
});
|
||||
let storage_instruction = StorageInstruction::ProofValidation { segment, proofs };
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
pub fn claim_reward(owner_pubkey: &Pubkey, storage_pubkey: &Pubkey) -> Instruction {
|
||||
let storage_instruction = StorageInstruction::ClaimStorageReward;
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*storage_pubkey, false),
|
||||
AccountMeta::new(clock::id(), false),
|
||||
AccountMeta::new(rewards::id(), false),
|
||||
AccountMeta::new(rewards_pools::random_id(), false),
|
||||
AccountMeta::new(*owner_pubkey, false),
|
||||
];
|
||||
Instruction::new(id(), &storage_instruction, account_metas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_size() {
|
||||
// check that if there's 50 proof per account, only 1 account can fit in a single tx
|
||||
assert_eq!(validation_account_limit(50), 1);
|
||||
}
|
||||
}
|
96
programs/storage/src/storage_processor.rs
Normal file
96
programs/storage/src/storage_processor.rs
Normal file
@ -0,0 +1,96 @@
|
||||
//! storage program
|
||||
//! Receive mining proofs from miners, validate the answers
|
||||
//! and give reward for good proofs.
|
||||
use crate::{storage_contract::StorageAccount, storage_instruction::StorageInstruction};
|
||||
use solana_sdk::{
|
||||
account::KeyedAccount,
|
||||
instruction::InstructionError,
|
||||
instruction_processor_utils::limited_deserialize,
|
||||
pubkey::Pubkey,
|
||||
sysvar::{clock::Clock, rewards::Rewards, Sysvar},
|
||||
};
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
) -> Result<(), InstructionError> {
|
||||
solana_logger::setup();
|
||||
|
||||
let (me, rest) = keyed_accounts.split_at_mut(1);
|
||||
let me_unsigned = me[0].signer_key().is_none();
|
||||
let mut storage_account = StorageAccount::new(*me[0].unsigned_key(), &mut me[0].account);
|
||||
|
||||
match limited_deserialize(data)? {
|
||||
StorageInstruction::InitializeStorage {
|
||||
owner,
|
||||
account_type,
|
||||
} => {
|
||||
if !rest.is_empty() {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
storage_account.initialize_storage(owner, account_type)
|
||||
}
|
||||
StorageInstruction::SubmitMiningProof {
|
||||
sha_state,
|
||||
segment_index,
|
||||
signature,
|
||||
blockhash,
|
||||
} => {
|
||||
if me_unsigned || rest.len() != 1 {
|
||||
// This instruction must be signed by `me`
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
let clock = Clock::from_keyed_account(&rest[0])?;
|
||||
storage_account.submit_mining_proof(
|
||||
sha_state,
|
||||
segment_index,
|
||||
signature,
|
||||
blockhash,
|
||||
clock,
|
||||
)
|
||||
}
|
||||
StorageInstruction::AdvertiseStorageRecentBlockhash { hash, segment } => {
|
||||
if me_unsigned || rest.len() != 1 {
|
||||
// This instruction must be signed by `me`
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
let clock = Clock::from_keyed_account(&rest[0])?;
|
||||
storage_account.advertise_storage_recent_blockhash(hash, segment, clock)
|
||||
}
|
||||
StorageInstruction::ClaimStorageReward => {
|
||||
if rest.len() != 4 {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
let (clock, 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 = Rewards::from_keyed_account(&rewards[0])?;
|
||||
let clock = Clock::from_keyed_account(&clock[0])?;
|
||||
let mut owner = StorageAccount::new(*owner[0].unsigned_key(), &mut owner[0].account);
|
||||
|
||||
storage_account.claim_storage_reward(&mut rewards_pools[0], clock, rewards, &mut owner)
|
||||
}
|
||||
StorageInstruction::ProofValidation { segment, proofs } => {
|
||||
if rest.is_empty() {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
let (clock, rest) = rest.split_at_mut(1);
|
||||
if me_unsigned || rest.is_empty() {
|
||||
// This instruction must be signed by `me` and `rest` cannot be empty
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
let me_id = storage_account.id;
|
||||
let clock = Clock::from_keyed_account(&clock[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, clock, segment, proofs, &mut rest)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user