Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Allow direct accounts manipulation #142

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
12 changes: 9 additions & 3 deletions crates/client/src/fuzzer/accounts_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>,
}
impl PdaStore {
Expand All @@ -28,21 +28,27 @@ pub struct ProgramStore {

pub struct AccountsStorage<T> {
accounts: HashMap<AccountId, T>,
pub max_accounts: u8,
_max_accounts: u8,
}

impl<T> AccountsStorage<T> {
pub fn new(max_accounts: u8) -> Self {
let accounts: HashMap<AccountId, T> = 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<AccountId, T> {
&mut self.accounts
}
}

impl<T> Default for AccountsStorage<T> {
Expand Down
28 changes: 22 additions & 6 deletions crates/client/src/fuzzer/data_builder.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -156,11 +157,15 @@ pub trait FuzzDeserialize<'info> {
) -> Result<Self::Ix, FuzzingError>;
}

/// 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,
Expand All @@ -172,23 +177,34 @@ pub trait FuzzClient {
delegated_amount: u64,
close_authority: Option<Pubkey>,
) -> Pubkey;

/// Create an SPL mint account
fn set_mint_account(
&mut self,
decimals: u8,
owner: &Pubkey,
freeze_authority: Option<Pubkey>,
) -> Pubkey;

/// Get the Keypair of the client's payer account
fn payer(&self) -> Keypair;

fn get_account(&mut self, key: &Pubkey) -> Result<Option<Account>, 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<Option<Account>, FuzzClientError>;

/// Get accounts based on the supplied meta information
fn get_accounts(
&mut self,
metas: &[AccountMeta],
) -> Result<Vec<Option<Account>>, FuzzClientErrorWithOrigin>;

/// Get last blockhash
fn get_last_blockhash(&self) -> Hash;

/// Get the cluster rent
fn get_rent(&mut self) -> Result<Rent, FuzzClientError>;

/// Send a transaction and return until the transaction has been finalized or rejected.
fn process_transaction(
&mut self,
transaction: impl Into<VersionedTransaction>,
Expand Down
8 changes: 8 additions & 0 deletions crates/client/src/fuzzer/program_test_client_blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rent, FuzzClientError> {
Ok(self.rt.block_on(self.ctx.banks_client.get_rent())?)
}
}
76 changes: 76 additions & 0 deletions documentation/docs/fuzzing/howto/fuzzing-howto-p3.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,79 @@ fn get_accounts(
}
```
Notice especially the helper method `fuzz_accounts.<account_name>.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<T>` 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<T>` struct that exposes the underlying `HashMap<AccountId, T>` 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<Vec<u8>> = seeds.iter().map(|&seed| seed.to_vec())
.collect();
PdaStore {
pubkey: pda,
seeds: vec_of_seeds,
}
}).pubkey();
```
2 changes: 1 addition & 1 deletion documentation/docs/fuzzing/howto/fuzzing-howto-p7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading