Skip to content

Commit

Permalink
πŸ₯… fuzzer error handling (#118)
Browse files Browse the repository at this point in the history
* πŸ₯… WIP: error handling

* πŸ”₯ update FuzzTestExecutor to the default state

* πŸš‘οΈ box banksclient and minor error updates

* 🎨 fmt

* πŸš€ update snapshot generator

* βœ… update tests

* βœ… update fuzz_example3

* 🎨 Cosmetic non-functional changes.

---------

Co-authored-by: lukacan <[email protected]>
Co-authored-by: Ikrk <[email protected]>
  • Loading branch information
3 people committed May 20, 2024
1 parent fe99c42 commit 60e2066
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 223 deletions.
1 change: 1 addition & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ trdelnik-derive-displayix = { path = "./derive/display_ix" }
trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" }
trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" }
pathdiff = "0.2.1"
solana-banks-client = "1.16.19"
18 changes: 13 additions & 5 deletions crates/client/derive/fuzz_test_executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream {
quote! {
#enum_name::#variant_name (ix) => {
let (mut signers, metas) =
if let Ok(acc) = ix.get_accounts(client, &mut accounts.borrow_mut()) {
if let Ok(acc) = ix.get_accounts(client, &mut accounts.borrow_mut())
.map_err(|e| e.with_origin(Origin::Instruction(self.to_string()))) {
acc
} else {
return Ok(());
};
let mut snaphot = Snapshot::new(&metas, ix);
// this can return FuzzClientErrorWithOrigin
snaphot.capture_before(client).unwrap();
let data =
if let Ok(data) = ix.get_data(client, &mut accounts.borrow_mut()) {
if let Ok(data) = ix.get_data(client, &mut accounts.borrow_mut())
.map_err(|e| e.with_origin(Origin::Instruction(self.to_string()))) {
data
} else {
return Ok(());
Expand All @@ -38,10 +41,15 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream {
let sig: Vec<&Keypair> = signers.iter().collect();
transaction.sign(&sig, client.get_last_blockhash());

let res = client.process_transaction(transaction);
let res = client.process_transaction(transaction)
.map_err(|e| e.with_origin(Origin::Instruction(self.to_string())));

// this can return FuzzClientErrorWithOrigin
snaphot.capture_after(client).unwrap();
let (acc_before, acc_after) = snaphot.get_snapshot().unwrap(); // we want to panic if we cannot unwrap to cause a crash
if let Err(e) = ix.check(acc_before, acc_after, data) {
let (acc_before, acc_after) = snaphot.get_snapshot()
.map_err(|e| e.with_origin(Origin::Instruction(self.to_string()))).unwrap(); // we want to panic if we cannot unwrap to cause a crash

if let Err(e) = ix.check(acc_before, acc_after, data).map_err(|e| e.with_origin(Origin::Instruction(self.to_string()))) {
eprintln!(
"Custom check after the {} instruction did not pass with the error message: {}",
self, e
Expand Down
32 changes: 8 additions & 24 deletions crates/client/src/fuzzer/data_builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Display;

use anchor_client::anchor_lang::solana_program::account_info::{Account as Acc, AccountInfo};
use anchor_client::anchor_lang::solana_program::hash::Hash;
use arbitrary::Arbitrary;
Expand All @@ -11,6 +7,11 @@ use solana_sdk::instruction::AccountMeta;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_sdk::transaction::VersionedTransaction;
use std::cell::RefCell;
use std::error::Error;
use std::fmt::Display;

use crate::error::*;

pub struct FuzzData<T, U> {
pub pre_ixs: Vec<T>,
Expand Down Expand Up @@ -139,7 +140,7 @@ pub trait IxOps<'info> {
pre_ix: Self::IxSnapshot,
post_ix: Self::IxSnapshot,
ix_data: Self::IxData,
) -> Result<(), &'static str> {
) -> Result<(), FuzzingError> {
Ok(())
}
}
Expand Down Expand Up @@ -184,7 +185,7 @@ pub trait FuzzClient {
fn get_accounts(
&mut self,
metas: &[AccountMeta],
) -> Result<Vec<Option<Account>>, FuzzClientError>;
) -> Result<Vec<Option<Account>>, FuzzClientErrorWithOrigin>;

fn get_last_blockhash(&self) -> Hash;

Expand All @@ -194,23 +195,6 @@ pub trait FuzzClient {
) -> Result<(), FuzzClientError>;
}

#[derive(Debug)]
pub enum FuzzClientError {
CannotGetAccounts,
CannotProcessTransaction, // TODO add also custom error
ClientInitError,
}

#[derive(Debug)]
pub enum FuzzingError {
// TODO Add context with_account_name()
CannotGetAccounts,
CannotGetInstructionData,
CannotDeserializeAccount,
NotEnoughAccounts, // TODO add also custom error
AccountNotFound,
}

#[macro_export]
macro_rules! fuzz_trd {
($ix:ident: $ix_dty:ident , |$buf:ident: $dty:ident| $body:block) => {
Expand Down Expand Up @@ -246,7 +230,7 @@ pub fn build_ix_fuzz_data<U: for<'a> Arbitrary<'a>, T: FuzzDataBuilder<U>, V: De
pub fn get_account_infos_option<'info>(
accounts: &'info mut [Option<Account>],
metas: &'info [AccountMeta],
) -> Result<Vec<Option<AccountInfo<'info>>>, Box<dyn Error + 'static>> {
) -> Result<Vec<Option<AccountInfo<'info>>>, FuzzingError> {
let iter = accounts.iter_mut().zip(metas);
let r = iter
.map(|(account, meta)| {
Expand Down
172 changes: 172 additions & 0 deletions crates/client/src/fuzzer/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use solana_banks_client::BanksClientError;
use solana_sdk::pubkey::Pubkey;
use std::fmt::{Debug, Display};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum FuzzClientError {
#[error("Custom fuzzing error: {0}")]
Custom(u32),
#[error("Not able to initialize client: {0}")]
ClientInitError(#[from] std::io::Error),
// Box for Error variant too Long warnings
#[error("Banks Client Error: {0}")]
BanksError(Box<BanksClientError>),
}

#[derive(Debug, Error)]
pub enum FuzzingError {
#[error("Custom fuzzing error: {0}\n")]
Custom(u32),
#[error("Not able to deserialize account: {0}\n")]
CannotDeserializeAccount(String),
#[error("Optional Account not provided: {0}\n")]
OptionalAccountNotProvided(String),
#[error("Not enough Accounts: {0}\n")]
NotEnoughAccounts(String),
#[error("Account not Found: {0}\n")]
AccountNotFound(String),
#[error("Not Able To Obtain AccountInfos\n")]
NotAbleToObtainAccountInfos,
#[error("Balance Mismatch\n")]
BalanceMismatch,
#[error("Data Mismatch\n")]
DataMismatch,
#[error("Unable to obtain Data\n")]
UnableToObtainData,
}

impl From<BanksClientError> for FuzzClientError {
fn from(value: BanksClientError) -> Self {
Self::BanksError(Box::new(value))
}
}

impl FuzzClientError {
pub fn with_origin(self, origin: Origin) -> FuzzClientErrorWithOrigin {
let mut error_with_origin = FuzzClientErrorWithOrigin::from(self);
error_with_origin.origin = Some(origin);
error_with_origin
}
pub fn with_context(self, context: Context) -> FuzzClientErrorWithOrigin {
let mut error_with_origin = FuzzClientErrorWithOrigin::from(self);
error_with_origin.context = Some(context);
error_with_origin
}
}

impl FuzzingError {
pub fn with_origin(self, origin: Origin) -> FuzzingErrorWithOrigin {
let mut error_with_origin = FuzzingErrorWithOrigin::from(self);
error_with_origin.origin = Some(origin);
error_with_origin
}
pub fn with_context(self, context: Context) -> FuzzingErrorWithOrigin {
let mut error_with_origin = FuzzingErrorWithOrigin::from(self);
error_with_origin.context = Some(context);
error_with_origin
}
}

#[derive(Debug)]
pub enum Origin {
Instruction(String),
Account(Pubkey),
}

impl Display for Origin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Origin: {:#?}", self)
}
}

#[derive(Debug)]
pub enum Context {
Pre,
Post,
}

impl Display for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Context: {:#?}", self)
}
}

#[derive(Debug)]
pub struct FuzzClientErrorWithOrigin {
pub client_error: FuzzClientError,
pub origin: Option<Origin>,
pub context: Option<Context>,
}

#[derive(Debug)]
pub struct FuzzingErrorWithOrigin {
pub fuzzing_error: FuzzingError,
pub origin: Option<Origin>,
pub context: Option<Context>,
}

impl From<FuzzClientError> for FuzzClientErrorWithOrigin {
fn from(client_error: FuzzClientError) -> Self {
Self {
client_error,
origin: None,
context: None,
}
}
}

impl From<FuzzingError> for FuzzingErrorWithOrigin {
fn from(fuzzing_error: FuzzingError) -> Self {
Self {
fuzzing_error,
origin: None,
context: None,
}
}
}
impl Display for FuzzClientErrorWithOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.client_error, f)?;
if let Some(o) = &self.origin {
Display::fmt(o, f)?;
}
if let Some(c) = &self.context {
Display::fmt(c, f)?;
}
Ok(())
}
}
impl Display for FuzzingErrorWithOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.fuzzing_error, f)?;
if let Some(o) = &self.origin {
Display::fmt(o, f)?;
}
if let Some(c) = &self.context {
Display::fmt(c, f)?;
}
Ok(())
}
}

impl FuzzClientErrorWithOrigin {
pub fn with_origin(mut self, origin: Origin) -> Self {
self.origin = Some(origin);
self
}
pub fn with_context(mut self, context: Context) -> Self {
self.context = Some(context);
self
}
}
impl FuzzingErrorWithOrigin {
pub fn with_origin(mut self, origin: Origin) -> Self {
self.origin = Some(origin);
self
}
pub fn with_context(mut self, context: Context) -> Self {
self.context = Some(context);
self
}
}
2 changes: 2 additions & 0 deletions crates/client/src/fuzzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pub mod snapshot;
pub mod snapshot_generator;

pub type AccountId = u8;

pub mod error;
32 changes: 17 additions & 15 deletions crates/client/src/fuzzer/program_test_client_blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use solana_sdk::{
use spl_token::state::Mint;
use tokio::runtime::Builder;

use crate::data_builder::{FuzzClient, FuzzClientError};
use crate::data_builder::FuzzClient;
use crate::error::*;

pub struct ProgramTestClientBlocking {
ctx: ProgramTestContext,
Expand All @@ -28,10 +29,7 @@ impl ProgramTestClientBlocking {
// .enable_all()
// .build()
// .map_err(|_| FuzzClientError::ClientInitError)?;
let rt: tokio::runtime::Runtime = Builder::new_current_thread()
.enable_all()
.build()
.map_err(|_| FuzzClientError::ClientInitError)?;
let rt: tokio::runtime::Runtime = Builder::new_current_thread().enable_all().build()?;

let ctx = rt.block_on(program_test.start_with_context());
Ok(Self { ctx, rt })
Expand Down Expand Up @@ -137,32 +135,36 @@ impl FuzzClient for ProgramTestClientBlocking {
}

fn get_account(&mut self, key: &Pubkey) -> Result<Option<Account>, FuzzClientError> {
self.rt
Ok(self
.rt
.block_on(self.ctx.banks_client.get_account_with_commitment(
*key,
solana_sdk::commitment_config::CommitmentLevel::Confirmed,
))
.map_err(|_| FuzzClientError::CannotGetAccounts)
))?)
}

fn get_accounts(
&mut self,
metas: &[AccountMeta],
) -> Result<Vec<Option<Account>>, FuzzClientError> {
let result: Vec<_> = metas.iter().map(|m| self.get_account(&m.pubkey)).collect();
) -> Result<Vec<Option<Account>>, FuzzClientErrorWithOrigin> {
let result: Vec<_> = metas
.iter()
.map(|m| {
self.get_account(&m.pubkey)
.map_err(|e| e.with_origin(Origin::Account(m.pubkey)))
})
.collect();
result.into_iter().collect()
}

fn get_last_blockhash(&self) -> Hash {
self.ctx.last_blockhash
}

fn process_transaction(
&mut self,
transaction: impl Into<VersionedTransaction>,
) -> Result<(), FuzzClientError> {
self.rt
.block_on(self.ctx.banks_client.process_transaction(transaction))
.map_err(|_| FuzzClientError::CannotProcessTransaction)
Ok(self
.rt
.block_on(self.ctx.banks_client.process_transaction(transaction))?)
}
}
Loading

0 comments on commit 60e2066

Please sign in to comment.