Get access to runtime errors in Budget unit-tests
This commit is contained in:
parent
60437a8dcb
commit
70b45de012
|
@ -1,11 +1,12 @@
|
||||||
//! budget program
|
//! budget program
|
||||||
use bincode::deserialize;
|
use bincode::{deserialize, serialize};
|
||||||
use chrono::prelude::{DateTime, Utc};
|
use chrono::prelude::{DateTime, Utc};
|
||||||
use log::*;
|
use log::*;
|
||||||
use solana_budget_api::budget_instruction::BudgetInstruction;
|
use solana_budget_api::budget_instruction::BudgetInstruction;
|
||||||
use solana_budget_api::budget_state::{BudgetError, BudgetState};
|
use solana_budget_api::budget_state::{BudgetError, BudgetState};
|
||||||
use solana_budget_api::payment_plan::Witness;
|
use solana_budget_api::payment_plan::Witness;
|
||||||
use solana_sdk::account::KeyedAccount;
|
use solana_sdk::account::KeyedAccount;
|
||||||
|
use solana_sdk::native_program::ProgramError;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
/// Process a Witness Signature. Any payment plans waiting on this signature
|
||||||
|
@ -16,10 +17,7 @@ fn apply_signature(
|
||||||
) -> Result<(), BudgetError> {
|
) -> Result<(), BudgetError> {
|
||||||
let mut final_payment = None;
|
let mut final_payment = None;
|
||||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||||
let key = match keyed_accounts[0].signer_key() {
|
let key = keyed_accounts[0].signer_key().unwrap();
|
||||||
None => return Err(BudgetError::UnsignedKey),
|
|
||||||
Some(key) => key,
|
|
||||||
};
|
|
||||||
expr.apply_witness(&Witness::Signature, key);
|
expr.apply_witness(&Witness::Signature, key);
|
||||||
final_payment = expr.final_payment();
|
final_payment = expr.final_payment();
|
||||||
}
|
}
|
||||||
|
@ -55,10 +53,7 @@ fn apply_timestamp(
|
||||||
let mut final_payment = None;
|
let mut final_payment = None;
|
||||||
|
|
||||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||||
let key = match keyed_accounts[0].signer_key() {
|
let key = keyed_accounts[0].signer_key().unwrap();
|
||||||
None => return Err(BudgetError::UnsignedKey),
|
|
||||||
Some(key) => key,
|
|
||||||
};
|
|
||||||
expr.apply_witness(&Witness::Timestamp(dt), key);
|
expr.apply_witness(&Witness::Timestamp(dt), key);
|
||||||
final_payment = expr.final_payment();
|
final_payment = expr.final_payment();
|
||||||
}
|
}
|
||||||
|
@ -75,81 +70,73 @@ fn apply_timestamp(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_debits(
|
pub fn process_instruction(
|
||||||
|
_program_id: &Pubkey,
|
||||||
keyed_accounts: &mut [KeyedAccount],
|
keyed_accounts: &mut [KeyedAccount],
|
||||||
instruction: &BudgetInstruction,
|
data: &[u8],
|
||||||
) -> Result<(), BudgetError> {
|
) -> Result<(), ProgramError> {
|
||||||
|
let instruction = deserialize(data).map_err(|err| {
|
||||||
|
info!("Invalid transaction data: {:?} {:?}", data, err);
|
||||||
|
ProgramError::InvalidInstructionData
|
||||||
|
})?;
|
||||||
|
|
||||||
|
trace!("process_instruction: {:?}", instruction);
|
||||||
|
|
||||||
match instruction {
|
match instruction {
|
||||||
BudgetInstruction::InitializeAccount(expr) => {
|
BudgetInstruction::InitializeAccount(expr) => {
|
||||||
let expr = expr.clone();
|
let expr = expr.clone();
|
||||||
if let Some(payment) = expr.final_payment() {
|
if let Some(payment) = expr.final_payment() {
|
||||||
keyed_accounts[1].account.lamports = 0;
|
keyed_accounts[1].account.lamports = 0;
|
||||||
keyed_accounts[0].account.lamports += payment.lamports;
|
keyed_accounts[0].account.lamports += payment.lamports;
|
||||||
Ok(())
|
return Ok(());
|
||||||
} else {
|
}
|
||||||
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
|
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
|
||||||
if Some(true) == existing.map(|x| x.initialized) {
|
if Some(true) == existing.map(|x| x.initialized) {
|
||||||
trace!("contract already exists");
|
trace!("contract already exists");
|
||||||
Err(BudgetError::ContractAlreadyExists)
|
return Err(ProgramError::AccountAlreadyInitialized);
|
||||||
} else {
|
}
|
||||||
let mut budget_state = BudgetState::default();
|
let mut budget_state = BudgetState::default();
|
||||||
budget_state.pending_budget = Some(expr);
|
budget_state.pending_budget = Some(expr);
|
||||||
budget_state.initialized = true;
|
budget_state.initialized = true;
|
||||||
budget_state.serialize(&mut keyed_accounts[0].account.data)
|
budget_state.serialize(&mut keyed_accounts[0].account.data)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
BudgetInstruction::ApplyTimestamp(dt) => {
|
BudgetInstruction::ApplyTimestamp(dt) => {
|
||||||
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
|
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||||
{
|
|
||||||
if !budget_state.is_pending() {
|
if !budget_state.is_pending() {
|
||||||
Err(BudgetError::ContractNotPending)
|
return Ok(()); // Nothing to do here.
|
||||||
} else if !budget_state.initialized {
|
}
|
||||||
|
if !budget_state.initialized {
|
||||||
trace!("contract is uninitialized");
|
trace!("contract is uninitialized");
|
||||||
Err(BudgetError::UninitializedContract)
|
return Err(ProgramError::UninitializedAccount);
|
||||||
} else {
|
}
|
||||||
|
if keyed_accounts[0].signer_key().is_none() {
|
||||||
|
return Err(ProgramError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
trace!("apply timestamp");
|
trace!("apply timestamp");
|
||||||
apply_timestamp(&mut budget_state, keyed_accounts, *dt)?;
|
apply_timestamp(&mut budget_state, keyed_accounts, dt)
|
||||||
|
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
|
||||||
trace!("apply timestamp committed");
|
trace!("apply timestamp committed");
|
||||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err(BudgetError::UninitializedContract)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BudgetInstruction::ApplySignature => {
|
BudgetInstruction::ApplySignature => {
|
||||||
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
|
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||||
{
|
|
||||||
if !budget_state.is_pending() {
|
if !budget_state.is_pending() {
|
||||||
Err(BudgetError::ContractNotPending)
|
return Ok(()); // Nothing to do here.
|
||||||
} else if !budget_state.initialized {
|
}
|
||||||
|
if !budget_state.initialized {
|
||||||
trace!("contract is uninitialized");
|
trace!("contract is uninitialized");
|
||||||
Err(BudgetError::UninitializedContract)
|
return Err(ProgramError::UninitializedAccount);
|
||||||
} else {
|
}
|
||||||
|
if keyed_accounts[0].signer_key().is_none() {
|
||||||
|
return Err(ProgramError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
trace!("apply signature");
|
trace!("apply signature");
|
||||||
apply_signature(&mut budget_state, keyed_accounts)?;
|
apply_signature(&mut budget_state, keyed_accounts)
|
||||||
|
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
|
||||||
trace!("apply signature committed");
|
trace!("apply signature committed");
|
||||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err(BudgetError::UninitializedContract)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
|
@ -162,12 +149,12 @@ mod test {
|
||||||
use solana_sdk::hash::Hash;
|
use solana_sdk::hash::Hash;
|
||||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||||
use solana_sdk::system_program;
|
use solana_sdk::system_program;
|
||||||
use solana_sdk::transaction::Transaction;
|
use solana_sdk::transaction::{InstructionError, Transaction, TransactionError};
|
||||||
|
|
||||||
fn process_transaction(
|
fn process_transaction(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
tx_accounts: &mut Vec<Account>,
|
tx_accounts: &mut Vec<Account>,
|
||||||
) -> Result<(), BudgetError> {
|
) -> Result<(), TransactionError> {
|
||||||
runtime::process_transaction(tx, tx_accounts, process_instruction)
|
runtime::process_transaction(tx, tx_accounts, process_instruction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +206,10 @@ mod test {
|
||||||
// Ensure the transaction fails because of the unsigned key.
|
// Ensure the transaction fails because of the unsigned key.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
process_transaction(&tx, &mut accounts),
|
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.
|
// Ensure the transaction fails because of the unsigned key.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
process_transaction(&tx, &mut accounts),
|
process_transaction(&tx, &mut accounts),
|
||||||
Err(BudgetError::UnsignedKey)
|
Err(TransactionError::InstructionError(
|
||||||
|
0,
|
||||||
|
InstructionError::ProgramError(ProgramError::MissingRequiredSignature)
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,8 +288,13 @@ mod test {
|
||||||
Hash::default(),
|
Hash::default(),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
process_transaction(&tx, &mut accounts),
|
process_transaction(&tx, &mut accounts).unwrap_err(),
|
||||||
Err(BudgetError::DestinationMissing)
|
TransactionError::InstructionError(
|
||||||
|
0,
|
||||||
|
InstructionError::ProgramError(ProgramError::CustomError(
|
||||||
|
serialize(&BudgetError::DestinationMissing).unwrap()
|
||||||
|
))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(accounts[from_account].lamports, 0);
|
assert_eq!(accounts[from_account].lamports, 0);
|
||||||
assert_eq!(accounts[contract_account].lamports, 1);
|
assert_eq!(accounts[contract_account].lamports, 1);
|
||||||
|
@ -323,10 +321,7 @@ mod test {
|
||||||
assert!(!budget_state.is_pending());
|
assert!(!budget_state.is_pending());
|
||||||
|
|
||||||
// try to replay the timestamp contract
|
// try to replay the timestamp contract
|
||||||
assert_eq!(
|
process_transaction(&tx, &mut accounts).unwrap();
|
||||||
process_transaction(&tx, &mut accounts),
|
|
||||||
Err(BudgetError::ContractNotPending)
|
|
||||||
);
|
|
||||||
assert_eq!(accounts[from_account].lamports, 0);
|
assert_eq!(accounts[from_account].lamports, 0);
|
||||||
assert_eq!(accounts[contract_account].lamports, 0);
|
assert_eq!(accounts[contract_account].lamports, 0);
|
||||||
assert_eq!(accounts[to_account].lamports, 1);
|
assert_eq!(accounts[to_account].lamports, 1);
|
||||||
|
@ -391,10 +386,7 @@ mod test {
|
||||||
&from.pubkey(),
|
&from.pubkey(),
|
||||||
Hash::default(),
|
Hash::default(),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
process_transaction(&tx, &mut accounts).unwrap();
|
||||||
process_transaction(&tx, &mut accounts),
|
|
||||||
Err(BudgetError::ContractNotPending)
|
|
||||||
);
|
|
||||||
assert_eq!(accounts[from_account].lamports, 1);
|
assert_eq!(accounts[from_account].lamports, 1);
|
||||||
assert_eq!(accounts[contract_account].lamports, 0);
|
assert_eq!(accounts[contract_account].lamports, 0);
|
||||||
assert_eq!(accounts.get(pay_account), None);
|
assert_eq!(accounts.get(pay_account), None);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
mod budget_program;
|
mod budget_program;
|
||||||
|
|
||||||
use crate::budget_program::process_instruction;
|
use crate::budget_program::process_instruction;
|
||||||
use bincode::serialize;
|
|
||||||
use log::*;
|
use log::*;
|
||||||
use solana_sdk::account::KeyedAccount;
|
use solana_sdk::account::KeyedAccount;
|
||||||
use solana_sdk::native_program::ProgramError;
|
use solana_sdk::native_program::ProgramError;
|
||||||
|
@ -20,5 +19,4 @@ fn entrypoint(
|
||||||
trace!("process_instruction: {:?}", data);
|
trace!("process_instruction: {:?}", data);
|
||||||
trace!("keyed_accounts: {:?}", keyed_accounts);
|
trace!("keyed_accounts: {:?}", keyed_accounts);
|
||||||
process_instruction(program_id, keyed_accounts, data)
|
process_instruction(program_id, keyed_accounts, data)
|
||||||
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,11 @@
|
||||||
use crate::budget_expr::BudgetExpr;
|
use crate::budget_expr::BudgetExpr;
|
||||||
use bincode::{self, deserialize, serialize_into};
|
use bincode::{self, deserialize, serialize_into};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use solana_sdk::native_program::ProgramError;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub enum BudgetError {
|
pub enum BudgetError {
|
||||||
InsufficientFunds,
|
|
||||||
ContractAlreadyExists,
|
|
||||||
ContractNotPending,
|
|
||||||
SourceIsPendingContract,
|
|
||||||
UninitializedContract,
|
|
||||||
DestinationMissing,
|
DestinationMissing,
|
||||||
FailedWitness,
|
|
||||||
AccountDataTooSmall,
|
|
||||||
AccountDataDeserializeFailure,
|
|
||||||
UnsignedKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||||
|
@ -35,14 +27,12 @@ impl BudgetState {
|
||||||
self.pending_budget.is_some()
|
self.pending_budget.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), BudgetError> {
|
pub fn serialize(&self, output: &mut [u8]) -> Result<(), ProgramError> {
|
||||||
serialize_into(output, self).map_err(|err| match *err {
|
serialize_into(output, self).map_err(|_| ProgramError::AccountDataTooSmall)
|
||||||
_ => BudgetError::AccountDataTooSmall,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize(input: &[u8]) -> bincode::Result<Self> {
|
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
|
||||||
deserialize(input)
|
deserialize(input).map_err(|_| ProgramError::InvalidAccountData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +57,7 @@ mod test {
|
||||||
let b = BudgetState::default();
|
let b = BudgetState::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
b.serialize(&mut a.data),
|
b.serialize(&mut a.data),
|
||||||
Err(BudgetError::AccountDataTooSmall)
|
Err(ProgramError::AccountDataTooSmall)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,19 +189,20 @@ pub fn execute_transaction(
|
||||||
|
|
||||||
/// A utility function for unit-tests. Same as execute_transaction(), but bypasses the loaders
|
/// A utility function for unit-tests. Same as execute_transaction(), but bypasses the loaders
|
||||||
/// for easier usage and better stack traces.
|
/// for easier usage and better stack traces.
|
||||||
pub fn process_transaction<F, E>(
|
pub fn process_transaction<F>(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
tx_accounts: &mut Vec<Account>,
|
tx_accounts: &mut Vec<Account>,
|
||||||
process_instruction: F,
|
process_instruction: F,
|
||||||
) -> Result<(), E>
|
) -> Result<(), TransactionError>
|
||||||
where
|
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() {
|
for _ in tx_accounts.len()..tx.account_keys.len() {
|
||||||
tx_accounts.push(Account::new(0, 0, &system_program::id()));
|
tx_accounts.push(Account::new(0, 0, &system_program::id()));
|
||||||
}
|
}
|
||||||
for (i, ix) in tx.instructions.iter().enumerate() {
|
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
|
let mut keyed_accounts: Vec<_> = ix
|
||||||
.accounts
|
.accounts
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -215,12 +216,14 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let program_id = tx.program_id(i);
|
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)
|
crate::system_program::entrypoint(&program_id, &mut keyed_accounts, &ix.data, 0)
|
||||||
.unwrap();
|
|
||||||
} else {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,12 @@ pub enum ProgramError {
|
||||||
/// A signature was required but not found
|
/// A signature was required but not found
|
||||||
MissingRequiredSignature,
|
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
|
/// 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
|
/// 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
|
/// to a Vec of bytes, max length 32 bytes. Any CustomError Vec greater than this length will
|
||||||
|
|
Loading…
Reference in New Issue