diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d2ede4..02cc7ea7 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/allow direct accounts manipulation and storage ([#142](https://github.com/Ackee-Blockchain/trdelnik/pull/142)) - feat/support of non-corresponding instruction and context names ([#130](https://github.com/Ackee-Blockchain/trdelnik/pull/130)) - feat/refactored and improved program flow during init and build, added activity indicator ([#129](https://github.com/Ackee-Blockchain/trdelnik/pull/129)) - feat/allow solana versions up to v1.17.* and pin Rust 1.77 nightly compiler ([#128](https://github.com/Ackee-Blockchain/trdelnik/pull/128)) diff --git a/crates/client/src/fuzzer/accounts_storage.rs b/crates/client/src/fuzzer/accounts_storage.rs index ffb44ef1..01e2ef11 100644 --- a/crates/client/src/fuzzer/accounts_storage.rs +++ b/crates/client/src/fuzzer/accounts_storage.rs @@ -5,7 +5,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use crate::{data_builder::FuzzClient, AccountId}; pub struct PdaStore { - pubkey: Pubkey, + pub pubkey: Pubkey, pub seeds: Vec>, } impl PdaStore { @@ -28,7 +28,7 @@ pub struct ProgramStore { pub struct AccountsStorage { accounts: HashMap, - pub max_accounts: u8, + _max_accounts: u8, } impl AccountsStorage { @@ -36,13 +36,19 @@ impl AccountsStorage { let accounts: HashMap = HashMap::new(); Self { accounts, - max_accounts, + _max_accounts: max_accounts, } } + /// Gets a reference to the account with the given account ID pub fn get(&self, account_id: AccountId) -> Option<&T> { self.accounts.get(&account_id) } + + /// Returns a mutable reference to the underlying HashMap that stores accounts with IDs as keys + pub fn storage(&mut self) -> &mut HashMap { + &mut self.accounts + } } impl Default for AccountsStorage { diff --git a/crates/client/src/fuzzer/data_builder.rs b/crates/client/src/fuzzer/data_builder.rs index 77f47270..fec6f499 100644 --- a/crates/client/src/fuzzer/data_builder.rs +++ b/crates/client/src/fuzzer/data_builder.rs @@ -1,8 +1,9 @@ use anchor_client::anchor_lang::solana_program::account_info::{Account as Acc, AccountInfo}; use anchor_client::anchor_lang::solana_program::hash::Hash; +use anchor_lang::prelude::Rent; use arbitrary::Arbitrary; use arbitrary::Unstructured; -use solana_sdk::account::Account; +use solana_sdk::account::{Account, AccountSharedData}; use solana_sdk::instruction::AccountMeta; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; @@ -156,11 +157,15 @@ pub trait FuzzDeserialize<'info> { ) -> Result; } +/// A trait providing methods to read and write (manipulate) accounts pub trait FuzzClient { - // TODO add method to add another program - // TODO add methods to modify current accounts - // TODO check if self must be mutable + /// Create an empty account and add lamports to it fn set_account(&mut self, lamports: u64) -> Keypair; + + /// Create or overwrite a custom account, subverting normal runtime checks. + fn set_account_custom(&mut self, address: &Pubkey, account: &AccountSharedData); + + /// Create an SPL token account #[allow(clippy::too_many_arguments)] fn set_token_account( &mut self, @@ -172,23 +177,34 @@ pub trait FuzzClient { delegated_amount: u64, close_authority: Option, ) -> Pubkey; + + /// Create an SPL mint account fn set_mint_account( &mut self, decimals: u8, owner: &Pubkey, freeze_authority: Option, ) -> Pubkey; + + /// Get the Keypair of the client's payer account fn payer(&self) -> Keypair; - fn get_account(&mut self, key: &Pubkey) -> Result, FuzzClientError>; // TODO support dynamic errors - // TODO add interface to modify existing accounts + /// Get the account at the given address + fn get_account(&mut self, key: &Pubkey) -> Result, FuzzClientError>; + + /// Get accounts based on the supplied meta information fn get_accounts( &mut self, metas: &[AccountMeta], ) -> Result>, FuzzClientErrorWithOrigin>; + /// Get last blockhash fn get_last_blockhash(&self) -> Hash; + /// Get the cluster rent + fn get_rent(&mut self) -> Result; + + /// Send a transaction and return until the transaction has been finalized or rejected. fn process_transaction( &mut self, transaction: impl Into, diff --git a/crates/client/src/fuzzer/program_test_client_blocking.rs b/crates/client/src/fuzzer/program_test_client_blocking.rs index 5353013a..b1d79bb0 100644 --- a/crates/client/src/fuzzer/program_test_client_blocking.rs +++ b/crates/client/src/fuzzer/program_test_client_blocking.rs @@ -163,4 +163,12 @@ impl FuzzClient for ProgramTestClientBlocking { .rt .block_on(self.ctx.banks_client.process_transaction(transaction))?) } + + fn set_account_custom(&mut self, address: &Pubkey, account: &AccountSharedData) { + self.ctx.set_account(address, account); + } + + fn get_rent(&mut self) -> Result { + Ok(self.rt.block_on(self.ctx.banks_client.get_rent())?) + } } diff --git a/documentation/docs/fuzzing/howto/fuzzing-howto-p3.md b/documentation/docs/fuzzing/howto/fuzzing-howto-p3.md index 9140d08b..94076ab8 100644 --- a/documentation/docs/fuzzing/howto/fuzzing-howto-p3.md +++ b/documentation/docs/fuzzing/howto/fuzzing-howto-p3.md @@ -36,3 +36,79 @@ fn get_accounts( } ``` Notice especially the helper method `fuzz_accounts..get_or_create_account` that is used to create a Keypair or retrieve the Public key of the already existing account. + +## Create an arbitrary account +The `AccountsStorage` type provides an implementation of the `get_or_create_account` method that helps you create new or read already existing accounts. There are different implementations for different types of storage (`Keypair`, `TokenStore`, `MintStore`, `PdaStore`) to simplify the creation of new accounts. + +However, there are cases when the provided implementation is not sufficient and it is necessary to create an account manually. These cases can be (but are not limited to) for example: + +- you need to create a new account with a predefined address +- you need to create a new account that is not owned by the system program +- you need to create and initialize a new PDA account +- your program expects an account to be initialized in a previous instruction + +In that case, you can use the `storage` method of the `AccountsStorage` struct that exposes the underlying `HashMap` and you can add new accounts directly to it. + +It is possible to create and store any kind of account. For example: + +- to add an account that uses the `#[account(zero)]` anchor constraint (must be rent exempt, owned by your program, with empty data): + +```rust +let state = fuzz_accounts + .state + // gets the storage of all `state` account variants + .storage() + // returns the Keypair of the `state` account with + // the given `AccountId` if it has been added previously + .entry(self.accounts.state) + .or_insert_with(|| { + let space = State::SIZE; + let rent_exempt_lamports = client.get_rent().unwrap() + .minimum_balance(space); + let keypair = Keypair::new(); + let account = AccountSharedData::new_data_with_space::<[u8; 0]>( + rent_exempt_lamports, + &[], + space, + &my_program::id(), + ).unwrap(); + // insert the custom account also into the client + client.set_account_custom(&keypair.pubkey(), &account); + keypair + }); +``` + +- to add a new system-owned account with a specific PDA (address): + +```rust +let rent_exempt_for_token_acc = client + .get_rent() + .unwrap() + .minimum_balance(anchor_spl::token::spl_token::state::Account::LEN); + +let my_pda = fuzz_accounts + .my_pda + // gets the storage of all `my_pda` account variants + .storage() + // returns the PdaStore struct of the `my_pda` account with + // the given `AccountId` if it has been added previously + .entry(self.accounts.my_pda) + .or_insert_with(|| { + let seeds = &[b"some-seeds"]; + let pda = Pubkey::find_program_address(seeds, &my_program::id()).0; + let account = AccountSharedData::new_data_with_space::<[u8; 0]>( + rent_exempt_for_token_acc, + &[], + 0, + &SYSTEM_PROGRAM_ID, + ).unwrap(); + // insert the custom account also into the client + client.set_account_custom(&pda, &account); + let vec_of_seeds: Vec> = seeds.iter().map(|&seed| seed.to_vec()) + .collect(); + PdaStore { + pubkey: pda, + seeds: vec_of_seeds, + } + }).pubkey(); +``` diff --git a/documentation/docs/fuzzing/howto/fuzzing-howto-p7.md b/documentation/docs/fuzzing/howto/fuzzing-howto-p7.md index f7b0f854..0abc59e9 100644 --- a/documentation/docs/fuzzing/howto/fuzzing-howto-p7.md +++ b/documentation/docs/fuzzing/howto/fuzzing-howto-p7.md @@ -33,7 +33,7 @@ pub fn _init_vesting( ) -> Result<()> { ... // the Instruction Data arguments are not completely random - // and should the have following restrictions + // and should have the following restrictions require!(amount > 0, VestingError::InvalidAmount); require!(end_at > start_at, VestingError::InvalidTimeRange); require!(end_at - start_at > interval, VestingError::InvalidInterval);