diff --git a/Cargo.lock b/Cargo.lock index c614701e1..bb5d5e01f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,8 +2445,11 @@ name = "solana-storage-api" version = "0.13.0" dependencies = [ "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.13.0", + "solana-runtime 0.13.0", "solana-sdk 0.13.0", ] @@ -2454,12 +2457,8 @@ dependencies = [ name = "solana-storage-program" version = "0.13.0" dependencies = [ - "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "solana-logger 0.13.0", - "solana-runtime 0.13.0", "solana-sdk 0.13.0", "solana-storage-api 0.13.0", ] @@ -2469,8 +2468,10 @@ name = "solana-token-api" version = "0.13.0" dependencies = [ "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.13.0", "solana-sdk 0.13.0", ] @@ -2478,12 +2479,10 @@ dependencies = [ name = "solana-token-program" version = "0.13.0" dependencies = [ - "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "solana-logger 0.13.0", "solana-sdk 0.13.0", + "solana-token-api 0.13.0", ] [[package]] @@ -2502,6 +2501,9 @@ dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.13.0", + "solana-metrics 0.13.0", + "solana-runtime 0.13.0", "solana-sdk 0.13.0", ] @@ -2509,11 +2511,8 @@ dependencies = [ name = "solana-vote-program" version = "0.13.0" dependencies = [ - "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "solana-logger 0.13.0", - "solana-metrics 0.13.0", - "solana-runtime 0.13.0", "solana-sdk 0.13.0", "solana-vote-api 0.13.0", ] diff --git a/programs/budget_api/src/budget_processor.rs b/programs/budget_api/src/budget_processor.rs index 21718b69c..a32ccaf27 100644 --- a/programs/budget_api/src/budget_processor.rs +++ b/programs/budget_api/src/budget_processor.rs @@ -141,7 +141,7 @@ pub fn process_instruction( } #[cfg(test)] -mod test { +mod tests { use super::*; use crate::budget_instruction::BudgetInstruction; use crate::budget_script::BudgetScript; diff --git a/programs/storage_api/Cargo.toml b/programs/storage_api/Cargo.toml index 9dd759a68..c682715da 100644 --- a/programs/storage_api/Cargo.toml +++ b/programs/storage_api/Cargo.toml @@ -10,10 +10,15 @@ edition = "2018" [dependencies] bincode = "1.1.2" +log = "0.4.2" serde = "1.0.89" serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } +[dev-dependencies] +solana-runtime = { path = "../../runtime", version = "0.13.0" } + [lib] name = "solana_storage_api" crate-type = ["lib"] diff --git a/programs/storage_api/src/lib.rs b/programs/storage_api/src/lib.rs index d9a9c7f5d..a9435982c 100644 --- a/programs/storage_api/src/lib.rs +++ b/programs/storage_api/src/lib.rs @@ -1,3 +1,5 @@ +pub mod storage_processor; + use serde_derive::{Deserialize, Serialize}; use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; diff --git a/programs/storage_api/src/storage_processor.rs b/programs/storage_api/src/storage_processor.rs new file mode 100644 index 000000000..f4dce6806 --- /dev/null +++ b/programs/storage_api/src/storage_processor.rs @@ -0,0 +1,460 @@ +//! storage program +//! Receive mining proofs from miners, validate the answers +//! and give reward for good proofs. + +use crate::*; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::InstructionError; + +pub const TOTAL_VALIDATOR_REWARDS: u64 = 1000; +pub const TOTAL_REPLICATOR_REWARDS: u64 = 1000; + +fn count_valid_proofs(proofs: &[ProofStatus]) -> u64 { + let mut num = 0; + for proof in proofs { + if let ProofStatus::Valid = proof { + num += 1; + } + } + num +} + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], + _tick_height: u64, +) -> Result<(), InstructionError> { + solana_logger::setup(); + + if keyed_accounts.len() != 1 { + // keyed_accounts[1] should be the main storage key + // to access its data + Err(InstructionError::InvalidArgument)?; + } + + // accounts_keys[0] must be signed + if keyed_accounts[0].signer_key().is_none() { + info!("account[0] is unsigned"); + Err(InstructionError::GenericError)?; + } + + if let Ok(syscall) = bincode::deserialize(data) { + let mut storage_account_state = if let Ok(storage_account_state) = + bincode::deserialize(&keyed_accounts[0].account.data) + { + storage_account_state + } else { + StorageProgramState::default() + }; + + debug!( + "deserialized state height: {}", + storage_account_state.entry_height + ); + match syscall { + StorageProgram::SubmitMiningProof { + sha_state, + entry_height, + signature, + } => { + let segment_index = get_segment_from_entry(entry_height); + let current_segment_index = + get_segment_from_entry(storage_account_state.entry_height); + if segment_index >= current_segment_index { + return Err(InstructionError::InvalidArgument); + } + + debug!( + "Mining proof submitted with state {:?} entry_height: {}", + sha_state, entry_height + ); + + let proof_info = ProofInfo { + id: *keyed_accounts[0].signer_key().unwrap(), + sha_state, + signature, + }; + storage_account_state.proofs[segment_index].push(proof_info); + } + StorageProgram::AdvertiseStorageRecentBlockhash { hash, entry_height } => { + let original_segments = storage_account_state.entry_height / ENTRIES_PER_SEGMENT; + let segments = entry_height / ENTRIES_PER_SEGMENT; + debug!( + "advertise new last id segments: {} orig: {}", + segments, original_segments + ); + if segments <= original_segments { + return Err(InstructionError::InvalidArgument); + } + + storage_account_state.entry_height = entry_height; + storage_account_state.hash = hash; + + // move the proofs to previous_proofs + storage_account_state.previous_proofs = storage_account_state.proofs.clone(); + storage_account_state.proofs.clear(); + storage_account_state + .proofs + .resize(segments as usize, Vec::new()); + + // move lockout_validations to reward_validations + storage_account_state.reward_validations = + storage_account_state.lockout_validations.clone(); + storage_account_state.lockout_validations.clear(); + storage_account_state + .lockout_validations + .resize(segments as usize, Vec::new()); + } + StorageProgram::ProofValidation { + entry_height, + proof_mask, + } => { + if entry_height >= storage_account_state.entry_height { + return Err(InstructionError::InvalidArgument); + } + + let segment_index = get_segment_from_entry(entry_height); + if storage_account_state.previous_proofs[segment_index].len() != proof_mask.len() { + return Err(InstructionError::InvalidArgument); + } + + // TODO: Check that each proof mask matches the signature + /*for (i, entry) in proof_mask.iter().enumerate() { + if storage_account_state.previous_proofs[segment_index][i] != signature.as_ref[0] { + return Err(InstructionError::InvalidArgument); + } + }*/ + + let info = ValidationInfo { + id: *keyed_accounts[0].signer_key().unwrap(), + proof_mask, + }; + storage_account_state.lockout_validations[segment_index].push(info); + } + StorageProgram::ClaimStorageReward { entry_height } => { + let claims_index = get_segment_from_entry(entry_height); + let account_key = keyed_accounts[0].signer_key().unwrap(); + let mut num_validations = 0; + let mut total_validations = 0; + for validation in &storage_account_state.reward_validations[claims_index] { + if *account_key == validation.id { + num_validations += count_valid_proofs(&validation.proof_mask); + } else { + total_validations += count_valid_proofs(&validation.proof_mask); + } + } + total_validations += num_validations; + if total_validations > 0 { + keyed_accounts[0].account.lamports += + (TOTAL_VALIDATOR_REWARDS * num_validations) / total_validations; + } + } + } + + if bincode::serialize_into( + &mut keyed_accounts[0].account.data[..], + &storage_account_state, + ) + .is_err() + { + return Err(InstructionError::AccountDataTooSmall); + } + + Ok(()) + } else { + info!("Invalid instruction data: {:?}", data); + Err(InstructionError::InvalidInstructionData) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ProofStatus, StorageTransaction, ENTRIES_PER_SEGMENT}; + use bincode::deserialize; + use solana_runtime::bank::Bank; + use solana_sdk::account::{create_keyed_accounts, Account}; + use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::hash::{hash, Hash}; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; + use solana_sdk::system_transaction::SystemTransaction; + use solana_sdk::transaction::{CompiledInstruction, Transaction}; + + fn test_transaction( + tx: &Transaction, + program_accounts: &mut [Account], + ) -> Result<(), InstructionError> { + assert_eq!(tx.instructions.len(), 1); + let CompiledInstruction { + ref accounts, + ref data, + .. + } = tx.instructions[0]; + + info!("accounts: {:?}", accounts); + + let mut keyed_accounts: Vec<_> = accounts + .iter() + .map(|&index| { + let index = index as usize; + let key = &tx.account_keys[index]; + (key, index < tx.signatures.len()) + }) + .zip(program_accounts.iter_mut()) + .map(|((key, is_signer), account)| KeyedAccount::new(key, is_signer, account)) + .collect(); + + let ret = process_instruction(&id(), &mut keyed_accounts, &data, 42); + info!("ret: {:?}", ret); + ret + } + + #[test] + fn test_storage_tx() { + let keypair = Keypair::new(); + let mut accounts = [(keypair.pubkey(), Account::default())]; + let mut keyed_accounts = create_keyed_accounts(&mut accounts); + assert!(process_instruction(&id(), &mut keyed_accounts, &[], 42).is_err()); + } + + #[test] + fn test_serialize_overflow() { + let keypair = Keypair::new(); + let mut keyed_accounts = Vec::new(); + let mut user_account = Account::default(); + let pubkey = keypair.pubkey(); + keyed_accounts.push(KeyedAccount::new(&pubkey, true, &mut user_account)); + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &keypair, + Hash::default(), + Hash::default(), + ENTRIES_PER_SEGMENT, + ); + + assert_eq!( + process_instruction(&id(), &mut keyed_accounts, &tx.instructions[0].data, 42), + Err(InstructionError::AccountDataTooSmall) + ); + } + + #[test] + fn test_invalid_accounts_len() { + let keypair = Keypair::new(); + let mut accounts = [Account::default()]; + + let tx = StorageTransaction::new_mining_proof( + &keypair, + Hash::default(), + Hash::default(), + 0, + Signature::default(), + ); + assert!(test_transaction(&tx, &mut accounts).is_err()); + + let mut accounts = [Account::default(), Account::default(), Account::default()]; + + assert!(test_transaction(&tx, &mut accounts).is_err()); + } + + #[test] + fn test_submit_mining_invalid_entry_height() { + solana_logger::setup(); + let keypair = Keypair::new(); + let mut accounts = [Account::default(), Account::default()]; + accounts[1].data.resize(16 * 1024, 0); + + let tx = StorageTransaction::new_mining_proof( + &keypair, + Hash::default(), + Hash::default(), + 0, + Signature::default(), + ); + + // Haven't seen a transaction to roll over the epoch, so this should fail + assert!(test_transaction(&tx, &mut accounts).is_err()); + } + + #[test] + fn test_submit_mining_ok() { + solana_logger::setup(); + let keypair = Keypair::new(); + let mut accounts = [Account::default(), Account::default()]; + accounts[0].data.resize(16 * 1024, 0); + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &keypair, + Hash::default(), + Hash::default(), + ENTRIES_PER_SEGMENT, + ); + + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_mining_proof( + &keypair, + Hash::default(), + Hash::default(), + 0, + Signature::default(), + ); + + test_transaction(&tx, &mut accounts).unwrap(); + } + + #[test] + fn test_validate_mining() { + solana_logger::setup(); + let keypair = Keypair::new(); + let mut accounts = [Account::default(), Account::default()]; + accounts[0].data.resize(16 * 1024, 0); + + let entry_height = 0; + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &keypair, + Hash::default(), + Hash::default(), + ENTRIES_PER_SEGMENT, + ); + + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_mining_proof( + &keypair, + Hash::default(), + Hash::default(), + entry_height, + Signature::default(), + ); + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &keypair, + Hash::default(), + Hash::default(), + ENTRIES_PER_SEGMENT * 2, + ); + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_proof_validation( + &keypair, + Hash::default(), + entry_height, + vec![ProofStatus::Valid], + ); + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &keypair, + Hash::default(), + Hash::default(), + ENTRIES_PER_SEGMENT * 3, + ); + test_transaction(&tx, &mut accounts).unwrap(); + + let tx = StorageTransaction::new_reward_claim(&keypair, Hash::default(), entry_height); + test_transaction(&tx, &mut accounts).unwrap(); + + assert!(accounts[0].lamports == TOTAL_VALIDATOR_REWARDS); + } + + fn get_storage_entry_height(bank: &Bank, account: &Pubkey) -> u64 { + match bank.get_account(&account) { + Some(storage_system_account) => { + let state = deserialize(&storage_system_account.data); + if let Ok(state) = state { + let state: StorageProgramState = state; + return state.entry_height; + } + } + None => { + info!("error in reading entry_height"); + } + } + 0 + } + + fn get_storage_blockhash(bank: &Bank, account: &Pubkey) -> Hash { + if let Some(storage_system_account) = bank.get_account(&account) { + let state = deserialize(&storage_system_account.data); + if let Ok(state) = state { + let state: StorageProgramState = state; + return state.hash; + } + } + Hash::default() + } + + #[test] + fn test_bank_storage() { + let (mut genesis_block, alice) = GenesisBlock::new(1000); + genesis_block + .native_programs + .push(("solana_storage_program".to_string(), id())); + let bank = Bank::new(&genesis_block); + + let bob = Keypair::new(); + let jack = Keypair::new(); + let jill = Keypair::new(); + + let x = 42; + let blockhash = genesis_block.hash(); + let x2 = x * 2; + let storage_blockhash = hash(&[x2]); + + bank.register_tick(&blockhash); + + bank.transfer(10, &alice, &jill.pubkey(), blockhash) + .unwrap(); + + bank.transfer(10, &alice, &bob.pubkey(), blockhash).unwrap(); + bank.transfer(10, &alice, &jack.pubkey(), blockhash) + .unwrap(); + + let tx = SystemTransaction::new_program_account( + &alice, + &bob.pubkey(), + blockhash, + 1, + 4 * 1024, + &id(), + 0, + ); + + bank.process_transaction(&tx).unwrap(); + + let tx = StorageTransaction::new_advertise_recent_blockhash( + &bob, + storage_blockhash, + blockhash, + ENTRIES_PER_SEGMENT, + ); + + bank.process_transaction(&tx).unwrap(); + + let entry_height = 0; + let tx = StorageTransaction::new_mining_proof( + &bob, + Hash::default(), + blockhash, + entry_height, + Signature::default(), + ); + let _result = bank.process_transaction(&tx).unwrap(); + + assert_eq!( + get_storage_entry_height(&bank, &bob.pubkey()), + ENTRIES_PER_SEGMENT + ); + assert_eq!( + get_storage_blockhash(&bank, &bob.pubkey()), + storage_blockhash + ); + } +} diff --git a/programs/storage_program/Cargo.toml b/programs/storage_program/Cargo.toml index 1a5fecb74..ee21c6d3c 100644 --- a/programs/storage_program/Cargo.toml +++ b/programs/storage_program/Cargo.toml @@ -9,17 +9,11 @@ homepage = "https://solana.com/" edition = "2018" [dependencies] -bincode = "1.1.2" log = "0.4.2" -serde = "1.0.89" -serde_derive = "1.0.89" solana-logger = { path = "../../logger", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } solana-storage-api = { path = "../storage_api", version = "0.13.0" } -[dev-dependencies] -solana-runtime = { path = "../../runtime", version = "0.13.0" } - [lib] name = "solana_storage_program" crate-type = ["cdylib"] diff --git a/programs/storage_program/src/lib.rs b/programs/storage_program/src/lib.rs index 6e25ffc5f..c0b0b4b90 100644 --- a/programs/storage_program/src/lib.rs +++ b/programs/storage_program/src/lib.rs @@ -1,365 +1,3 @@ -//! storage program -//! Receive mining proofs from miners, validate the answers -//! and give reward for good proofs. +use solana_storage_api::storage_processor::process_instruction; -use log::*; -extern crate solana_sdk; - -use solana_sdk::account::KeyedAccount; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; -use solana_storage_api::*; - -pub const TOTAL_VALIDATOR_REWARDS: u64 = 1000; -pub const TOTAL_REPLICATOR_REWARDS: u64 = 1000; - -fn count_valid_proofs(proofs: &[ProofStatus]) -> u64 { - let mut num = 0; - for proof in proofs { - if let ProofStatus::Valid = proof { - num += 1; - } - } - num -} - -solana_entrypoint!(entrypoint); -fn entrypoint( - _program_id: &Pubkey, - keyed_accounts: &mut [KeyedAccount], - data: &[u8], - _tick_height: u64, -) -> Result<(), InstructionError> { - solana_logger::setup(); - - if keyed_accounts.len() != 1 { - // keyed_accounts[1] should be the main storage key - // to access its data - Err(InstructionError::InvalidArgument)?; - } - - // accounts_keys[0] must be signed - if keyed_accounts[0].signer_key().is_none() { - info!("account[0] is unsigned"); - Err(InstructionError::GenericError)?; - } - - if let Ok(syscall) = bincode::deserialize(data) { - let mut storage_account_state = if let Ok(storage_account_state) = - bincode::deserialize(&keyed_accounts[0].account.data) - { - storage_account_state - } else { - StorageProgramState::default() - }; - - debug!( - "deserialized state height: {}", - storage_account_state.entry_height - ); - match syscall { - StorageProgram::SubmitMiningProof { - sha_state, - entry_height, - signature, - } => { - let segment_index = get_segment_from_entry(entry_height); - let current_segment_index = - get_segment_from_entry(storage_account_state.entry_height); - if segment_index >= current_segment_index { - return Err(InstructionError::InvalidArgument); - } - - debug!( - "Mining proof submitted with state {:?} entry_height: {}", - sha_state, entry_height - ); - - let proof_info = ProofInfo { - id: *keyed_accounts[0].signer_key().unwrap(), - sha_state, - signature, - }; - storage_account_state.proofs[segment_index].push(proof_info); - } - StorageProgram::AdvertiseStorageRecentBlockhash { hash, entry_height } => { - let original_segments = storage_account_state.entry_height / ENTRIES_PER_SEGMENT; - let segments = entry_height / ENTRIES_PER_SEGMENT; - debug!( - "advertise new last id segments: {} orig: {}", - segments, original_segments - ); - if segments <= original_segments { - return Err(InstructionError::InvalidArgument); - } - - storage_account_state.entry_height = entry_height; - storage_account_state.hash = hash; - - // move the proofs to previous_proofs - storage_account_state.previous_proofs = storage_account_state.proofs.clone(); - storage_account_state.proofs.clear(); - storage_account_state - .proofs - .resize(segments as usize, Vec::new()); - - // move lockout_validations to reward_validations - storage_account_state.reward_validations = - storage_account_state.lockout_validations.clone(); - storage_account_state.lockout_validations.clear(); - storage_account_state - .lockout_validations - .resize(segments as usize, Vec::new()); - } - StorageProgram::ProofValidation { - entry_height, - proof_mask, - } => { - if entry_height >= storage_account_state.entry_height { - return Err(InstructionError::InvalidArgument); - } - - let segment_index = get_segment_from_entry(entry_height); - if storage_account_state.previous_proofs[segment_index].len() != proof_mask.len() { - return Err(InstructionError::InvalidArgument); - } - - // TODO: Check that each proof mask matches the signature - /*for (i, entry) in proof_mask.iter().enumerate() { - if storage_account_state.previous_proofs[segment_index][i] != signature.as_ref[0] { - return Err(InstructionError::InvalidArgument); - } - }*/ - - let info = ValidationInfo { - id: *keyed_accounts[0].signer_key().unwrap(), - proof_mask, - }; - storage_account_state.lockout_validations[segment_index].push(info); - } - StorageProgram::ClaimStorageReward { entry_height } => { - let claims_index = get_segment_from_entry(entry_height); - let account_key = keyed_accounts[0].signer_key().unwrap(); - let mut num_validations = 0; - let mut total_validations = 0; - for validation in &storage_account_state.reward_validations[claims_index] { - if *account_key == validation.id { - num_validations += count_valid_proofs(&validation.proof_mask); - } else { - total_validations += count_valid_proofs(&validation.proof_mask); - } - } - total_validations += num_validations; - if total_validations > 0 { - keyed_accounts[0].account.lamports += - (TOTAL_VALIDATOR_REWARDS * num_validations) / total_validations; - } - } - } - - if bincode::serialize_into( - &mut keyed_accounts[0].account.data[..], - &storage_account_state, - ) - .is_err() - { - return Err(InstructionError::AccountDataTooSmall); - } - - Ok(()) - } else { - info!("Invalid instruction data: {:?}", data); - Err(InstructionError::InvalidInstructionData) - } -} - -#[cfg(test)] -mod test { - use super::*; - use solana_sdk::account::{create_keyed_accounts, Account}; - use solana_sdk::hash::Hash; - use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; - use solana_sdk::transaction::{CompiledInstruction, Transaction}; - use solana_storage_api::{ProofStatus, StorageTransaction}; - - fn test_transaction( - tx: &Transaction, - program_accounts: &mut [Account], - ) -> Result<(), InstructionError> { - assert_eq!(tx.instructions.len(), 1); - let CompiledInstruction { - ref accounts, - ref data, - .. - } = tx.instructions[0]; - - info!("accounts: {:?}", accounts); - - let mut keyed_accounts: Vec<_> = accounts - .iter() - .map(|&index| { - let index = index as usize; - let key = &tx.account_keys[index]; - (key, index < tx.signatures.len()) - }) - .zip(program_accounts.iter_mut()) - .map(|((key, is_signer), account)| KeyedAccount::new(key, is_signer, account)) - .collect(); - - let ret = entrypoint(&id(), &mut keyed_accounts, &data, 42); - info!("ret: {:?}", ret); - ret - } - - #[test] - fn test_storage_tx() { - let keypair = Keypair::new(); - let mut accounts = [(keypair.pubkey(), Account::default())]; - let mut keyed_accounts = create_keyed_accounts(&mut accounts); - assert!(entrypoint(&id(), &mut keyed_accounts, &[], 42).is_err()); - } - - #[test] - fn test_serialize_overflow() { - let keypair = Keypair::new(); - let mut keyed_accounts = Vec::new(); - let mut user_account = Account::default(); - let pubkey = keypair.pubkey(); - keyed_accounts.push(KeyedAccount::new(&pubkey, true, &mut user_account)); - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), - Hash::default(), - ENTRIES_PER_SEGMENT, - ); - - assert_eq!( - entrypoint(&id(), &mut keyed_accounts, &tx.instructions[0].data, 42), - Err(InstructionError::AccountDataTooSmall) - ); - } - - #[test] - fn test_invalid_accounts_len() { - let keypair = Keypair::new(); - let mut accounts = [Account::default()]; - - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); - assert!(test_transaction(&tx, &mut accounts).is_err()); - - let mut accounts = [Account::default(), Account::default(), Account::default()]; - - assert!(test_transaction(&tx, &mut accounts).is_err()); - } - - #[test] - fn test_submit_mining_invalid_entry_height() { - solana_logger::setup(); - let keypair = Keypair::new(); - let mut accounts = [Account::default(), Account::default()]; - accounts[1].data.resize(16 * 1024, 0); - - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); - - // Haven't seen a transaction to roll over the epoch, so this should fail - assert!(test_transaction(&tx, &mut accounts).is_err()); - } - - #[test] - fn test_submit_mining_ok() { - solana_logger::setup(); - let keypair = Keypair::new(); - let mut accounts = [Account::default(), Account::default()]; - accounts[0].data.resize(16 * 1024, 0); - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), - Hash::default(), - ENTRIES_PER_SEGMENT, - ); - - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - 0, - Signature::default(), - ); - - test_transaction(&tx, &mut accounts).unwrap(); - } - - #[test] - fn test_validate_mining() { - solana_logger::setup(); - let keypair = Keypair::new(); - let mut accounts = [Account::default(), Account::default()]; - accounts[0].data.resize(16 * 1024, 0); - - let entry_height = 0; - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), - Hash::default(), - ENTRIES_PER_SEGMENT, - ); - - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_mining_proof( - &keypair, - Hash::default(), - Hash::default(), - entry_height, - Signature::default(), - ); - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), - Hash::default(), - ENTRIES_PER_SEGMENT * 2, - ); - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_proof_validation( - &keypair, - Hash::default(), - entry_height, - vec![ProofStatus::Valid], - ); - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &keypair, - Hash::default(), - Hash::default(), - ENTRIES_PER_SEGMENT * 3, - ); - test_transaction(&tx, &mut accounts).unwrap(); - - let tx = StorageTransaction::new_reward_claim(&keypair, Hash::default(), entry_height); - test_transaction(&tx, &mut accounts).unwrap(); - - assert!(accounts[0].lamports == TOTAL_VALIDATOR_REWARDS); - } -} +solana_sdk::process_instruction_entrypoint!(process_instruction); diff --git a/programs/storage_program/tests/storage.rs b/programs/storage_program/tests/storage.rs deleted file mode 100644 index c3573cbf2..000000000 --- a/programs/storage_program/tests/storage.rs +++ /dev/null @@ -1,104 +0,0 @@ -use bincode::deserialize; -use log::*; -use solana_runtime::bank::Bank; -use solana_sdk::genesis_block::GenesisBlock; -use solana_sdk::hash::{hash, Hash}; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, KeypairUtil, Signature}; -use solana_sdk::system_transaction::SystemTransaction; -use solana_storage_api::{StorageTransaction, ENTRIES_PER_SEGMENT}; - -fn get_storage_entry_height(bank: &Bank, account: &Pubkey) -> u64 { - match bank.get_account(&account) { - Some(storage_system_account) => { - let state = deserialize(&storage_system_account.data); - if let Ok(state) = state { - let state: solana_storage_api::StorageProgramState = state; - return state.entry_height; - } - } - None => { - info!("error in reading entry_height"); - } - } - 0 -} - -fn get_storage_blockhash(bank: &Bank, account: &Pubkey) -> Hash { - if let Some(storage_system_account) = bank.get_account(&account) { - let state = deserialize(&storage_system_account.data); - if let Ok(state) = state { - let state: solana_storage_api::StorageProgramState = state; - return state.hash; - } - } - Hash::default() -} - -#[test] -fn test_bank_storage() { - let (mut genesis_block, alice) = GenesisBlock::new(1000); - genesis_block.native_programs.push(( - "solana_storage_program".to_string(), - solana_storage_api::id(), - )); - let bank = Bank::new(&genesis_block); - - let bob = Keypair::new(); - let jack = Keypair::new(); - let jill = Keypair::new(); - - let x = 42; - let blockhash = genesis_block.hash(); - let x2 = x * 2; - let storage_blockhash = hash(&[x2]); - - bank.register_tick(&blockhash); - - bank.transfer(10, &alice, &jill.pubkey(), blockhash) - .unwrap(); - - bank.transfer(10, &alice, &bob.pubkey(), blockhash).unwrap(); - bank.transfer(10, &alice, &jack.pubkey(), blockhash) - .unwrap(); - - let tx = SystemTransaction::new_program_account( - &alice, - &bob.pubkey(), - blockhash, - 1, - 4 * 1024, - &solana_storage_api::id(), - 0, - ); - - bank.process_transaction(&tx).unwrap(); - - let tx = StorageTransaction::new_advertise_recent_blockhash( - &bob, - storage_blockhash, - blockhash, - ENTRIES_PER_SEGMENT, - ); - - bank.process_transaction(&tx).unwrap(); - - let entry_height = 0; - let tx = StorageTransaction::new_mining_proof( - &bob, - Hash::default(), - blockhash, - entry_height, - Signature::default(), - ); - let _result = bank.process_transaction(&tx).unwrap(); - - assert_eq!( - get_storage_entry_height(&bank, &bob.pubkey()), - ENTRIES_PER_SEGMENT - ); - assert_eq!( - get_storage_blockhash(&bank, &bob.pubkey()), - storage_blockhash - ); -} diff --git a/programs/token_api/Cargo.toml b/programs/token_api/Cargo.toml index 7974eb19c..76cecc52d 100644 --- a/programs/token_api/Cargo.toml +++ b/programs/token_api/Cargo.toml @@ -10,8 +10,10 @@ edition = "2018" [dependencies] bincode = "1.1.2" +log = "0.4.2" serde = "1.0.89" serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } [lib] diff --git a/programs/token_api/src/lib.rs b/programs/token_api/src/lib.rs index b285e6747..148d20f99 100644 --- a/programs/token_api/src/lib.rs +++ b/programs/token_api/src/lib.rs @@ -1,3 +1,6 @@ +pub mod token_processor; +mod token_state; + use solana_sdk::pubkey::Pubkey; const TOKEN_PROGRAM_ID: [u8; 32] = [ diff --git a/programs/token_api/src/token_processor.rs b/programs/token_api/src/token_processor.rs new file mode 100644 index 000000000..f98b08dd7 --- /dev/null +++ b/programs/token_api/src/token_processor.rs @@ -0,0 +1,20 @@ +use crate::token_state::TokenState; +use bincode::serialize; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::InstructionError; + +pub fn process_instruction( + program_id: &Pubkey, + info: &mut [KeyedAccount], + input: &[u8], + _tick_height: u64, +) -> Result<(), InstructionError> { + solana_logger::setup(); + + TokenState::process(program_id, info, input).map_err(|e| { + error!("error: {:?}", e); + InstructionError::CustomError(serialize(&e).unwrap()) + }) +} diff --git a/programs/token_program/src/token_program.rs b/programs/token_api/src/token_state.rs similarity index 79% rename from programs/token_program/src/token_program.rs rename to programs/token_api/src/token_state.rs index 6e25a54d8..cbf7a82bb 100644 --- a/programs/token_program/src/token_program.rs +++ b/programs/token_api/src/token_state.rs @@ -92,54 +92,54 @@ enum Command { } #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum TokenProgram { +pub enum TokenState { Unallocated, Token(TokenInfo), Account(TokenAccountInfo), Invalid, } -impl Default for TokenProgram { - fn default() -> TokenProgram { - TokenProgram::Unallocated +impl Default for TokenState { + fn default() -> TokenState { + TokenState::Unallocated } } -impl TokenProgram { +impl TokenState { #[allow(clippy::needless_pass_by_value)] fn map_to_invalid_args(err: std::boxed::Box) -> Error { warn!("invalid argument: {:?}", err); Error::InvalidArgument } - pub fn deserialize(input: &[u8]) -> Result { + pub fn deserialize(input: &[u8]) -> Result { if input.is_empty() { Err(Error::InvalidArgument)?; } match input[0] { - 0 => Ok(TokenProgram::Unallocated), - 1 => Ok(TokenProgram::Token( + 0 => Ok(TokenState::Unallocated), + 1 => Ok(TokenState::Token( bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?, )), - 2 => Ok(TokenProgram::Account( + 2 => Ok(TokenState::Account( bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?, )), _ => Err(Error::InvalidArgument), } } - fn serialize(self: &TokenProgram, output: &mut [u8]) -> Result<()> { + fn serialize(self: &TokenState, output: &mut [u8]) -> Result<()> { if output.is_empty() { warn!("serialize fail: ouput.len is 0"); Err(Error::InvalidArgument)?; } match self { - TokenProgram::Unallocated | TokenProgram::Invalid => Err(Error::InvalidArgument), - TokenProgram::Token(token_info) => { + TokenState::Unallocated | TokenState::Invalid => Err(Error::InvalidArgument), + TokenState::Token(token_info) => { output[0] = 1; let writer = std::io::BufWriter::new(&mut output[1..]); bincode::serialize_into(writer, &token_info).map_err(Self::map_to_invalid_args) } - TokenProgram::Account(account_info) => { + TokenState::Account(account_info) => { output[0] = 2; let writer = std::io::BufWriter::new(&mut output[1..]); bincode::serialize_into(writer, &account_info).map_err(Self::map_to_invalid_args) @@ -149,7 +149,7 @@ impl TokenProgram { #[allow(dead_code)] pub fn amount(&self) -> Result { - if let TokenProgram::Account(account_info) = self { + if let TokenState::Account(account_info) = self { Ok(account_info.amount) } else { Err(Error::InvalidArgument) @@ -159,28 +159,28 @@ impl TokenProgram { #[allow(dead_code)] pub fn only_owner(&self, key: &Pubkey) -> Result<()> { if *key != Pubkey::default() { - if let TokenProgram::Account(account_info) = self { + if let TokenState::Account(account_info) = self { if account_info.owner == *key { return Ok(()); } } } - warn!("TokenProgram: non-owner rejected"); + warn!("TokenState: non-owner rejected"); Err(Error::NotOwner) } pub fn process_command_newtoken( info: &mut [KeyedAccount], token_info: TokenInfo, - input_program_accounts: &[TokenProgram], - output_program_accounts: &mut Vec<(usize, TokenProgram)>, + input_program_accounts: &[TokenState], + output_program_accounts: &mut Vec<(usize, TokenState)>, ) -> Result<()> { if input_program_accounts.len() != 2 { error!("Expected 2 accounts"); Err(Error::InvalidArgument)?; } - if let TokenProgram::Account(dest_account) = &input_program_accounts[1] { + if let TokenState::Account(dest_account) = &input_program_accounts[1] { if info[0].signer_key().unwrap() != &dest_account.token { error!("account 1 token mismatch"); Err(Error::InvalidArgument)?; @@ -193,24 +193,24 @@ impl TokenProgram { let mut output_dest_account = dest_account.clone(); output_dest_account.amount = token_info.supply; - output_program_accounts.push((1, TokenProgram::Account(output_dest_account))); + output_program_accounts.push((1, TokenState::Account(output_dest_account))); } else { error!("account 1 invalid"); Err(Error::InvalidArgument)?; } - if input_program_accounts[0] != TokenProgram::Unallocated { + if input_program_accounts[0] != TokenState::Unallocated { error!("account 0 not available"); Err(Error::InvalidArgument)?; } - output_program_accounts.push((0, TokenProgram::Token(token_info))); + output_program_accounts.push((0, TokenState::Token(token_info))); Ok(()) } pub fn process_command_newaccount( info: &mut [KeyedAccount], - input_program_accounts: &[TokenProgram], - output_program_accounts: &mut Vec<(usize, TokenProgram)>, + input_program_accounts: &[TokenState], + output_program_accounts: &mut Vec<(usize, TokenState)>, ) -> Result<()> { // key 0 - Destination new token account // key 1 - Owner of the account @@ -220,7 +220,7 @@ impl TokenProgram { error!("Expected 3 accounts"); Err(Error::InvalidArgument)?; } - if input_program_accounts[0] != TokenProgram::Unallocated { + if input_program_accounts[0] != TokenState::Unallocated { error!("account 0 is already allocated"); Err(Error::InvalidArgument)?; } @@ -236,22 +236,22 @@ impl TokenProgram { original_amount: 0, }); } - output_program_accounts.push((0, TokenProgram::Account(token_account_info))); + output_program_accounts.push((0, TokenState::Account(token_account_info))); Ok(()) } pub fn process_command_transfer( info: &mut [KeyedAccount], amount: u64, - input_program_accounts: &[TokenProgram], - output_program_accounts: &mut Vec<(usize, TokenProgram)>, + input_program_accounts: &[TokenState], + output_program_accounts: &mut Vec<(usize, TokenState)>, ) -> Result<()> { if input_program_accounts.len() < 3 { error!("Expected 3 accounts"); Err(Error::InvalidArgument)?; } - if let (TokenProgram::Account(source_account), TokenProgram::Account(dest_account)) = + if let (TokenState::Account(source_account), TokenState::Account(dest_account)) = (&input_program_accounts[1], &input_program_accounts[2]) { if source_account.token != dest_account.token { @@ -275,7 +275,7 @@ impl TokenProgram { let mut output_source_account = source_account.clone(); output_source_account.amount -= amount; - output_program_accounts.push((1, TokenProgram::Account(output_source_account))); + output_program_accounts.push((1, TokenState::Account(output_source_account))); if let Some(ref delegate_info) = source_account.delegate { if input_program_accounts.len() != 4 { @@ -284,7 +284,7 @@ impl TokenProgram { } let delegate_account = source_account; - if let TokenProgram::Account(source_account) = &input_program_accounts[3] { + if let TokenState::Account(source_account) = &input_program_accounts[3] { if source_account.token != delegate_account.token { error!("account 1/3 token mismatch"); Err(Error::InvalidArgument)?; @@ -300,7 +300,7 @@ impl TokenProgram { let mut output_source_account = source_account.clone(); output_source_account.amount -= amount; - output_program_accounts.push((3, TokenProgram::Account(output_source_account))); + output_program_accounts.push((3, TokenState::Account(output_source_account))); } else { error!("account 3 is an invalid account"); Err(Error::InvalidArgument)?; @@ -309,7 +309,7 @@ impl TokenProgram { let mut output_dest_account = dest_account.clone(); output_dest_account.amount += amount; - output_program_accounts.push((2, TokenProgram::Account(output_dest_account))); + output_program_accounts.push((2, TokenState::Account(output_dest_account))); } else { error!("account 1 and/or 2 are invalid accounts"); Err(Error::InvalidArgument)?; @@ -320,15 +320,15 @@ impl TokenProgram { pub fn process_command_approve( info: &mut [KeyedAccount], amount: u64, - input_program_accounts: &[TokenProgram], - output_program_accounts: &mut Vec<(usize, TokenProgram)>, + input_program_accounts: &[TokenState], + output_program_accounts: &mut Vec<(usize, TokenState)>, ) -> Result<()> { if input_program_accounts.len() != 3 { error!("Expected 3 accounts"); Err(Error::InvalidArgument)?; } - if let (TokenProgram::Account(source_account), TokenProgram::Account(delegate_account)) = + if let (TokenState::Account(source_account), TokenState::Account(delegate_account)) = (&input_program_accounts[1], &input_program_accounts[2]) { if source_account.token != delegate_account.token { @@ -363,8 +363,7 @@ impl TokenProgram { source: delegate_info.source, original_amount: amount, }); - output_program_accounts - .push((2, TokenProgram::Account(output_delegate_account))); + output_program_accounts.push((2, TokenState::Account(output_delegate_account))); } } } else { @@ -376,15 +375,15 @@ impl TokenProgram { pub fn process_command_setowner( info: &mut [KeyedAccount], - input_program_accounts: &[TokenProgram], - output_program_accounts: &mut Vec<(usize, TokenProgram)>, + input_program_accounts: &[TokenState], + output_program_accounts: &mut Vec<(usize, TokenState)>, ) -> Result<()> { if input_program_accounts.len() < 3 { error!("Expected 3 accounts"); Err(Error::InvalidArgument)?; } - if let TokenProgram::Account(source_account) = &input_program_accounts[1] { + if let TokenState::Account(source_account) = &input_program_accounts[1] { if info[0].signer_key().unwrap() != &source_account.owner { info!("owner of account 1 not present"); Err(Error::InvalidArgument)?; @@ -392,7 +391,7 @@ impl TokenProgram { let mut output_source_account = source_account.clone(); output_source_account.owner = *info[2].unsigned_key(); - output_program_accounts.push((1, TokenProgram::Account(output_source_account))); + output_program_accounts.push((1, TokenState::Account(output_source_account))); } else { info!("account 1 is invalid"); Err(Error::InvalidArgument)?; @@ -408,7 +407,7 @@ impl TokenProgram { Err(Error::InvalidArgument)?; } - let input_program_accounts: Vec = info + let input_program_accounts: Vec = info .iter() .map(|keyed_account| { let account = &keyed_account.account; @@ -417,11 +416,11 @@ impl TokenProgram { Ok(token_program) => token_program, Err(err) => { error!("deserialize failed: {:?}", err); - TokenProgram::Invalid + TokenState::Invalid } } } else { - TokenProgram::Invalid + TokenState::Invalid } }) .collect(); @@ -482,47 +481,47 @@ mod test { use super::*; #[test] pub fn serde() { - assert_eq!(TokenProgram::deserialize(&[0]), Ok(TokenProgram::default())); + assert_eq!(TokenState::deserialize(&[0]), Ok(TokenState::default())); let mut data = vec![0; 256]; - let account = TokenProgram::Account(TokenAccountInfo { + let account = TokenState::Account(TokenAccountInfo { token: Pubkey::new(&[1; 32]), owner: Pubkey::new(&[2; 32]), amount: 123, delegate: None, }); account.serialize(&mut data).unwrap(); - assert_eq!(TokenProgram::deserialize(&data), Ok(account)); + assert_eq!(TokenState::deserialize(&data), Ok(account)); - let account = TokenProgram::Token(TokenInfo { + let account = TokenState::Token(TokenInfo { supply: 12345, decimals: 2, name: "A test token".to_string(), symbol: "TEST".to_string(), }); account.serialize(&mut data).unwrap(); - assert_eq!(TokenProgram::deserialize(&data), Ok(account)); + assert_eq!(TokenState::deserialize(&data), Ok(account)); } #[test] pub fn serde_expect_fail() { let mut data = vec![0; 256]; - // Certain TokenProgram's may not be serialized - let account = TokenProgram::default(); - assert_eq!(account, TokenProgram::Unallocated); + // Certain TokenState's may not be serialized + let account = TokenState::default(); + assert_eq!(account, TokenState::Unallocated); assert!(account.serialize(&mut data).is_err()); assert!(account.serialize(&mut data).is_err()); - let account = TokenProgram::Invalid; + let account = TokenState::Invalid; assert!(account.serialize(&mut data).is_err()); // Bad deserialize data - assert!(TokenProgram::deserialize(&[]).is_err()); - assert!(TokenProgram::deserialize(&[1]).is_err()); - assert!(TokenProgram::deserialize(&[1, 2]).is_err()); - assert!(TokenProgram::deserialize(&[2, 2]).is_err()); - assert!(TokenProgram::deserialize(&[3]).is_err()); + assert!(TokenState::deserialize(&[]).is_err()); + assert!(TokenState::deserialize(&[1]).is_err()); + assert!(TokenState::deserialize(&[1, 2]).is_err()); + assert!(TokenState::deserialize(&[2, 2]).is_err()); + assert!(TokenState::deserialize(&[3]).is_err()); } // Note: business logic tests are located in the @solana/web3.js test suite diff --git a/programs/token_program/Cargo.toml b/programs/token_program/Cargo.toml index 4657dfe87..a7cb58730 100644 --- a/programs/token_program/Cargo.toml +++ b/programs/token_program/Cargo.toml @@ -9,12 +9,10 @@ homepage = "https://solana.com/" edition = "2018" [dependencies] -bincode = "1.1.2" log = "0.4.2" -serde = "1.0.89" -serde_derive = "1.0.89" solana-logger = { path = "../../logger", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } +solana-token-api = { path = "../token_api", version = "0.13.0" } [lib] name = "solana_token_program" diff --git a/programs/token_program/src/lib.rs b/programs/token_program/src/lib.rs index d57be6f5d..73e05822a 100644 --- a/programs/token_program/src/lib.rs +++ b/programs/token_program/src/lib.rs @@ -1,23 +1,3 @@ -use bincode::serialize; -use log::*; -use solana_sdk::account::KeyedAccount; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; +use solana_token_api::token_processor::process_instruction; -mod token_program; - -solana_entrypoint!(entrypoint); -fn entrypoint( - program_id: &Pubkey, - info: &mut [KeyedAccount], - input: &[u8], - _tick_height: u64, -) -> Result<(), InstructionError> { - solana_logger::setup(); - - token_program::TokenProgram::process(program_id, info, input).map_err(|e| { - error!("error: {:?}", e); - InstructionError::CustomError(serialize(&e).unwrap()) - }) -} +solana_sdk::process_instruction_entrypoint!(process_instruction); diff --git a/programs/vote_api/Cargo.toml b/programs/vote_api/Cargo.toml index 9daaa1fc6..227a2b04c 100644 --- a/programs/vote_api/Cargo.toml +++ b/programs/vote_api/Cargo.toml @@ -13,8 +13,13 @@ bincode = "1.1.2" log = "0.4.2" serde = "1.0.89" serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-metrics = { path = "../../metrics", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } +[dev-dependencies] +solana-runtime = { path = "../../runtime", version = "0.13.0" } + [lib] name = "solana_vote_api" crate-type = ["lib"] diff --git a/programs/vote_api/src/lib.rs b/programs/vote_api/src/lib.rs index 6869d8fe4..06b43641a 100644 --- a/programs/vote_api/src/lib.rs +++ b/programs/vote_api/src/lib.rs @@ -1,4 +1,5 @@ pub mod vote_instruction; +pub mod vote_processor; pub mod vote_state; pub mod vote_transaction; diff --git a/programs/vote_api/src/vote_processor.rs b/programs/vote_api/src/vote_processor.rs new file mode 100644 index 000000000..29cc3d3ab --- /dev/null +++ b/programs/vote_api/src/vote_processor.rs @@ -0,0 +1,192 @@ +//! Vote program +//! Receive and processes votes from validators + +use crate::vote_instruction::VoteInstruction; +use crate::vote_state; +use bincode::deserialize; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::InstructionError; + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], + _tick_height: u64, +) -> Result<(), InstructionError> { + solana_logger::setup(); + + trace!("process_instruction: {:?}", data); + trace!("keyed_accounts: {:?}", keyed_accounts); + + match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { + VoteInstruction::InitializeAccount => vote_state::initialize_account(keyed_accounts), + VoteInstruction::DelegateStake(delegate_id) => { + vote_state::delegate_stake(keyed_accounts, &delegate_id) + } + VoteInstruction::AuthorizeVoter(voter_id) => { + vote_state::authorize_voter(keyed_accounts, &voter_id) + } + VoteInstruction::Vote(vote) => { + debug!("{:?} by {}", vote, keyed_accounts[0].signer_key().unwrap()); + solana_metrics::submit( + solana_metrics::influxdb::Point::new("vote-native") + .add_field("count", solana_metrics::influxdb::Value::Integer(1)) + .to_owned(), + ); + vote_state::process_vote(keyed_accounts, vote) + } + VoteInstruction::ClearCredits => vote_state::clear_credits(keyed_accounts), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::id; + use crate::vote_instruction::{Vote, VoteInstruction}; + use crate::vote_state::VoteState; + use crate::vote_transaction::VoteTransaction; + use solana_runtime::bank::{Bank, Result}; + use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::hash::hash; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_sdk::system_instruction::SystemInstruction; + use solana_sdk::transaction::{ + AccountMeta, Instruction, InstructionError, Transaction, TransactionError, + }; + + fn create_bank(lamports: u64) -> (Bank, Keypair) { + let (genesis_block, mint_keypair) = GenesisBlock::new(lamports); + let mut bank = Bank::new(&genesis_block); + bank.add_instruction_processor(id(), process_instruction); + (bank, mint_keypair) + } + + struct VoteBank<'a> { + bank: &'a Bank, + } + + impl<'a> VoteBank<'a> { + fn new(bank: &'a Bank) -> Self { + Self { bank } + } + + fn create_vote_account( + &self, + from_keypair: &Keypair, + vote_id: &Pubkey, + lamports: u64, + ) -> Result<()> { + let blockhash = self.bank.last_blockhash(); + let tx = VoteTransaction::new_account(from_keypair, vote_id, blockhash, lamports, 0); + self.bank.process_transaction(&tx) + } + + fn create_vote_account_with_delegate( + &self, + from_keypair: &Keypair, + vote_keypair: &Keypair, + delegate_id: &Pubkey, + lamports: u64, + ) -> Result<()> { + let blockhash = self.bank.last_blockhash(); + let tx = VoteTransaction::new_account_with_delegate( + from_keypair, + vote_keypair, + delegate_id, + blockhash, + lamports, + 0, + ); + self.bank.process_transaction(&tx) + } + + fn submit_vote( + &self, + staking_account: &Pubkey, + vote_keypair: &Keypair, + tick_height: u64, + ) -> Result { + let blockhash = self.bank.last_blockhash(); + let tx = + VoteTransaction::new_vote(staking_account, vote_keypair, tick_height, blockhash, 0); + self.bank.process_transaction(&tx)?; + self.bank.register_tick(&hash(blockhash.as_ref())); + + let vote_account = self.bank.get_account(&vote_keypair.pubkey()).unwrap(); + Ok(VoteState::deserialize(&vote_account.data).unwrap()) + } + } + + #[test] + fn test_vote_bank_basic() { + let (bank, from_keypair) = create_bank(10_000); + let vote_bank = VoteBank::new(&bank); + + let vote_keypair = Keypair::new(); + let vote_id = vote_keypair.pubkey(); + vote_bank + .create_vote_account(&from_keypair, &vote_id, 100) + .unwrap(); + + let vote_state = vote_bank.submit_vote(&vote_id, &vote_keypair, 0).unwrap(); + assert_eq!(vote_state.votes.len(), 1); + } + + #[test] + fn test_vote_bank_delegate() { + let (bank, from_keypair) = create_bank(10_000); + let vote_bank = VoteBank::new(&bank); + let vote_keypair = Keypair::new(); + let delegate_keypair = Keypair::new(); + let delegate_id = delegate_keypair.pubkey(); + vote_bank + .create_vote_account_with_delegate(&from_keypair, &vote_keypair, &delegate_id, 100) + .unwrap(); + } + + #[test] + fn test_vote_via_bank_with_no_signature() { + let (bank, mallory_keypair) = create_bank(10_000); + let vote_bank = VoteBank::new(&bank); + + let vote_keypair = Keypair::new(); + let vote_id = vote_keypair.pubkey(); + vote_bank + .create_vote_account(&mallory_keypair, &vote_id, 100) + .unwrap(); + + let mallory_id = mallory_keypair.pubkey(); + let blockhash = bank.last_blockhash(); + let vote_ix = Instruction::new( + id(), + &VoteInstruction::Vote(Vote::new(0)), + vec![AccountMeta::new(vote_id, false)], // <--- attack!! No signer required. + ); + + // Sneak in an instruction so that the transaction is signed but + // the 0th account in the second instruction is not! The program + // needs to check that it's signed. + let move_ix = SystemInstruction::new_move(&mallory_id, &vote_id, 1); + let mut tx = Transaction::new(vec![move_ix, vote_ix]); + tx.sign(&[&mallory_keypair], blockhash); + + let result = bank.process_transaction(&tx); + + // And ensure there's no vote. + let vote_account = bank.get_account(&vote_id).unwrap(); + let vote_state = VoteState::deserialize(&vote_account.data).unwrap(); + assert_eq!(vote_state.votes.len(), 0); + + assert_eq!( + result, + Err(TransactionError::InstructionError( + 1, + InstructionError::InvalidArgument + )) + ); + } +} diff --git a/programs/vote_program/Cargo.toml b/programs/vote_program/Cargo.toml index c09cf9be6..706ae689f 100644 --- a/programs/vote_program/Cargo.toml +++ b/programs/vote_program/Cargo.toml @@ -9,16 +9,11 @@ homepage = "https://solana.com/" edition = "2018" [dependencies] -bincode = "1.1.2" log = "0.4.2" solana-logger = { path = "../../logger", version = "0.13.0" } -solana-metrics = { path = "../../metrics", version = "0.13.0" } solana-sdk = { path = "../../sdk", version = "0.13.0" } solana-vote-api = { path = "../vote_api", version = "0.13.0" } -[dev-dependencies] -solana-runtime = { path = "../../runtime", version = "0.13.0" } - [lib] name = "solana_vote_program" crate-type = ["cdylib"] diff --git a/programs/vote_program/src/lib.rs b/programs/vote_program/src/lib.rs index f885aab34..ffc494db4 100644 --- a/programs/vote_program/src/lib.rs +++ b/programs/vote_program/src/lib.rs @@ -1,44 +1,3 @@ -//! Vote program -//! Receive and processes votes from validators +use solana_vote_api::vote_processor::process_instruction; -use bincode::deserialize; -use log::*; -use solana_sdk::account::KeyedAccount; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::solana_entrypoint; -use solana_sdk::transaction::InstructionError; -use solana_vote_api::vote_instruction::VoteInstruction; -use solana_vote_api::vote_state; - -solana_entrypoint!(entrypoint); -fn entrypoint( - _program_id: &Pubkey, - keyed_accounts: &mut [KeyedAccount], - data: &[u8], - _tick_height: u64, -) -> Result<(), InstructionError> { - solana_logger::setup(); - - trace!("process_instruction: {:?}", data); - trace!("keyed_accounts: {:?}", keyed_accounts); - - match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { - VoteInstruction::InitializeAccount => vote_state::initialize_account(keyed_accounts), - VoteInstruction::DelegateStake(delegate_id) => { - vote_state::delegate_stake(keyed_accounts, &delegate_id) - } - VoteInstruction::AuthorizeVoter(voter_id) => { - vote_state::authorize_voter(keyed_accounts, &voter_id) - } - VoteInstruction::Vote(vote) => { - debug!("{:?} by {}", vote, keyed_accounts[0].signer_key().unwrap()); - solana_metrics::submit( - solana_metrics::influxdb::Point::new("vote-native") - .add_field("count", solana_metrics::influxdb::Value::Integer(1)) - .to_owned(), - ); - vote_state::process_vote(keyed_accounts, vote) - } - VoteInstruction::ClearCredits => vote_state::clear_credits(keyed_accounts), - } -} +solana_sdk::process_instruction_entrypoint!(process_instruction); diff --git a/programs/vote_program/tests/vote.rs b/programs/vote_program/tests/vote.rs deleted file mode 100644 index 5a07f3f01..000000000 --- a/programs/vote_program/tests/vote.rs +++ /dev/null @@ -1,141 +0,0 @@ -use solana_runtime::bank::{Bank, Result}; -use solana_sdk::genesis_block::GenesisBlock; -use solana_sdk::hash::hash; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, KeypairUtil}; -use solana_sdk::system_instruction::SystemInstruction; -use solana_sdk::transaction::{ - AccountMeta, Instruction, InstructionError, Transaction, TransactionError, -}; -use solana_vote_api::vote_instruction::{Vote, VoteInstruction}; -use solana_vote_api::vote_state::VoteState; -use solana_vote_api::vote_transaction::VoteTransaction; - -struct VoteBank<'a> { - bank: &'a Bank, -} - -impl<'a> VoteBank<'a> { - fn new(bank: &'a Bank) -> Self { - bank.add_native_program("solana_vote_program", &solana_vote_api::id()); - Self { bank } - } - - fn create_vote_account( - &self, - from_keypair: &Keypair, - vote_id: &Pubkey, - lamports: u64, - ) -> Result<()> { - let blockhash = self.bank.last_blockhash(); - let tx = VoteTransaction::new_account(from_keypair, vote_id, blockhash, lamports, 0); - self.bank.process_transaction(&tx) - } - - fn create_vote_account_with_delegate( - &self, - from_keypair: &Keypair, - vote_keypair: &Keypair, - delegate_id: &Pubkey, - lamports: u64, - ) -> Result<()> { - let blockhash = self.bank.last_blockhash(); - let tx = VoteTransaction::new_account_with_delegate( - from_keypair, - vote_keypair, - delegate_id, - blockhash, - lamports, - 0, - ); - self.bank.process_transaction(&tx) - } - - fn submit_vote( - &self, - staking_account: &Pubkey, - vote_keypair: &Keypair, - tick_height: u64, - ) -> Result { - let blockhash = self.bank.last_blockhash(); - let tx = - VoteTransaction::new_vote(staking_account, vote_keypair, tick_height, blockhash, 0); - self.bank.process_transaction(&tx)?; - self.bank.register_tick(&hash(blockhash.as_ref())); - - let vote_account = self.bank.get_account(&vote_keypair.pubkey()).unwrap(); - Ok(VoteState::deserialize(&vote_account.data).unwrap()) - } -} - -#[test] -fn test_vote_bank_basic() { - let (genesis_block, from_keypair) = GenesisBlock::new(10_000); - let bank = Bank::new(&genesis_block); - let vote_bank = VoteBank::new(&bank); - - let vote_keypair = Keypair::new(); - let vote_id = vote_keypair.pubkey(); - vote_bank - .create_vote_account(&from_keypair, &vote_id, 100) - .unwrap(); - - let vote_state = vote_bank.submit_vote(&vote_id, &vote_keypair, 0).unwrap(); - assert_eq!(vote_state.votes.len(), 1); -} - -#[test] -fn test_vote_bank_delegate() { - let (genesis_block, from_keypair) = GenesisBlock::new(10_000); - let bank = Bank::new(&genesis_block); - let vote_bank = VoteBank::new(&bank); - let vote_keypair = Keypair::new(); - let delegate_keypair = Keypair::new(); - let delegate_id = delegate_keypair.pubkey(); - vote_bank - .create_vote_account_with_delegate(&from_keypair, &vote_keypair, &delegate_id, 100) - .unwrap(); -} - -#[test] -fn test_vote_via_bank_with_no_signature() { - let (genesis_block, mallory_keypair) = GenesisBlock::new(10_000); - let bank = Bank::new(&genesis_block); - let vote_bank = VoteBank::new(&bank); - - let vote_keypair = Keypair::new(); - let vote_id = vote_keypair.pubkey(); - vote_bank - .create_vote_account(&mallory_keypair, &vote_id, 100) - .unwrap(); - - let mallory_id = mallory_keypair.pubkey(); - let blockhash = bank.last_blockhash(); - let vote_ix = Instruction::new( - solana_vote_api::id(), - &VoteInstruction::Vote(Vote::new(0)), - vec![AccountMeta::new(vote_id, false)], // <--- attack!! No signer required. - ); - - // Sneak in an instruction so that the transaction is signed but - // the 0th account in the second instruction is not! The program - // needs to check that it's signed. - let move_ix = SystemInstruction::new_move(&mallory_id, &vote_id, 1); - let mut tx = Transaction::new(vec![move_ix, vote_ix]); - tx.sign(&[&mallory_keypair], blockhash); - - let result = bank.process_transaction(&tx); - - // And ensure there's no vote. - let vote_account = bank.get_account(&vote_id).unwrap(); - let vote_state = VoteState::deserialize(&vote_account.data).unwrap(); - assert_eq!(vote_state.votes.len(), 0); - - assert_eq!( - result, - Err(TransactionError::InstructionError( - 1, - InstructionError::InvalidArgument - )) - ); -}