diff --git a/Anchor.toml b/Anchor.toml index 3902748c..f2f23fe3 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -11,10 +11,12 @@ world = "WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n" component-small = "9yBADAhoTWCkNRB6hbfpwUgPpxyJiF9uEiWVPR6k7A4y" escrow-funding = "4Um2d8SvyfWyLLtfu2iJMFhM77DdjjyQusEy7K3VhPkd" example-bundle = "CgfPBUeDUL3GT6b5AUDFE56KKgU4ycWA9ERjEWsfMZCj" +large = "DEABKyknxGaCbkthh1mPSSMQnJrSLRJyZShdKpsyrjdL" position = "Fn1JzzEdyb55fsyduWS94mYHizGhJZuhvjX6DVvrmGbQ" system-apply-velocity = "6LHhFVwif6N9Po3jHtSmMVtPjF6zRfL3xMosSzcrQAS8" system-fly = "HT2YawJjkNmqWcLNfPAMvNsLdWwPvvvbKA5bpMw4eUpq" system-simple-movement = "FSa6qoJXFBR3a7ThQkTAMrC15p6NkchPEjBdd4n6dXxA" +with-large-component = "4X7t7eCjd2pvNThY9DQ3rKQKNLeeHhgEv74JGwh7jJEz" velocity = "CbHEFbSQdRN4Wnoby9r16umnJ1zWbULBHg4yqzGQonU1" with-1-component = "BsVKJF2H9GN1P9WrexdgEY4ztiweKvfQo6ydLWUEw6n7" with-10-components = "C69UYWaXBQXUbhHQGtG8pB7DHSgh2z5Sm9ifyAnM1kkt" @@ -35,7 +37,7 @@ cluster = "localnet" wallet = "./tests/fixtures/provider.json" [workspace] -members = ["crates/programs/world", "examples/component-position", "examples/component-velocity", "examples/system-apply-velocity", "examples/system-fly", "examples/system-simple-movement", "examples/component-small", "examples/system-with-1-component", "examples/system-with-2-components", "examples/system-with-3-components", "examples/system-with-4-components", "examples/system-with-5-components", "examples/system-with-6-components", "examples/system-with-7-components", "examples/system-with-8-components", "examples/system-with-9-components", "examples/system-with-10-components", "examples/escrow-funding", "examples/bundle"] +members = ["crates/programs/world", "examples/component-position", "examples/component-velocity", "examples/system-apply-velocity", "examples/system-fly", "examples/system-simple-movement", "examples/component-small", "examples/component-large", "examples/with-large-component", "examples/system-with-1-component", "examples/system-with-2-components", "examples/system-with-3-components", "examples/system-with-4-components", "examples/system-with-5-components", "examples/system-with-6-components", "examples/system-with-7-components", "examples/system-with-8-components", "examples/system-with-9-components", "examples/system-with-10-components", "examples/escrow-funding", "examples/bundle"] [scripts] test = "tests/script.sh" diff --git a/Cargo.lock b/Cargo.lock index 3b965c58..eee49441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2686,6 +2686,13 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "large" +version = "0.2.6" +dependencies = [ + "bolt-lang", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -7176,6 +7183,15 @@ dependencies = [ "small", ] +[[package]] +name = "with-large-component" +version = "0.2.6" +dependencies = [ + "bolt-lang", + "large", + "serde", +] + [[package]] name = "world" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index 6f61907f..a1d46e27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ bolt-attribute-bolt-component-id = { path = "crates/bolt-lang/attribute/componen bolt-utils = { path = "crates/bolt-lang/utils", version = "=0.2.6" } world = { path = "crates/programs/world", features = ["cpi"], version = "=0.2.6"} small = { path = "examples/component-small", features = ["cpi"], version = "=0.2.6"} -component-large = { path = "examples/component-large", features = ["cpi"], version = "=0.2.6"} +large = { path = "examples/component-large", features = ["cpi"], version = "=0.2.6"} ## External crates session-keys = { version = "^2", features = ["no-entrypoint"] } diff --git a/clients/typescript/test/framework.ts b/clients/typescript/test/framework.ts index 031830e8..c7b2977b 100644 --- a/clients/typescript/test/framework.ts +++ b/clients/typescript/test/framework.ts @@ -27,6 +27,8 @@ import { With8Components } from "../../../target/types/with_8_components"; import { With9Components } from "../../../target/types/with_9_components"; import { With10Components } from "../../../target/types/with_10_components"; import { ExampleBundle } from "../../../target/types/example_bundle"; +import { WithLargeComponent } from "../../../target/types/with_large_component"; +import { Large } from "../../../target/types/large"; export class Framework { provider: anchor.AnchorProvider; @@ -48,7 +50,9 @@ export class Framework { systemWith8Components: anchor.Program; systemWith9Components: anchor.Program; systemWith10Components: anchor.Program; + systemWithLargeComponent: anchor.Program; componentSmall: anchor.Program; + componentLarge: anchor.Program; worldPda: PublicKey; worldId: BN; @@ -76,6 +80,8 @@ export class Framework { this.systemFly = anchor.workspace.SystemFly; this.systemApplyVelocity = anchor.workspace.SystemApplyVelocity; this.componentSmall = anchor.workspace.Small; + this.componentLarge = anchor.workspace.Large; + this.systemWithLargeComponent = anchor.workspace.WithLargeComponent; this.systemWith1Component = anchor.workspace.With1Component; this.systemWith2Components = anchor.workspace.With2Components; this.systemWith3Components = anchor.workspace.With3Components; diff --git a/clients/typescript/test/intermediate-level/ecs.ts b/clients/typescript/test/intermediate-level/ecs.ts index 031bc8ed..beff88eb 100644 --- a/clients/typescript/test/intermediate-level/ecs.ts +++ b/clients/typescript/test/intermediate-level/ecs.ts @@ -427,19 +427,27 @@ export function ecs(framework: Framework) { })), }); - let signature = await framework.provider.sendAndConfirm( - applySystem.transaction, - ); - - let transactionResponse: any; - do { - transactionResponse = - await framework.provider.connection.getTransaction(signature, { - commitment: "confirmed", - }); - } while (transactionResponse?.meta?.logMessages === undefined); - let report = framework.report(transactionResponse?.meta?.logMessages); - reports.push(report); + try { + let signature = await framework.provider.sendAndConfirm( + applySystem.transaction, + ); + + let transactionResponse: any; + do { + transactionResponse = + await framework.provider.connection.getTransaction(signature, { + commitment: "confirmed", + }); + } while (transactionResponse?.meta?.logMessages === undefined); + let report = framework.report(transactionResponse?.meta?.logMessages); + reports.push(report); + } catch (error) { + reports.push({ + cpiCount: 0, + totalCu: 0, + totalCpiCU: 0, + }); + } } framework.saveReport(reports); diff --git a/clients/typescript/test/low-level/ecs.ts b/clients/typescript/test/low-level/ecs.ts index 985f1390..0b2d6c0f 100644 --- a/clients/typescript/test/low-level/ecs.ts +++ b/clients/typescript/test/low-level/ecs.ts @@ -89,6 +89,51 @@ export function ecs(framework) { await framework.provider.sendAndConfirm(transaction); }); + it("Initialize Large Component on Entity 4", async () => { + const componentId = framework.componentLarge.programId; + framework.componentLargeEntity4Pda = FindComponentPda({ + componentId, + entity: framework.entity4Pda, + }); + const instruction = await framework.worldProgram.methods + .initializeComponent() + .accounts({ + payer: framework.provider.wallet.publicKey, + entity: framework.entity4Pda, + data: framework.componentLargeEntity4Pda, + componentProgram: componentId, + authority: framework.worldProgram.programId, + }) + .instruction(); + const transaction = new anchor.web3.Transaction().add(instruction); + await framework.provider.sendAndConfirm(transaction); + }); + + it("Apply system with large component on Entity 4", async () => { + const instruction = await framework.worldProgram.methods + .apply(SerializeArgs()) + .accounts({ + authority: framework.provider.wallet.publicKey, + boltSystem: framework.systemWithLargeComponent.programId, + world: framework.worldPda, + }) + .remainingAccounts([ + { + pubkey: framework.componentLarge.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: framework.componentLargeEntity4Pda, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + const transaction = new anchor.web3.Transaction().add(instruction); + await framework.provider.sendAndConfirm(transaction); + }); + it("Initialize Velocity Component on Entity 1 (with seed)", async () => { const componentId = framework.exampleComponentVelocity.programId; framework.componentVelocityEntity1Pda = FindComponentPda({ diff --git a/crates/bolt-lang/attribute/src/bundle/mod.rs b/crates/bolt-lang/attribute/src/bundle/mod.rs index 3a520900..ce87ccaa 100644 --- a/crates/bolt-lang/attribute/src/bundle/mod.rs +++ b/crates/bolt-lang/attribute/src/bundle/mod.rs @@ -11,6 +11,8 @@ pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream { let bundle_mod = parse_macro_input!(item as ItemMod); let mut program = generate_program(&bundle_mod.ident.to_string()); component::generate_update(&mut program); + component::generate_set_owner(&mut program); + component::generate_recover_metadata(&mut program); let mut delegate_components = Vec::new(); if let Some((_, items)) = bundle_mod.content { for item in items { diff --git a/crates/bolt-lang/attribute/src/common/mod.rs b/crates/bolt-lang/attribute/src/common/mod.rs index f94e6379..c178edc7 100644 --- a/crates/bolt-lang/attribute/src/common/mod.rs +++ b/crates/bolt-lang/attribute/src/common/mod.rs @@ -20,7 +20,7 @@ pub fn inject_program(module: &mut syn::ItemMod) { items.insert( 1, syn::Item::Struct(syn::parse_quote! { - #[derive(Accounts)] + #[derive(Accounts, Clone)] pub struct VariadicBoltComponents<'info> { pub authority: Signer<'info>, } diff --git a/crates/bolt-lang/attribute/src/component/generate/program.rs b/crates/bolt-lang/attribute/src/component/generate/program.rs index 4c92b1d9..1c68d776 100644 --- a/crates/bolt-lang/attribute/src/component/generate/program.rs +++ b/crates/bolt-lang/attribute/src/component/generate/program.rs @@ -187,6 +187,54 @@ fn generate_initialize( ) } +/// Generates the set_owner function and struct. +pub fn generate_set_owner(module: &mut ItemMod) { + let set_owner_fn = quote! { + #[automatically_derived] + pub fn set_owner(ctx: Context, owner: Pubkey) -> Result<()> { + bolt_lang::instructions::set_owner(&ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.component.to_account_info(), owner) + } + }; + let set_owner_struct = quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct SetOwner<'info> { + /// CHECK: This is a component account + #[account(mut)] + pub component: AccountInfo<'info>, + #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] + pub instruction_sysvar_account: AccountInfo<'info>, + } + }; + module.content.as_mut().map(|(brace, items)| { + items.extend(vec![set_owner_fn, set_owner_struct].into_iter().map(|item| syn::parse2(item).expect("Failed to parse generate set_owner item")).collect::>()); + (brace, items.clone()) + }); +} + +pub fn generate_recover_metadata(module: &mut ItemMod) { + let recover_metadata_fn = parse_quote! { + #[automatically_derived] + pub fn recover_metadata(ctx: Context, original_size: u32, discriminator: Vec, bolt_metadata: bolt_lang::BoltMetadata) -> Result<()> { + bolt_lang::instructions::recover_metadata(&ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.bolt_component, original_size, discriminator, bolt_metadata)?; + Ok(()) + } + }; + let recover_metadata_struct = parse_quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct RecoverMetadata<'info> { + #[account(mut)] + pub bolt_component: AccountInfo<'info>, + #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] + pub instruction_sysvar_account: AccountInfo<'info>, + } + }; + module.content.as_mut().map(|(brace, items)| { + items.extend(vec![recover_metadata_fn, recover_metadata_struct]); + (brace, items.clone()) + }); +} /// Generates the instructions and related structs to inject in the component. pub fn generate_update(module: &mut ItemMod) { let update_fn = quote! { diff --git a/crates/bolt-lang/attribute/src/component/mod.rs b/crates/bolt-lang/attribute/src/component/mod.rs index ba731c6e..5bf8ef24 100644 --- a/crates/bolt-lang/attribute/src/component/mod.rs +++ b/crates/bolt-lang/attribute/src/component/mod.rs @@ -21,6 +21,8 @@ pub fn process(attr: TokenStream, item: TokenStream) -> TokenStream { inject_delegate_items(&mut program, vec![(type_.ident.clone(), "".to_string())]); } generate_update(&mut program); + generate_set_owner(&mut program); + generate_recover_metadata(&mut program); enrich_type(&mut type_); let expanded = quote! { diff --git a/crates/bolt-lang/attribute/src/system/mod.rs b/crates/bolt-lang/attribute/src/system/mod.rs index 5981c1fa..e8122cfc 100644 --- a/crates/bolt-lang/attribute/src/system/mod.rs +++ b/crates/bolt-lang/attribute/src/system/mod.rs @@ -25,11 +25,23 @@ fn generate_bolt_execute_wrapper( bumps_ident: Ident, ) -> Item { parse_quote! { - pub fn #fn_ident<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, VariadicBoltComponents<'info>>, args: Vec) -> Result>> { - let mut components = #components_ident::try_from(&ctx)?; + pub fn #fn_ident<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, VariadicBoltComponents<'info>>, input: bolt_lang::context::BoltExecuteInput) -> Result<()> { + let mut input = input; + let input: &'info mut bolt_lang::context::BoltExecuteInput = unsafe { std::mem::transmute(&mut input) }; + let (rebuilt_context_data, args, buffer) = bolt_lang::context::ContextData::rebuild_from(&ctx, input); + let mut variadic = VariadicBoltComponents { + authority: ctx.accounts.authority.clone(), + }; + let variadic_bumps = VariadicBoltComponentsBumps {}; + let var_ctx = Context::new(ctx.program_id, &mut variadic, rebuilt_context_data.remaining_accounts, variadic_bumps); + let mut components = #components_ident::try_from(&var_ctx).expect("Failed to convert context to components"); let bumps = #bumps_ident {}; let context = Context::new(ctx.program_id, &mut components, ctx.remaining_accounts, bumps); - #callee_ident(context, args) + let result = #callee_ident(context, args)?; + let result = result.try_to_vec()?; + buffer.realloc(result.len(), false)?; + buffer.data.borrow_mut().copy_from_slice(&result); + Ok(()) } } } @@ -50,13 +62,37 @@ pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream { ); if !has_variadic { let variadic_struct: Item = parse_quote! { - #[derive(Accounts)] + #[derive(Accounts, Clone)] pub struct VariadicBoltComponents<'info> { pub authority: Signer<'info>, } }; items.insert(1, variadic_struct); } + let has_set_owner = items.iter().any( + |it| matches!(it, syn::Item::Fn(f) if f.sig.ident == "set_owner"), + ); + if !has_set_owner { + let set_owner_fn: Item = parse_quote! { + #[automatically_derived] + pub fn set_owner(ctx: Context, new_owner: Pubkey) -> Result<()> { + bolt_lang::instructions::set_owner(&ctx.accounts.instruction_sysvar_account.to_account_info(), &ctx.accounts.component.to_account_info(), new_owner) + } + }; + items.push(set_owner_fn); + let set_owner_struct = parse_quote! { + #[automatically_derived] + #[derive(Accounts)] + pub struct SetOwner<'info> { + /// CHECK: This is a component account + #[account(mut)] + pub component: AccountInfo<'info>, + #[account(address = anchor_lang::solana_program::sysvar::instructions::id())] + pub instruction_sysvar_account: AccountInfo<'info>, + } + }; + items.push(set_owner_struct); + } let wrapper = generate_bolt_execute_wrapper( Ident::new("bolt_execute", Span::call_site()), Ident::new("execute", Span::call_site()), diff --git a/crates/bolt-lang/src/account.rs b/crates/bolt-lang/src/account.rs new file mode 100644 index 00000000..0eba0563 --- /dev/null +++ b/crates/bolt-lang/src/account.rs @@ -0,0 +1,137 @@ +// TODO: Not used. Can be removed. + +use anchor_lang::prelude::*; + +/// BoltAccount used as a workaround for altering the account ownership check during deserialization. +/// P0 and P1 are the two parts of the pubkey and it's used on the Owner trait implementation. +#[derive(Clone)] +pub struct BoltAccount(T); + +impl Discriminator for BoltAccount { + const DISCRIMINATOR: &'static [u8] = T::DISCRIMINATOR; +} + +impl + anchor_lang::AccountDeserialize for BoltAccount +{ + fn try_deserialize(data: &mut &[u8]) -> Result { + Ok(BoltAccount(T::try_deserialize(data)?)) + } + + fn try_deserialize_unchecked(data: &mut &[u8]) -> Result { + Ok(BoltAccount(T::try_deserialize_unchecked(data)?)) + } +} + +impl anchor_lang::AccountSerialize + for BoltAccount +{ + fn try_serialize(&self, writer: &mut W) -> Result<()> { + self.0.try_serialize(writer) + } +} + +impl anchor_lang::Owner + for BoltAccount +{ + fn owner() -> Pubkey { + pubkey_from_array([P0, P1]) + } +} + +impl<'info, T: anchor_lang::ToAccountInfos<'info>, const P0: u128, const P1: u128> + anchor_lang::ToAccountInfos<'info> for BoltAccount +{ + fn to_account_infos(&self) -> Vec> { + self.0.to_account_infos() + } +} + +impl anchor_lang::ToAccountMetas + for BoltAccount +{ + fn to_account_metas(&self, is_signer: Option) -> Vec { + self.0.to_account_metas(is_signer) + } +} + +impl< + 'info, + T: anchor_lang::ToAccountInfos<'info> + + anchor_lang::ToAccountInfo<'info> + + anchor_lang::AccountSerialize + + anchor_lang::AccountsExit<'info>, + const P0: u128, + const P1: u128, + > anchor_lang::AccountsExit<'info> for BoltAccount +{ + fn exit(&self, _program_id: &Pubkey) -> Result<()> { + let info = self.0.to_account_info(); + let mut data = info.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut writer = crate::bpf_writer::BpfWriter::new(dst); + self.0.try_serialize(&mut writer)?; + Ok(()) + } +} + +impl std::ops::Deref for BoltAccount { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for BoltAccount { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(feature = "idl-build")] +impl anchor_lang::IdlBuild + for BoltAccount +{ + fn create_type() -> Option { + T::create_type() + } + fn insert_types( + types: &mut std::collections::BTreeMap, + ) { + T::insert_types(types); + } + fn get_full_path() -> String { + T::get_full_path() + } +} + +pub const fn pubkey_p0(key: Pubkey) -> u128 { + let bytes = key.to_bytes(); + u128::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], + bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + ]) +} + +pub const fn pubkey_p1(key: Pubkey) -> u128 { + let bytes = key.to_bytes(); + u128::from_le_bytes([ + bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]) +} + +pub const fn pubkey_from_u128s(p0: u128, p1: u128) -> Pubkey { + pubkey_from_array([p0, p1]) +} + +pub const fn pubkey_from_array(array: [u128; 2]) -> Pubkey { + let p0 = array[0].to_le_bytes(); + let p1 = array[1].to_le_bytes(); + Pubkey::new_from_array([ + p0[0], p0[1], p0[2], p0[3], p0[4], p0[5], p0[6], p0[7], p0[8], p0[9], p0[10], p0[11], + p0[12], p0[13], p0[14], p0[15], p1[0], p1[1], p1[2], p1[3], p1[4], p1[5], p1[6], p1[7], + p1[8], p1[9], p1[10], p1[11], p1[12], p1[13], p1[14], p1[15], + ]) +} \ No newline at end of file diff --git a/crates/bolt-lang/src/bpf_writer.rs b/crates/bolt-lang/src/bpf_writer.rs new file mode 100644 index 00000000..33432b7e --- /dev/null +++ b/crates/bolt-lang/src/bpf_writer.rs @@ -0,0 +1,47 @@ +/// Implementation from Anchor. +use solana_program::program_memory::sol_memcpy; +use std::cmp; +use std::io::{self, Write}; + +#[derive(Debug, Default)] +pub struct BpfWriter { + inner: T, + pub pos: u64, +} + +impl BpfWriter { + pub fn new(inner: T) -> Self { + Self { inner, pos: 0 } + } +} + +impl Write for BpfWriter<&mut [u8]> { + fn write(&mut self, buf: &[u8]) -> io::Result { + if self.pos >= self.inner.len() as u64 { + return Ok(0); + } + + let amt = cmp::min( + self.inner.len().saturating_sub(self.pos as usize), + buf.len(), + ); + sol_memcpy(&mut self.inner[(self.pos as usize)..], buf, amt); + self.pos += amt as u64; + Ok(amt) + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + if self.write(buf)? == buf.len() { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write whole buffer", + )) + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} \ No newline at end of file diff --git a/crates/bolt-lang/src/context/mod.rs b/crates/bolt-lang/src/context/mod.rs new file mode 100644 index 00000000..58dfbc8f --- /dev/null +++ b/crates/bolt-lang/src/context/mod.rs @@ -0,0 +1,252 @@ +use anchor_lang::{Bumps, prelude::*}; +use std::{cell::RefCell, rc::Rc}; + +pub struct ContextData<'info, T: Bumps> { + pub program_id: Pubkey, + pub accounts: T, + pub bumps: T::Bumps, + pub remaining_accounts: &'info [AccountInfo<'info>], + // Own storage for rebuilt remaining accounts when we need to override the first one + pub rebuilt_remaining_storage: Option]>>, + // Own storage for the first component's data and lamports to back AccountInfo::new + pub first_component_data: Option>, + pub first_component_lamports: u64, +} + +impl<'info, T: Bumps> ContextData<'info, T> { + pub fn rebuild_from<'a, 'b, 'c>(ctx: &Context<'a, 'b, 'c, 'info, T>, input: &'info mut BoltExecuteInput) -> (Self, Vec, AccountInfo<'info>) + where + T: Clone, + 'c: 'info, + { + let BoltExecuteInput { owner, pda_data, args } = input; + // Use the first remaining account as buffer (removed from remaining_accounts). + let buffer = ctx + .remaining_accounts + .get(0) + .expect("expected at least one remaining account") + .clone(); + // Rebuild first component account info using same key/owner/rent_epoch, writable and with data from input. + let key = buffer.key; + let rent_epoch = buffer.rent_epoch; + + // Build the context data and stash owned storage so the references live long enough + let bumps: T::Bumps = unsafe { std::mem::transmute_copy(&ctx.bumps) }; + let mut rebuilt = Self::new(*ctx.program_id, ctx.accounts.clone(), bumps, &[]); + // initialize owned storages + rebuilt.first_component_lamports = buffer.lamports(); + rebuilt.first_component_data = Some(std::mem::take(pda_data)); + // Create AccountInfo that borrows from rebuilt's owned fields + { + let is_signer = false; + let is_writable = true; + let executable = false; + // SAFETY: casting references to 'info is safe because `rebuilt` (returned Self) + // owns the backing storage for these references for at least 'info. + let lamports_ref: &'info mut u64 = + unsafe { &mut *(&mut rebuilt.first_component_lamports as *mut u64) }; + let data_slice_ref: &'info mut [u8] = unsafe { + let slice_ptr = rebuilt.first_component_data.as_mut().unwrap().as_mut_slice() as *mut [u8]; + &mut *slice_ptr + }; + let first_component = + AccountInfo::new(key, is_signer, is_writable, lamports_ref, data_slice_ref, owner, executable, rent_epoch); + // Build new remaining accounts storage: first is rebuilt component, rest are original (skipping buffer) + let mut storage_vec: Vec> = Vec::with_capacity(ctx.remaining_accounts.len()); + storage_vec.push(first_component); + storage_vec.extend_from_slice(&ctx.remaining_accounts[1..]); + rebuilt.rebuilt_remaining_storage = Some(storage_vec.into_boxed_slice()); + } + // Now point remaining_accounts to our owned storage + let ra_ref: &[AccountInfo<'info>] = rebuilt.rebuilt_remaining_storage.as_ref().unwrap().as_ref(); + // Extend lifetime to 'info; safe here because rebuilt owns the storage + rebuilt.remaining_accounts = unsafe { std::mem::transmute::<&[AccountInfo<'info>], &'info [AccountInfo<'info>]>(ra_ref) }; + (rebuilt, std::mem::take(args), buffer) + } + + pub fn new(program_id: Pubkey, accounts: T, bumps: T::Bumps, remaining_accounts: &'info [AccountInfo<'info>]) -> Self { + Self { + program_id, + accounts, + bumps, + remaining_accounts, + rebuilt_remaining_storage: None, + first_component_data: None, + first_component_lamports: 0, + } + } + + pub fn to_context(&'info mut self) -> Context<'info, 'info, 'info, 'info, T> + { + let bumps: T::Bumps = unsafe { std::mem::transmute_copy(&self.bumps) }; + Context { + program_id: &self.program_id, + accounts: &mut self.accounts, + remaining_accounts: self.remaining_accounts, + bumps + } + } + + #[cfg(test)] + pub fn test_data(components: T, bumps: T::Bumps, remaining_accounts: &'info [AccountInfo<'info>]) -> Self { + Self::new(crate::ID, components, bumps, remaining_accounts) + } +} +pub struct AccountData { + key: Pubkey, + owner: Pubkey, + data: Vec, + lamports: u64, + rent_epoch: u64, + is_signer: bool, + is_writable: bool, + executable: bool, +} + +impl AccountData { + pub fn new(key: Pubkey, owner: Pubkey, data: Vec, lamports: u64, rent_epoch: u64, is_signer: bool, is_writable: bool) -> Self { + let executable = false; + Self { + key, + owner, + data, + lamports, + rent_epoch, + is_signer, + is_writable, + executable, + } + } + + pub fn to_account_info<'info>(&'info mut self) -> AccountInfo<'info> { + AccountInfo { + key: &self.key, + lamports: Rc::new(RefCell::new(&mut self.lamports)), + data: Rc::new(RefCell::new(&mut self.data)), + owner: &self.owner, + rent_epoch: self.rent_epoch, + is_signer: self.is_signer, + is_writable: self.is_writable, + executable: self.executable, + } + } +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct BoltExecuteInput { + pub owner: Pubkey, + pub pda_data: Vec, + pub args: Vec, +} + +#[cfg(test)] +mod tests { + use anchor_lang::prelude::*; + + use super::*; + + impl AccountData { + pub fn test_data() -> [AccountData; 2] { + let one = Pubkey::from_str_const("11111111111111111111111111111111111111111111"); + + let two = Pubkey::from_str_const("22222222222222222222222222222222222222222222"); + let some_account = SomeAccount { data: 12u64 }; + let mut data = Vec::new(); + some_account.try_serialize(&mut data).expect("Failed to serialize account data"); + + [ + AccountData::new(one, crate::ID, vec![], 111, 1, true, false), + AccountData::new(two, crate::ID, data, 222, 1, false, true) + ] + } + } + + #[account] + pub struct SomeAccount { + pub data: u64, + } + + #[derive(Accounts, Clone)] + pub struct VariadicBoltComponents<'info> { + pub authority: Signer<'info>, + } + + impl<'info> VariadicBoltComponents<'info> { + pub fn test_data(authority: &'info AccountInfo<'info>) -> (Self, VariadicBoltComponentsBumps) { + ( + Self { + authority: Signer::try_from(authority).unwrap(), + }, + VariadicBoltComponentsBumps {} + ) + } + } + + fn bolt_execute<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, VariadicBoltComponents<'info>>, input: BoltExecuteInput) -> Result<()> { + let mut input = input; + let input: &'info mut BoltExecuteInput = unsafe { std::mem::transmute(&mut input) }; + let (rebuilt_context_data, args, buffer) = ContextData::rebuild_from(&ctx, input); + let mut variadic = VariadicBoltComponents { + authority: ctx.accounts.authority.clone(), + }; + let variadic_bumps = VariadicBoltComponentsBumps {}; + let var_ctx = Context::new(ctx.program_id, &mut variadic, rebuilt_context_data.remaining_accounts, variadic_bumps); + let mut components = Components::try_from(&var_ctx).expect("Failed to convert context to components"); + let bumps = ComponentsBumps {}; + let context = Context::new(ctx.program_id, &mut components, ctx.remaining_accounts, bumps); + let result = execute(context, args)?; + let _result = result.try_to_vec()?; + // Doesn't work on test + // buffer.realloc(result.len(), false)?; + // buffer.data.borrow_mut().copy_from_slice(&result); + Ok(()) + } + + fn execute<'a, 'b, 'info>(ctx: Context<'a, 'b, 'info, 'info, Components<'info>>, args: Vec) -> Result>> { + assert_eq!(args, vec![1, 2, 3]); + assert_eq!(ctx.accounts.account.data, 12); + Ok(vec![vec![], vec![]]) + } + + #[derive(Accounts)] + pub struct Components<'info> { + pub authority: Signer<'info>, + pub account: Account<'info, SomeAccount>, + } + + impl<'a, 'b, 'c, 'info> TryFrom<&Context<'a, 'b, 'c, 'info, VariadicBoltComponents<'info>>> for Components<'info> + where + 'c: 'info, + { + type Error = ErrorCode; + + fn try_from(context: &Context<'a, 'b, 'c, 'info, VariadicBoltComponents<'info>>) -> std::result::Result { + Ok(Self { + authority: context.accounts.authority.clone(), + account: Account::try_from(context.remaining_accounts.as_ref().get(0).ok_or_else(|| ErrorCode::ConstraintAccountIsNone)?).expect("Failed to convert context to components"), + }) + } + } + + fn prepare_execution_input(first_component: &mut AccountData) -> Result { + let pda_data = std::mem::take(&mut first_component.data); + let args = vec![1, 2, 3]; + Ok(BoltExecuteInput { owner: SomeAccount::owner(), pda_data, args }) + } + + #[test] + fn context_creation() -> Result<()> { + let [mut signer, mut component_a] = AccountData::test_data(); + let input= prepare_execution_input(&mut component_a)?; + let (signer_account_info, component_a_info) = (signer.to_account_info(), component_a.to_account_info()); + let (components, bumps) = VariadicBoltComponents::test_data(&signer_account_info); + let remaining_accounts = &[component_a_info.clone()]; + let mut context = ContextData::test_data(components, bumps, remaining_accounts); + let context = context.to_context(); + + assert!(context.accounts.authority.data_is_empty()); + assert!(context.remaining_accounts[0].data_is_empty()); + + bolt_execute(context, input) + } +} \ No newline at end of file diff --git a/crates/bolt-lang/src/instructions/mod.rs b/crates/bolt-lang/src/instructions/mod.rs index ec33eb69..a82c87b0 100644 --- a/crates/bolt-lang/src/instructions/mod.rs +++ b/crates/bolt-lang/src/instructions/mod.rs @@ -1,7 +1,11 @@ mod destroy; mod initialize; mod update; +mod set_owner; +mod recover_metadata; pub use destroy::*; pub use initialize::*; pub use update::*; +pub use set_owner::*; +pub use recover_metadata::*; \ No newline at end of file diff --git a/crates/bolt-lang/src/instructions/recover_metadata.rs b/crates/bolt-lang/src/instructions/recover_metadata.rs new file mode 100644 index 00000000..15975ee7 --- /dev/null +++ b/crates/bolt-lang/src/instructions/recover_metadata.rs @@ -0,0 +1,17 @@ + +use crate::{BoltMetadata, prelude::*}; + +pub fn recover_metadata<'info>( + instruction_sysvar_account: &AccountInfo<'info>, + component: &AccountInfo<'info>, + original_size: u32, + discriminator: Vec, + bolt_metadata: BoltMetadata, +) -> Result<()> { + crate::cpi::check(instruction_sysvar_account)?; + component.realloc(original_size as usize, false)?; + let mut account_data = component.try_borrow_mut_data()?; + account_data[0..8].copy_from_slice(&discriminator); + account_data[8..8 + BoltMetadata::INIT_SPACE].copy_from_slice(&bolt_metadata.try_to_vec()?); + Ok(()) +} \ No newline at end of file diff --git a/crates/bolt-lang/src/instructions/set_owner.rs b/crates/bolt-lang/src/instructions/set_owner.rs new file mode 100644 index 00000000..49b1d766 --- /dev/null +++ b/crates/bolt-lang/src/instructions/set_owner.rs @@ -0,0 +1,12 @@ +use crate::prelude::*; + +pub fn set_owner<'info>( + instruction_sysvar_account: &AccountInfo<'info>, + component: &AccountInfo<'info>, + owner: Pubkey, +) -> Result<()> { + crate::cpi::check(instruction_sysvar_account)?; + component.realloc(0, false)?; + component.assign(&owner); + Ok(()) +} \ No newline at end of file diff --git a/crates/bolt-lang/src/lib.rs b/crates/bolt-lang/src/lib.rs index 12d1d82f..1c69f424 100644 --- a/crates/bolt-lang/src/lib.rs +++ b/crates/bolt-lang/src/lib.rs @@ -1,3 +1,7 @@ +#[cfg(test)] +/// Used for allowing usage of #[account] in tests. +const ID: Pubkey = Pubkey::from_str_const("tEsT3eV6RFCWs1BZ7AXTzasHqTtMnMLCB2tjQ42TDXD"); + pub mod prelude; pub use anchor_lang; @@ -10,8 +14,14 @@ pub use anchor_lang::{ pub mod cpi; pub mod instructions; +pub mod bpf_writer; +pub mod account; +pub use account::BoltAccount; + pub use session_keys; +pub mod context; + pub use bolt_attribute_bolt_arguments::arguments; pub use bolt_attribute_bolt_bundle::bundle; pub use bolt_attribute_bolt_component::component; diff --git a/crates/programs/world/src/lib.rs b/crates/programs/world/src/lib.rs index 4f8f9ec2..d21aa2c4 100644 --- a/crates/programs/world/src/lib.rs +++ b/crates/programs/world/src/lib.rs @@ -1,8 +1,7 @@ #![allow(clippy::manual_unwrap_or_default)] use anchor_lang::{ - prelude::*, - solana_program::instruction::{AccountMeta, Instruction}, - solana_program::program::invoke, + prelude::{borsh::BorshSerialize, *}, + solana_program::{instruction::{AccountMeta, Instruction}, program::invoke}, }; use error::WorldError; use std::collections::BTreeSet; @@ -10,6 +9,7 @@ use std::collections::BTreeSet; pub mod utils; use utils::discriminator_for; + #[cfg(not(feature = "no-entrypoint"))] use solana_security_txt::security_txt; @@ -18,6 +18,8 @@ pub const INITIALIZE: [u8; 8] = discriminator_for("global:initialize"); pub const DESTROY: [u8; 8] = discriminator_for("global:destroy"); pub const UPDATE: [u8; 8] = discriminator_for("global:update"); pub const UPDATE_WITH_SESSION: [u8; 8] = discriminator_for("global:update_with_session"); +pub const SET_OWNER: [u8; 8] = discriminator_for("global:set_owner"); +pub const RECOVER_METADATA: [u8; 8] = discriminator_for("global:recover_metadata"); declare_id!("WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"); @@ -433,6 +435,7 @@ pub fn apply_impl<'info>( ) -> Result<()> { let (pairs, results) = system_execute( &ctx.accounts.authority, + &ctx.accounts.instruction_sysvar_account, &ctx.accounts.world, &ctx.accounts.bolt_system, system_discriminator, @@ -461,7 +464,7 @@ pub fn apply_impl<'info>( invoke( &ix, &[ - component.clone(), + component, ctx.accounts.authority.to_account_info(), ctx.accounts.instruction_sysvar_account.to_account_info(), ], @@ -477,6 +480,7 @@ pub fn apply_with_session_impl<'info>( ) -> Result<()> { let (pairs, results) = system_execute( &ctx.accounts.authority, + &ctx.accounts.instruction_sysvar_account, &ctx.accounts.world, &ctx.accounts.bolt_system, system_discriminator, @@ -516,9 +520,25 @@ pub fn apply_with_session_impl<'info>( Ok(()) } +#[derive(BorshSerialize)] +pub struct BoltExecuteInput { + pub owner: Pubkey, + pub pda_data: Vec, + pub args: Vec, +} + +// TODO: Deduplicate with bolt-lang +/// Metadata for the component. +#[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Default, Copy, Clone)] +pub struct BoltMetadata { + pub authority: Pubkey, +} + + #[allow(clippy::type_complexity)] fn system_execute<'info>( authority: &Signer<'info>, + instruction_sysvar_account: &UncheckedAccount<'info>, world: &Account<'info, World>, bolt_system: &UncheckedAccount<'info>, system_discriminator: Vec, @@ -547,21 +567,49 @@ fn system_execute<'info>( pairs.push((program, component)); } - let mut components_accounts = pairs - .iter() - .map(|(_, component)| component) - .cloned() - .collect::>(); + let (first_program, first_component) = pairs.get(0).ok_or(WorldError::InvalidSystemOutput)?; + let pda_data = first_component.try_borrow_data()?.to_vec(); + let original_size = pda_data.len() as u32; + let first_component_discriminator = first_component.try_borrow_data()?[0..8].to_vec(); + let first_component_bolt_metadata = BoltMetadata::try_from_slice(&first_component.try_borrow_data()?[8..8 + BoltMetadata::INIT_SPACE])?; + + let owner = bolt_system.key().to_bytes(); + let mut data = SET_OWNER.to_vec(); + data.extend_from_slice(owner.as_slice()); + + // set owner + invoke(&Instruction { + program_id: first_program.key(), + accounts: vec![ + AccountMeta::new(first_component.key(), false), + AccountMeta::new_readonly(instruction_sysvar_account.key(), false), + ], + data, + }, &[ + first_component.to_account_info(), + instruction_sysvar_account.to_account_info(), + ])?; + + // Ensure the first component (used as buffer by the bolt system) is the + // first remaining account forwarded to the system program. + let expected_capacity = 1 + pairs.len() + remaining_accounts.len(); + let mut components_accounts = + Vec::with_capacity(expected_capacity); + components_accounts.extend( + pairs + .iter() + .map(|(_, component)| component) + .cloned(), + ); components_accounts.append(&mut remaining_accounts); + let remaining_accounts = components_accounts; let mut data = system_discriminator; - let len_le = (args.len() as u32).to_le_bytes(); - data.extend_from_slice(&len_le); - data.extend_from_slice(args.as_slice()); - - use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; - use anchor_lang::solana_program::program::invoke; + { + let input = BoltExecuteInput { owner: first_program.key(), pda_data, args }; + data.extend_from_slice(&input.try_to_vec()?); + } let mut accounts = vec![AccountMeta { pubkey: authority.key(), @@ -589,16 +637,40 @@ fn system_execute<'info>( invoke(&ix, &account_infos)?; - // Extract return data using Solana SDK - use anchor_lang::solana_program::program::get_return_data; - let (pid, data) = get_return_data().ok_or(WorldError::InvalidSystemOutput)?; - require_keys_eq!(pid, bolt_system.key(), WorldError::InvalidSystemOutput); - let results: Vec> = borsh::BorshDeserialize::try_from_slice(&data) + let results: Vec> = borsh::BorshDeserialize::try_from_slice(&first_component.try_borrow_data()?) .map_err(|_| WorldError::InvalidSystemOutput)?; - if results.len() != pairs.len() { - return Err(WorldError::InvalidSystemOutput.into()); - } + // invoke set_owner + let owner = first_program.key().to_bytes(); + let mut data = SET_OWNER.to_vec(); + data.extend_from_slice(owner.as_slice()); + + // set owner + invoke(&Instruction { + program_id: bolt_system.key(), + accounts: vec![ + AccountMeta::new(first_component.key(), false), + AccountMeta::new_readonly(instruction_sysvar_account.key(), false), + ], + data, + }, &[ + first_component.to_account_info(), + instruction_sysvar_account.to_account_info(), + ])?; + + // invoke recover_metadata + let mut data = RECOVER_METADATA.to_vec(); + // Borsh encodes Vec as u32 length (LE) followed by bytes + let disc_len_le = (first_component_discriminator.len() as u32).to_le_bytes(); + data.extend_from_slice(&original_size.to_le_bytes()); + data.extend_from_slice(&disc_len_le); + data.extend_from_slice(&first_component_discriminator); + data.extend_from_slice(&first_component_bolt_metadata.try_to_vec()?); + invoke(&Instruction { + program_id: first_program.key(), + accounts: vec![AccountMeta::new(first_component.key(), false), AccountMeta::new_readonly(instruction_sysvar_account.key(), false)], + data, + }, &[first_component.to_account_info(), instruction_sysvar_account.to_account_info()])?; Ok((pairs, results)) } diff --git a/docs/REPORT.md b/docs/REPORT.md index eb739685..349c3c08 100644 --- a/docs/REPORT.md +++ b/docs/REPORT.md @@ -2,8 +2,8 @@ %%{init: {"xyChart": {"width": 1200, "height": 400, "xAxis": {}}}}%% xychart title "Bolt Apply System Cost" - x-axis ["1C-CPIs:2","2C-CPIs:3","3C-CPIs:4","4C-CPIs:5","5C-CPIs:6","6C-CPIs:7","7C-CPIs:8","8C-CPIs:9","9C-CPIs:10","10C-CPIs:11"] + x-axis ["1C-CPIs:5","2C-CPIs:6","3C-CPIs:7","4C-CPIs:8","5C-CPIs:9","6C-CPIs:10","7C-CPIs:11","8C-CPIs:12","9C-CPIs:13","10C-CPIs:14"] y-axis "CU" 5000 --> 200000 - bar [13110,20640,28678,37169,46306,55754,65697,76307,87375,98746] - bar [4581,8633,13163,18120,23526,29382,35706,42669,49889,57556] + bar [27920,36122,44840,53998,63807,73921,84532,95616,107355,119393] + bar [12942,17735,23014,28707,34850,41442,48502,56008,63964,72367] ``` diff --git a/examples/component-large/Cargo.toml b/examples/component-large/Cargo.toml new file mode 100644 index 00000000..7958d0a4 --- /dev/null +++ b/examples/component-large/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "large" +version = "0.2.6" +description = "Created with Bolt" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "large" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["bolt-lang/idl-build"] +anchor-debug = ["bolt-lang/anchor-debug"] +custom-heap = [] +custom-panic = [] + +[dependencies] +bolt-lang.workspace = true diff --git a/examples/component-large/Xargo.toml b/examples/component-large/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/examples/component-large/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/component-large/src/lib.rs b/examples/component-large/src/lib.rs new file mode 100644 index 00000000..e5530e59 --- /dev/null +++ b/examples/component-large/src/lib.rs @@ -0,0 +1,16 @@ +use bolt_lang::*; + +declare_id!("DEABKyknxGaCbkthh1mPSSMQnJrSLRJyZShdKpsyrjdL"); + +#[component] +pub struct Large { + data: [u8; 1024], +} + +impl Default for Large { + fn default() -> Self { + let bolt_metadata = BoltMetadata::default(); + let data = [0; 1024]; + Self { bolt_metadata, data } + } +} \ No newline at end of file diff --git a/examples/with-large-component/Cargo.toml b/examples/with-large-component/Cargo.toml new file mode 100644 index 00000000..554f0995 --- /dev/null +++ b/examples/with-large-component/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "with-large-component" +version = "0.2.6" +description = "Created with Bolt" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "with_large_component" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["bolt-lang/idl-build"] +anchor-debug = ["bolt-lang/anchor-debug"] +custom-heap = [] +custom-panic = [] + + +[dependencies] +bolt-lang.workspace = true +serde = { version = "1.0", features = ["derive"] } +large.workspace = true \ No newline at end of file diff --git a/examples/with-large-component/Xargo.toml b/examples/with-large-component/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/examples/with-large-component/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/examples/with-large-component/src/lib.rs b/examples/with-large-component/src/lib.rs new file mode 100644 index 00000000..d634bf01 --- /dev/null +++ b/examples/with-large-component/src/lib.rs @@ -0,0 +1,18 @@ +use bolt_lang::*; +use large::Large; + +declare_id!("4X7t7eCjd2pvNThY9DQ3rKQKNLeeHhgEv74JGwh7jJEz"); + +#[system] +pub mod with_large_component { + + pub fn execute(ctx: Context, _args_p: Vec) -> Result { + Ok(ctx.accounts) + } + + #[system_input] + pub struct Components { + pub large: Large, + } + +} \ No newline at end of file