From 65a7699b955051e374efd36a0db23c89cc68b794 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Wed, 31 Jan 2024 14:14:23 +0100 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20Parsing=20of=20additional=20a?= =?UTF-8?q?nchor=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/fuzzer/snapshot_generator.rs | 308 ++++++++++++++++-- 1 file changed, 279 insertions(+), 29 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 4423de5a..916544c5 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -1,12 +1,17 @@ +// To generate the snapshot data types, we need to first find all context struct within the program and parse theirs accounts. +// The parsing was mostly taken over from Anchor implementation: +// https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/parser/accounts/mod.rs + use std::{error::Error, fs::File, io::Read}; use cargo_metadata::camino::Utf8PathBuf; -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; use syn::{ - parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments, TypePath, + parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, Path, PathArguments, + TypePath, }; pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result { @@ -170,25 +175,41 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result ty_path.path.clone(), - _ => { - return Err(ParseError::new( - field_type.span(), - "invalid account type given", - )) - } - }; - let id = path.segments[0].clone(); + // let field_type = &field.ty; + + // let path = match &field_type { + // syn::Type::Path(ty_path) => ty_path.path.clone(), + // _ => { + // return Err(ParseError::new( + // field_type.span(), + // "invalid account type given", + // )) + // } + // }; + // let id: syn::PathSegment = path.segments[0].clone(); // println!("field name: {}, type: {}", field_name, id.ident); - let ty = match id.ident.to_string().as_str() { + let (ident, _optional, path) = ident_string(field)?; + let ty = match ident.as_str() { "AccountInfo" => AnchorType::AccountInfo, "Signer" => AnchorType::Signer, "Account" => AnchorType::Account(parse_account_ty(&path)?), "Program" => AnchorType::Program(parse_program_ty(&path)?), // TODO - _ => return Err(ParseError::new(id.span(), "invalid account type given")), + "Sysvar" => AnchorType::Sysvar(parse_sysvar(&path)?), + "UncheckedAccount" => AnchorType::UncheckedAccount, + "AccountLoader" => { + AnchorType::AccountLoader(parse_program_account_loader(&path)?) + } + "Interface" => AnchorType::Interface(parse_interface_ty(&path)?), + "InterfaceAccount" => { + AnchorType::InterfaceAccount(parse_interface_account_ty(&path)?) + } + "SystemAccount" => AnchorType::SystemAccount, + _ => { + return Err(ParseError::new( + field.ty.span(), + "invalid account type given", + )) + } }; let deser_tokens = match ty.to_tokens() { Some((return_type, deser_method)) => { @@ -239,15 +260,84 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +pub struct ProgramTy { + // The struct type of the account. + pub account_type_path: TypePath, +} + +pub struct AccountTy { + // The struct type of the account. + pub account_type_path: TypePath, + // True if the account has been boxed via `Box`. + pub boxed: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AccountLoaderTy { + // The struct type of the account. + pub account_type_path: TypePath, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InterfaceTy { + // The struct type of the account. + pub account_type_path: TypePath, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InterfaceAccountTy { + // The struct type of the account. + pub account_type_path: TypePath, + // True if the account has been boxed via `Box`. + pub boxed: bool, } impl AnchorType { pub fn to_tokens(&self) -> Option<(TokenStream, TokenStream)> { let (return_type, deser_method) = match self { - AnchorType::AccountInfo => return None, + AnchorType::AccountInfo | AnchorType::UncheckedAccount => return None, + AnchorType::SystemAccount => ( + quote! { SystemAccount<'_>}, + quote!(anchor_lang::accounts::system_account::SystemAccount::try_from(&acc)), + ), + AnchorType::Sysvar(sysvar) => { + let id = syn::Ident::new(sysvar.to_string().as_str(), Span::call_site()); + ( + quote! { SysVar<#id>}, + quote!(anchor_lang::accounts::sysvar::Sysvar::try_from(&acc)), + ) + } AnchorType::Signer => ( quote! { Signer<'_>}, quote!(anchor_lang::accounts::signer::Signer::try_from(&acc)), @@ -259,6 +349,13 @@ impl AnchorType { quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, ) } + AnchorType::AccountLoader(acc) => { + let path = &acc.account_type_path; + ( + quote! { anchor_lang::accounts::account::Account<#path>}, + quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, + ) + } AnchorType::Program(prog) => { let path = &prog.account_type_path; ( @@ -266,23 +363,25 @@ impl AnchorType { quote!(anchor_lang::accounts::program::Program::try_from(&acc)), ) } + AnchorType::Interface(interf) => { + let path = &interf.account_type_path; + ( + quote! { anchor_lang::accounts::interface::Interface<#path>}, + quote! {anchor_lang::accounts::interface::Interface::try_from(&acc)}, + ) + } + AnchorType::InterfaceAccount(interf_acc) => { + let path = &interf_acc.account_type_path; + ( + quote! { anchor_lang::accounts::interface_account::InterfaceAccount<#path>}, + quote! {anchor_lang::accounts::interface_account::InterfaceAccount::try_from(&acc)}, + ) + } }; Some((return_type, deser_method)) } } -pub struct ProgramTy { - // The struct type of the account. - pub account_type_path: TypePath, -} - -pub struct AccountTy { - // The struct type of the account. - pub account_type_path: TypePath, - // True if the account has been boxed via `Box`. - pub boxed: bool, -} - fn deserialize_tokens( name: &syn::Ident, return_type: TokenStream, @@ -306,6 +405,7 @@ fn acc_info_tokens(name: &syn::Ident) -> TokenStream { } } +// Copied from Anchor fn parse_account_ty(path: &syn::Path) -> ParseResult { let account_type_path = parse_account(path)?; let boxed = tts_to_string(path) @@ -317,17 +417,46 @@ fn parse_account_ty(path: &syn::Path) -> ParseResult { }) } +// Copied from Anchor fn parse_program_ty(path: &syn::Path) -> ParseResult { let account_type_path = parse_account(path)?; Ok(ProgramTy { account_type_path }) } +// Copied from Anchor +fn parse_program_account_loader(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(path)?; + Ok(AccountLoaderTy { + account_type_path: account_ident, + }) +} + +// Copied from Anchor +fn parse_interface_ty(path: &syn::Path) -> ParseResult { + let account_type_path = parse_account(path)?; + Ok(InterfaceTy { account_type_path }) +} + +// Copied from Anchor +fn parse_interface_account_ty(path: &syn::Path) -> ParseResult { + let account_type_path = parse_account(path)?; + let boxed = tts_to_string(path) + .replace(' ', "") + .starts_with("Box(item: T) -> String { let mut tts = proc_macro2::TokenStream::new(); item.to_tokens(&mut tts); tts.to_string() } +// Copied from Anchor fn parse_account(mut path: &syn::Path) -> ParseResult { let path_str = tts_to_string(path).replace(' ', ""); if path_str.starts_with("Box ParseResult { } } +// Copied from Anchor +fn parse_sysvar(path: &syn::Path) -> ParseResult { + let segments = &path.segments[0]; + let account_ident = match &segments.arguments { + syn::PathArguments::AngleBracketed(args) => { + // Expected: <'info, MyType>. + if args.args.len() != 2 { + return Err(ParseError::new( + args.args.span(), + "bracket arguments must be the lifetime and type", + )); + } + match &args.args[1] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { + // TODO: allow segmented paths. + if ty_path.path.segments.len() != 1 { + return Err(ParseError::new( + ty_path.path.span(), + "segmented paths are not currently allowed", + )); + } + let path_segment = &ty_path.path.segments[0]; + path_segment.ident.clone() + } + _ => { + return Err(ParseError::new( + args.args[1].span(), + "first bracket argument must be a lifetime", + )) + } + } + } + _ => { + return Err(ParseError::new( + segments.arguments.span(), + "expected angle brackets with a lifetime and type", + )) + } + }; + let ty = match account_ident.to_string().as_str() { + "Clock" => SysvarTy::Clock, + "Rent" => SysvarTy::Rent, + "EpochSchedule" => SysvarTy::EpochSchedule, + "Fees" => SysvarTy::Fees, + "RecentBlockhashes" => SysvarTy::RecentBlockhashes, + "SlotHashes" => SysvarTy::SlotHashes, + "SlotHistory" => SysvarTy::SlotHistory, + "StakeHistory" => SysvarTy::StakeHistory, + "Instructions" => SysvarTy::Instructions, + "Rewards" => SysvarTy::Rewards, + _ => { + return Err(ParseError::new( + account_ident.span(), + "invalid sysvar provided", + )) + } + }; + Ok(ty) +} + fn has_program_attribute(attrs: &Vec) -> bool { for attr in attrs { if attr.path.is_ident("program") { @@ -395,3 +584,64 @@ fn has_program_attribute(attrs: &Vec) -> bool { } false } + +// Copied from Anchor +fn option_to_inner_path(path: &Path) -> ParseResult { + let segment_0 = path.segments[0].clone(); + match segment_0.arguments { + syn::PathArguments::AngleBracketed(args) => { + if args.args.len() != 1 { + return Err(ParseError::new( + args.args.span(), + "can only have one argument in option", + )); + } + match &args.args[0] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.path.clone()), + _ => Err(ParseError::new( + args.args[1].span(), + "first bracket argument must be a lifetime", + )), + } + } + _ => Err(ParseError::new( + segment_0.arguments.span(), + "expected angle brackets with a lifetime and type", + )), + } +} + +// Copied from Anchor +fn ident_string(f: &syn::Field) -> ParseResult<(String, bool, Path)> { + let mut path = match &f.ty { + syn::Type::Path(ty_path) => ty_path.path.clone(), + _ => return Err(ParseError::new(f.ty.span(), "invalid account type given")), + }; + let mut optional = false; + if tts_to_string(&path).replace(' ', "").starts_with("Option<") { + path = option_to_inner_path(&path)?; + optional = true; + } + if tts_to_string(&path) + .replace(' ', "") + .starts_with("Box Date: Thu, 1 Feb 2024 18:39:22 +0100 Subject: [PATCH 02/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Accounts=20parsin?= =?UTF-8?q?g=20using=20Anchor=20syn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/Cargo.toml | 1 + crates/client/src/fuzzer/data_builder.rs | 1 + .../client/src/fuzzer/snapshot_generator.rs | 266 +++++++++++++++--- crates/client/src/lib.rs | 2 +- 4 files changed, 230 insertions(+), 40 deletions(-) diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 59d9fb01..cfc45350 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -24,6 +24,7 @@ macrotest = "1.0.9" [dependencies] trdelnik-test = { workspace = true } +anchor-lang = { version = "0.29.0", features = ["idl-build"] } solana-sdk = { workspace = true } solana-cli-output = { workspace = true } solana-transaction-status = { workspace = true } diff --git a/crates/client/src/fuzzer/data_builder.rs b/crates/client/src/fuzzer/data_builder.rs index 30b53501..df892c9c 100644 --- a/crates/client/src/fuzzer/data_builder.rs +++ b/crates/client/src/fuzzer/data_builder.rs @@ -208,6 +208,7 @@ pub enum FuzzingError { CannotGetInstructionData, CannotDeserializeAccount, NotEnoughAccounts, // TODO add also custom error + AccountNotFound, } #[macro_export] diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 916544c5..fe3414f1 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -4,6 +4,7 @@ use std::{error::Error, fs::File, io::Read}; +use anchor_lang::anchor_syn::{AccountField, Ty}; use cargo_metadata::camino::Utf8PathBuf; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; @@ -14,6 +15,8 @@ use syn::{ TypePath, }; +use anchor_lang::anchor_syn::parser::accounts::parse_account_field; + pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result { let code = code_path.iter().map(|(code, path)| { let mut mod_program = None::; @@ -24,7 +27,7 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< let parse_result = syn::parse_file(&content).map_err(|e| e.to_string())?; - // locate the program module to extract instructions and corresponding Context structs + // locate the program module to extract instructions and corresponding Context structs. for item in parse_result.items.iter() { if let Item::Mod(module) = item { // Check if the module has the #[program] attribute @@ -81,14 +84,28 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< // TODO add support for types with fully qualified path such as ix::Initialize } let ty = ty.ok_or(format!("malformed parameters of {} instruction", pair.0))?; - // println!("{} - {}", pair.0, ty); - // recursively find the context struct and create a new version with wrapped field into Option + // recursively find the context struct and create a new version with wrapped fields into Option if let Some(ctx) = get_ctx_struct(&parse_result.items, &ty) { - let wrapped_struct = wrap_fields_in_option(ctx).unwrap(); - // println!("{}", wrapped_struct); - let deser_code = deserialize_ctx_struct(ctx).unwrap(); - // println!("{}", deser_code); + let fields_parsed = if let Fields::Named(f) = ctx.fields.clone() { + let field_deser: ParseResult> = f + .named + .iter() + .map(|field| parse_account_field(field)) + .collect(); + field_deser + } else { + Err(ParseError::new( + ctx.fields.span(), + "Context struct parse errror.", + )) + } + .map_err(|e| e.to_string())?; + + let wrapped_struct = wrap_fields_in_option(ctx, &fields_parsed).unwrap(); + let deser_code = deserialize_ctx_struct_anchor2(ctx, &fields_parsed) + .map_err(|e| e.to_string())?; + // let deser_code = deserialize_ctx_struct(ctx).unwrap(); structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); desers = format!("{}{}", desers, deser_code.into_token_stream()); } else { @@ -112,7 +129,6 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option for item in items { if let Item::Struct(struct_item) = item { if struct_item.ident == *name { - // println!("we found the struct {}", name); return Some(struct_item); } } @@ -133,17 +149,57 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option None } -fn wrap_fields_in_option(orig_struct: &ItemStruct) -> Result> { +fn is_optional(parsed_field: &AccountField) -> bool { + let is_optional = match parsed_field { + AccountField::Field(field) => field.is_optional, + AccountField::CompositeField(_) => false, + }; + let constraints = match parsed_field { + AccountField::Field(f) => &f.constraints, + AccountField::CompositeField(f) => &f.constraints, + }; + + (constraints.init.is_some() || constraints.is_close()) && !is_optional +} + +fn deserialize_as_option(parsed_field: &AccountField) -> bool { + let is_optional = match parsed_field { + AccountField::Field(field) => field.is_optional, + AccountField::CompositeField(_) => false, + }; + let constraints = match parsed_field { + AccountField::Field(f) => &f.constraints, + AccountField::CompositeField(f) => &f.constraints, + }; + + constraints.init.is_some() || constraints.is_close() || is_optional +} + +fn wrap_fields_in_option( + orig_struct: &ItemStruct, + parsed_fields: &[AccountField], +) -> Result> { let struct_name = format_ident!("{}Snapshot", orig_struct.ident); let wrapped_fields = match orig_struct.fields.clone() { Fields::Named(named) => { - let field_wrappers = named.named.iter().map(|field| { - let field_name = &field.ident; - let field_type = &field.ty; - quote! { - pub #field_name: Option<#field_type>, - } - }); + let field_wrappers = + named + .named + .iter() + .zip(parsed_fields) + .map(|(field, parsed_field)| { + let field_name = &field.ident; + let field_type = &field.ty; + if is_optional(parsed_field) { + quote! { + pub #field_name: Option<#field_type>, + } + } else { + quote! { + pub #field_name: #field_type, + } + } + }); quote! { { #(#field_wrappers)* } @@ -166,6 +222,7 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result { let field_deser = named.named.iter().map(|field| { + let res = parse_account_field(field); let field_name = match &field.ident { Some(name) => name, None => { @@ -175,19 +232,7 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result ty_path.path.clone(), - // _ => { - // return Err(ParseError::new( - // field_type.span(), - // "invalid account type given", - // )) - // } - // }; - // let id: syn::PathSegment = path.segments[0].clone(); - // println!("field name: {}, type: {}", field_name, id.ident); + let (ident, _optional, path) = ident_string(field)?; let ty = match ident.as_str() { "AccountInfo" => AnchorType::AccountInfo, @@ -213,7 +258,7 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result { - deserialize_tokens(field_name, return_type, deser_method) + deserialize_account_tokens(field_name, true, return_type, deser_method) } None => acc_info_tokens(field_name), }; @@ -257,6 +302,77 @@ fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result Result> { + let impl_name = format_ident!("{}Snapshot", snapshot_struct.ident); + let names_deser_pairs: Result, _> = parsed_fields + .iter() + .map(|parsed_f| match parsed_f { + AccountField::Field(f) => { + let field_name = f.ident.clone(); + let deser_tokens = match ty_to_tokens(&f.ty) { + Some((return_type, deser_method)) => deserialize_account_tokens( + &field_name, + deserialize_as_option(parsed_f), + return_type, + deser_method, + ), + None => acc_info_tokens(&field_name), + }; + Ok(( + quote! {#field_name}, + quote! { + #deser_tokens + }, + )) + } + AccountField::CompositeField(_) => Err("CompositeFields not supported!"), + }) + .collect(); + + let (names, fields_deser): (Vec<_>, Vec<_>) = names_deser_pairs?.iter().cloned().unzip(); + + let generated_deser_impl: syn::Item = parse_quote! { + impl<'info> #impl_name<'info> { + pub fn deserialize_option( + metas: &'info [AccountMeta], + accounts: &'info mut [Option], + ) -> core::result::Result { + let accounts = get_account_infos_option(accounts, metas) + .map_err(|_| FuzzingError::CannotGetAccounts)?; + + let mut accounts_iter = accounts.into_iter(); + + #(#fields_deser)* + + Ok(Self { + #(#names),* + }) + } + } + }; + + Ok(generated_deser_impl.to_token_stream()) +} + +fn sysvar_to_ident(sysvar: &anchor_lang::anchor_syn::SysvarTy) -> String { + let str = match sysvar { + anchor_lang::anchor_syn::SysvarTy::Clock => "Clock", + anchor_lang::anchor_syn::SysvarTy::Rent => "Rent", + anchor_lang::anchor_syn::SysvarTy::EpochSchedule => "EpochSchedule", + anchor_lang::anchor_syn::SysvarTy::Fees => "Fees", + anchor_lang::anchor_syn::SysvarTy::RecentBlockhashes => "RecentBlockhashes", + anchor_lang::anchor_syn::SysvarTy::SlotHashes => "SlotHashes", + anchor_lang::anchor_syn::SysvarTy::SlotHistory => "SlotHistory", + anchor_lang::anchor_syn::SysvarTy::StakeHistory => "StakeHistory", + anchor_lang::anchor_syn::SysvarTy::Instructions => "Instructions", + anchor_lang::anchor_syn::SysvarTy::Rewards => "Rewards", + }; + str.into() +} + // TODO add all account types as in https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/parser/accounts/mod.rs#L351 pub enum AnchorType { AccountInfo, @@ -334,7 +450,7 @@ impl AnchorType { AnchorType::Sysvar(sysvar) => { let id = syn::Ident::new(sysvar.to_string().as_str(), Span::call_site()); ( - quote! { SysVar<#id>}, + quote! { Sysvar<#id>}, quote!(anchor_lang::accounts::sysvar::Sysvar::try_from(&acc)), ) } @@ -382,18 +498,90 @@ impl AnchorType { } } -fn deserialize_tokens( +pub fn ty_to_tokens(ty: &anchor_lang::anchor_syn::Ty) -> Option<(TokenStream, TokenStream)> { + let (return_type, deser_method) = match ty { + Ty::AccountInfo | Ty::UncheckedAccount => return None, + Ty::SystemAccount => ( + quote! { SystemAccount<'_>}, + quote!(anchor_lang::accounts::system_account::SystemAccount::try_from(&acc)), + ), + Ty::Sysvar(sysvar) => { + let id = syn::Ident::new(&sysvar_to_ident(sysvar), Span::call_site()); + ( + quote! { Sysvar<#id>}, + quote!(anchor_lang::accounts::sysvar::Sysvar::from_account_info( + &acc + )), + ) + } + Ty::Signer => ( + quote! { Signer<'_>}, + quote!(anchor_lang::accounts::signer::Signer::try_from(&acc)), + ), + Ty::Account(acc) => { + let path = &acc.account_type_path; + ( + quote! { anchor_lang::accounts::account::Account<#path>}, + quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, + ) + } + Ty::AccountLoader(acc) => { + let path = &acc.account_type_path; + ( + quote! { anchor_lang::accounts::account_loader::AccountLoader<#path>}, + quote! {anchor_lang::accounts::account_loader::AccountLoader::try_from(&acc)}, + ) + } + Ty::Program(prog) => { + let path = &prog.account_type_path; + ( + quote! { anchor_lang::accounts::program::Program<#path>}, + quote!(anchor_lang::accounts::program::Program::try_from(&acc)), + ) + } + Ty::Interface(interf) => { + let path = &interf.account_type_path; + ( + quote! { anchor_lang::accounts::interface::Interface<#path>}, + quote! {anchor_lang::accounts::interface::Interface::try_from(&acc)}, + ) + } + Ty::InterfaceAccount(interf_acc) => { + let path = &interf_acc.account_type_path; + ( + quote! { anchor_lang::accounts::interface_account::InterfaceAccount<#path>}, + quote! {anchor_lang::accounts::interface_account::InterfaceAccount::try_from(&acc)}, + ) + } + Ty::ProgramData => return None, + }; + Some((return_type, deser_method)) +} + +fn deserialize_account_tokens( name: &syn::Ident, + is_optional: bool, return_type: TokenStream, deser_method: TokenStream, ) -> TokenStream { - quote! { - let #name:Option<#return_type> = accounts_iter - .next() - .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| #deser_method) - .transpose() - .unwrap_or(None); + if is_optional { + quote! { + let #name:Option<#return_type> = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(|acc| #deser_method) + .transpose() + .unwrap_or(None); + } + } else { + quote! { + let #name: #return_type = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(|acc| #deser_method) + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + } } } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 08fdab61..da7301b9 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -19,6 +19,7 @@ pub use anyhow::{self, Error}; #[cfg(feature = "fuzzing")] pub mod fuzzing { + pub use self::anchor_lang::solana_program::instruction::AccountMeta; pub use super::{ anchor_lang, anchor_lang::system_program::ID as SYSTEM_PROGRAM_ID, anchor_lang::InstructionData, anchor_lang::ToAccountInfo, anchor_lang::ToAccountMetas, @@ -26,7 +27,6 @@ pub mod fuzzing { Keypair, Pubkey, Signer, TempClone, }; pub use anchor_client::anchor_lang::solana_program::hash::Hash; - pub use anchor_lang::solana_program::instruction::AccountMeta; pub use arbitrary; pub use arbitrary::Arbitrary; pub use honggfuzz::fuzz; From ef1ac8053465f18622e494d7c3fb8ba516ec926a Mon Sep 17 00:00:00 2001 From: Ikrk Date: Thu, 1 Feb 2024 19:03:04 +0100 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=94=A5=20Removed=20previous=20imple?= =?UTF-8?q?mentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/fuzzer/snapshot_generator.rs | 460 +----------------- 1 file changed, 6 insertions(+), 454 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index fe3414f1..84b000f4 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -1,5 +1,5 @@ // To generate the snapshot data types, we need to first find all context struct within the program and parse theirs accounts. -// The parsing was mostly taken over from Anchor implementation: +// The parsing of individual Anchor accounts is done using Anchor syn parser: // https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/parser/accounts/mod.rs use std::{error::Error, fs::File, io::Read}; @@ -10,10 +10,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; -use syn::{ - parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, Path, PathArguments, - TypePath, -}; +use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments}; use anchor_lang::anchor_syn::parser::accounts::parse_account_field; @@ -88,11 +85,8 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< // recursively find the context struct and create a new version with wrapped fields into Option if let Some(ctx) = get_ctx_struct(&parse_result.items, &ty) { let fields_parsed = if let Fields::Named(f) = ctx.fields.clone() { - let field_deser: ParseResult> = f - .named - .iter() - .map(|field| parse_account_field(field)) - .collect(); + let field_deser: ParseResult> = + f.named.iter().map(parse_account_field).collect(); field_deser } else { Err(ParseError::new( @@ -103,7 +97,7 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< .map_err(|e| e.to_string())?; let wrapped_struct = wrap_fields_in_option(ctx, &fields_parsed).unwrap(); - let deser_code = deserialize_ctx_struct_anchor2(ctx, &fields_parsed) + let deser_code = deserialize_ctx_struct_anchor(ctx, &fields_parsed) .map_err(|e| e.to_string())?; // let deser_code = deserialize_ctx_struct(ctx).unwrap(); structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); @@ -217,92 +211,7 @@ fn wrap_fields_in_option( Ok(generated_struct.to_token_stream()) } -fn deserialize_ctx_struct(orig_struct: &ItemStruct) -> Result> { - let impl_name = format_ident!("{}Snapshot", orig_struct.ident); - let names_deser_pairs = match orig_struct.fields.clone() { - Fields::Named(named) => { - let field_deser = named.named.iter().map(|field| { - let res = parse_account_field(field); - let field_name = match &field.ident { - Some(name) => name, - None => { - return Err(ParseError::new( - field.ident.span(), - "invalid account name given", - )) - } - }; - - let (ident, _optional, path) = ident_string(field)?; - let ty = match ident.as_str() { - "AccountInfo" => AnchorType::AccountInfo, - "Signer" => AnchorType::Signer, - "Account" => AnchorType::Account(parse_account_ty(&path)?), - "Program" => AnchorType::Program(parse_program_ty(&path)?), // TODO - "Sysvar" => AnchorType::Sysvar(parse_sysvar(&path)?), - "UncheckedAccount" => AnchorType::UncheckedAccount, - "AccountLoader" => { - AnchorType::AccountLoader(parse_program_account_loader(&path)?) - } - "Interface" => AnchorType::Interface(parse_interface_ty(&path)?), - "InterfaceAccount" => { - AnchorType::InterfaceAccount(parse_interface_account_ty(&path)?) - } - "SystemAccount" => AnchorType::SystemAccount, - _ => { - return Err(ParseError::new( - field.ty.span(), - "invalid account type given", - )) - } - }; - let deser_tokens = match ty.to_tokens() { - Some((return_type, deser_method)) => { - deserialize_account_tokens(field_name, true, return_type, deser_method) - } - None => acc_info_tokens(field_name), - }; - Ok(( - quote! {#field_name}, - quote! { - #deser_tokens - }, - )) - }); - let result: Result, _> = - field_deser.into_iter().collect(); - result - } - - _ => return Err("Only structs with named fields are supported".into()), - }?; - - let (names, fields_deser): (Vec<_>, Vec<_>) = names_deser_pairs.iter().cloned().unzip(); - - let generated_deser_impl: syn::Item = parse_quote! { - impl<'info> #impl_name<'info> { - pub fn deserialize_option( - metas: &'info [AccountMeta], - accounts: &'info mut [Option], - ) -> core::result::Result { - let accounts = get_account_infos_option(accounts, metas) - .map_err(|_| FuzzingError::CannotGetAccounts)?; - - let mut accounts_iter = accounts.into_iter(); - - #(#fields_deser)* - - Ok(Self { - #(#names),* - }) - } - } - }; - - Ok(generated_deser_impl.to_token_stream()) -} - -fn deserialize_ctx_struct_anchor2( +fn deserialize_ctx_struct_anchor( snapshot_struct: &ItemStruct, parsed_fields: &[AccountField], ) -> Result> { @@ -373,131 +282,6 @@ fn sysvar_to_ident(sysvar: &anchor_lang::anchor_syn::SysvarTy) -> String { str.into() } -// TODO add all account types as in https://github.com/coral-xyz/anchor/blob/master/lang/syn/src/parser/accounts/mod.rs#L351 -pub enum AnchorType { - AccountInfo, - UncheckedAccount, - Signer, - Account(AccountTy), - AccountLoader(AccountLoaderTy), - Program(ProgramTy), - SystemAccount, - Sysvar(SysvarTy), - Interface(InterfaceTy), - InterfaceAccount(InterfaceAccountTy), -} - -#[derive(Debug, PartialEq, Eq)] -pub enum SysvarTy { - Clock, - Rent, - EpochSchedule, - Fees, - RecentBlockhashes, - SlotHashes, - SlotHistory, - StakeHistory, - Instructions, - Rewards, -} - -impl std::fmt::Display for SysvarTy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -pub struct ProgramTy { - // The struct type of the account. - pub account_type_path: TypePath, -} - -pub struct AccountTy { - // The struct type of the account. - pub account_type_path: TypePath, - // True if the account has been boxed via `Box`. - pub boxed: bool, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct AccountLoaderTy { - // The struct type of the account. - pub account_type_path: TypePath, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct InterfaceTy { - // The struct type of the account. - pub account_type_path: TypePath, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct InterfaceAccountTy { - // The struct type of the account. - pub account_type_path: TypePath, - // True if the account has been boxed via `Box`. - pub boxed: bool, -} - -impl AnchorType { - pub fn to_tokens(&self) -> Option<(TokenStream, TokenStream)> { - let (return_type, deser_method) = match self { - AnchorType::AccountInfo | AnchorType::UncheckedAccount => return None, - AnchorType::SystemAccount => ( - quote! { SystemAccount<'_>}, - quote!(anchor_lang::accounts::system_account::SystemAccount::try_from(&acc)), - ), - AnchorType::Sysvar(sysvar) => { - let id = syn::Ident::new(sysvar.to_string().as_str(), Span::call_site()); - ( - quote! { Sysvar<#id>}, - quote!(anchor_lang::accounts::sysvar::Sysvar::try_from(&acc)), - ) - } - AnchorType::Signer => ( - quote! { Signer<'_>}, - quote!(anchor_lang::accounts::signer::Signer::try_from(&acc)), - ), - AnchorType::Account(acc) => { - let path = &acc.account_type_path; - ( - quote! { anchor_lang::accounts::account::Account<#path>}, - quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, - ) - } - AnchorType::AccountLoader(acc) => { - let path = &acc.account_type_path; - ( - quote! { anchor_lang::accounts::account::Account<#path>}, - quote! {anchor_lang::accounts::account::Account::try_from(&acc)}, - ) - } - AnchorType::Program(prog) => { - let path = &prog.account_type_path; - ( - quote! { anchor_lang::accounts::program::Program<#path>}, - quote!(anchor_lang::accounts::program::Program::try_from(&acc)), - ) - } - AnchorType::Interface(interf) => { - let path = &interf.account_type_path; - ( - quote! { anchor_lang::accounts::interface::Interface<#path>}, - quote! {anchor_lang::accounts::interface::Interface::try_from(&acc)}, - ) - } - AnchorType::InterfaceAccount(interf_acc) => { - let path = &interf_acc.account_type_path; - ( - quote! { anchor_lang::accounts::interface_account::InterfaceAccount<#path>}, - quote! {anchor_lang::accounts::interface_account::InterfaceAccount::try_from(&acc)}, - ) - } - }; - Some((return_type, deser_method)) - } -} - pub fn ty_to_tokens(ty: &anchor_lang::anchor_syn::Ty) -> Option<(TokenStream, TokenStream)> { let (return_type, deser_method) = match ty { Ty::AccountInfo | Ty::UncheckedAccount => return None, @@ -593,177 +377,6 @@ fn acc_info_tokens(name: &syn::Ident) -> TokenStream { } } -// Copied from Anchor -fn parse_account_ty(path: &syn::Path) -> ParseResult { - let account_type_path = parse_account(path)?; - let boxed = tts_to_string(path) - .replace(' ', "") - .starts_with("Box ParseResult { - let account_type_path = parse_account(path)?; - Ok(ProgramTy { account_type_path }) -} - -// Copied from Anchor -fn parse_program_account_loader(path: &syn::Path) -> ParseResult { - let account_ident = parse_account(path)?; - Ok(AccountLoaderTy { - account_type_path: account_ident, - }) -} - -// Copied from Anchor -fn parse_interface_ty(path: &syn::Path) -> ParseResult { - let account_type_path = parse_account(path)?; - Ok(InterfaceTy { account_type_path }) -} - -// Copied from Anchor -fn parse_interface_account_ty(path: &syn::Path) -> ParseResult { - let account_type_path = parse_account(path)?; - let boxed = tts_to_string(path) - .replace(' ', "") - .starts_with("Box(item: T) -> String { - let mut tts = proc_macro2::TokenStream::new(); - item.to_tokens(&mut tts); - tts.to_string() -} - -// Copied from Anchor -fn parse_account(mut path: &syn::Path) -> ParseResult { - let path_str = tts_to_string(path).replace(' ', ""); - if path_str.starts_with("Box { - // Expected: <'info, MyType>. - if args.args.len() != 1 { - return Err(ParseError::new( - args.args.span(), - "bracket arguments must be the lifetime and type", - )); - } - match &args.args[0] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { - path = &ty_path.path; - } - _ => { - return Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )) - } - } - } - _ => { - return Err(ParseError::new( - segments.arguments.span(), - "expected angle brackets with a lifetime and type", - )) - } - } - } - - let segments = &path.segments[0]; - match &segments.arguments { - syn::PathArguments::AngleBracketed(args) => { - // Expected: <'info, MyType>. - if args.args.len() != 2 { - return Err(ParseError::new( - args.args.span(), - "bracket arguments must be the lifetime and type", - )); - } - match &args.args[1] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.clone()), - _ => Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )), - } - } - _ => Err(ParseError::new( - segments.arguments.span(), - "expected angle brackets with a lifetime and type", - )), - } -} - -// Copied from Anchor -fn parse_sysvar(path: &syn::Path) -> ParseResult { - let segments = &path.segments[0]; - let account_ident = match &segments.arguments { - syn::PathArguments::AngleBracketed(args) => { - // Expected: <'info, MyType>. - if args.args.len() != 2 { - return Err(ParseError::new( - args.args.span(), - "bracket arguments must be the lifetime and type", - )); - } - match &args.args[1] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => { - // TODO: allow segmented paths. - if ty_path.path.segments.len() != 1 { - return Err(ParseError::new( - ty_path.path.span(), - "segmented paths are not currently allowed", - )); - } - let path_segment = &ty_path.path.segments[0]; - path_segment.ident.clone() - } - _ => { - return Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )) - } - } - } - _ => { - return Err(ParseError::new( - segments.arguments.span(), - "expected angle brackets with a lifetime and type", - )) - } - }; - let ty = match account_ident.to_string().as_str() { - "Clock" => SysvarTy::Clock, - "Rent" => SysvarTy::Rent, - "EpochSchedule" => SysvarTy::EpochSchedule, - "Fees" => SysvarTy::Fees, - "RecentBlockhashes" => SysvarTy::RecentBlockhashes, - "SlotHashes" => SysvarTy::SlotHashes, - "SlotHistory" => SysvarTy::SlotHistory, - "StakeHistory" => SysvarTy::StakeHistory, - "Instructions" => SysvarTy::Instructions, - "Rewards" => SysvarTy::Rewards, - _ => { - return Err(ParseError::new( - account_ident.span(), - "invalid sysvar provided", - )) - } - }; - Ok(ty) -} - fn has_program_attribute(attrs: &Vec) -> bool { for attr in attrs { if attr.path.is_ident("program") { @@ -772,64 +385,3 @@ fn has_program_attribute(attrs: &Vec) -> bool { } false } - -// Copied from Anchor -fn option_to_inner_path(path: &Path) -> ParseResult { - let segment_0 = path.segments[0].clone(); - match segment_0.arguments { - syn::PathArguments::AngleBracketed(args) => { - if args.args.len() != 1 { - return Err(ParseError::new( - args.args.span(), - "can only have one argument in option", - )); - } - match &args.args[0] { - syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.path.clone()), - _ => Err(ParseError::new( - args.args[1].span(), - "first bracket argument must be a lifetime", - )), - } - } - _ => Err(ParseError::new( - segment_0.arguments.span(), - "expected angle brackets with a lifetime and type", - )), - } -} - -// Copied from Anchor -fn ident_string(f: &syn::Field) -> ParseResult<(String, bool, Path)> { - let mut path = match &f.ty { - syn::Type::Path(ty_path) => ty_path.path.clone(), - _ => return Err(ParseError::new(f.ty.span(), "invalid account type given")), - }; - let mut optional = false; - if tts_to_string(&path).replace(' ', "").starts_with("Option<") { - path = option_to_inner_path(&path)?; - optional = true; - } - if tts_to_string(&path) - .replace(' ', "") - .starts_with("Box Date: Fri, 2 Feb 2024 16:46:02 +0100 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=8E=A8=20Modularized=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/fuzzer/snapshot_generator.rs | 167 +++++++++++------- 1 file changed, 99 insertions(+), 68 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 84b000f4..d358138c 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -6,7 +6,7 @@ use std::{error::Error, fs::File, io::Read}; use anchor_lang::anchor_syn::{AccountField, Ty}; use cargo_metadata::camino::Utf8PathBuf; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; @@ -40,86 +40,102 @@ pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result< .content .ok_or("the content of program module is missing")?; - let mut ix_ctx_pairs = Vec::new(); - for item in items { - // Iterate through items in program module and find functions with the Context<_> parameter. Save the function name and the Context's inner type. - if let syn::Item::Fn(func) = item { - let func_name = &func.sig.ident; - let first_param_type = if let Some(param) = func.sig.inputs.into_iter().next() { - let mut ty = None::; - if let syn::FnArg::Typed(t) = param { - if let syn::Type::Path(tp) = *t.ty.clone() { - if let Some(seg) = tp.path.segments.into_iter().next() { - if let PathArguments::AngleBracketed(arg) = seg.arguments { - ty = arg.args.first().cloned(); - } - } - } - } - ty - } else { - None - }; + let ix_ctx_pairs = get_ix_ctx_pairs(&items)?; - let first_param_type = first_param_type.ok_or(format!( - "The function {} does not have the Context parameter and is malformed.", - func_name - ))?; + let (structs, impls) = get_snapshot_structs_and_impls(code, &ix_ctx_pairs)?; - ix_ctx_pairs.push((func_name.clone(), first_param_type)); - } - } - - // Find definition of each Context struct and create new struct with fields wrapped in Option<_> - let mut structs = String::new(); - let mut desers = String::new(); - let parse_result = syn::parse_file(code).map_err(|e| e.to_string())?; - for pair in ix_ctx_pairs { - let mut ty = None; - if let GenericArgument::Type(syn::Type::Path(tp)) = &pair.1 { - ty = tp.path.get_ident().cloned(); - // TODO add support for types with fully qualified path such as ix::Initialize - } - let ty = ty.ok_or(format!("malformed parameters of {} instruction", pair.0))?; - - // recursively find the context struct and create a new version with wrapped fields into Option - if let Some(ctx) = get_ctx_struct(&parse_result.items, &ty) { - let fields_parsed = if let Fields::Named(f) = ctx.fields.clone() { - let field_deser: ParseResult> = - f.named.iter().map(parse_account_field).collect(); - field_deser - } else { - Err(ParseError::new( - ctx.fields.span(), - "Context struct parse errror.", - )) - } - .map_err(|e| e.to_string())?; - - let wrapped_struct = wrap_fields_in_option(ctx, &fields_parsed).unwrap(); - let deser_code = deserialize_ctx_struct_anchor(ctx, &fields_parsed) - .map_err(|e| e.to_string())?; - // let deser_code = deserialize_ctx_struct(ctx).unwrap(); - structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); - desers = format!("{}{}", desers, deser_code.into_token_stream()); - } else { - return Err(format!("The Context struct {} was not found", ty)); - } - } let use_statements = quote! { use trdelnik_client::anchor_lang::{prelude::*, self}; use trdelnik_client::anchor_lang::solana_program::instruction::AccountMeta; use trdelnik_client::fuzzing::{get_account_infos_option, FuzzingError}; } .into_token_stream(); - Ok(format!("{}{}{}", use_statements, structs, desers)) + Ok(format!("{}{}{}", use_statements, structs, impls)) }); code.into_iter().collect() } +/// Creates new snapshot structs with fields wrapped in Option<_> if approriate and the +/// respective implementations with snapshot deserialization methods +fn get_snapshot_structs_and_impls( + code: &str, + ix_ctx_pairs: &[(Ident, GenericArgument)], +) -> Result<(String, String), String> { + let mut structs = String::new(); + let mut impls = String::new(); + let parse_result = syn::parse_file(code).map_err(|e| e.to_string())?; + for pair in ix_ctx_pairs { + let mut ty = None; + if let GenericArgument::Type(syn::Type::Path(tp)) = &pair.1 { + ty = tp.path.get_ident().cloned(); + // TODO add support for types with fully qualified path such as ix::Initialize + } + let ty = ty.ok_or(format!("malformed parameters of {} instruction", pair.0))?; + + // recursively find the context struct and create a new version with wrapped fields into Option + if let Some(ctx) = find_ctx_struct(&parse_result.items, &ty) { + let fields_parsed = if let Fields::Named(f) = ctx.fields.clone() { + let field_deser: ParseResult> = + f.named.iter().map(parse_account_field).collect(); + field_deser + } else { + Err(ParseError::new( + ctx.fields.span(), + "Context struct parse errror.", + )) + } + .map_err(|e| e.to_string())?; + + let wrapped_struct = wrap_fields_in_option(ctx, &fields_parsed).unwrap(); + let deser_code = + deserialize_ctx_struct_anchor(ctx, &fields_parsed).map_err(|e| e.to_string())?; + // let deser_code = deserialize_ctx_struct(ctx).unwrap(); + structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); + impls = format!("{}{}", impls, deser_code.into_token_stream()); + } else { + return Err(format!("The Context struct {} was not found", ty)); + } + } + + Ok((structs, impls)) +} + +/// Iterates through items and finds functions with the Context<_> parameter. Returns pairs with the function name and the Context's inner type. +fn get_ix_ctx_pairs(items: &[Item]) -> Result, String> { + let mut ix_ctx_pairs = Vec::new(); + for item in items { + if let syn::Item::Fn(func) = item { + let func_name = &func.sig.ident; + let first_param_type = if let Some(param) = func.sig.inputs.iter().next() { + let mut ty = None::; + if let syn::FnArg::Typed(t) = param { + if let syn::Type::Path(tp) = *t.ty.clone() { + if let Some(seg) = tp.path.segments.iter().next() { + if let PathArguments::AngleBracketed(arg) = &seg.arguments { + ty = arg.args.first().cloned(); + } + } + } + } + ty + } else { + None + }; + + let first_param_type = first_param_type.ok_or(format!( + "The function {} does not have the Context parameter and is malformed.", + func_name + ))?; + + ix_ctx_pairs.push((func_name.clone(), first_param_type)); + } + } + Ok(ix_ctx_pairs) +} + /// Recursively find a struct with a given `name` -fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option<&'a ItemStruct> { +fn find_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option<&'a ItemStruct> { for item in items { if let Item::Struct(struct_item) = item { if struct_item.ident == *name { @@ -132,7 +148,7 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option for item in items { if let Item::Mod(mod_item) = item { if let Some((_, items)) = &mod_item.content { - let r = get_ctx_struct(items, name); + let r = find_ctx_struct(items, name); if r.is_some() { return r; } @@ -143,6 +159,9 @@ fn get_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Option None } +/// Determines if an Account should be wrapped into the `Option` type. +/// The function returns true if the account has the init or close constraints set +/// and is not already wrapped into the `Option` type. fn is_optional(parsed_field: &AccountField) -> bool { let is_optional = match parsed_field { AccountField::Field(field) => field.is_optional, @@ -156,6 +175,10 @@ fn is_optional(parsed_field: &AccountField) -> bool { (constraints.init.is_some() || constraints.is_close()) && !is_optional } +/// Determines if an Accout should be deserialized as optional. +/// The function returns true if the account has the init or close constraints set +/// or if it is explicitly optional (it was wrapped into the `Option` type already +/// in the definition of it's corresponding context structure). fn deserialize_as_option(parsed_field: &AccountField) -> bool { let is_optional = match parsed_field { AccountField::Field(field) => field.is_optional, @@ -211,6 +234,7 @@ fn wrap_fields_in_option( Ok(generated_struct.to_token_stream()) } +/// Generates code to deserialize the snapshot structs. fn deserialize_ctx_struct_anchor( snapshot_struct: &ItemStruct, parsed_fields: &[AccountField], @@ -266,6 +290,7 @@ fn deserialize_ctx_struct_anchor( Ok(generated_deser_impl.to_token_stream()) } +/// Get the identifier (name) of the passed sysvar type. fn sysvar_to_ident(sysvar: &anchor_lang::anchor_syn::SysvarTy) -> String { let str = match sysvar { anchor_lang::anchor_syn::SysvarTy::Clock => "Clock", @@ -282,6 +307,9 @@ fn sysvar_to_ident(sysvar: &anchor_lang::anchor_syn::SysvarTy) -> String { str.into() } +/// Converts passed account type to token streams. The function returns a pair of streams where the first +/// variable in the pair is the type itself and the second is a fully qualified function to deserialize +/// the given type. pub fn ty_to_tokens(ty: &anchor_lang::anchor_syn::Ty) -> Option<(TokenStream, TokenStream)> { let (return_type, deser_method) = match ty { Ty::AccountInfo | Ty::UncheckedAccount => return None, @@ -342,6 +370,7 @@ pub fn ty_to_tokens(ty: &anchor_lang::anchor_syn::Ty) -> Option<(TokenStream, To Some((return_type, deser_method)) } +/// Generates the code necessary to deserialize an account fn deserialize_account_tokens( name: &syn::Ident, is_optional: bool, @@ -369,6 +398,7 @@ fn deserialize_account_tokens( } } +/// Generates the code used with raw accounts as AccountInfo fn acc_info_tokens(name: &syn::Ident) -> TokenStream { quote! { let #name = accounts_iter @@ -377,6 +407,7 @@ fn acc_info_tokens(name: &syn::Ident) -> TokenStream { } } +/// Checks if the program attribute is present fn has_program_attribute(attrs: &Vec) -> bool { for attr in attrs { if attr.path.is_ident("program") { From 8225bb9c0dca1c423a2db801d681ef3bf0211a64 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Mon, 5 Feb 2024 10:15:02 +0100 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=85=20Adapted=20tests=20to=20new=20?= =?UTF-8?q?snapshots=20generator.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expected_accounts_snapshots.rs | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs index 851a6ce0..256d2552 100644 --- a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs +++ b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs @@ -2,23 +2,23 @@ use trdelnik_client::anchor_lang::solana_program::instruction::AccountMeta; use trdelnik_client::anchor_lang::{self, prelude::*}; use trdelnik_client::fuzzing::{get_account_infos_option, FuzzingError}; pub struct InitVestingSnapshot<'info> { - pub sender: Option>, - pub sender_token_account: Option>, + pub sender: Signer<'info>, + pub sender_token_account: Account<'info, TokenAccount>, pub escrow: Option>, - pub escrow_token_account: Option>, - pub mint: Option>, - pub token_program: Option>, - pub system_program: Option>, + pub escrow_token_account: Account<'info, TokenAccount>, + pub mint: Account<'info, Mint>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, } pub struct WithdrawUnlockedSnapshot<'info> { - pub recipient: Option>, - pub recipient_token_account: Option>, + pub recipient: Signer<'info>, + pub recipient_token_account: Account<'info, TokenAccount>, pub escrow: Option>, - pub escrow_token_account: Option>, - pub escrow_pda_authority: Option>, - pub mint: Option>, - pub token_program: Option>, - pub system_program: Option>, + pub escrow_token_account: Account<'info, TokenAccount>, + pub escrow_pda_authority: AccountInfo<'info>, + pub mint: Account<'info, Mint>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, } impl<'info> InitVestingSnapshot<'info> { pub fn deserialize_option( @@ -28,50 +28,50 @@ impl<'info> InitVestingSnapshot<'info> { let accounts = get_account_infos_option(accounts, metas) .map_err(|_| FuzzingError::CannotGetAccounts)?; let mut accounts_iter = accounts.into_iter(); - let sender: Option> = accounts_iter + let sender: Signer<'_> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::signer::Signer::try_from(&acc)) - .transpose() - .unwrap_or(None); - let sender_token_account: Option> = + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let sender_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) .transpose() .unwrap_or(None); - let escrow_token_account: Option> = + let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let mint: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let mint: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let token_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let token_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); - let system_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let system_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; Ok(Self { sender, sender_token_account, @@ -91,53 +91,53 @@ impl<'info> WithdrawUnlockedSnapshot<'info> { let accounts = get_account_infos_option(accounts, metas) .map_err(|_| FuzzingError::CannotGetAccounts)?; let mut accounts_iter = accounts.into_iter(); - let recipient: Option> = accounts_iter + let recipient: Signer<'_> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::signer::Signer::try_from(&acc)) - .transpose() - .unwrap_or(None); - let recipient_token_account: Option> = + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let recipient_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) .transpose() .unwrap_or(None); - let escrow_token_account: Option> = + let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow_pda_authority = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)?; - let mint: Option> = accounts_iter + let mint: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) - .transpose() - .unwrap_or(None); - let token_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let token_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); - let system_program: Option> = accounts_iter + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; + let system_program: anchor_lang::accounts::program::Program = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? .map(|acc| anchor_lang::accounts::program::Program::try_from(&acc)) - .transpose() - .unwrap_or(None); + .ok_or(FuzzingError::AccountNotFound)? + .map_err(|_| FuzzingError::CannotDeserializeAccount)?; Ok(Self { recipient, recipient_token_account, From 6795b029ce116c38944dca292efda2bf49ec261f Mon Sep 17 00:00:00 2001 From: Ikrk Date: Thu, 8 Feb 2024 15:31:08 +0100 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20AccountInfo=20and?= =?UTF-8?q?=20UncheckedAccount=20deserialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/fuzzer/snapshot_generator.rs | 17 ++++++++++++++++- .../expected_accounts_snapshots.rs | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index d358138c..b83f34c5 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -252,6 +252,9 @@ fn deserialize_ctx_struct_anchor( return_type, deser_method, ), + None if matches!(&f.ty, Ty::UncheckedAccount) => { + acc_unchecked_tokens(&field_name) + } None => acc_info_tokens(&field_name), }; Ok(( @@ -403,7 +406,19 @@ fn acc_info_tokens(name: &syn::Ident) -> TokenStream { quote! { let #name = accounts_iter .next() - .ok_or(FuzzingError::NotEnoughAccounts)?; + .ok_or(FuzzingError::NotEnoughAccounts)? + .ok_or(FuzzingError::AccountNotFound)?; + } +} + +/// Generates the code used with Unchecked accounts +fn acc_unchecked_tokens(name: &syn::Ident) -> TokenStream { + quote! { + let #name = accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts)? + .map(anchor_lang::accounts::unchecked_account::UncheckedAccount::try_from) + .ok_or(FuzzingError::AccountNotFound)?; } } diff --git a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs index 256d2552..a3c2c671 100644 --- a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs +++ b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs @@ -119,7 +119,8 @@ impl<'info> WithdrawUnlockedSnapshot<'info> { .map_err(|_| FuzzingError::CannotDeserializeAccount)?; let escrow_pda_authority = accounts_iter .next() - .ok_or(FuzzingError::NotEnoughAccounts)?; + .ok_or(FuzzingError::NotEnoughAccounts)? + .ok_or(FuzzingError::AccountNotFound)?; let mint: anchor_lang::accounts::account::Account = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? From bdbbf4593f19b5a621f4efb9f43bec614339efa9 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 11:32:44 +0100 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20generation=20of=20?= =?UTF-8?q?Boxed=20accounts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/fuzzer/snapshot_generator.rs | 93 ++++++++++++------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index b83f34c5..660c1e84 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -10,7 +10,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Error as ParseError, Result as ParseResult}; use syn::spanned::Spanned; -use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments}; +use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, PathArguments, Type}; use anchor_lang::anchor_syn::parser::accounts::parse_account_field; @@ -87,7 +87,7 @@ fn get_snapshot_structs_and_impls( } .map_err(|e| e.to_string())?; - let wrapped_struct = wrap_fields_in_option(ctx, &fields_parsed).unwrap(); + let wrapped_struct = create_snapshot_struct(ctx, &fields_parsed).unwrap(); let deser_code = deserialize_ctx_struct_anchor(ctx, &fields_parsed).map_err(|e| e.to_string())?; // let deser_code = deserialize_ctx_struct(ctx).unwrap(); @@ -159,40 +159,26 @@ fn find_ctx_struct<'a>(items: &'a Vec, name: &'a syn::Ident) -> Optio None } +fn is_boxed(ty: &anchor_lang::anchor_syn::Ty) -> bool { + match ty { + Ty::Account(acc) => acc.boxed, + Ty::InterfaceAccount(acc) => acc.boxed, + _ => false, + } +} + /// Determines if an Account should be wrapped into the `Option` type. /// The function returns true if the account has the init or close constraints set /// and is not already wrapped into the `Option` type. fn is_optional(parsed_field: &AccountField) -> bool { - let is_optional = match parsed_field { - AccountField::Field(field) => field.is_optional, - AccountField::CompositeField(_) => false, - }; - let constraints = match parsed_field { - AccountField::Field(f) => &f.constraints, - AccountField::CompositeField(f) => &f.constraints, - }; - - (constraints.init.is_some() || constraints.is_close()) && !is_optional -} - -/// Determines if an Accout should be deserialized as optional. -/// The function returns true if the account has the init or close constraints set -/// or if it is explicitly optional (it was wrapped into the `Option` type already -/// in the definition of it's corresponding context structure). -fn deserialize_as_option(parsed_field: &AccountField) -> bool { - let is_optional = match parsed_field { + match parsed_field { AccountField::Field(field) => field.is_optional, AccountField::CompositeField(_) => false, - }; - let constraints = match parsed_field { - AccountField::Field(f) => &f.constraints, - AccountField::CompositeField(f) => &f.constraints, - }; - - constraints.init.is_some() || constraints.is_close() || is_optional + } } -fn wrap_fields_in_option( +/// Creates new Snapshot struct from the context struct. Removes Box<> types. +fn create_snapshot_struct( orig_struct: &ItemStruct, parsed_fields: &[AccountField], ) -> Result> { @@ -206,18 +192,40 @@ fn wrap_fields_in_option( .zip(parsed_fields) .map(|(field, parsed_field)| { let field_name = &field.ident; - let field_type = &field.ty; + let mut field_type = &field.ty; + if let AccountField::Field(f) = parsed_field { + if f.is_optional { + // remove option + if let Some(ty) = extract_inner_type(field_type) { + field_type = ty; + } + } + if is_boxed(&f.ty) { + // remove box + if let Some(ty) = extract_inner_type(field_type) { + field_type = ty; + } + } + } else { + return Err( + "Composite field types in context structs not supported".into() + ); + } + if is_optional(parsed_field) { - quote! { + Ok(quote! { pub #field_name: Option<#field_type>, - } + }) } else { - quote! { + Ok(quote! { pub #field_name: #field_type, - } + }) } }); + let field_wrappers: Result, Box> = + field_wrappers.into_iter().collect(); + let field_wrappers = field_wrappers?; quote! { { #(#field_wrappers)* } } @@ -234,6 +242,23 @@ fn wrap_fields_in_option( Ok(generated_struct.to_token_stream()) } +fn extract_inner_type(field_type: &Type) -> Option<&Type> { + if let syn::Type::Path(type_path) = field_type { + let segment = type_path.path.segments.last()?; + let ident = &segment.ident; + + if ident == "Box" || ident == "Option" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() { + return Some(inner_type); + } + } + } + } + + None +} + /// Generates code to deserialize the snapshot structs. fn deserialize_ctx_struct_anchor( snapshot_struct: &ItemStruct, @@ -248,7 +273,7 @@ fn deserialize_ctx_struct_anchor( let deser_tokens = match ty_to_tokens(&f.ty) { Some((return_type, deser_method)) => deserialize_account_tokens( &field_name, - deserialize_as_option(parsed_f), + is_optional(parsed_f), return_type, deser_method, ), From e7807e1220800a5e219ab6fc3e2104ce6b9be489 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 23:25:36 +0100 Subject: [PATCH 08/14] =?UTF-8?q?=E2=9C=A8=20Optional=20accounts=20for=20i?= =?UTF-8?q?nit=20and=20close=20constraints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/fuzzer/snapshot_generator.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 660c1e84..a2716f5e 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -169,12 +169,18 @@ fn is_boxed(ty: &anchor_lang::anchor_syn::Ty) -> bool { /// Determines if an Account should be wrapped into the `Option` type. /// The function returns true if the account has the init or close constraints set -/// and is not already wrapped into the `Option` type. +/// or if it is wrapped into the `Option` type. fn is_optional(parsed_field: &AccountField) -> bool { - match parsed_field { + let is_optional = match parsed_field { AccountField::Field(field) => field.is_optional, AccountField::CompositeField(_) => false, - } + }; + let constraints = match parsed_field { + AccountField::Field(f) => &f.constraints, + AccountField::CompositeField(f) => &f.constraints, + }; + + constraints.init.is_some() || constraints.is_close() || is_optional } /// Creates new Snapshot struct from the context struct. Removes Box<> types. From dacd155787dde86402a3552d4a809b0d1955056b Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 23:27:29 +0100 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20missing=20optional?= =?UTF-8?q?=20accounts=20deserialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/fuzzer/snapshot_generator.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index a2716f5e..3a1e0bde 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -416,7 +416,13 @@ fn deserialize_account_tokens( let #name:Option<#return_type> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| #deser_method) + .map(|acc| { + if acc.key() != PROGRAM_ID { + #deser_method.map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) .transpose() .unwrap_or(None); } From 94f967b9d718016e9cf692848f10a7881fa2a6c1 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 23:35:39 +0100 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20deserialization=20?= =?UTF-8?q?of=20missing=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/fuzzer/snapshot.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/client/src/fuzzer/snapshot.rs b/crates/client/src/fuzzer/snapshot.rs index 99882892..c6c141dd 100644 --- a/crates/client/src/fuzzer/snapshot.rs +++ b/crates/client/src/fuzzer/snapshot.rs @@ -45,7 +45,25 @@ where Ok(accounts) } + fn set_missing_accounts_to_default(accounts: &mut [Option]) { + for acc in accounts.iter_mut() { + if acc.is_none() { + *acc = Some(solana_sdk::account::Account::default()); + } + } + } + pub fn get_snapshot(&'info mut self) -> Result<(T::Ix, T::Ix), FuzzingError> { + // 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 + // AccountInfo, Signer or UncheckedAccount, Anchor will not return an error. However + // when we try to fetch "on-chain" accounts and an account is not initilized, this + // account simply does not exist and the get_account() method returns None. To prevent + // errors during deserialization due to missing accounts, we replace the missing accounts + // with default values similar as the runtime does. + Self::set_missing_accounts_to_default(&mut self.before); + Self::set_missing_accounts_to_default(&mut self.after); + let pre_ix = self.ix.deserialize_option(self.metas, &mut self.before)?; let post_ix = self.ix.deserialize_option(self.metas, &mut self.after)?; Ok((pre_ix, post_ix)) From 22d8afa88a86d264381e1f63bf80e1817cde50cc Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 23:47:47 +0100 Subject: [PATCH 11/14] =?UTF-8?q?=E2=9C=85=20Snapshot=20generator=20tests?= =?UTF-8?q?=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expected_accounts_snapshots.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs index a3c2c671..f21787bf 100644 --- a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs +++ b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs @@ -44,7 +44,14 @@ impl<'info> InitVestingSnapshot<'info> { let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(&acc) + .map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) .transpose() .unwrap_or(None); let escrow_token_account: anchor_lang::accounts::account::Account = @@ -107,7 +114,14 @@ impl<'info> WithdrawUnlockedSnapshot<'info> { let escrow: Option> = accounts_iter .next() .ok_or(FuzzingError::NotEnoughAccounts)? - .map(|acc| anchor_lang::accounts::account::Account::try_from(&acc)) + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(&acc) + .map_err(|e| e.to_string()) + } else { + Err("Optional account not provided".to_string()) + } + }) .transpose() .unwrap_or(None); let escrow_token_account: anchor_lang::accounts::account::Account = From 721451deb4b63796b6efbae94eba1aa71667a11b Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 13 Feb 2024 23:56:33 +0100 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=94=A7=20Cargo.lock=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 190 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d221855..c91dd86a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faa5be5b72abea167f87c868379ba3c2be356bfca9e6f474fd055fa0f7eeb4f2" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", @@ -121,13 +121,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-access-control" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-account" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f468970344c7c9f9d03b4da854fd7c54f21305059f53789d0045c1dd803f0018" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "bs58 0.5.0", "proc-macro2 1.0.66", @@ -136,62 +148,120 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-account" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" +dependencies = [ + "anchor-syn 0.29.0", + "bs58 0.5.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-constant" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59948e7f9ef8144c2aefb3f32a40c5fce2798baeec765ba038389e82301017ef" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "proc-macro2 1.0.66", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-constant" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-error" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc753c9d1c7981cb8948cf7e162fb0f64558999c0413058e2d43df1df5448086" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-error" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-event" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38b4e172ba1b52078f53fdc9f11e3dc0668ad27997838a0aad2d148afac8c97" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-event" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-attribute-program" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eebd21543606ab61e2d83d9da37d24d3886a49f390f9c43a1964735e8c0f0d5" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-attribute-program" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-client" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8434a6bf33efba0c93157f7fa2fafac658cb26ab75396886dcedd87c2a8ad445" dependencies = [ - "anchor-lang", + "anchor-lang 0.28.0", "anyhow", "futures", "regex", @@ -210,13 +280,37 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec4720d899b3686396cced9508f23dab420f1308344456ec78ef76f98fda42af" dependencies = [ - "anchor-syn", + "anchor-syn 0.28.0", "anyhow", "proc-macro2 1.0.66", "quote 1.0.32", "syn 1.0.109", ] +[[package]] +name = "anchor-derive-accounts" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" +dependencies = [ + "anchor-syn 0.29.0", + "quote 1.0.32", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" +dependencies = [ + "anchor-syn 0.29.0", + "borsh-derive-internal 0.10.3", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-derive-space" version = "0.28.0" @@ -228,20 +322,57 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-derive-space" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "anchor-lang" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d2d4b20100f1310a774aba3471ef268e5c4ba4d5c28c0bbe663c2658acbc414" dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-constant", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-program", - "anchor-derive-accounts", - "anchor-derive-space", + "anchor-attribute-access-control 0.28.0", + "anchor-attribute-account 0.28.0", + "anchor-attribute-constant 0.28.0", + "anchor-attribute-error 0.28.0", + "anchor-attribute-event 0.28.0", + "anchor-attribute-program 0.28.0", + "anchor-derive-accounts 0.28.0", + "anchor-derive-space 0.28.0", + "arrayref", + "base64 0.13.1", + "bincode", + "borsh 0.10.3", + "bytemuck", + "getrandom 0.2.10", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-lang" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" +dependencies = [ + "anchor-attribute-access-control 0.29.0", + "anchor-attribute-account 0.29.0", + "anchor-attribute-constant 0.29.0", + "anchor-attribute-error 0.29.0", + "anchor-attribute-event 0.29.0", + "anchor-attribute-program 0.29.0", + "anchor-derive-accounts 0.29.0", + "anchor-derive-serde", + "anchor-derive-space 0.29.0", + "anchor-syn 0.29.0", "arrayref", "base64 0.13.1", "bincode", @@ -270,6 +401,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anchor-syn" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" +dependencies = [ + "anyhow", + "bs58 0.5.0", + "heck 0.3.3", + "proc-macro2 1.0.66", + "quote 1.0.32", + "serde", + "serde_json", + "sha2 0.10.7", + "syn 1.0.109", + "thiserror", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -2891,6 +3040,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.4.0" @@ -5711,6 +5866,7 @@ name = "trdelnik-client" version = "0.5.0" dependencies = [ "anchor-client", + "anchor-lang 0.29.0", "anyhow", "arbitrary", "bincode", @@ -5723,6 +5879,8 @@ dependencies = [ "honggfuzz", "lazy_static", "log", + "macrotest", + "pathdiff", "pretty_assertions", "proc-macro2 1.0.66", "quinn-proto", From f4a294343e8fff3c01836bdffc1edafb87ea2d4f Mon Sep 17 00:00:00 2001 From: Ikrk Date: Wed, 14 Feb 2024 00:01:31 +0100 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=94=A7=20Cargo=20install=20--locked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/setup-rust/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 9e45bc83..ae7f7358 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -18,7 +18,7 @@ runs: rustup component add rustfmt clippy shell: bash - name: Install Cargo Expand - run: cargo install cargo-expand + run: cargo install --locked cargo-expand shell: bash - name: Get rustc version id: rust-version From 4b1cd7b8007bffe27117f614ba1198de556c7459 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Wed, 14 Feb 2024 00:32:29 +0100 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=94=A7=20Bump=20rust=20toolchain=20?= =?UTF-8?q?version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index aa464261..743f7cd9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.71.0" +channel = "1.72.0"