Get access to runtime errors in Budget unit-tests

This commit is contained in:
Greg Fitzgerald 2019-03-15 16:10:00 -06:00 committed by Grimes
parent 60437a8dcb
commit 70b45de012
5 changed files with 97 additions and 108 deletions

View File

@ -1,11 +1,12 @@
//! budget program
use bincode::deserialize;
use bincode::{deserialize, serialize};
use chrono::prelude::{DateTime, Utc};
use log::*;
use solana_budget_api::budget_instruction::BudgetInstruction;
use solana_budget_api::budget_state::{BudgetError, BudgetState};
use solana_budget_api::payment_plan::Witness;
use solana_sdk::account::KeyedAccount;
use solana_sdk::native_program::ProgramError;
use solana_sdk::pubkey::Pubkey;
/// Process a Witness Signature. Any payment plans waiting on this signature
@ -16,10 +17,7 @@ fn apply_signature(
) -> Result<(), BudgetError> {
let mut final_payment = None;
if let Some(ref mut expr) = budget_state.pending_budget {
let key = match keyed_accounts[0].signer_key() {
None => return Err(BudgetError::UnsignedKey),
Some(key) => key,
};
let key = keyed_accounts[0].signer_key().unwrap();
expr.apply_witness(&Witness::Signature, key);
final_payment = expr.final_payment();
}
@ -55,10 +53,7 @@ fn apply_timestamp(
let mut final_payment = None;
if let Some(ref mut expr) = budget_state.pending_budget {
let key = match keyed_accounts[0].signer_key() {
None => return Err(BudgetError::UnsignedKey),
Some(key) => key,
};
let key = keyed_accounts[0].signer_key().unwrap();
expr.apply_witness(&Witness::Timestamp(dt), key);
final_payment = expr.final_payment();
}
@ -75,83 +70,75 @@ fn apply_timestamp(
Ok(())
}
fn apply_debits(
pub fn process_instruction(
_program_id: &Pubkey,
keyed_accounts: &mut [KeyedAccount],
instruction: &BudgetInstruction,
) -> Result<(), BudgetError> {
data: &[u8],
) -> Result<(), ProgramError> {
let instruction = deserialize(data).map_err(|err| {
info!("Invalid transaction data: {:?} {:?}", data, err);
ProgramError::InvalidInstructionData
})?;
trace!("process_instruction: {:?}", instruction);
match instruction {
BudgetInstruction::InitializeAccount(expr) => {
let expr = expr.clone();
if let Some(payment) = expr.final_payment() {
keyed_accounts[1].account.lamports = 0;
keyed_accounts[0].account.lamports += payment.lamports;
Ok(())
} else {
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
if Some(true) == existing.map(|x| x.initialized) {
trace!("contract already exists");
Err(BudgetError::ContractAlreadyExists)
} else {
let mut budget_state = BudgetState::default();
budget_state.pending_budget = Some(expr);
budget_state.initialized = true;
budget_state.serialize(&mut keyed_accounts[0].account.data)
}
return Ok(());
}
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
if Some(true) == existing.map(|x| x.initialized) {
trace!("contract already exists");
return Err(ProgramError::AccountAlreadyInitialized);
}
let mut budget_state = BudgetState::default();
budget_state.pending_budget = Some(expr);
budget_state.initialized = true;
budget_state.serialize(&mut keyed_accounts[0].account.data)
}
BudgetInstruction::ApplyTimestamp(dt) => {
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
{
if !budget_state.is_pending() {
Err(BudgetError::ContractNotPending)
} else if !budget_state.initialized {
trace!("contract is uninitialized");
Err(BudgetError::UninitializedContract)
} else {
trace!("apply timestamp");
apply_timestamp(&mut budget_state, keyed_accounts, *dt)?;
trace!("apply timestamp committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
} else {
Err(BudgetError::UninitializedContract)
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
if !budget_state.is_pending() {
return Ok(()); // Nothing to do here.
}
if !budget_state.initialized {
trace!("contract is uninitialized");
return Err(ProgramError::UninitializedAccount);
}
if keyed_accounts[0].signer_key().is_none() {
return Err(ProgramError::MissingRequiredSignature);
}
trace!("apply timestamp");
apply_timestamp(&mut budget_state, keyed_accounts, dt)
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
trace!("apply timestamp committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
BudgetInstruction::ApplySignature => {
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
{
if !budget_state.is_pending() {
Err(BudgetError::ContractNotPending)
} else if !budget_state.initialized {
trace!("contract is uninitialized");
Err(BudgetError::UninitializedContract)
} else {
trace!("apply signature");
apply_signature(&mut budget_state, keyed_accounts)?;
trace!("apply signature committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
} else {
Err(BudgetError::UninitializedContract)
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
if !budget_state.is_pending() {
return Ok(()); // Nothing to do here.
}
if !budget_state.initialized {
trace!("contract is uninitialized");
return Err(ProgramError::UninitializedAccount);
}
if keyed_accounts[0].signer_key().is_none() {
return Err(ProgramError::MissingRequiredSignature);
}
trace!("apply signature");
apply_signature(&mut budget_state, keyed_accounts)
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
trace!("apply signature committed");
budget_state.serialize(&mut keyed_accounts[1].account.data)
}
}
}
pub fn process_instruction(
_program_id: &Pubkey,
keyed_accounts: &mut [KeyedAccount],
data: &[u8],
) -> Result<(), BudgetError> {
let instruction = deserialize(data).map_err(|err| {
info!("Invalid transaction data: {:?} {:?}", data, err);
BudgetError::AccountDataDeserializeFailure
})?;
trace!("process_instruction: {:?}", instruction);
apply_debits(keyed_accounts, &instruction)
}
#[cfg(test)]
mod test {
use super::*;
@ -162,12 +149,12 @@ mod test {
use solana_sdk::hash::Hash;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_program;
use solana_sdk::transaction::Transaction;
use solana_sdk::transaction::{InstructionError, Transaction, TransactionError};
fn process_transaction(
tx: &Transaction,
tx_accounts: &mut Vec<Account>,
) -> Result<(), BudgetError> {
) -> Result<(), TransactionError> {
runtime::process_transaction(tx, tx_accounts, process_instruction)
}
@ -219,7 +206,10 @@ mod test {
// Ensure the transaction fails because of the unsigned key.
assert_eq!(
process_transaction(&tx, &mut accounts),
Err(BudgetError::UnsignedKey)
Err(TransactionError::InstructionError(
0,
InstructionError::ProgramError(ProgramError::MissingRequiredSignature)
))
);
}
@ -255,7 +245,10 @@ mod test {
// Ensure the transaction fails because of the unsigned key.
assert_eq!(
process_transaction(&tx, &mut accounts),
Err(BudgetError::UnsignedKey)
Err(TransactionError::InstructionError(
0,
InstructionError::ProgramError(ProgramError::MissingRequiredSignature)
))
);
}
@ -295,8 +288,13 @@ mod test {
Hash::default(),
);
assert_eq!(
process_transaction(&tx, &mut accounts),
Err(BudgetError::DestinationMissing)
process_transaction(&tx, &mut accounts).unwrap_err(),
TransactionError::InstructionError(
0,
InstructionError::ProgramError(ProgramError::CustomError(
serialize(&BudgetError::DestinationMissing).unwrap()
))
)
);
assert_eq!(accounts[from_account].lamports, 0);
assert_eq!(accounts[contract_account].lamports, 1);
@ -323,10 +321,7 @@ mod test {
assert!(!budget_state.is_pending());
// try to replay the timestamp contract
assert_eq!(
process_transaction(&tx, &mut accounts),
Err(BudgetError::ContractNotPending)
);
process_transaction(&tx, &mut accounts).unwrap();
assert_eq!(accounts[from_account].lamports, 0);
assert_eq!(accounts[contract_account].lamports, 0);
assert_eq!(accounts[to_account].lamports, 1);
@ -391,10 +386,7 @@ mod test {
&from.pubkey(),
Hash::default(),
);
assert_eq!(
process_transaction(&tx, &mut accounts),
Err(BudgetError::ContractNotPending)
);
process_transaction(&tx, &mut accounts).unwrap();
assert_eq!(accounts[from_account].lamports, 1);
assert_eq!(accounts[contract_account].lamports, 0);
assert_eq!(accounts.get(pay_account), None);

View File

@ -1,7 +1,6 @@
mod budget_program;
use crate::budget_program::process_instruction;
use bincode::serialize;
use log::*;
use solana_sdk::account::KeyedAccount;
use solana_sdk::native_program::ProgramError;
@ -20,5 +19,4 @@ fn entrypoint(
trace!("process_instruction: {:?}", data);
trace!("keyed_accounts: {:?}", keyed_accounts);
process_instruction(program_id, keyed_accounts, data)
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))
}

View File

@ -2,19 +2,11 @@
use crate::budget_expr::BudgetExpr;
use bincode::{self, deserialize, serialize_into};
use serde_derive::{Deserialize, Serialize};
use solana_sdk::native_program::ProgramError;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum BudgetError {
InsufficientFunds,
ContractAlreadyExists,
ContractNotPending,
SourceIsPendingContract,
UninitializedContract,
DestinationMissing,
FailedWitness,
AccountDataTooSmall,
AccountDataDeserializeFailure,
UnsignedKey,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
@ -35,14 +27,12 @@ impl BudgetState {
self.pending_budget.is_some()
}
pub fn serialize(&self, output: &mut [u8]) -> Result<(), BudgetError> {
serialize_into(output, self).map_err(|err| match *err {
_ => BudgetError::AccountDataTooSmall,
})
pub fn serialize(&self, output: &mut [u8]) -> Result<(), ProgramError> {
serialize_into(output, self).map_err(|_| ProgramError::AccountDataTooSmall)
}
pub fn deserialize(input: &[u8]) -> bincode::Result<Self> {
deserialize(input)
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
deserialize(input).map_err(|_| ProgramError::InvalidAccountData)
}
}
@ -67,7 +57,7 @@ mod test {
let b = BudgetState::default();
assert_eq!(
b.serialize(&mut a.data),
Err(BudgetError::AccountDataTooSmall)
Err(ProgramError::AccountDataTooSmall)
);
}
}

View File

@ -189,19 +189,20 @@ pub fn execute_transaction(
/// A utility function for unit-tests. Same as execute_transaction(), but bypasses the loaders
/// for easier usage and better stack traces.
pub fn process_transaction<F, E>(
pub fn process_transaction<F>(
tx: &Transaction,
tx_accounts: &mut Vec<Account>,
process_instruction: F,
) -> Result<(), E>
) -> Result<(), TransactionError>
where
F: Fn(&Pubkey, &mut [KeyedAccount], &[u8]) -> Result<(), E>,
F: Fn(&Pubkey, &mut [KeyedAccount], &[u8]) -> Result<(), ProgramError>,
{
for _ in tx_accounts.len()..tx.account_keys.len() {
tx_accounts.push(Account::new(0, 0, &system_program::id()));
}
for (i, ix) in tx.instructions.iter().enumerate() {
let mut ix_accounts = get_subset_unchecked_mut(tx_accounts, &ix.accounts).unwrap();
let mut ix_accounts = get_subset_unchecked_mut(tx_accounts, &ix.accounts)
.map_err(|err| TransactionError::InstructionError(i as u8, err))?;
let mut keyed_accounts: Vec<_> = ix
.accounts
.iter()
@ -215,12 +216,14 @@ where
.collect();
let program_id = tx.program_id(i);
if system_program::check_id(&program_id) {
let result = if system_program::check_id(&program_id) {
crate::system_program::entrypoint(&program_id, &mut keyed_accounts, &ix.data, 0)
.unwrap();
} else {
process_instruction(&program_id, &mut keyed_accounts, &ix.data)?;
}
process_instruction(&program_id, &mut keyed_accounts, &ix.data)
};
result.map_err(|err| {
TransactionError::InstructionError(i as u8, InstructionError::ProgramError(err))
})?;
}
Ok(())
}

View File

@ -26,6 +26,12 @@ pub enum ProgramError {
/// A signature was required but not found
MissingRequiredSignature,
/// An initialize instruction was sent to an account that has already been initialized.
AccountAlreadyInitialized,
/// An attempt to operate on an account that hasn't been initialized.
UninitializedAccount,
/// CustomError allows on-chain programs to implement program-specific error types and see
/// them returned by the Solana runtime. A CustomError may be any type that is serialized
/// to a Vec of bytes, max length 32 bytes. Any CustomError Vec greater than this length will