From b128f5b74bda2c5a0852ca497e6b4328bb5696cb Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Tue, 27 Dec 2022 09:41:16 +0100 Subject: [PATCH 01/18] lang: add the InitSpace macro --- Cargo.lock | 14 +++- lang/Cargo.toml | 1 + lang/derive/space/Cargo.toml | 17 +++++ lang/derive/space/src/lib.rs | 122 +++++++++++++++++++++++++++++++++++ lang/src/lib.rs | 15 ++++- lang/tests/space.rs | 103 +++++++++++++++++++++++++++++ 6 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 lang/derive/space/Cargo.toml create mode 100644 lang/derive/space/src/lib.rs create mode 100644 lang/tests/space.rs diff --git a/Cargo.lock b/Cargo.lock index eb92e59591..79f9bf68e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,15 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "anchor-derive-space" +version = "0.26.0" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.103", +] + [[package]] name = "anchor-lang" version = "0.26.0" @@ -231,6 +240,7 @@ dependencies = [ "anchor-attribute-event", "anchor-attribute-program", "anchor-derive-accounts", + "anchor-derive-space", "arrayref", "base64 0.13.1", "bincode", @@ -2235,9 +2245,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "opaque-debug" diff --git a/lang/Cargo.toml b/lang/Cargo.toml index 5901be7f0c..5c9592c28c 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -32,6 +32,7 @@ anchor-attribute-error = { path = "./attribute/error", version = "0.26.0" } anchor-attribute-program = { path = "./attribute/program", version = "0.26.0" } anchor-attribute-event = { path = "./attribute/event", version = "0.26.0" } anchor-derive-accounts = { path = "./derive/accounts", version = "0.26.0" } +anchor-derive-space = { path = "./derive/space", version = "0.26.0" } arrayref = "0.3.6" base64 = "0.13.0" borsh = "0.9" diff --git a/lang/derive/space/Cargo.toml b/lang/derive/space/Cargo.toml new file mode 100644 index 0000000000..49f40ce35e --- /dev/null +++ b/lang/derive/space/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "anchor-derive-space" +version = "0.26.0" +authors = ["Serum Foundation "] +repository = "https://github.com/coral-xyz/anchor" +license = "Apache-2.0" +description = "Anchor Derive macro to automatically calculate the size of a structure or an enum" +rust-version = "1.59" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs new file mode 100644 index 0000000000..3c8a38a420 --- /dev/null +++ b/lang/derive/space/src/lib.rs @@ -0,0 +1,122 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{ + parse_macro_input, Attribute, DeriveInput, Fields, GenericArgument, LitInt, PathArguments, + Type, TypeArray, +}; + +#[proc_macro_derive(InitSpace, attributes(max_len))] +pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let name = input.ident; + + let expanded: TokenStream2 = match input.data { + syn::Data::Struct(strct) => match strct.fields { + Fields::Named(named) => { + let recurse = named + .named + .into_iter() + .map(|f| len_from_type(f.ty, &f.attrs)); + + quote! { + #[automatically_derived] + impl anchor_lang::Space for #name { + const INIT_SPACE: u64 = 0 #(+ #recurse)*; + } + } + } + _ => panic!("Please use named fields in account structure"), + }, + syn::Data::Enum(enm) => { + let variants = enm.variants.into_iter().map(|v| { + let len = v.fields.into_iter().map(|f| len_from_type(f.ty, &f.attrs)); + + quote! { + 0 #(+ #len)* + } + }); + + let max = gen_max(variants); + + quote! { + #[automatically_derived] + impl anchor_lang::Space for #name { + const INIT_SPACE: u64 = 1 + #max; + } + } + } + _ => unimplemented!(), + }; + + TokenStream::from(expanded) +} + +fn gen_max>(mut iter: T) -> TokenStream2 { + if let Some(item) = iter.next() { + let next_item = gen_max(iter); + quote!(anchor_lang::__private::max(#item, #next_item)) + } else { + quote!(0) + } +} + +fn len_from_type(ty: Type, attrs: &[Attribute]) -> TokenStream2 { + match ty { + Type::Array(TypeArray { elem, len, .. }) => { + let array_len = len.to_token_stream(); + let type_len = len_from_type(*elem, attrs); + quote!(#array_len * #type_len) + } + Type::Path(path) => { + let path_segment = path.path.segments.last().unwrap(); + let type_name = path_segment.ident.to_string(); + + match type_name.as_str() { + "i8" | "u8" | "bool" => quote!(1), + "i16" | "u16" => quote!(2), + "i32" | "u32" | "f32" => quote!(4), + "i64" | "u64" | "f64" => quote!(8), + "i128" | "u128" => quote!(16), + "String" => { + let max_len = get_max_len(attrs); + quote!(4 + #max_len ) + } + "Pubkey" => quote!(32), + "Vec" => match &path_segment.arguments { + PathArguments::AngleBracketed(args) => { + let ty = args + .args + .iter() + .find_map(|el| match el { + GenericArgument::Type(ty) => Some(ty.to_owned()), + _ => None, + }) + .unwrap(); + let max_len = get_max_len(attrs); + let type_len = len_from_type(ty, attrs); + + quote!(4 + (#type_len * #max_len)) + } + _ => panic!("Invalid argument in Vec"), + }, + _ => { + let ty = &path_segment.ident; + quote!(<#ty as anchor_lang::Space>::INIT_SPACE) + } + } + } + _ => panic!("Type {:?} is not supported", ty), + } +} + +fn get_max_len(attributes: &[Attribute]) -> u64 { + let attr = attributes + .iter() + .find(|a| a.path.is_ident("max_len")) + .expect("Expected max_len attribute"); + + attr.parse_args::() + .and_then(|l| l.base10_parse()) + .expect("Can't parse the max_len value") +} diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 68fd7d36c2..79d10e0067 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -51,6 +51,7 @@ pub use anchor_attribute_error::*; pub use anchor_attribute_event::{emit, event}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; +pub use anchor_derive_space::InitSpace; /// Borsh is the default serialization format for instructions and accounts. pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use solana_program; @@ -209,6 +210,11 @@ pub trait Discriminator { } } +/// Defines the space of an account for initialization. +pub trait Space { + const INIT_SPACE: u64; +} + /// Bump seed for program derived addresses. pub trait Bump { fn seed(&self) -> u8; @@ -247,7 +253,7 @@ pub mod prelude { require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, - AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, Key, Owner, + AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, ProgramData, Result, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; pub use anchor_attribute_error::*; @@ -288,6 +294,13 @@ pub mod __private { use solana_program::pubkey::Pubkey; + // Used to calculate the maximum between two expressions. + // It is necessary for the calculation of the enum space. + #[doc(hidden)] + pub const fn max(a: u64, b: u64) -> u64 { + [a, b][(a < b) as usize] + } + // Very experimental trait. #[doc(hidden)] pub trait ZeroCopyAccessor { diff --git a/lang/tests/space.rs b/lang/tests/space.rs new file mode 100644 index 0000000000..aa80281232 --- /dev/null +++ b/lang/tests/space.rs @@ -0,0 +1,103 @@ +use anchor_lang::{prelude::*, Space}; + +// Needed to declare accounts. +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +#[derive(InitSpace)] +pub enum TestBasicEnum { + Basic1, + Basic2 { + test_u8: u8, + }, + Basic3 { + test_u16: u16, + }, + Basic4 { + #[max_len(10)] + test_vec: Vec, + }, +} + +#[account] +#[derive(InitSpace)] +pub struct TestEmptyAccount {} + +#[account] +#[derive(InitSpace)] +pub struct TestBasicVarAccount { + pub test_u8: u8, + pub test_u16: u16, + pub test_u32: u32, + pub test_u64: u64, + pub test_u128: u128, +} + +#[account] +#[derive(InitSpace)] +pub struct TestComplexeVarAccount { + pub test_key: Pubkey, + #[max_len(10)] + pub test_vec: Vec, + #[max_len(10)] + pub test_string: String, +} + +#[derive(InitSpace)] +pub struct TestNonAccountStruct { + pub test_bool: bool, +} + +#[account(zero_copy)] +#[derive(InitSpace)] +pub struct TestZeroCopyStruct { + pub test_array: [u8; 10], + pub test_u32: u32, +} + +#[derive(InitSpace)] +pub struct ChildStruct { + #[max_len(10)] + pub test_string: String, +} + +#[derive(InitSpace)] +pub struct TestNestedStruct { + pub test_struct: ChildStruct, + pub test_enum: TestBasicEnum, +} + +#[test] +fn test_empty_struct() { + assert_eq!(TestEmptyAccount::INIT_SPACE, 0); +} + +#[test] +fn test_basic_struct() { + assert_eq!(TestBasicVarAccount::INIT_SPACE, 1 + 2 + 4 + 8 + 16); +} + +#[test] +fn test_complexe_struct() { + assert_eq!( + TestComplexeVarAccount::INIT_SPACE, + 32 + (4 + (1 * 10)) + (4 + 10) + ) +} + +#[test] +fn test_zero_copy_struct() { + assert_eq!(TestZeroCopyStruct::INIT_SPACE, (1 * 10) + 4) +} + +#[test] +fn test_basic_enum() { + assert_eq!(TestBasicEnum::INIT_SPACE, 1 + 14); +} + +#[test] +fn test_nested_struct() { + assert_eq!( + TestNestedStruct::INIT_SPACE, + (4 + 10) + TestBasicEnum::INIT_SPACE + ) +} From dfdbbb6fadf72d855f23457ded9344b6a8f5cfde Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Tue, 27 Dec 2022 17:10:01 +0100 Subject: [PATCH 02/18] Add quote_spanned --- lang/derive/space/src/lib.rs | 62 +++++++++++++++++++++++------------- lang/tests/space.rs | 20 ++++++++---- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index 3c8a38a420..d3f1130877 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -1,9 +1,10 @@ use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::{quote, quote_spanned, ToTokens}; use syn::{ - parse_macro_input, Attribute, DeriveInput, Fields, GenericArgument, LitInt, PathArguments, - Type, TypeArray, + parse_macro_input, + punctuated::{IntoIter, Punctuated}, + Attribute, DeriveInput, Fields, GenericArgument, LitInt, PathArguments, Token, Type, TypeArray, }; #[proc_macro_derive(InitSpace, attributes(max_len))] @@ -14,10 +15,10 @@ pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { let expanded: TokenStream2 = match input.data { syn::Data::Struct(strct) => match strct.fields { Fields::Named(named) => { - let recurse = named - .named - .into_iter() - .map(|f| len_from_type(f.ty, &f.attrs)); + let recurse = named.named.into_iter().map(|f| { + let mut max_len_args = get_max_len_args(&f.attrs); + len_from_type(f.ty, &mut max_len_args) + }); quote! { #[automatically_derived] @@ -30,7 +31,10 @@ pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { }, syn::Data::Enum(enm) => { let variants = enm.variants.into_iter().map(|v| { - let len = v.fields.into_iter().map(|f| len_from_type(f.ty, &f.attrs)); + let len = v.fields.into_iter().map(|f| { + let mut max_len_args = get_max_len_args(&f.attrs); + len_from_type(f.ty, &mut max_len_args) + }); quote! { 0 #(+ #len)* @@ -61,16 +65,17 @@ fn gen_max>(mut iter: T) -> TokenStream2 { } } -fn len_from_type(ty: Type, attrs: &[Attribute]) -> TokenStream2 { +fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 { match ty { Type::Array(TypeArray { elem, len, .. }) => { let array_len = len.to_token_stream(); let type_len = len_from_type(*elem, attrs); - quote!(#array_len * #type_len) + quote!((#array_len * #type_len)) } Type::Path(path) => { let path_segment = path.path.segments.last().unwrap(); - let type_name = path_segment.ident.to_string(); + let ident = &path_segment.ident; + let type_name = ident.to_string(); match type_name.as_str() { "i8" | "u8" | "bool" => quote!(1), @@ -79,8 +84,8 @@ fn len_from_type(ty: Type, attrs: &[Attribute]) -> TokenStream2 { "i64" | "u64" | "f64" => quote!(8), "i128" | "u128" => quote!(16), "String" => { - let max_len = get_max_len(attrs); - quote!(4 + #max_len ) + let max_len = get_next_arg(ident, attrs); + quote!((4 + #max_len)) } "Pubkey" => quote!(32), "Vec" => match &path_segment.arguments { @@ -93,10 +98,11 @@ fn len_from_type(ty: Type, attrs: &[Attribute]) -> TokenStream2 { _ => None, }) .unwrap(); - let max_len = get_max_len(attrs); + + let max_len = get_next_arg(ident, attrs); let type_len = len_from_type(ty, attrs); - quote!(4 + (#type_len * #max_len)) + quote!((4 + #type_len * #max_len)) } _ => panic!("Invalid argument in Vec"), }, @@ -110,13 +116,25 @@ fn len_from_type(ty: Type, attrs: &[Attribute]) -> TokenStream2 { } } -fn get_max_len(attributes: &[Attribute]) -> u64 { - let attr = attributes +fn get_max_len_args(attributes: &[Attribute]) -> Option> { + attributes .iter() .find(|a| a.path.is_ident("max_len")) - .expect("Expected max_len attribute"); + .and_then(|a| { + a.parse_args_with(Punctuated::::parse_terminated) + .ok() + }) + .map(|p| p.into_iter()) +} - attr.parse_args::() - .and_then(|l| l.base10_parse()) - .expect("Can't parse the max_len value") +fn get_next_arg(ident: &Ident, args: &mut Option>) -> TokenStream2 { + if let Some(arg_list) = args { + if let Some(arg) = arg_list.next() { + quote!(#arg) + } else { + quote_spanned!(ident.span() => compile_error!("The number of lengths are invalid.")) + } + } else { + quote_spanned!(ident.span() => compile_error!("Expected max_len attribute.")) + } } diff --git a/lang/tests/space.rs b/lang/tests/space.rs index aa80281232..92f6d65808 100644 --- a/lang/tests/space.rs +++ b/lang/tests/space.rs @@ -66,6 +66,12 @@ pub struct TestNestedStruct { pub test_enum: TestBasicEnum, } +#[derive(InitSpace)] +pub struct TestMatrixStruct { + #[max_len(2, 4)] + pub test_matrix: Vec>, +} + #[test] fn test_empty_struct() { assert_eq!(TestEmptyAccount::INIT_SPACE, 0); @@ -78,15 +84,12 @@ fn test_basic_struct() { #[test] fn test_complexe_struct() { - assert_eq!( - TestComplexeVarAccount::INIT_SPACE, - 32 + (4 + (1 * 10)) + (4 + 10) - ) + assert_eq!(TestComplexeVarAccount::INIT_SPACE, 32 + 4 + 10 + (4 + 10)) } #[test] fn test_zero_copy_struct() { - assert_eq!(TestZeroCopyStruct::INIT_SPACE, (1 * 10) + 4) + assert_eq!(TestZeroCopyStruct::INIT_SPACE, 10 + 4) } #[test] @@ -98,6 +101,11 @@ fn test_basic_enum() { fn test_nested_struct() { assert_eq!( TestNestedStruct::INIT_SPACE, - (4 + 10) + TestBasicEnum::INIT_SPACE + ChildStruct::INIT_SPACE + TestBasicEnum::INIT_SPACE ) } + +#[test] +fn test_matrix_struct() { + assert_eq!(TestMatrixStruct::INIT_SPACE, 4 + (2 * (4 + 4))) +} From 15469b095fb658c1dc718d01c3711145c42ee320 Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Wed, 28 Dec 2022 15:30:52 +0100 Subject: [PATCH 03/18] Fix generics --- lang/attribute/account/src/lib.rs | 7 +++++ lang/derive/space/src/lib.rs | 50 ++++++++++++++++++++++--------- lang/src/lib.rs | 2 +- lang/tests/generics_test.rs | 6 ++-- lang/tests/space.rs | 3 -- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 273be17322..36e0f23339 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -16,6 +16,7 @@ mod id; /// - [`Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html) /// - [`Discriminator`](./trait.Discriminator.html) /// - [`Owner`](./trait.Owner.html) +/// - [`InitSpace`](./trait.InitSpace.html) /// /// When implementing account serialization traits the first 8 bytes are /// reserved for a unique account discriminator, self described by the first 8 @@ -89,6 +90,11 @@ pub fn account( let account_name = &account_strct.ident; let account_name_str = account_name.to_string(); let (impl_gen, type_gen, where_clause) = account_strct.generics.split_for_impl(); + let init_space = if namespace == "internal" { + quote!() + } else { + quote!(#[derive(InitSpace)]) + }; let discriminator: proc_macro2::TokenStream = { // Namespace the discriminator to prevent collisions. @@ -170,6 +176,7 @@ pub fn account( } } else { quote! { + #init_space #[derive(AnchorSerialize, AnchorDeserialize, Clone)] #account_strct diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index d3f1130877..a86ae1b75a 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -7,9 +7,29 @@ use syn::{ Attribute, DeriveInput, Fields, GenericArgument, LitInt, PathArguments, Token, Type, TypeArray, }; +/// Implements a [`Space`](./trait.Space.html) trait on the given +/// struct or enum. +/// +/// # Example +/// ```rust +/// #[account] +/// pub struct ExampleAccount { +/// pub data: u64, +/// } +/// +/// #[derive(Accounts)] +/// pub struct Initialize<'info> { +/// #[account(mut)] +/// pub payer: Signer<'info>, +/// pub system_program: Program<'info, System>, +/// #[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)] +/// pub data: Account<'info, ExampleAccount>, +/// } +/// ``` #[proc_macro_derive(InitSpace, attributes(max_len))] pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let name = input.ident; let expanded: TokenStream2 = match input.data { @@ -22,7 +42,7 @@ pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { quote! { #[automatically_derived] - impl anchor_lang::Space for #name { + impl #impl_generics anchor_lang::Space for #name #ty_generics #where_clause { const INIT_SPACE: u64 = 0 #(+ #recurse)*; } } @@ -88,24 +108,16 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 quote!((4 + #max_len)) } "Pubkey" => quote!(32), - "Vec" => match &path_segment.arguments { - PathArguments::AngleBracketed(args) => { - let ty = args - .args - .iter() - .find_map(|el| match el { - GenericArgument::Type(ty) => Some(ty.to_owned()), - _ => None, - }) - .unwrap(); - + "Vec" => { + if let Some(ty) = get_first_ty_arg(&path_segment.arguments) { let max_len = get_next_arg(ident, attrs); let type_len = len_from_type(ty, attrs); quote!((4 + #type_len * #max_len)) + } else { + quote_spanned!(ident.span() => compile_error!("Invalid argument in Vec")) } - _ => panic!("Invalid argument in Vec"), - }, + } _ => { let ty = &path_segment.ident; quote!(<#ty as anchor_lang::Space>::INIT_SPACE) @@ -116,6 +128,16 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 } } +fn get_first_ty_arg(args: &PathArguments) -> Option { + match args { + PathArguments::AngleBracketed(bracket) => bracket.args.iter().find_map(|el| match el { + GenericArgument::Type(ty) => Some(ty.to_owned()), + _ => None, + }), + _ => None, + } +} + fn get_max_len_args(attributes: &[Attribute]) -> Option> { attributes .iter() diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 79d10e0067..c116f357a7 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -254,7 +254,7 @@ pub mod prelude { require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, - ProgramData, Result, ToAccountInfo, ToAccountInfos, ToAccountMetas, + ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; pub use anchor_attribute_error::*; pub use borsh; diff --git a/lang/tests/generics_test.rs b/lang/tests/generics_test.rs index 3ecb8f3257..2d28bf7a58 100644 --- a/lang/tests/generics_test.rs +++ b/lang/tests/generics_test.rs @@ -11,8 +11,8 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[derive(Accounts)] pub struct GenericsTest<'info, T, U, const N: usize> where - T: AccountSerialize + AccountDeserialize + Owner + Clone, - U: BorshSerialize + BorshDeserialize + Default + Clone, + T: AccountSerialize + AccountDeserialize + Space + Owner + Clone, + U: BorshSerialize + BorshDeserialize + Space + Default + Clone, { pub non_generic: AccountInfo<'info>, pub generic: Account<'info, T>, @@ -31,7 +31,7 @@ pub struct FooAccount { #[derive(Default)] pub struct Associated where - T: BorshDeserialize + BorshSerialize + Default, + T: BorshDeserialize + BorshSerialize + Space + Default, { pub data: T, } diff --git a/lang/tests/space.rs b/lang/tests/space.rs index 92f6d65808..c191e99723 100644 --- a/lang/tests/space.rs +++ b/lang/tests/space.rs @@ -19,11 +19,9 @@ pub enum TestBasicEnum { } #[account] -#[derive(InitSpace)] pub struct TestEmptyAccount {} #[account] -#[derive(InitSpace)] pub struct TestBasicVarAccount { pub test_u8: u8, pub test_u16: u16, @@ -33,7 +31,6 @@ pub struct TestBasicVarAccount { } #[account] -#[derive(InitSpace)] pub struct TestComplexeVarAccount { pub test_key: Pubkey, #[max_len(10)] From 6da2d23906cba3a757356ec78d9e2879dc672bcb Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Thu, 29 Dec 2022 13:45:24 +0100 Subject: [PATCH 04/18] Add option type --- lang/attribute/account/src/lib.rs | 16 +- lang/derive/space/src/lib.rs | 18 +- lang/src/idl.rs | 2 +- lang/src/lib.rs | 4 +- lang/tests/space.rs | 8 +- .../programs/cashiers-check/src/lib.rs | 2 +- .../programs/native-system/src/lib.rs | 2 +- tests/multisig/programs/multisig/src/lib.rs | 178 ++++++++++++------ tests/multisig/tests/multisig.js | 58 ++++-- tests/realloc/programs/realloc/src/lib.rs | 2 +- 10 files changed, 200 insertions(+), 90 deletions(-) diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 36e0f23339..977be83d25 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -67,30 +67,32 @@ pub fn account( ) -> proc_macro::TokenStream { let mut namespace = "".to_string(); let mut is_zero_copy = false; + let mut skip_init_space = false; let args_str = args.to_string(); let args: Vec<&str> = args_str.split(',').collect(); if args.len() > 2 { panic!("Only two args are allowed to the account attribute.") } for arg in args { - let ns = arg + let ns: String = arg .to_string() .replace('\"', "") .chars() .filter(|c| !c.is_whitespace()) .collect(); - if ns == "zero_copy" { - is_zero_copy = true; - } else { - namespace = ns; - } + + match ns.as_str() { + "zero_copy" => is_zero_copy = true, + "skip_space" => skip_init_space = true, + _ => namespace = ns, + }; } let account_strct = parse_macro_input!(input as syn::ItemStruct); let account_name = &account_strct.ident; let account_name_str = account_name.to_string(); let (impl_gen, type_gen, where_clause) = account_strct.generics.split_for_impl(); - let init_space = if namespace == "internal" { + let init_space = if skip_init_space { quote!() } else { quote!(#[derive(InitSpace)]) diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index a86ae1b75a..d95350d6c0 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -11,7 +11,7 @@ use syn::{ /// struct or enum. /// /// # Example -/// ```rust +/// ```ignore /// #[account] /// pub struct ExampleAccount { /// pub data: u64, @@ -43,7 +43,7 @@ pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { quote! { #[automatically_derived] impl #impl_generics anchor_lang::Space for #name #ty_generics #where_clause { - const INIT_SPACE: u64 = 0 #(+ #recurse)*; + const INIT_SPACE: usize = 0 #(+ #recurse)*; } } } @@ -66,7 +66,7 @@ pub fn derive_anchor_deserialize(item: TokenStream) -> TokenStream { quote! { #[automatically_derived] impl anchor_lang::Space for #name { - const INIT_SPACE: u64 = 1 + #max; + const INIT_SPACE: usize = 1 + #max; } } } @@ -96,6 +96,7 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 let path_segment = path.path.segments.last().unwrap(); let ident = &path_segment.ident; let type_name = ident.to_string(); + let first_ty = get_first_ty_arg(&path_segment.arguments); match type_name.as_str() { "i8" | "u8" | "bool" => quote!(1), @@ -108,8 +109,17 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 quote!((4 + #max_len)) } "Pubkey" => quote!(32), + "Option" => { + if let Some(ty) = first_ty { + let type_len = len_from_type(ty, attrs); + + quote!((1 + #type_len)) + } else { + quote_spanned!(ident.span() => compile_error!("Invalid argument in Vec")) + } + } "Vec" => { - if let Some(ty) = get_first_ty_arg(&path_segment.arguments) { + if let Some(ty) = first_ty { let max_len = get_next_arg(ident, attrs); let type_len = len_from_type(ty, attrs); diff --git a/lang/src/idl.rs b/lang/src/idl.rs index 8bbd89316a..b008b8f60f 100644 --- a/lang/src/idl.rs +++ b/lang/src/idl.rs @@ -90,7 +90,7 @@ pub struct IdlSetBuffer<'info> { // // Note: we use the same account for the "write buffer", similar to the // bpf upgradeable loader's mechanism. -#[account("internal")] +#[account("internal", skip_space)] #[derive(Debug)] pub struct IdlAccount { // Address that can modify the IDL. diff --git a/lang/src/lib.rs b/lang/src/lib.rs index c116f357a7..92278b4e13 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -212,7 +212,7 @@ pub trait Discriminator { /// Defines the space of an account for initialization. pub trait Space { - const INIT_SPACE: u64; + const INIT_SPACE: usize; } /// Bump seed for program derived addresses. @@ -297,7 +297,7 @@ pub mod __private { // Used to calculate the maximum between two expressions. // It is necessary for the calculation of the enum space. #[doc(hidden)] - pub const fn max(a: u64, b: u64) -> u64 { + pub const fn max(a: usize, b: usize) -> usize { [a, b][(a < b) as usize] } diff --git a/lang/tests/space.rs b/lang/tests/space.rs index c191e99723..65fe5cb965 100644 --- a/lang/tests/space.rs +++ b/lang/tests/space.rs @@ -1,4 +1,4 @@ -use anchor_lang::{prelude::*, Space}; +use anchor_lang::prelude::*; // Needed to declare accounts. declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -37,6 +37,7 @@ pub struct TestComplexeVarAccount { pub test_vec: Vec, #[max_len(10)] pub test_string: String, + pub test_option: Option, } #[derive(InitSpace)] @@ -81,7 +82,10 @@ fn test_basic_struct() { #[test] fn test_complexe_struct() { - assert_eq!(TestComplexeVarAccount::INIT_SPACE, 32 + 4 + 10 + (4 + 10)) + assert_eq!( + TestComplexeVarAccount::INIT_SPACE, + 32 + 4 + 10 + (4 + 10) + 3 + ) } #[test] diff --git a/tests/cashiers-check/programs/cashiers-check/src/lib.rs b/tests/cashiers-check/programs/cashiers-check/src/lib.rs index 9cdae60d7d..ed04ef54ee 100644 --- a/tests/cashiers-check/programs/cashiers-check/src/lib.rs +++ b/tests/cashiers-check/programs/cashiers-check/src/lib.rs @@ -151,7 +151,7 @@ pub struct CancelCheck<'info> { token_program: AccountInfo<'info>, } -#[account] +#[account(skip_space)] pub struct Check { from: Pubkey, to: Pubkey, diff --git a/tests/custom-coder/programs/native-system/src/lib.rs b/tests/custom-coder/programs/native-system/src/lib.rs index 8ac2b71574..99fd5ae66c 100644 --- a/tests/custom-coder/programs/native-system/src/lib.rs +++ b/tests/custom-coder/programs/native-system/src/lib.rs @@ -204,7 +204,7 @@ pub struct FeeCalculator { pub lamports_per_signature: u64, } -#[account] +#[account(skip_space)] pub struct Nonce { pub version: u32, pub state: u32, diff --git a/tests/multisig/programs/multisig/src/lib.rs b/tests/multisig/programs/multisig/src/lib.rs index 91ac9ff799..7a783c6dd8 100644 --- a/tests/multisig/programs/multisig/src/lib.rs +++ b/tests/multisig/programs/multisig/src/lib.rs @@ -17,16 +17,16 @@ //! the `execute_transaction`, once enough (i.e. `threshold`) of the owners have //! signed. -use anchor_lang::accounts::program_account::ProgramAccount; use anchor_lang::prelude::*; use anchor_lang::solana_program; use anchor_lang::solana_program::instruction::Instruction; use std::convert::Into; +use std::ops::Deref; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] -pub mod multisig { +pub mod coral_multisig { use super::*; // Initializes a new multisig account with a set of owners and a threshold. @@ -36,10 +36,18 @@ pub mod multisig { threshold: u64, nonce: u8, ) -> Result<()> { + assert_unique_owners(&owners)?; + require!( + threshold > 0 && threshold <= owners.len() as u64, + MultisigError::InvalidThreshold + ); + require!(!owners.is_empty(), MultisigError::InvalidOwnersLen); + let multisig = &mut ctx.accounts.multisig; multisig.owners = owners; multisig.threshold = threshold; multisig.nonce = nonce; + multisig.owner_set_seqno = 0; Ok(()) } @@ -57,7 +65,7 @@ pub mod multisig { .owners .iter() .position(|a| a == ctx.accounts.proposer.key) - .ok_or(error!(ErrorCode::InvalidOwner))?; + .ok_or(MultisigError::InvalidOwner)?; let mut signers = Vec::new(); signers.resize(ctx.accounts.multisig.owners.len(), false); @@ -68,8 +76,9 @@ pub mod multisig { tx.accounts = accs; tx.data = data; tx.signers = signers; - tx.multisig = *ctx.accounts.multisig.to_account_info().key; + tx.multisig = ctx.accounts.multisig.key(); tx.did_execute = false; + tx.owner_set_seqno = ctx.accounts.multisig.owner_set_seqno; Ok(()) } @@ -82,16 +91,37 @@ pub mod multisig { .owners .iter() .position(|a| a == ctx.accounts.owner.key) - .ok_or(error!(ErrorCode::InvalidOwner))?; + .ok_or(MultisigError::InvalidOwner)?; ctx.accounts.transaction.signers[owner_index] = true; Ok(()) } + // Set owners and threshold at once. + pub fn set_owners_and_change_threshold<'info>( + ctx: Context<'_, '_, '_, 'info, Auth<'info>>, + owners: Vec, + threshold: u64, + ) -> Result<()> { + set_owners( + Context::new( + ctx.program_id, + ctx.accounts, + ctx.remaining_accounts, + ctx.bumps.clone(), + ), + owners, + )?; + change_threshold(ctx, threshold) + } + // Sets the owners field on the multisig. The only way this can be invoked // is via a recursive call from execute_transaction -> set_owners. pub fn set_owners(ctx: Context, owners: Vec) -> Result<()> { + assert_unique_owners(&owners)?; + require!(!owners.is_empty(), MultisigError::InvalidOwnersLen); + let multisig = &mut ctx.accounts.multisig; if (owners.len() as u64) < multisig.threshold { @@ -99,6 +129,8 @@ pub mod multisig { } multisig.owners = owners; + multisig.owner_set_seqno += 1; + Ok(()) } @@ -106,9 +138,12 @@ pub mod multisig { // invoked is via a recursive call from execute_transaction -> // change_threshold. pub fn change_threshold(ctx: Context, threshold: u64) -> Result<()> { - if threshold > ctx.accounts.multisig.owners.len() as u64 { - return err!(ErrorCode::InvalidThreshold); - } + require_gt!( + threshold, + ctx.accounts.multisig.owners.len() as u64, + MultisigError::InvalidThreshold + ); + let multisig = &mut ctx.accounts.multisig; multisig.threshold = threshold; Ok(()) @@ -118,45 +153,39 @@ pub mod multisig { pub fn execute_transaction(ctx: Context) -> Result<()> { // Has this been executed already? if ctx.accounts.transaction.did_execute { - return err!(ErrorCode::AlreadyExecuted); + return Err(MultisigError::AlreadyExecuted.into()); } - // Do we have enough signers? + // Do we have enough signers. let sig_count = ctx .accounts .transaction .signers .iter() - .filter_map(|s| match s { - false => None, - true => Some(true), - }) - .collect::>() - .len() as u64; + .filter(|&did_sign| *did_sign) + .count() as u64; if sig_count < ctx.accounts.multisig.threshold { - return err!(ErrorCode::NotEnoughSigners); + return Err(MultisigError::NotEnoughSigners.into()); } // Execute the transaction signed by the multisig. - let mut ix: Instruction = (&*ctx.accounts.transaction).into(); + let mut ix: Instruction = (*ctx.accounts.transaction).deref().into(); ix.accounts = ix .accounts .iter() .map(|acc| { + let mut acc = acc.clone(); if &acc.pubkey == ctx.accounts.multisig_signer.key { - AccountMeta::new_readonly(acc.pubkey, true) - } else { - acc.clone() + acc.is_signer = true; } + acc }) .collect(); - let seeds = &[ - ctx.accounts.multisig.to_account_info().key.as_ref(), - &[ctx.accounts.multisig.nonce], - ]; + let multisig_key = ctx.accounts.multisig.key(); + let seeds = &[multisig_key.as_ref(), &[ctx.accounts.multisig.nonce]]; let signer = &[&seeds[..]]; let accounts = ctx.remaining_accounts; - solana_program::program::invoke_signed(&ix, &accounts, signer)?; + solana_program::program::invoke_signed(&ix, accounts, signer)?; // Burn the transaction to ensure one time use. ctx.accounts.transaction.did_execute = true; @@ -167,24 +196,25 @@ pub mod multisig { #[derive(Accounts)] pub struct CreateMultisig<'info> { - #[account(zero)] - multisig: ProgramAccount<'info, Multisig>, + #[account(zero, signer)] + multisig: Box>, } #[derive(Accounts)] pub struct CreateTransaction<'info> { - multisig: ProgramAccount<'info, Multisig>, - #[account(zero)] - transaction: ProgramAccount<'info, Transaction>, + multisig: Box>, + #[account(zero, signer)] + transaction: Box>, // One of the owners. Checked in the handler. proposer: Signer<'info>, } #[derive(Accounts)] pub struct Approve<'info> { - multisig: ProgramAccount<'info, Multisig>, + #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] + multisig: Box>, #[account(mut, has_one = multisig)] - transaction: ProgramAccount<'info, Transaction>, + transaction: Box>, // One of the multisig owners. Checked in the handler. owner: Signer<'info>, } @@ -192,55 +222,59 @@ pub struct Approve<'info> { #[derive(Accounts)] pub struct Auth<'info> { #[account(mut)] - multisig: ProgramAccount<'info, Multisig>, + multisig: Box>, #[account( - signer, - seeds = [multisig.to_account_info().key.as_ref()], + seeds = [multisig.key().as_ref()], bump = multisig.nonce, )] - multisig_signer: AccountInfo<'info>, + multisig_signer: Signer<'info>, } #[derive(Accounts)] pub struct ExecuteTransaction<'info> { - multisig: ProgramAccount<'info, Multisig>, + #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] + multisig: Box>, + /// CHECK: multisig_signer is a PDA program signer. Data is never read or written to #[account( - seeds = [multisig.to_account_info().key.as_ref()], + seeds = [multisig.key().as_ref()], bump = multisig.nonce, )] - multisig_signer: AccountInfo<'info>, + multisig_signer: UncheckedAccount<'info>, #[account(mut, has_one = multisig)] - transaction: ProgramAccount<'info, Transaction>, + transaction: Box>, } -#[account] +#[account(skip_space)] pub struct Multisig { - owners: Vec, - threshold: u64, - nonce: u8, + pub owners: Vec, + pub threshold: u64, + pub nonce: u8, + pub owner_set_seqno: u32, } -#[account] +#[account(skip_space)] pub struct Transaction { // The multisig account this transaction belongs to. - multisig: Pubkey, + pub multisig: Pubkey, // Target program to execute against. - program_id: Pubkey, - // Accounts required for the transaction. - accounts: Vec, + pub program_id: Pubkey, + // Accounts requried for the transaction. + pub accounts: Vec, // Instruction data for the transaction. - data: Vec, + pub data: Vec, // signers[index] is true iff multisig.owners[index] signed the transaction. - signers: Vec, + pub signers: Vec, // Boolean ensuring one time execution. - did_execute: bool, + pub did_execute: bool, + // Owner set sequence number. + pub owner_set_seqno: u32, } impl From<&Transaction> for Instruction { fn from(tx: &Transaction) -> Instruction { Instruction { program_id: tx.program_id, - accounts: tx.accounts.clone().into_iter().map(Into::into).collect(), + accounts: tx.accounts.iter().map(Into::into).collect(), data: tx.data.clone(), } } @@ -248,13 +282,13 @@ impl From<&Transaction> for Instruction { #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct TransactionAccount { - pubkey: Pubkey, - is_signer: bool, - is_writable: bool, + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, } -impl From for AccountMeta { - fn from(account: TransactionAccount) -> AccountMeta { +impl From<&TransactionAccount> for AccountMeta { + fn from(account: &TransactionAccount) -> AccountMeta { match account.is_writable { false => AccountMeta::new_readonly(account.pubkey, account.is_signer), true => AccountMeta::new(account.pubkey, account.is_signer), @@ -262,10 +296,32 @@ impl From for AccountMeta { } } +impl From<&AccountMeta> for TransactionAccount { + fn from(account_meta: &AccountMeta) -> TransactionAccount { + TransactionAccount { + pubkey: account_meta.pubkey, + is_signer: account_meta.is_signer, + is_writable: account_meta.is_writable, + } + } +} + +fn assert_unique_owners(owners: &[Pubkey]) -> Result<()> { + for (i, owner) in owners.iter().enumerate() { + require!( + !owners.iter().skip(i + 1).any(|item| item == owner), + MultisigError::UniqueOwners + ) + } + Ok(()) +} + #[error_code] -pub enum ErrorCode { +pub enum MultisigError { #[msg("The given owner is not part of this multisig.")] InvalidOwner, + #[msg("Owners length must be non zero.")] + InvalidOwnersLen, #[msg("Not enough owners signed this transaction.")] NotEnoughSigners, #[msg("Cannot delete a transaction that has been signed by an owner.")] @@ -278,4 +334,6 @@ pub enum ErrorCode { AlreadyExecuted, #[msg("Threshold must be less than or equal to the number of owners.")] InvalidThreshold, + #[msg("Owners must be unique")] + UniqueOwners, } diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.js index f65e7d1822..c88e777ee4 100644 --- a/tests/multisig/tests/multisig.js +++ b/tests/multisig/tests/multisig.js @@ -3,9 +3,9 @@ const { assert } = require("chai"); describe("multisig", () => { // Configure the client to use the local cluster. - anchor.setProvider(anchor.AnchorProvider.env()); + anchor.setProvider(anchor.getProvider()); - const program = anchor.workspace.Multisig; + const program = anchor.workspace.CoralMultisig; it("Tests the multisig program", async () => { const multisig = anchor.web3.Keypair.generate(); @@ -26,7 +26,6 @@ describe("multisig", () => { await program.rpc.createMultisig(owners, threshold, nonce, { accounts: { multisig: multisig.publicKey, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, }, instructions: [ await program.account.multisig.createInstruction( @@ -40,10 +39,10 @@ describe("multisig", () => { let multisigAccount = await program.account.multisig.fetch( multisig.publicKey ); - assert.strictEqual(multisigAccount.nonce, nonce); assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); - assert.deepEqual(multisigAccount.owners, owners); + assert.deepStrictEqual(multisigAccount.owners, owners); + assert.isTrue(multisigAccount.ownerSetSeqno === 0); const pid = program.programId; const accounts = [ @@ -70,7 +69,6 @@ describe("multisig", () => { multisig: multisig.publicKey, transaction: transaction.publicKey, proposer: ownerA.publicKey, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, }, instructions: [ await program.account.transaction.createInstruction( @@ -86,10 +84,11 @@ describe("multisig", () => { ); assert.isTrue(txAccount.programId.equals(pid)); - assert.deepEqual(txAccount.accounts, accounts); - assert.deepEqual(txAccount.data, data); + assert.deepStrictEqual(txAccount.accounts, accounts); + assert.deepStrictEqual(txAccount.data, data); assert.isTrue(txAccount.multisig.equals(multisig.publicKey)); - assert.strictEqual(txAccount.didExecute, false); + assert.deepStrictEqual(txAccount.didExecute, false); + assert.isTrue(txAccount.ownerSetSeqno === 0); // Other owner approves transaction. await program.rpc.approve({ @@ -130,6 +129,43 @@ describe("multisig", () => { assert.strictEqual(multisigAccount.nonce, nonce); assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); - assert.deepEqual(multisigAccount.owners, newOwners); + assert.deepStrictEqual(multisigAccount.owners, newOwners); + assert.isTrue(multisigAccount.ownerSetSeqno === 1); + }); + + it("Assert Unique Owners", async () => { + const multisig = anchor.web3.Keypair.generate(); + const [_multisigSigner, nonce] = + await anchor.web3.PublicKey.findProgramAddress( + [multisig.publicKey.toBuffer()], + program.programId + ); + const multisigSize = 200; // Big enough. + + const ownerA = anchor.web3.Keypair.generate(); + const ownerB = anchor.web3.Keypair.generate(); + const owners = [ownerA.publicKey, ownerB.publicKey, ownerA.publicKey]; + + const threshold = new anchor.BN(2); + try { + await program.rpc.createMultisig(owners, threshold, nonce, { + accounts: { + multisig: multisig.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + instructions: [ + await program.account.multisig.createInstruction( + multisig, + multisigSize + ), + ], + signers: [multisig], + }); + assert.fail(); + } catch (err) { + const error = err.error; + assert.strictEqual(error.errorCode.number, 6008); + assert.strictEqual(error.errorMessage, "Owners must be unique"); + } }); -}); +}); \ No newline at end of file diff --git a/tests/realloc/programs/realloc/src/lib.rs b/tests/realloc/programs/realloc/src/lib.rs index a854e6a0a7..217a52e6f5 100644 --- a/tests/realloc/programs/realloc/src/lib.rs +++ b/tests/realloc/programs/realloc/src/lib.rs @@ -99,7 +99,7 @@ pub struct Realloc2<'info> { pub system_program: Program<'info, System>, } -#[account] +#[account(skip_space)] pub struct Sample { pub data: Vec, pub bump: u8, From a131a86ba903d9d4d8943a571173f4dc8af9250a Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Thu, 29 Dec 2022 13:59:04 +0100 Subject: [PATCH 05/18] Fix lint --- tests/ido-pool/programs/ido-pool/src/lib.rs | 10 +++------- tests/multisig/tests/multisig.js | 8 ++++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/ido-pool/programs/ido-pool/src/lib.rs b/tests/ido-pool/programs/ido-pool/src/lib.rs index 8b97466ccf..e7610771d3 100644 --- a/tests/ido-pool/programs/ido-pool/src/lib.rs +++ b/tests/ido-pool/programs/ido-pool/src/lib.rs @@ -296,7 +296,7 @@ pub struct InitializePool<'info> { seeds = [ido_name.as_bytes()], bump, payer = ido_authority, - space = IdoAccount::LEN + 8 + space = 8 + IdoAccount::INIT_SPACE )] pub ido_account: Box>, // TODO Confirm USDC mint address on mainnet or leave open as an option for other stables @@ -560,11 +560,7 @@ pub struct IdoAccount { pub ido_times: IdoTimes, // 32 } -impl IdoAccount { - pub const LEN: usize = 10 + 4 + 32 + 5 * 32 + 8 + 32; -} - -#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone, Copy)] +#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Default, Clone, Copy)] pub struct IdoTimes { pub start_ido: i64, // 8 pub end_deposits: i64, // 8 @@ -572,7 +568,7 @@ pub struct IdoTimes { pub end_escrow: i64, // 8 } -#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)] +#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Default, Clone)] pub struct PoolBumps { pub ido_account: u8, // 1 pub redeemable_mint: u8, // 1 diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.js index c88e777ee4..2b68b1da29 100644 --- a/tests/multisig/tests/multisig.js +++ b/tests/multisig/tests/multisig.js @@ -5,12 +5,12 @@ describe("multisig", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.getProvider()); - const program = anchor.workspace.CoralMultisig; + const program = anchor.workspace.Multisig; it("Tests the multisig program", async () => { const multisig = anchor.web3.Keypair.generate(); const [multisigSigner, nonce] = - await anchor.web3.PublicKey.findProgramAddress( + anchor.web3.PublicKey.findProgramAddressSync( [multisig.publicKey.toBuffer()], program.programId ); @@ -136,7 +136,7 @@ describe("multisig", () => { it("Assert Unique Owners", async () => { const multisig = anchor.web3.Keypair.generate(); const [_multisigSigner, nonce] = - await anchor.web3.PublicKey.findProgramAddress( + anchor.web3.PublicKey.findProgramAddressSync( [multisig.publicKey.toBuffer()], program.programId ); @@ -168,4 +168,4 @@ describe("multisig", () => { assert.strictEqual(error.errorMessage, "Owners must be unique"); } }); -}); \ No newline at end of file +}); From 9bfea0fe802ce206e038a54cb8449be6b6a7649a Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 30 Dec 2022 11:32:19 +0100 Subject: [PATCH 06/18] Fix chat example --- tests/chat/programs/chat/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/chat/programs/chat/src/lib.rs b/tests/chat/programs/chat/src/lib.rs index 8849471714..19bee6a6fd 100644 --- a/tests/chat/programs/chat/src/lib.rs +++ b/tests/chat/programs/chat/src/lib.rs @@ -46,7 +46,7 @@ pub struct CreateUser<'info> { seeds = [authority.key().as_ref()], bump, payer = authority, - space = 320, + space = 8 + User::INIT_SPACE, )] user: Account<'info, User>, #[account(mut)] @@ -75,6 +75,7 @@ pub struct SendMessage<'info> { #[account] pub struct User { + #[max_len(200)] name: String, authority: Pubkey, bump: u8, From 5491ba4c44805d4e00ecbc057c05b12577c28f5f Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 30 Dec 2022 13:59:54 +0100 Subject: [PATCH 07/18] Fix multisig example --- tests/multisig/Anchor.toml | 2 +- tests/multisig/programs/multisig/src/lib.rs | 46 ++++-- .../tests/{multisig.js => multisig.ts} | 134 +++++++++--------- tests/multisig/tsconfig.json | 10 ++ 4 files changed, 108 insertions(+), 84 deletions(-) rename tests/multisig/tests/{multisig.js => multisig.ts} (62%) create mode 100644 tests/multisig/tsconfig.json diff --git a/tests/multisig/Anchor.toml b/tests/multisig/Anchor.toml index ee9f941dcc..ec450a19a7 100644 --- a/tests/multisig/Anchor.toml +++ b/tests/multisig/Anchor.toml @@ -6,6 +6,6 @@ wallet = "~/.config/solana/id.json" multisig = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" [scripts] -test = "yarn run mocha -t 1000000 tests/" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" [features] diff --git a/tests/multisig/programs/multisig/src/lib.rs b/tests/multisig/programs/multisig/src/lib.rs index 7a783c6dd8..472a63c38a 100644 --- a/tests/multisig/programs/multisig/src/lib.rs +++ b/tests/multisig/programs/multisig/src/lib.rs @@ -169,7 +169,7 @@ pub mod coral_multisig { } // Execute the transaction signed by the multisig. - let mut ix: Instruction = (*ctx.accounts.transaction).deref().into(); + let mut ix: Instruction = ctx.accounts.transaction.deref().into(); ix.accounts = ix .accounts .iter() @@ -196,25 +196,39 @@ pub mod coral_multisig { #[derive(Accounts)] pub struct CreateMultisig<'info> { - #[account(zero, signer)] - multisig: Box>, + #[account(mut)] + payer: Signer<'info>, + #[account( + init, + payer = payer, + space = 8 + Multisig::INIT_SPACE + )] + multisig: Account<'info, Multisig>, + system_program: Program<'info, System>, } #[derive(Accounts)] pub struct CreateTransaction<'info> { - multisig: Box>, - #[account(zero, signer)] - transaction: Box>, + #[account(mut)] + payer: Signer<'info>, + multisig: Account<'info, Multisig>, + #[account( + init, + payer = payer, + space = 8 + Transaction::INIT_SPACE + )] + transaction: Account<'info, Transaction>, // One of the owners. Checked in the handler. proposer: Signer<'info>, + system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Approve<'info> { #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] - multisig: Box>, + multisig: Account<'info, Multisig>, #[account(mut, has_one = multisig)] - transaction: Box>, + transaction: Account<'info, Transaction>, // One of the multisig owners. Checked in the handler. owner: Signer<'info>, } @@ -222,7 +236,7 @@ pub struct Approve<'info> { #[derive(Accounts)] pub struct Auth<'info> { #[account(mut)] - multisig: Box>, + multisig: Account<'info, Multisig>, #[account( seeds = [multisig.key().as_ref()], bump = multisig.nonce, @@ -233,7 +247,7 @@ pub struct Auth<'info> { #[derive(Accounts)] pub struct ExecuteTransaction<'info> { #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] - multisig: Box>, + multisig: Account<'info, Multisig>, /// CHECK: multisig_signer is a PDA program signer. Data is never read or written to #[account( seeds = [multisig.key().as_ref()], @@ -241,28 +255,32 @@ pub struct ExecuteTransaction<'info> { )] multisig_signer: UncheckedAccount<'info>, #[account(mut, has_one = multisig)] - transaction: Box>, + transaction: Account<'info, Transaction>, } -#[account(skip_space)] +#[account] pub struct Multisig { + #[max_len(5)] pub owners: Vec, pub threshold: u64, pub nonce: u8, pub owner_set_seqno: u32, } -#[account(skip_space)] +#[account] pub struct Transaction { // The multisig account this transaction belongs to. pub multisig: Pubkey, // Target program to execute against. pub program_id: Pubkey, // Accounts requried for the transaction. + #[max_len(20)] pub accounts: Vec, // Instruction data for the transaction. + #[max_len(256)] pub data: Vec, // signers[index] is true iff multisig.owners[index] signed the transaction. + #[max_len(5)] pub signers: Vec, // Boolean ensuring one time execution. pub did_execute: bool, @@ -280,7 +298,7 @@ impl From<&Transaction> for Instruction { } } -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Clone)] pub struct TransactionAccount { pub pubkey: Pubkey, pub is_signer: bool, diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.ts similarity index 62% rename from tests/multisig/tests/multisig.js rename to tests/multisig/tests/multisig.ts index 2b68b1da29..786c162e66 100644 --- a/tests/multisig/tests/multisig.js +++ b/tests/multisig/tests/multisig.ts @@ -1,11 +1,13 @@ -const anchor = require("@coral-xyz/anchor"); -const { assert } = require("chai"); +import * as anchor from "@coral-xyz/anchor"; +import { AnchorError } from "@coral-xyz/anchor"; +import { assert } from "chai"; +import { Multisig } from "../target/types/multisig"; describe("multisig", () => { // Configure the client to use the local cluster. - anchor.setProvider(anchor.getProvider()); + anchor.setProvider(anchor.AnchorProvider.env()); - const program = anchor.workspace.Multisig; + const program = anchor.workspace.Multisig as anchor.Program; it("Tests the multisig program", async () => { const multisig = anchor.web3.Keypair.generate(); @@ -14,7 +16,6 @@ describe("multisig", () => { [multisig.publicKey.toBuffer()], program.programId ); - const multisigSize = 200; // Big enough. const ownerA = anchor.web3.Keypair.generate(); const ownerB = anchor.web3.Keypair.generate(); @@ -23,18 +24,15 @@ describe("multisig", () => { const owners = [ownerA.publicKey, ownerB.publicKey, ownerC.publicKey]; const threshold = new anchor.BN(2); - await program.rpc.createMultisig(owners, threshold, nonce, { - accounts: { + await program.methods + .createMultisig(owners, threshold, nonce) + .accounts({ + payer: program.provider.publicKey, multisig: multisig.publicKey, - }, - instructions: [ - await program.account.multisig.createInstruction( - multisig, - multisigSize - ), - ], - signers: [multisig], - }); + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([multisig]) + .rpc(); let multisigAccount = await program.account.multisig.fetch( multisig.publicKey @@ -42,7 +40,6 @@ describe("multisig", () => { assert.strictEqual(multisigAccount.nonce, nonce); assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); assert.deepStrictEqual(multisigAccount.owners, owners); - assert.isTrue(multisigAccount.ownerSetSeqno === 0); const pid = program.programId; const accounts = [ @@ -63,21 +60,18 @@ describe("multisig", () => { }); const transaction = anchor.web3.Keypair.generate(); - const txSize = 1000; // Big enough, cuz I'm lazy. - await program.rpc.createTransaction(pid, accounts, data, { - accounts: { + + await program.methods + .createTransaction(pid, accounts, data) + .accounts({ + payer: program.provider.publicKey, multisig: multisig.publicKey, transaction: transaction.publicKey, proposer: ownerA.publicKey, - }, - instructions: [ - await program.account.transaction.createInstruction( - transaction, - txSize - ), - ], - signers: [transaction, ownerA], - }); + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([transaction, ownerA]) + .rpc(); const txAccount = await program.account.transaction.fetch( transaction.publicKey @@ -88,49 +82,55 @@ describe("multisig", () => { assert.deepStrictEqual(txAccount.data, data); assert.isTrue(txAccount.multisig.equals(multisig.publicKey)); assert.deepStrictEqual(txAccount.didExecute, false); - assert.isTrue(txAccount.ownerSetSeqno === 0); // Other owner approves transaction. - await program.rpc.approve({ - accounts: { + await program.methods + .approve() + .accounts({ multisig: multisig.publicKey, transaction: transaction.publicKey, owner: ownerB.publicKey, - }, - signers: [ownerB], - }); + }) + .signers([ownerB]) + .rpc(); + + const setOwnersIntruction = await program.methods + .setOwners([]) + .accounts({ + multisig: multisig.publicKey, + multisigSigner, + }) + .instruction(); // Now that we've reached the threshold, send the transaction. - await program.rpc.executeTransaction({ - accounts: { + await program.methods + .executeTransaction() + .accounts({ multisig: multisig.publicKey, multisigSigner, transaction: transaction.publicKey, - }, - remainingAccounts: program.instruction.setOwners - .accounts({ - multisig: multisig.publicKey, - multisigSigner, - }) - // Change the signer status on the vendor signer since it's signed by the program, not the client. - .map((meta) => - meta.pubkey.equals(multisigSigner) - ? { ...meta, isSigner: false } - : meta - ) - .concat({ - pubkey: program.programId, - isWritable: false, - isSigner: false, - }), - }); + }) + .remainingAccounts( + setOwnersIntruction.keys + // Change the signer status on the vendor signer since it's signed by the program, not the client. + .map((meta) => + meta.pubkey.equals(multisigSigner) + ? { ...meta, isSigner: false } + : meta + ) + .concat({ + pubkey: program.programId, + isWritable: false, + isSigner: false, + }) + ) + .rpc(); multisigAccount = await program.account.multisig.fetch(multisig.publicKey); assert.strictEqual(multisigAccount.nonce, nonce); assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); assert.deepStrictEqual(multisigAccount.owners, newOwners); - assert.isTrue(multisigAccount.ownerSetSeqno === 1); }); it("Assert Unique Owners", async () => { @@ -140,7 +140,6 @@ describe("multisig", () => { [multisig.publicKey.toBuffer()], program.programId ); - const multisigSize = 200; // Big enough. const ownerA = anchor.web3.Keypair.generate(); const ownerB = anchor.web3.Keypair.generate(); @@ -148,21 +147,18 @@ describe("multisig", () => { const threshold = new anchor.BN(2); try { - await program.rpc.createMultisig(owners, threshold, nonce, { - accounts: { + await program.methods + .createMultisig(owners, threshold, nonce) + .accounts({ + payer: program.provider.publicKey, multisig: multisig.publicKey, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - }, - instructions: [ - await program.account.multisig.createInstruction( - multisig, - multisigSize - ), - ], - signers: [multisig], - }); + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([multisig]) + .rpc(); assert.fail(); } catch (err) { + console.log(err); const error = err.error; assert.strictEqual(error.errorCode.number, 6008); assert.strictEqual(error.errorMessage, "Owners must be unique"); diff --git a/tests/multisig/tsconfig.json b/tests/multisig/tsconfig.json new file mode 100644 index 0000000000..cd5d2e3d06 --- /dev/null +++ b/tests/multisig/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} From 76b4fd3cede56bf6bc7187e202a27ae89a54b2ab Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 30 Dec 2022 14:13:12 +0100 Subject: [PATCH 08/18] Try to run multisig another time --- tests/multisig/tests/multisig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/multisig/tests/multisig.ts b/tests/multisig/tests/multisig.ts index 786c162e66..8df932bce6 100644 --- a/tests/multisig/tests/multisig.ts +++ b/tests/multisig/tests/multisig.ts @@ -1,5 +1,4 @@ import * as anchor from "@coral-xyz/anchor"; -import { AnchorError } from "@coral-xyz/anchor"; import { assert } from "chai"; import { Multisig } from "../target/types/multisig"; From 51540cd979aa3f31309c978218eee38896eb907a Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Thu, 5 Jan 2023 15:43:09 +0100 Subject: [PATCH 09/18] Revert InitSpace derive by default --- lang/attribute/account/src/lib.rs | 20 +- lang/src/idl.rs | 2 +- lang/tests/space.rs | 3 + tests/multisig/Anchor.toml | 2 +- tests/multisig/programs/multisig/src/lib.rs | 194 ++++++-------------- tests/multisig/tests/multisig.js | 135 ++++++++++++++ tests/multisig/tests/multisig.ts | 166 ----------------- tests/multisig/tsconfig.json | 10 - tests/realloc/programs/realloc/src/lib.rs | 2 +- 9 files changed, 206 insertions(+), 328 deletions(-) create mode 100644 tests/multisig/tests/multisig.js delete mode 100644 tests/multisig/tests/multisig.ts delete mode 100644 tests/multisig/tsconfig.json diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index 977be83d25..b8bc989b6e 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -67,36 +67,29 @@ pub fn account( ) -> proc_macro::TokenStream { let mut namespace = "".to_string(); let mut is_zero_copy = false; - let mut skip_init_space = false; let args_str = args.to_string(); let args: Vec<&str> = args_str.split(',').collect(); if args.len() > 2 { panic!("Only two args are allowed to the account attribute.") } for arg in args { - let ns: String = arg + let ns = arg .to_string() .replace('\"', "") .chars() .filter(|c| !c.is_whitespace()) .collect(); - - match ns.as_str() { - "zero_copy" => is_zero_copy = true, - "skip_space" => skip_init_space = true, - _ => namespace = ns, - }; + if ns == "zero_copy" { + is_zero_copy = true; + } else { + namespace = ns; + } } let account_strct = parse_macro_input!(input as syn::ItemStruct); let account_name = &account_strct.ident; let account_name_str = account_name.to_string(); let (impl_gen, type_gen, where_clause) = account_strct.generics.split_for_impl(); - let init_space = if skip_init_space { - quote!() - } else { - quote!(#[derive(InitSpace)]) - }; let discriminator: proc_macro2::TokenStream = { // Namespace the discriminator to prevent collisions. @@ -178,7 +171,6 @@ pub fn account( } } else { quote! { - #init_space #[derive(AnchorSerialize, AnchorDeserialize, Clone)] #account_strct diff --git a/lang/src/idl.rs b/lang/src/idl.rs index b008b8f60f..8bbd89316a 100644 --- a/lang/src/idl.rs +++ b/lang/src/idl.rs @@ -90,7 +90,7 @@ pub struct IdlSetBuffer<'info> { // // Note: we use the same account for the "write buffer", similar to the // bpf upgradeable loader's mechanism. -#[account("internal", skip_space)] +#[account("internal")] #[derive(Debug)] pub struct IdlAccount { // Address that can modify the IDL. diff --git a/lang/tests/space.rs b/lang/tests/space.rs index 65fe5cb965..49bda6f6ff 100644 --- a/lang/tests/space.rs +++ b/lang/tests/space.rs @@ -19,9 +19,11 @@ pub enum TestBasicEnum { } #[account] +#[derive(InitSpace)] pub struct TestEmptyAccount {} #[account] +#[derive(InitSpace)] pub struct TestBasicVarAccount { pub test_u8: u8, pub test_u16: u16, @@ -31,6 +33,7 @@ pub struct TestBasicVarAccount { } #[account] +#[derive(InitSpace)] pub struct TestComplexeVarAccount { pub test_key: Pubkey, #[max_len(10)] diff --git a/tests/multisig/Anchor.toml b/tests/multisig/Anchor.toml index ec450a19a7..ee9f941dcc 100644 --- a/tests/multisig/Anchor.toml +++ b/tests/multisig/Anchor.toml @@ -6,6 +6,6 @@ wallet = "~/.config/solana/id.json" multisig = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" +test = "yarn run mocha -t 1000000 tests/" [features] diff --git a/tests/multisig/programs/multisig/src/lib.rs b/tests/multisig/programs/multisig/src/lib.rs index 472a63c38a..91ac9ff799 100644 --- a/tests/multisig/programs/multisig/src/lib.rs +++ b/tests/multisig/programs/multisig/src/lib.rs @@ -17,16 +17,16 @@ //! the `execute_transaction`, once enough (i.e. `threshold`) of the owners have //! signed. +use anchor_lang::accounts::program_account::ProgramAccount; use anchor_lang::prelude::*; use anchor_lang::solana_program; use anchor_lang::solana_program::instruction::Instruction; use std::convert::Into; -use std::ops::Deref; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] -pub mod coral_multisig { +pub mod multisig { use super::*; // Initializes a new multisig account with a set of owners and a threshold. @@ -36,18 +36,10 @@ pub mod coral_multisig { threshold: u64, nonce: u8, ) -> Result<()> { - assert_unique_owners(&owners)?; - require!( - threshold > 0 && threshold <= owners.len() as u64, - MultisigError::InvalidThreshold - ); - require!(!owners.is_empty(), MultisigError::InvalidOwnersLen); - let multisig = &mut ctx.accounts.multisig; multisig.owners = owners; multisig.threshold = threshold; multisig.nonce = nonce; - multisig.owner_set_seqno = 0; Ok(()) } @@ -65,7 +57,7 @@ pub mod coral_multisig { .owners .iter() .position(|a| a == ctx.accounts.proposer.key) - .ok_or(MultisigError::InvalidOwner)?; + .ok_or(error!(ErrorCode::InvalidOwner))?; let mut signers = Vec::new(); signers.resize(ctx.accounts.multisig.owners.len(), false); @@ -76,9 +68,8 @@ pub mod coral_multisig { tx.accounts = accs; tx.data = data; tx.signers = signers; - tx.multisig = ctx.accounts.multisig.key(); + tx.multisig = *ctx.accounts.multisig.to_account_info().key; tx.did_execute = false; - tx.owner_set_seqno = ctx.accounts.multisig.owner_set_seqno; Ok(()) } @@ -91,37 +82,16 @@ pub mod coral_multisig { .owners .iter() .position(|a| a == ctx.accounts.owner.key) - .ok_or(MultisigError::InvalidOwner)?; + .ok_or(error!(ErrorCode::InvalidOwner))?; ctx.accounts.transaction.signers[owner_index] = true; Ok(()) } - // Set owners and threshold at once. - pub fn set_owners_and_change_threshold<'info>( - ctx: Context<'_, '_, '_, 'info, Auth<'info>>, - owners: Vec, - threshold: u64, - ) -> Result<()> { - set_owners( - Context::new( - ctx.program_id, - ctx.accounts, - ctx.remaining_accounts, - ctx.bumps.clone(), - ), - owners, - )?; - change_threshold(ctx, threshold) - } - // Sets the owners field on the multisig. The only way this can be invoked // is via a recursive call from execute_transaction -> set_owners. pub fn set_owners(ctx: Context, owners: Vec) -> Result<()> { - assert_unique_owners(&owners)?; - require!(!owners.is_empty(), MultisigError::InvalidOwnersLen); - let multisig = &mut ctx.accounts.multisig; if (owners.len() as u64) < multisig.threshold { @@ -129,8 +99,6 @@ pub mod coral_multisig { } multisig.owners = owners; - multisig.owner_set_seqno += 1; - Ok(()) } @@ -138,12 +106,9 @@ pub mod coral_multisig { // invoked is via a recursive call from execute_transaction -> // change_threshold. pub fn change_threshold(ctx: Context, threshold: u64) -> Result<()> { - require_gt!( - threshold, - ctx.accounts.multisig.owners.len() as u64, - MultisigError::InvalidThreshold - ); - + if threshold > ctx.accounts.multisig.owners.len() as u64 { + return err!(ErrorCode::InvalidThreshold); + } let multisig = &mut ctx.accounts.multisig; multisig.threshold = threshold; Ok(()) @@ -153,39 +118,45 @@ pub mod coral_multisig { pub fn execute_transaction(ctx: Context) -> Result<()> { // Has this been executed already? if ctx.accounts.transaction.did_execute { - return Err(MultisigError::AlreadyExecuted.into()); + return err!(ErrorCode::AlreadyExecuted); } - // Do we have enough signers. + // Do we have enough signers? let sig_count = ctx .accounts .transaction .signers .iter() - .filter(|&did_sign| *did_sign) - .count() as u64; + .filter_map(|s| match s { + false => None, + true => Some(true), + }) + .collect::>() + .len() as u64; if sig_count < ctx.accounts.multisig.threshold { - return Err(MultisigError::NotEnoughSigners.into()); + return err!(ErrorCode::NotEnoughSigners); } // Execute the transaction signed by the multisig. - let mut ix: Instruction = ctx.accounts.transaction.deref().into(); + let mut ix: Instruction = (&*ctx.accounts.transaction).into(); ix.accounts = ix .accounts .iter() .map(|acc| { - let mut acc = acc.clone(); if &acc.pubkey == ctx.accounts.multisig_signer.key { - acc.is_signer = true; + AccountMeta::new_readonly(acc.pubkey, true) + } else { + acc.clone() } - acc }) .collect(); - let multisig_key = ctx.accounts.multisig.key(); - let seeds = &[multisig_key.as_ref(), &[ctx.accounts.multisig.nonce]]; + let seeds = &[ + ctx.accounts.multisig.to_account_info().key.as_ref(), + &[ctx.accounts.multisig.nonce], + ]; let signer = &[&seeds[..]]; let accounts = ctx.remaining_accounts; - solana_program::program::invoke_signed(&ix, accounts, signer)?; + solana_program::program::invoke_signed(&ix, &accounts, signer)?; // Burn the transaction to ensure one time use. ctx.accounts.transaction.did_execute = true; @@ -196,39 +167,24 @@ pub mod coral_multisig { #[derive(Accounts)] pub struct CreateMultisig<'info> { - #[account(mut)] - payer: Signer<'info>, - #[account( - init, - payer = payer, - space = 8 + Multisig::INIT_SPACE - )] - multisig: Account<'info, Multisig>, - system_program: Program<'info, System>, + #[account(zero)] + multisig: ProgramAccount<'info, Multisig>, } #[derive(Accounts)] pub struct CreateTransaction<'info> { - #[account(mut)] - payer: Signer<'info>, - multisig: Account<'info, Multisig>, - #[account( - init, - payer = payer, - space = 8 + Transaction::INIT_SPACE - )] - transaction: Account<'info, Transaction>, + multisig: ProgramAccount<'info, Multisig>, + #[account(zero)] + transaction: ProgramAccount<'info, Transaction>, // One of the owners. Checked in the handler. proposer: Signer<'info>, - system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Approve<'info> { - #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] - multisig: Account<'info, Multisig>, + multisig: ProgramAccount<'info, Multisig>, #[account(mut, has_one = multisig)] - transaction: Account<'info, Transaction>, + transaction: ProgramAccount<'info, Transaction>, // One of the multisig owners. Checked in the handler. owner: Signer<'info>, } @@ -236,77 +192,69 @@ pub struct Approve<'info> { #[derive(Accounts)] pub struct Auth<'info> { #[account(mut)] - multisig: Account<'info, Multisig>, + multisig: ProgramAccount<'info, Multisig>, #[account( - seeds = [multisig.key().as_ref()], + signer, + seeds = [multisig.to_account_info().key.as_ref()], bump = multisig.nonce, )] - multisig_signer: Signer<'info>, + multisig_signer: AccountInfo<'info>, } #[derive(Accounts)] pub struct ExecuteTransaction<'info> { - #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] - multisig: Account<'info, Multisig>, - /// CHECK: multisig_signer is a PDA program signer. Data is never read or written to + multisig: ProgramAccount<'info, Multisig>, #[account( - seeds = [multisig.key().as_ref()], + seeds = [multisig.to_account_info().key.as_ref()], bump = multisig.nonce, )] - multisig_signer: UncheckedAccount<'info>, + multisig_signer: AccountInfo<'info>, #[account(mut, has_one = multisig)] - transaction: Account<'info, Transaction>, + transaction: ProgramAccount<'info, Transaction>, } #[account] pub struct Multisig { - #[max_len(5)] - pub owners: Vec, - pub threshold: u64, - pub nonce: u8, - pub owner_set_seqno: u32, + owners: Vec, + threshold: u64, + nonce: u8, } #[account] pub struct Transaction { // The multisig account this transaction belongs to. - pub multisig: Pubkey, + multisig: Pubkey, // Target program to execute against. - pub program_id: Pubkey, - // Accounts requried for the transaction. - #[max_len(20)] - pub accounts: Vec, + program_id: Pubkey, + // Accounts required for the transaction. + accounts: Vec, // Instruction data for the transaction. - #[max_len(256)] - pub data: Vec, + data: Vec, // signers[index] is true iff multisig.owners[index] signed the transaction. - #[max_len(5)] - pub signers: Vec, + signers: Vec, // Boolean ensuring one time execution. - pub did_execute: bool, - // Owner set sequence number. - pub owner_set_seqno: u32, + did_execute: bool, } impl From<&Transaction> for Instruction { fn from(tx: &Transaction) -> Instruction { Instruction { program_id: tx.program_id, - accounts: tx.accounts.iter().map(Into::into).collect(), + accounts: tx.accounts.clone().into_iter().map(Into::into).collect(), data: tx.data.clone(), } } } -#[derive(AnchorSerialize, AnchorDeserialize, InitSpace, Clone)] +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct TransactionAccount { - pub pubkey: Pubkey, - pub is_signer: bool, - pub is_writable: bool, + pubkey: Pubkey, + is_signer: bool, + is_writable: bool, } -impl From<&TransactionAccount> for AccountMeta { - fn from(account: &TransactionAccount) -> AccountMeta { +impl From for AccountMeta { + fn from(account: TransactionAccount) -> AccountMeta { match account.is_writable { false => AccountMeta::new_readonly(account.pubkey, account.is_signer), true => AccountMeta::new(account.pubkey, account.is_signer), @@ -314,32 +262,10 @@ impl From<&TransactionAccount> for AccountMeta { } } -impl From<&AccountMeta> for TransactionAccount { - fn from(account_meta: &AccountMeta) -> TransactionAccount { - TransactionAccount { - pubkey: account_meta.pubkey, - is_signer: account_meta.is_signer, - is_writable: account_meta.is_writable, - } - } -} - -fn assert_unique_owners(owners: &[Pubkey]) -> Result<()> { - for (i, owner) in owners.iter().enumerate() { - require!( - !owners.iter().skip(i + 1).any(|item| item == owner), - MultisigError::UniqueOwners - ) - } - Ok(()) -} - #[error_code] -pub enum MultisigError { +pub enum ErrorCode { #[msg("The given owner is not part of this multisig.")] InvalidOwner, - #[msg("Owners length must be non zero.")] - InvalidOwnersLen, #[msg("Not enough owners signed this transaction.")] NotEnoughSigners, #[msg("Cannot delete a transaction that has been signed by an owner.")] @@ -352,6 +278,4 @@ pub enum MultisigError { AlreadyExecuted, #[msg("Threshold must be less than or equal to the number of owners.")] InvalidThreshold, - #[msg("Owners must be unique")] - UniqueOwners, } diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.js new file mode 100644 index 0000000000..e95e6702ac --- /dev/null +++ b/tests/multisig/tests/multisig.js @@ -0,0 +1,135 @@ +const anchor = require("@coral-xyz/anchor"); +const { assert } = require("chai"); + +describe("multisig", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + + const program = anchor.workspace.Multisig; + + it("Tests the multisig program", async () => { + const multisig = anchor.web3.Keypair.generate(); + const [multisigSigner, nonce] = + await anchor.web3.PublicKey.findProgramAddress( + [multisig.publicKey.toBuffer()], + program.programId + ); + const multisigSize = 200; // Big enough. + + const ownerA = anchor.web3.Keypair.generate(); + const ownerB = anchor.web3.Keypair.generate(); + const ownerC = anchor.web3.Keypair.generate(); + const ownerD = anchor.web3.Keypair.generate(); + const owners = [ownerA.publicKey, ownerB.publicKey, ownerC.publicKey]; + + const threshold = new anchor.BN(2); + await program.rpc.createMultisig(owners, threshold, nonce, { + accounts: { + multisig: multisig.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + instructions: [ + await program.account.multisig.createInstruction( + multisig, + multisigSize + ), + ], + signers: [multisig], + }); + + let multisigAccount = await program.account.multisig.fetch( + multisig.publicKey + ); + + assert.strictEqual(multisigAccount.nonce, nonce); + assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); + assert.deepEqual(multisigAccount.owners, owners); + + const pid = program.programId; + const accounts = [ + { + pubkey: multisig.publicKey, + isWritable: true, + isSigner: false, + }, + { + pubkey: multisigSigner, + isWritable: false, + isSigner: true, + }, + ]; + const newOwners = [ownerA.publicKey, ownerB.publicKey, ownerD.publicKey]; + const data = program.coder.instruction.encode("set_owners", { + owners: newOwners, + }); + + const transaction = anchor.web3.Keypair.generate(); + const txSize = 1000; // Big enough, cuz I'm lazy. + await program.rpc.createTransaction(pid, accounts, data, { + accounts: { + multisig: multisig.publicKey, + transaction: transaction.publicKey, + proposer: ownerA.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + instructions: [ + await program.account.transaction.createInstruction( + transaction, + txSize + ), + ], + signers: [transaction, ownerA], + }); + + const txAccount = await program.account.transaction.fetch( + transaction.publicKey + ); + + assert.isTrue(txAccount.programId.equals(pid)); + assert.deepEqual(txAccount.accounts, accounts); + assert.deepEqual(txAccount.data, data); + assert.isTrue(txAccount.multisig.equals(multisig.publicKey)); + assert.strictEqual(txAccount.didExecute, false); + + // Other owner approves transaction. + await program.rpc.approve({ + accounts: { + multisig: multisig.publicKey, + transaction: transaction.publicKey, + owner: ownerB.publicKey, + }, + signers: [ownerB], + }); + + // Now that we've reached the threshold, send the transaction. + await program.rpc.executeTransaction({ + accounts: { + multisig: multisig.publicKey, + multisigSigner, + transaction: transaction.publicKey, + }, + remainingAccounts: program.instruction.setOwners + .accounts({ + multisig: multisig.publicKey, + multisigSigner, + }) + // Change the signer status on the vendor signer since it's signed by the program, not the client. + .map((meta) => + meta.pubkey.equals(multisigSigner) + ? { ...meta, isSigner: false } + : meta + ) + .concat({ + pubkey: program.programId, + isWritable: false, + isSigner: false, + }), + }); + + multisigAccount = await program.account.multisig.fetch(multisig.publicKey); + + assert.strictEqual(multisigAccount.nonce, nonce); + assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); + assert.deepEqual(multisigAccount.owners, newOwners); + }); +}); \ No newline at end of file diff --git a/tests/multisig/tests/multisig.ts b/tests/multisig/tests/multisig.ts deleted file mode 100644 index 8df932bce6..0000000000 --- a/tests/multisig/tests/multisig.ts +++ /dev/null @@ -1,166 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { assert } from "chai"; -import { Multisig } from "../target/types/multisig"; - -describe("multisig", () => { - // Configure the client to use the local cluster. - anchor.setProvider(anchor.AnchorProvider.env()); - - const program = anchor.workspace.Multisig as anchor.Program; - - it("Tests the multisig program", async () => { - const multisig = anchor.web3.Keypair.generate(); - const [multisigSigner, nonce] = - anchor.web3.PublicKey.findProgramAddressSync( - [multisig.publicKey.toBuffer()], - program.programId - ); - - const ownerA = anchor.web3.Keypair.generate(); - const ownerB = anchor.web3.Keypair.generate(); - const ownerC = anchor.web3.Keypair.generate(); - const ownerD = anchor.web3.Keypair.generate(); - const owners = [ownerA.publicKey, ownerB.publicKey, ownerC.publicKey]; - - const threshold = new anchor.BN(2); - await program.methods - .createMultisig(owners, threshold, nonce) - .accounts({ - payer: program.provider.publicKey, - multisig: multisig.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([multisig]) - .rpc(); - - let multisigAccount = await program.account.multisig.fetch( - multisig.publicKey - ); - assert.strictEqual(multisigAccount.nonce, nonce); - assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); - assert.deepStrictEqual(multisigAccount.owners, owners); - - const pid = program.programId; - const accounts = [ - { - pubkey: multisig.publicKey, - isWritable: true, - isSigner: false, - }, - { - pubkey: multisigSigner, - isWritable: false, - isSigner: true, - }, - ]; - const newOwners = [ownerA.publicKey, ownerB.publicKey, ownerD.publicKey]; - const data = program.coder.instruction.encode("set_owners", { - owners: newOwners, - }); - - const transaction = anchor.web3.Keypair.generate(); - - await program.methods - .createTransaction(pid, accounts, data) - .accounts({ - payer: program.provider.publicKey, - multisig: multisig.publicKey, - transaction: transaction.publicKey, - proposer: ownerA.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([transaction, ownerA]) - .rpc(); - - const txAccount = await program.account.transaction.fetch( - transaction.publicKey - ); - - assert.isTrue(txAccount.programId.equals(pid)); - assert.deepStrictEqual(txAccount.accounts, accounts); - assert.deepStrictEqual(txAccount.data, data); - assert.isTrue(txAccount.multisig.equals(multisig.publicKey)); - assert.deepStrictEqual(txAccount.didExecute, false); - - // Other owner approves transaction. - await program.methods - .approve() - .accounts({ - multisig: multisig.publicKey, - transaction: transaction.publicKey, - owner: ownerB.publicKey, - }) - .signers([ownerB]) - .rpc(); - - const setOwnersIntruction = await program.methods - .setOwners([]) - .accounts({ - multisig: multisig.publicKey, - multisigSigner, - }) - .instruction(); - - // Now that we've reached the threshold, send the transaction. - await program.methods - .executeTransaction() - .accounts({ - multisig: multisig.publicKey, - multisigSigner, - transaction: transaction.publicKey, - }) - .remainingAccounts( - setOwnersIntruction.keys - // Change the signer status on the vendor signer since it's signed by the program, not the client. - .map((meta) => - meta.pubkey.equals(multisigSigner) - ? { ...meta, isSigner: false } - : meta - ) - .concat({ - pubkey: program.programId, - isWritable: false, - isSigner: false, - }) - ) - .rpc(); - - multisigAccount = await program.account.multisig.fetch(multisig.publicKey); - - assert.strictEqual(multisigAccount.nonce, nonce); - assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); - assert.deepStrictEqual(multisigAccount.owners, newOwners); - }); - - it("Assert Unique Owners", async () => { - const multisig = anchor.web3.Keypair.generate(); - const [_multisigSigner, nonce] = - anchor.web3.PublicKey.findProgramAddressSync( - [multisig.publicKey.toBuffer()], - program.programId - ); - - const ownerA = anchor.web3.Keypair.generate(); - const ownerB = anchor.web3.Keypair.generate(); - const owners = [ownerA.publicKey, ownerB.publicKey, ownerA.publicKey]; - - const threshold = new anchor.BN(2); - try { - await program.methods - .createMultisig(owners, threshold, nonce) - .accounts({ - payer: program.provider.publicKey, - multisig: multisig.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([multisig]) - .rpc(); - assert.fail(); - } catch (err) { - console.log(err); - const error = err.error; - assert.strictEqual(error.errorCode.number, 6008); - assert.strictEqual(error.errorMessage, "Owners must be unique"); - } - }); -}); diff --git a/tests/multisig/tsconfig.json b/tests/multisig/tsconfig.json deleted file mode 100644 index cd5d2e3d06..0000000000 --- a/tests/multisig/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "types": ["mocha", "chai"], - "typeRoots": ["./node_modules/@types"], - "lib": ["es2015"], - "module": "commonjs", - "target": "es6", - "esModuleInterop": true - } -} diff --git a/tests/realloc/programs/realloc/src/lib.rs b/tests/realloc/programs/realloc/src/lib.rs index 217a52e6f5..a854e6a0a7 100644 --- a/tests/realloc/programs/realloc/src/lib.rs +++ b/tests/realloc/programs/realloc/src/lib.rs @@ -99,7 +99,7 @@ pub struct Realloc2<'info> { pub system_program: Program<'info, System>, } -#[account(skip_space)] +#[account] pub struct Sample { pub data: Vec, pub bump: u8, From 5187401a66c7e98b7087dc9c7e2fa0871fd20e56 Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 6 Jan 2023 10:38:19 +0100 Subject: [PATCH 10/18] Fix examples --- tests/cashiers-check/programs/cashiers-check/src/lib.rs | 2 +- tests/chat/programs/chat/src/lib.rs | 1 + tests/ido-pool/programs/ido-pool/src/lib.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/cashiers-check/programs/cashiers-check/src/lib.rs b/tests/cashiers-check/programs/cashiers-check/src/lib.rs index ed04ef54ee..9cdae60d7d 100644 --- a/tests/cashiers-check/programs/cashiers-check/src/lib.rs +++ b/tests/cashiers-check/programs/cashiers-check/src/lib.rs @@ -151,7 +151,7 @@ pub struct CancelCheck<'info> { token_program: AccountInfo<'info>, } -#[account(skip_space)] +#[account] pub struct Check { from: Pubkey, to: Pubkey, diff --git a/tests/chat/programs/chat/src/lib.rs b/tests/chat/programs/chat/src/lib.rs index 19bee6a6fd..a0474a309a 100644 --- a/tests/chat/programs/chat/src/lib.rs +++ b/tests/chat/programs/chat/src/lib.rs @@ -74,6 +74,7 @@ pub struct SendMessage<'info> { } #[account] +#[derive(InitSpace)] pub struct User { #[max_len(200)] name: String, diff --git a/tests/ido-pool/programs/ido-pool/src/lib.rs b/tests/ido-pool/programs/ido-pool/src/lib.rs index e7610771d3..53afe43ea3 100644 --- a/tests/ido-pool/programs/ido-pool/src/lib.rs +++ b/tests/ido-pool/programs/ido-pool/src/lib.rs @@ -545,6 +545,7 @@ pub struct WithdrawFromEscrow<'info> { } #[account] +#[derive(InitSpace)] pub struct IdoAccount { pub ido_name: [u8; 10], // Setting an arbitrary max of ten characters in the ido name. // 10 pub bumps: PoolBumps, // 4 From a9dff4fd3ab6767446730a38f1b8d7fb295d797e Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 6 Jan 2023 10:39:13 +0100 Subject: [PATCH 11/18] fix lint --- tests/multisig/tests/multisig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/multisig/tests/multisig.js b/tests/multisig/tests/multisig.js index e95e6702ac..f65e7d1822 100644 --- a/tests/multisig/tests/multisig.js +++ b/tests/multisig/tests/multisig.js @@ -132,4 +132,4 @@ describe("multisig", () => { assert.isTrue(multisigAccount.threshold.eq(new anchor.BN(2))); assert.deepEqual(multisigAccount.owners, newOwners); }); -}); \ No newline at end of file +}); From 5c85b7959db4c62ee2f8084e07c726ff32be66cb Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Fri, 6 Jan 2023 11:04:48 +0100 Subject: [PATCH 12/18] Fix the doc --- lang/derive/space/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index d95350d6c0..104e626ad9 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -13,6 +13,7 @@ use syn::{ /// # Example /// ```ignore /// #[account] +/// #[derive(InitSpace)] /// pub struct ExampleAccount { /// pub data: u64, /// } From f4d89a79a330e16eee7107589aad0fe0a9ca27fb Mon Sep 17 00:00:00 2001 From: Henry-E Date: Thu, 19 Jan 2023 11:11:58 +0000 Subject: [PATCH 13/18] out of date doc --- lang/attribute/account/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lang/attribute/account/src/lib.rs b/lang/attribute/account/src/lib.rs index b8bc989b6e..273be17322 100644 --- a/lang/attribute/account/src/lib.rs +++ b/lang/attribute/account/src/lib.rs @@ -16,7 +16,6 @@ mod id; /// - [`Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html) /// - [`Discriminator`](./trait.Discriminator.html) /// - [`Owner`](./trait.Owner.html) -/// - [`InitSpace`](./trait.InitSpace.html) /// /// When implementing account serialization traits the first 8 bytes are /// reserved for a unique account discriminator, self described by the first 8 From c60f13337eaa848bc22276194ae32f9397cc62e6 Mon Sep 17 00:00:00 2001 From: Henry-E Date: Thu, 19 Jan 2023 11:18:21 +0000 Subject: [PATCH 14/18] outdated I think these are outdated since we removed space from being part of the default `account` attribute macro --- lang/tests/generics_test.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/tests/generics_test.rs b/lang/tests/generics_test.rs index 2d28bf7a58..3ecb8f3257 100644 --- a/lang/tests/generics_test.rs +++ b/lang/tests/generics_test.rs @@ -11,8 +11,8 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[derive(Accounts)] pub struct GenericsTest<'info, T, U, const N: usize> where - T: AccountSerialize + AccountDeserialize + Space + Owner + Clone, - U: BorshSerialize + BorshDeserialize + Space + Default + Clone, + T: AccountSerialize + AccountDeserialize + Owner + Clone, + U: BorshSerialize + BorshDeserialize + Default + Clone, { pub non_generic: AccountInfo<'info>, pub generic: Account<'info, T>, @@ -31,7 +31,7 @@ pub struct FooAccount { #[derive(Default)] pub struct Associated where - T: BorshDeserialize + BorshSerialize + Space + Default, + T: BorshDeserialize + BorshSerialize + Default, { pub data: T, } From 48fb358258bad727540b492c85416fcd76f186ac Mon Sep 17 00:00:00 2001 From: Henry-E Date: Thu, 19 Jan 2023 11:19:43 +0000 Subject: [PATCH 15/18] another outdated thing --- tests/custom-coder/programs/native-system/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/custom-coder/programs/native-system/src/lib.rs b/tests/custom-coder/programs/native-system/src/lib.rs index 99fd5ae66c..8ac2b71574 100644 --- a/tests/custom-coder/programs/native-system/src/lib.rs +++ b/tests/custom-coder/programs/native-system/src/lib.rs @@ -204,7 +204,7 @@ pub struct FeeCalculator { pub lamports_per_signature: u64, } -#[account(skip_space)] +#[account] pub struct Nonce { pub version: u32, pub state: u32, From 41414b831b8668e231fdfef9c5586f5a5d361bdb Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Wed, 25 Jan 2023 11:27:40 +0100 Subject: [PATCH 16/18] Add some doc --- lang/derive/space/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index 104e626ad9..6bdf7c1ceb 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -10,12 +10,19 @@ use syn::{ /// Implements a [`Space`](./trait.Space.html) trait on the given /// struct or enum. /// +/// For types that have a variable size like String and Vec, it is necessary to indicate the size by the `max_len` attribute. +/// For nested types, it is necessary to specify a size for each variable type (see example). +/// /// # Example /// ```ignore /// #[account] /// #[derive(InitSpace)] /// pub struct ExampleAccount { /// pub data: u64, +/// #[max_len(50)] +/// pub string_one: String, +/// #[max_len(10, 5)] +/// pub nested: Vec>, /// } /// /// #[derive(Accounts)] From 3969d4b18ace8820b78f1ee454938657b74bda71 Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Wed, 25 Jan 2023 18:29:09 +0100 Subject: [PATCH 17/18] fix full path issue --- lang/derive/space/src/lib.rs | 6 +++--- lang/tests/space.rs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lang/derive/space/src/lib.rs b/lang/derive/space/src/lib.rs index 6bdf7c1ceb..aba6840617 100644 --- a/lang/derive/space/src/lib.rs +++ b/lang/derive/space/src/lib.rs @@ -100,8 +100,8 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 let type_len = len_from_type(*elem, attrs); quote!((#array_len * #type_len)) } - Type::Path(path) => { - let path_segment = path.path.segments.last().unwrap(); + Type::Path(ty_path) => { + let path_segment = ty_path.path.segments.last().unwrap(); let ident = &path_segment.ident; let type_name = ident.to_string(); let first_ty = get_first_ty_arg(&path_segment.arguments); @@ -137,7 +137,7 @@ fn len_from_type(ty: Type, attrs: &mut Option>) -> TokenStream2 } } _ => { - let ty = &path_segment.ident; + let ty = &ty_path.path; quote!(<#ty as anchor_lang::Space>::INIT_SPACE) } } diff --git a/lang/tests/space.rs b/lang/tests/space.rs index 49bda6f6ff..47c154f563 100644 --- a/lang/tests/space.rs +++ b/lang/tests/space.rs @@ -3,6 +3,15 @@ use anchor_lang::prelude::*; // Needed to declare accounts. declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +mod inside_mod { + use super::*; + + #[derive(InitSpace)] + pub struct Data { + pub data: u64, + } +} + #[derive(InitSpace)] pub enum TestBasicEnum { Basic1, @@ -73,6 +82,12 @@ pub struct TestMatrixStruct { pub test_matrix: Vec>, } +#[derive(InitSpace)] +pub struct TestFullPath { + pub test_option_path: Option, + pub test_path: inside_mod::Data, +} + #[test] fn test_empty_struct() { assert_eq!(TestEmptyAccount::INIT_SPACE, 0); @@ -113,3 +128,8 @@ fn test_nested_struct() { fn test_matrix_struct() { assert_eq!(TestMatrixStruct::INIT_SPACE, 4 + (2 * (4 + 4))) } + +#[test] +fn test_full_path() { + assert_eq!(TestFullPath::INIT_SPACE, 8 + 9) +} From 169b3563f8eee2a33595db68589a8c01fa49de0b Mon Sep 17 00:00:00 2001 From: Jean Marchand Date: Thu, 26 Jan 2023 12:07:47 +0100 Subject: [PATCH 18/18] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c03d06286..ae88049490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features - cli: Add `env` option to verifiable builds ([#2325](https://github.com/coral-xyz/anchor/pull/2325)). +- lang: Add the `InitSpace` derive macro to automatically calculate the space at the initialization of an account ([#2346](https://github.com/coral-xyz/anchor/pull/2346)). ### Fixes