diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e6605963..b1a92a120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ incremented upon a breaking change and the patch version will be incremented for ## [Unreleased] ### Added +- feat/possibility to implement custom transaction error handling ([#145](https://github.com/Ackee-Blockchain/trident/pull/145)) - feat/support of automatically obtaining fully qualified paths of Data Accounts Custom types for `accounts_snapshots.rs` ([#141](https://github.com/Ackee-Blockchain/trident/pull/141)) - feat/allow direct accounts manipulation and storage ([#142](https://github.com/Ackee-Blockchain/trident/pull/142)) - feat/support of non-corresponding instruction and context names ([#130](https://github.com/Ackee-Blockchain/trident/pull/130)) diff --git a/crates/client/derive/fuzz_test_executor/src/lib.rs b/crates/client/derive/fuzz_test_executor/src/lib.rs index 916f74e2e..e7e23bfc7 100644 --- a/crates/client/derive/fuzz_test_executor/src/lib.rs +++ b/crates/client/derive/fuzz_test_executor/src/lib.rs @@ -37,22 +37,29 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream { let sig: Vec<&Keypair> = signers.iter().collect(); transaction.sign(&sig, client.get_last_blockhash()); - let tx_res = client.process_transaction(transaction) + let tx_result = client.process_transaction(transaction) .map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string()))); - if tx_res.is_ok() { - snaphot.capture_after(client).unwrap(); - let (acc_before, acc_after) = snaphot.get_snapshot() - .map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string()))) - .expect("Snapshot deserialization expect"); // we want to panic if we cannot unwrap to cause a crash + match tx_result { + Ok(_) => { + snaphot.capture_after(client).unwrap(); + let (acc_before, acc_after) = snaphot.get_snapshot() + .map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string()))) + .expect("Snapshot deserialization expect"); // 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_context_string()))) { - eprintln!( - "CRASH DETECTED! Custom check after the {} instruction did not pass!", - self.to_context_string()); - panic!("{}", e) + if let Err(e) = ix.check(acc_before, acc_after, data).map_err(|e| e.with_origin(Origin::Instruction(self.to_context_string()))) { + eprintln!( + "CRASH DETECTED! Custom check after the {} instruction did not pass!", + self.to_context_string()); + panic!("{}", e) + } + }, + Err(e) => { + let mut raw_accounts = snaphot.get_raw_pre_ix_accounts(); + ix.tx_error_handler(e, data, &mut raw_accounts)? } } + } } }); @@ -64,7 +71,7 @@ pub fn fuzz_test_executor(input: TokenStream) -> TokenStream { program_id: Pubkey, accounts: &RefCell, client: &mut impl FuzzClient, - ) -> core::result::Result<(), Box> { + ) -> core::result::Result<(), FuzzClientErrorWithOrigin> { match self { #(#display_match_arms)* } diff --git a/crates/client/src/fuzzer/data_builder.rs b/crates/client/src/fuzzer/data_builder.rs index 710d498fa..9b7519742 100644 --- a/crates/client/src/fuzzer/data_builder.rs +++ b/crates/client/src/fuzzer/data_builder.rs @@ -80,7 +80,13 @@ where #[cfg(fuzzing_debug)] eprintln!("Currently processing: {}", fuzz_ix); - fuzz_ix.run_fuzzer(program_id, &self.accounts, client)?; + if fuzz_ix + .run_fuzzer(program_id, &self.accounts, client) + .is_err() + { + // for now skip following instructions in case of error and move to the next fuzz iteration + return Ok(()); + } } Ok(()) } @@ -92,7 +98,7 @@ pub trait FuzzTestExecutor { program_id: Pubkey, accounts: &RefCell, client: &mut impl FuzzClient, - ) -> core::result::Result<(), Box>; + ) -> core::result::Result<(), FuzzClientErrorWithOrigin>; } #[allow(unused_variables)] @@ -118,24 +124,40 @@ pub trait FuzzDataBuilder Arbitrary<'a>> { } } +/// A trait providing methods to prepare data and accounts for the fuzzed instructions and allowing +/// users to implement custom invariants checks and transactions error handling. pub trait IxOps<'info> { + /// The data to be passed as instruction data parameter type IxData; + /// The accounts to be passed as instruction accounts type IxAccounts; + /// The structure to which the instruction accounts will be deserialized type IxSnapshot; - // TODO maybe generate the From trait and return Ok(self.data.into()) + + /// Provides instruction data for the fuzzed instruction. + /// It is assumed that the instruction data will be based on the fuzzer input stored in the `self.data` variable. + /// However it is on the developer to decide and it can be also for example a hardcoded constant. + /// You should only avoid any non-deterministic random values to preserve reproducibility of the tests. fn get_data( &self, client: &mut impl FuzzClient, fuzz_accounts: &mut Self::IxAccounts, ) -> Result; + /// Provides accounts required for the fuzzed instruction. The method returns a tuple of signers and account metas. fn get_accounts( &self, client: &mut impl FuzzClient, fuzz_accounts: &mut Self::IxAccounts, ) -> Result<(Vec, Vec), FuzzingError>; - // TODO implement better error with source and description + /// A method to implement custom invariants checks for a given instruction. This method is called after each + /// successfully executed instruction and by default does nothing. You can override this behavior by providing + /// your own implementation. You can access the snapshots of account states before and after the transaction for comparison. + /// + /// If you want to detect a crash, you have to return a `FuzzingError` (or alternativelly panic). + /// + /// If you want to perform checks also on a failed instruction execution, you can do so using the [`tx_error_handler`](trident_client::fuzzer::data_builder::IxOps::tx_error_handler) method. #[allow(unused_variables)] fn check( &self, @@ -145,6 +167,35 @@ pub trait IxOps<'info> { ) -> Result<(), FuzzingError> { Ok(()) } + + /// A method to implement custom error handler for failed transactions. + /// + /// The fuzzer might generate a sequence of one or more instructions that are executed sequentially. + /// By default, if the execution of one of the instructions fails, the remaining instructions are skipped + /// and are not executed. This can be overriden by implementing this method and returning `Ok(())` + /// instead of propagating the error. + /// + /// You can also check the kind of the transaction error by inspecting the `e` parameter. + /// If you would like to detect a crash on a specific error, call `panic!()`. + /// + /// If your accounts are malformed and the fuzzed program is unable to deserialize it, the transaction + /// execution will fail. In that case also the deserialization of accounts snapshot before executing + /// the instruction would fail. You are provided with the raw account infos snapshots and you are free + /// to deserialize the accounts by yourself and therefore also handling potential errors. To deserialize + /// the `pre_ix_acc_infos` raw accounts to a snapshot structure, you can call: + /// + /// ```rust,ignore + /// self.deserialize_option(pre_ix_acc_infos) + /// ``` + #[allow(unused_variables)] + fn tx_error_handler( + &self, + e: FuzzClientErrorWithOrigin, + ix_data: Self::IxData, + pre_ix_acc_infos: &'info mut [Option>], + ) -> Result<(), FuzzClientErrorWithOrigin> { + Err(e) + } } pub trait FuzzDeserialize<'info> { diff --git a/crates/client/src/fuzzer/snapshot.rs b/crates/client/src/fuzzer/snapshot.rs index 4f0820694..69cdfe73d 100644 --- a/crates/client/src/fuzzer/snapshot.rs +++ b/crates/client/src/fuzzer/snapshot.rs @@ -93,6 +93,11 @@ where } } + pub fn get_raw_pre_ix_accounts(&'info mut self) -> Vec>> { + Self::set_missing_accounts_to_default(&mut self.before); + Self::calculate_account_info(&mut self.before, self.metas) + } + pub fn get_snapshot(&'info mut self) -> Result<(T::Ix, T::Ix), FuzzingErrorWithOrigin> { // When user passes an account that is not initialized, the runtime will provide // a default empty account to the program. If the uninitialized account is of type diff --git a/crates/client/tests/test_data/fuzzer_macros/fuzz_fuzz_test_executor.expanded.rs b/crates/client/tests/test_data/fuzzer_macros/fuzz_fuzz_test_executor.expanded.rs index e9361a8ad..c9d3f4078 100644 --- a/crates/client/tests/test_data/fuzzer_macros/fuzz_fuzz_test_executor.expanded.rs +++ b/crates/client/tests/test_data/fuzzer_macros/fuzz_fuzz_test_executor.expanded.rs @@ -9,7 +9,7 @@ impl FuzzTestExecutor for FuzzInstruction { program_id: Pubkey, accounts: &RefCell, client: &mut impl FuzzClient, - ) -> core::result::Result<(), Box> { + ) -> core::result::Result<(), FuzzClientErrorWithOrigin> { match self { FuzzInstruction::InitVesting(ix) => { let (mut signers, metas) = ix @@ -38,38 +38,44 @@ impl FuzzTestExecutor for FuzzInstruction { signers.push(client.payer().clone()); let sig: Vec<&Keypair> = signers.iter().collect(); transaction.sign(&sig, client.get_last_blockhash()); - let tx_res = client + let tx_result = client .process_transaction(transaction) .map_err(|e| { e.with_origin(Origin::Instruction(self.to_context_string())) }); - if tx_res.is_ok() { - snaphot.capture_after(client).unwrap(); - let (acc_before, acc_after) = snaphot - .get_snapshot() - .map_err(|e| { - e.with_origin(Origin::Instruction(self.to_context_string())) - }) - .expect("Snapshot deserialization expect"); - if let Err(e) - = ix - .check(acc_before, acc_after, data) + match tx_result { + Ok(_) => { + snaphot.capture_after(client).unwrap(); + let (acc_before, acc_after) = snaphot + .get_snapshot() .map_err(|e| { e.with_origin(Origin::Instruction(self.to_context_string())) }) - { + .expect("Snapshot deserialization expect"); + if let Err(e) + = ix + .check(acc_before, acc_after, data) + .map_err(|e| { + e.with_origin(Origin::Instruction(self.to_context_string())) + }) { - ::std::io::_eprint( - format_args!( - "CRASH DETECTED! Custom check after the {0} instruction did not pass!\n", - self.to_context_string(), - ), - ); - }; - { - ::core::panicking::panic_display(&e); + { + ::std::io::_eprint( + format_args!( + "CRASH DETECTED! Custom check after the {0} instruction did not pass!\n", + self.to_context_string(), + ), + ); + }; + { + ::core::panicking::panic_display(&e); + } } } + Err(e) => { + let mut raw_accounts = snaphot.get_raw_pre_ix_accounts(); + ix.tx_error_handler(e, data, &mut raw_accounts)? + } } } FuzzInstruction::WithdrawUnlocked(ix) => { @@ -99,38 +105,44 @@ impl FuzzTestExecutor for FuzzInstruction { signers.push(client.payer().clone()); let sig: Vec<&Keypair> = signers.iter().collect(); transaction.sign(&sig, client.get_last_blockhash()); - let tx_res = client + let tx_result = client .process_transaction(transaction) .map_err(|e| { e.with_origin(Origin::Instruction(self.to_context_string())) }); - if tx_res.is_ok() { - snaphot.capture_after(client).unwrap(); - let (acc_before, acc_after) = snaphot - .get_snapshot() - .map_err(|e| { - e.with_origin(Origin::Instruction(self.to_context_string())) - }) - .expect("Snapshot deserialization expect"); - if let Err(e) - = ix - .check(acc_before, acc_after, data) + match tx_result { + Ok(_) => { + snaphot.capture_after(client).unwrap(); + let (acc_before, acc_after) = snaphot + .get_snapshot() .map_err(|e| { e.with_origin(Origin::Instruction(self.to_context_string())) }) - { + .expect("Snapshot deserialization expect"); + if let Err(e) + = ix + .check(acc_before, acc_after, data) + .map_err(|e| { + e.with_origin(Origin::Instruction(self.to_context_string())) + }) { - ::std::io::_eprint( - format_args!( - "CRASH DETECTED! Custom check after the {0} instruction did not pass!\n", - self.to_context_string(), - ), - ); - }; - { - ::core::panicking::panic_display(&e); + { + ::std::io::_eprint( + format_args!( + "CRASH DETECTED! Custom check after the {0} instruction did not pass!\n", + self.to_context_string(), + ), + ); + }; + { + ::core::panicking::panic_display(&e); + } } } + Err(e) => { + let mut raw_accounts = snaphot.get_raw_pre_ix_accounts(); + ix.tx_error_handler(e, data, &mut raw_accounts)? + } } } }