diff --git a/Cargo.lock b/Cargo.lock index 3a924c5b..0884ae64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -895,6 +895,14 @@ dependencies = [ "bolt-system", ] +[[package]] +name = "bolt-cpi-interface" +version = "0.2.3" +dependencies = [ + "pinocchio", + "pinocchio-pubkey", +] + [[package]] name = "bolt-lang" version = "0.2.3" @@ -943,6 +951,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bolt-world" +version = "0.2.3" +dependencies = [ + "bolt-cpi-interface", + "pinocchio", + "pinocchio-pubkey", + "pinocchio-system", +] + [[package]] name = "borsh" version = "0.10.4" @@ -1872,6 +1890,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -3220,6 +3253,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinocchio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c33b58567c11b07749cefbb8320ac023f3387c57807aeb8e3b1262501b6e9f0" + +[[package]] +name = "pinocchio-pubkey" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b20fcebc172c3cd3f54114b0241b48fa8e30893ced2eb4927aaba5e3a0ba5" +dependencies = [ + "five8_const", + "pinocchio", +] + +[[package]] +name = "pinocchio-system" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f75423420ae70aa748cf611cab14cfd00af08d0d2d3d258cb0cf5e2880ec19c" +dependencies = [ + "pinocchio", + "pinocchio-pubkey", +] + [[package]] name = "pkg-config" version = "0.3.31" diff --git a/Cargo.toml b/Cargo.toml index bb4511b4..bae8170f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/programs/bolt-component", "crates/programs/bolt-system", "crates/programs/world", + "crates/pinocchio/*", "crates/types", "examples/*", ] @@ -63,6 +64,10 @@ which = "7.0.2" tokio = { version = "1", features = ["full"] } sysinfo = "0.33.1" bytemuck_derive = "=1.8.1" +pinocchio = "0.8.2" +pinocchio-pubkey = "0.2.4" +pinocchio-system = "0.2.3" +bolt-cpi-interface = { path = "crates/pinocchio/cpi-interface"} [profile.release] diff --git a/crates/pinocchio/cpi-interface/Cargo.toml b/crates/pinocchio/cpi-interface/Cargo.toml new file mode 100644 index 00000000..2341dee8 --- /dev/null +++ b/crates/pinocchio/cpi-interface/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bolt-cpi-interface" +version = {workspace = true} +edition = {workspace = true} + +[lib] +crate-type = ["rlib"] + +[dependencies] +pinocchio = {workspace = true} +pinocchio-pubkey = {workspace = true} diff --git a/crates/pinocchio/cpi-interface/src/component/destroy.rs b/crates/pinocchio/cpi-interface/src/component/destroy.rs new file mode 100644 index 00000000..5df2c4f7 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/component/destroy.rs @@ -0,0 +1,68 @@ +use pinocchio::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction, Signer}, + program::invoke_signed, + pubkey::Pubkey, + ProgramResult, +}; + +pub struct Destroy<'a> { + /// Authority + pub authority: &'a AccountInfo, + /// Data account + pub receiver: &'a AccountInfo, + /// Entity + pub entity: &'a AccountInfo, + /// Authority + pub component: &'a AccountInfo, + /// Authority + pub component_program_data: &'a AccountInfo, + /// Instruction sysvar account + pub instruction_sysvar_account: &'a AccountInfo, + /// System program + pub system_program: &'a AccountInfo, + /// Component program + pub component_program: &'a Pubkey, +} + +impl Destroy<'_> { + pub const DISCRIMINATOR: [u8; 8] = [157, 40, 96, 3, 135, 203, 143, 74]; + + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metadata + let account_metas: [AccountMeta; 7] = [ + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::writable(self.receiver.key()), + AccountMeta::readonly(self.entity.key()), + AccountMeta::writable(self.component.key()), + AccountMeta::readonly(self.component_program_data.key()), + AccountMeta::readonly(self.instruction_sysvar_account.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: self.component_program, + accounts: &account_metas, + data: Self::DISCRIMINATOR.as_slice(), + }; + + invoke_signed( + &instruction, + &[ + self.authority, + self.receiver, + self.entity, + self.component, + self.component_program_data, + self.instruction_sysvar_account, + self.system_program, + ], + signers, + ) + } +} diff --git a/crates/pinocchio/cpi-interface/src/component/initialize.rs b/crates/pinocchio/cpi-interface/src/component/initialize.rs new file mode 100644 index 00000000..a033f2a2 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/component/initialize.rs @@ -0,0 +1,65 @@ +use pinocchio::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction, Signer}, + program::invoke_signed, + pubkey::Pubkey, + ProgramResult, +}; + + +pub struct Initialize<'a> { + /// Payer + pub payer: &'a AccountInfo, + /// Data account + pub data: &'a AccountInfo, + /// Entity + pub entity: &'a AccountInfo, + /// Authority + pub authority: &'a AccountInfo, + /// Instruction sysvar account + pub instruction_sysvar_account: &'a AccountInfo, + /// System program + pub system_program: &'a AccountInfo, + /// Component program + pub component_program: &'a Pubkey, +} + +impl Initialize<'_> { + pub const DISCRIMINATOR: [u8; 8] = [175, 175, 109, 31, 13, 152, 155, 237]; + + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metadata + let account_metas: [AccountMeta; 6] = [ + AccountMeta::writable_signer(self.payer.key()), + AccountMeta::writable(self.data.key()), + AccountMeta::readonly(self.entity.key()), + AccountMeta::readonly(self.authority.key()), + AccountMeta::readonly(self.instruction_sysvar_account.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: self.component_program, + accounts: &account_metas, + data: Self::DISCRIMINATOR.as_slice(), + }; + + invoke_signed( + &instruction, + &[ + self.payer, + self.data, + self.entity, + self.authority, + self.instruction_sysvar_account, + self.system_program, + ], + signers, + ) + } +} diff --git a/crates/pinocchio/cpi-interface/src/component/mod.rs b/crates/pinocchio/cpi-interface/src/component/mod.rs new file mode 100644 index 00000000..f4477d05 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/component/mod.rs @@ -0,0 +1,11 @@ +mod destroy; +pub use destroy::*; + +mod initialize; +pub use initialize::*; + +mod update; +pub use update::*; + +mod update_with_session; +pub use update_with_session::*; diff --git a/crates/pinocchio/cpi-interface/src/component/update.rs b/crates/pinocchio/cpi-interface/src/component/update.rs new file mode 100644 index 00000000..643d352c --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/component/update.rs @@ -0,0 +1,63 @@ +use pinocchio::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction, Signer}, + program::invoke_signed, + pubkey::Pubkey, + ProgramResult, +}; + + +pub struct Update<'a> { + /// Component + pub component: &'a AccountInfo, + /// Authority + pub authority: &'a AccountInfo, + /// Instruction sysvar account + pub instruction_sysvar_account: &'a AccountInfo, + /// Instruction + pub component_program: &'a Pubkey, + /// Component program + pub instruction_data: &'a [u8], +} + +impl Update<'_> { + pub const DISCRIMINATOR: [u8; 8] = [219, 200, 88, 176, 158, 63, 253, 127]; + + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metadata + let account_metas: [AccountMeta; 3] = [ + AccountMeta::writable(self.component.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.instruction_sysvar_account.key()), + ]; + + const DISCRIMATOR_LENGTH: usize = 8; + + let mut instruction_data = [0u8; 1024]; + + instruction_data[0..DISCRIMATOR_LENGTH].copy_from_slice(Self::DISCRIMINATOR.as_slice()); + instruction_data[DISCRIMATOR_LENGTH..DISCRIMATOR_LENGTH + self.instruction_data.len()] + .copy_from_slice(self.instruction_data); + + let instruction = Instruction { + program_id: self.component_program, + accounts: &account_metas, + data: &instruction_data[..DISCRIMATOR_LENGTH + self.instruction_data.len()], + }; + + invoke_signed( + &instruction, + &[ + self.component, + self.authority, + self.instruction_sysvar_account, + ], + signers, + ) + } +} diff --git a/crates/pinocchio/cpi-interface/src/component/update_with_session.rs b/crates/pinocchio/cpi-interface/src/component/update_with_session.rs new file mode 100644 index 00000000..d13530d0 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/component/update_with_session.rs @@ -0,0 +1,66 @@ +use pinocchio::{ + account_info::AccountInfo, + instruction::{AccountMeta, Instruction, Signer}, + program::invoke_signed, + pubkey::Pubkey, + ProgramResult, +}; + +pub struct UpdateWithSession<'a> { + /// Component + pub component: &'a AccountInfo, + /// Authority + pub authority: &'a AccountInfo, + /// Instruction sysvar account + pub instruction_sysvar_account: &'a AccountInfo, + /// Instruction sysvar account + pub session_token: &'a AccountInfo, + /// Instruction + pub component_program: &'a Pubkey, + /// Component program + pub instruction_data: &'a [u8], +} + +impl UpdateWithSession<'_> { + pub const DISCRIMINATOR: [u8; 8] = [221, 55, 212, 141, 57, 85, 61, 182]; + + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + // account metadata + let account_metas: [AccountMeta; 4] = [ + AccountMeta::writable(self.component.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.instruction_sysvar_account.key()), + AccountMeta::readonly(self.session_token.key()), + ]; + + const DISCRIMATOR_LENGTH: usize = 8; + + let mut instruction_data = [0u8; 1024]; + + instruction_data[0..DISCRIMATOR_LENGTH].copy_from_slice(Self::DISCRIMINATOR.as_slice()); + instruction_data[DISCRIMATOR_LENGTH..DISCRIMATOR_LENGTH + self.instruction_data.len()] + .copy_from_slice(self.instruction_data); + + let instruction = Instruction { + program_id: self.component_program, + accounts: &account_metas, + data: &instruction_data[..DISCRIMATOR_LENGTH + self.instruction_data.len()], + }; + + invoke_signed( + &instruction, + &[ + self.component, + self.authority, + self.instruction_sysvar_account, + self.session_token, + ], + signers, + ) + } +} diff --git a/crates/pinocchio/cpi-interface/src/lib.rs b/crates/pinocchio/cpi-interface/src/lib.rs new file mode 100644 index 00000000..1a3ef146 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] + +pub mod component; +pub mod system; diff --git a/crates/pinocchio/cpi-interface/src/system/execute.rs b/crates/pinocchio/cpi-interface/src/system/execute.rs new file mode 100644 index 00000000..03b642c6 --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/system/execute.rs @@ -0,0 +1,83 @@ +use core::mem::MaybeUninit; + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke_signed, MAX_CPI_ACCOUNTS}, + instruction::{AccountMeta, Instruction, Signer}, + pubkey::Pubkey, + ProgramResult, +}; + +pub struct Execute<'a> { + pub authority: &'a AccountInfo, + + pub components: &'a [&'a AccountInfo], + + pub remaining_accounts: &'a [&'a AccountInfo], + + pub system: &'a Pubkey, + + pub instruction_data: &'a [u8], +} + +impl Execute<'_> { + pub const DISCRIMINATOR: [u8; 8] = [75, 206, 62, 210, 52, 215, 104, 109]; + + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + const UNINIT: MaybeUninit<&AccountInfo> = MaybeUninit::<&AccountInfo>::uninit(); + const UNINIT_METAS: MaybeUninit = MaybeUninit::::uninit(); + + let mut maybe_account_infos = [UNINIT; MAX_CPI_ACCOUNTS]; + let mut maybe_account_metas = [UNINIT_METAS; MAX_CPI_ACCOUNTS]; + + let mut len = 0; + + maybe_account_infos[len].write(self.authority); + maybe_account_metas[len].write(AccountMeta::readonly_signer(self.authority.key())); + len += 1; + + if !self.components.is_empty() { + for i in 0..self.components.len() { + maybe_account_infos[len].write(self.components[i]); + maybe_account_metas[len].write(AccountMeta::writable(self.components[i].key())); + len += 1; + } + } + + if !self.remaining_accounts.is_empty() { + for i in 0..self.remaining_accounts.len() { + maybe_account_infos[len].write(self.remaining_accounts[i]); + maybe_account_metas[len] + .write(AccountMeta::readonly(self.remaining_accounts[i].key())); + len += 1; + } + } + + let account_infos = + unsafe { core::slice::from_raw_parts(maybe_account_infos.as_ptr() as _, len) }; + + let account_metas = + unsafe { core::slice::from_raw_parts(maybe_account_metas.as_ptr() as _, len) }; + + let mut instruction_data = [0u8; 1024]; + + const DISCRIMATOR_LENGTH: usize = 8; + + instruction_data[0..DISCRIMATOR_LENGTH].copy_from_slice(Self::DISCRIMINATOR.as_slice()); + instruction_data[DISCRIMATOR_LENGTH..DISCRIMATOR_LENGTH + self.instruction_data.len()] + .copy_from_slice(self.instruction_data); + + let instruction = Instruction { + program_id: self.system, + accounts: account_metas, + data: &instruction_data[..DISCRIMATOR_LENGTH + self.instruction_data.len()], + }; + + slice_invoke_signed(&instruction, account_infos, signers) + } +} diff --git a/crates/pinocchio/cpi-interface/src/system/mod.rs b/crates/pinocchio/cpi-interface/src/system/mod.rs new file mode 100644 index 00000000..f51120ae --- /dev/null +++ b/crates/pinocchio/cpi-interface/src/system/mod.rs @@ -0,0 +1,2 @@ +mod execute; +pub use execute::*; diff --git a/crates/pinocchio/world/Cargo.toml b/crates/pinocchio/world/Cargo.toml new file mode 100644 index 00000000..046e6daa --- /dev/null +++ b/crates/pinocchio/world/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bolt-world" +version = {workspace = true} +edition = {workspace = true} + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +bolt-cpi-interface = {workspace = true} +pinocchio = {workspace = true} +pinocchio-pubkey = {workspace = true} +pinocchio-system = {workspace = true} \ No newline at end of file diff --git a/crates/pinocchio/world/src/consts.rs b/crates/pinocchio/world/src/consts.rs new file mode 100644 index 00000000..5fc91087 --- /dev/null +++ b/crates/pinocchio/world/src/consts.rs @@ -0,0 +1 @@ +pub const DISCRIMATOR_LENGTH: usize = 8; diff --git a/crates/pinocchio/world/src/error.rs b/crates/pinocchio/world/src/error.rs new file mode 100644 index 00000000..91d3efe2 --- /dev/null +++ b/crates/pinocchio/world/src/error.rs @@ -0,0 +1,16 @@ +use pinocchio::program_error::ProgramError; + +pub enum WorldError { + InvalidAuthority = 6000, + InvalidSystemOutput, + WorldAccountMismatch, + TooManyAuthorities, + AuthorityNotFound, + SystemNotApproved, +} + +impl From for ProgramError { + fn from(e: WorldError) -> Self { + ProgramError::Custom(e as u32) + } +} \ No newline at end of file diff --git a/crates/pinocchio/world/src/instructions/add_authority.rs b/crates/pinocchio/world/src/instructions/add_authority.rs new file mode 100644 index 00000000..d6bae325 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/add_authority.rs @@ -0,0 +1,53 @@ +use crate::state::world::{World, WorldMut}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; + +pub fn add_authority(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [authority, new_authority, world_acct, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let world_id = u64::from_le_bytes(unsafe { (data.as_ptr() as *const [u8; 8]).read() }); + + // assert world pda + if &World::pda(&world_id.to_be_bytes()).0 != world_acct.key() { + return Err(ProgramError::InvalidSeeds); + } + + let mut world = WorldMut::from_account_info(world_acct)?; + + let authorities = world.authorities()?; + + if authorities.is_empty() + || (authorities.contains(authority.key()) && !authorities.contains(new_authority.key())) + { + let world_size = world.size()? + 32; + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(world_size); + + let lamports_diff = new_minimum_balance.saturating_sub(world_acct.lamports()); + + if lamports_diff > 0 { + pinocchio_system::instructions::Transfer { + lamports: lamports_diff, + from: authority, + to: world_acct, + } + .invoke()?; + } + + world_acct.realloc(world_size, false)?; + + world.add_new_authority(new_authority.key())?; + } + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/add_entity.rs b/crates/pinocchio/world/src/instructions/add_entity.rs new file mode 100644 index 00000000..a99e28c5 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/add_entity.rs @@ -0,0 +1,51 @@ +use crate::state::{ + entity::Entity, + transmutable::{Transmutable, TransmutableMut}, + world::WorldMut, +}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use pinocchio_system::instructions::CreateAccount; + +pub fn add_entity(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [payer, entity_acct, world_acct, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let world = WorldMut::from_account_info(world_acct)?; + + let (_, bump) = Entity::pda( + &world.metadata.id.to_be_bytes(), + &world.metadata.entities.to_be_bytes(), + data, + )?; + + let lamports_needed = Rent::get()?.minimum_balance(Entity::LEN); + + CreateAccount { + from: payer, + to: entity_acct, + lamports: lamports_needed, + space: Entity::LEN as u64, + owner: &crate::ID, + } + .invoke_signed(&[Entity::signer( + &world.metadata.id.to_be_bytes(), + &world.metadata.entities.to_be_bytes(), + data, + &[bump], + )? + .as_slice() + .into()])?; + + let entity = unsafe { Entity::load_mut_unchecked(entity_acct.borrow_mut_data_unchecked())? }; + entity.init(world.metadata.entities)?; + + world.metadata.entities += 1; + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/apply_system.rs b/crates/pinocchio/world/src/instructions/apply_system.rs new file mode 100644 index 00000000..b2825537 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/apply_system.rs @@ -0,0 +1,77 @@ +use crate::{error::WorldError, state::world::WorldRef, utils::init_execute_cpi_accounts}; +use core::mem::MaybeUninit; +use pinocchio::{ + account_info::AccountInfo, + cpi::{get_return_data, MAX_CPI_ACCOUNTS}, + program_error::ProgramError, + ProgramResult, +}; + +pub fn apply_system(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [system, authority, instruction_sysvar_account, world_acct, remaining @ ..] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() && authority.key() != &crate::ID { + return Err(WorldError::InvalidAuthority.into()); + } + + let world = WorldRef::from_account_info(world_acct)?; + + if !world.permissionless()? && world.systems.binary_search(system.key()).is_err() { + return Err(WorldError::SystemNotApproved.into()); + } + + const UNINIT_INFO: MaybeUninit<&AccountInfo> = MaybeUninit::uninit(); + + let mut ctx_accounts = [UNINIT_INFO; MAX_CPI_ACCOUNTS]; + + let (components, sep_idx, remaining_accounts) = + init_execute_cpi_accounts(remaining, &mut ctx_accounts)?; + + bolt_cpi_interface::system::Execute { + authority, + components, + remaining_accounts, + instruction_data: data, + system: system.key(), + } + .invoke()?; + + let return_data = get_return_data().ok_or(ProgramError::InvalidAccountData)?; + + let components_pair = &remaining[..sep_idx.unwrap_or(remaining.len())]; + + let (_, data) = return_data.as_slice().split_at(core::mem::size_of::()); + + let mut cursor = 0; + + for pair in components_pair.chunks_exact(2) { + let [component_program, component] = pair else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let mut size = core::mem::size_of::(); + + let len_bytes = &data[cursor..cursor + size]; + + let len = + u32::from_le_bytes(unsafe { (len_bytes.as_ptr() as *const [u8; 4]).read() }) as usize; + + size += len; + + bolt_cpi_interface::component::Update { + authority, + component, + component_program: component_program.key(), + instruction_data: &data[cursor..cursor + size], + instruction_sysvar_account, + } + .invoke()?; + + cursor += size; + } + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/apply_system_session.rs b/crates/pinocchio/world/src/instructions/apply_system_session.rs new file mode 100644 index 00000000..48645dd1 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/apply_system_session.rs @@ -0,0 +1,86 @@ +use crate::{error::WorldError, state::world::WorldRef, utils::init_execute_cpi_accounts}; +use core::mem::MaybeUninit; +use pinocchio::{ + account_info::AccountInfo, + cpi::{get_return_data, MAX_CPI_ACCOUNTS}, + program_error::ProgramError, + ProgramResult, +}; + +pub fn apply_system_session(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [system, authority, instruction_sysvar_account, world_acct, session_token, remaining @ ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() && authority.key() != &crate::ID { + return Err(WorldError::InvalidAuthority.into()); + } + + let world = WorldRef::from_account_info(world_acct)?; + + if !world.permissionless()? && world.systems.binary_search(system.key()).is_err() { + return Err(WorldError::SystemNotApproved.into()); + } + + const UNINIT_INFO: MaybeUninit<&AccountInfo> = MaybeUninit::uninit(); + + let mut ctx_accounts = [UNINIT_INFO; MAX_CPI_ACCOUNTS]; + + let (components, sep_idx, remaining_accounts) = + init_execute_cpi_accounts(remaining, &mut ctx_accounts)?; + + bolt_cpi_interface::system::Execute { + authority, + components, + remaining_accounts, + instruction_data: data, + system: system.key(), + } + .invoke()?; + + let return_data = get_return_data().ok_or(ProgramError::InvalidAccountData)?; + + let components_pair = &remaining[..sep_idx.unwrap_or(remaining.len())]; + + let (result_len_bytes, data) = return_data.as_slice().split_at(core::mem::size_of::()); + + let result_len = + u32::from_le_bytes(unsafe { (result_len_bytes.as_ptr() as *const [u8; 4]).read() }); + + if result_len as usize != components_pair.len().saturating_div(2) { + return Err(WorldError::InvalidSystemOutput.into()); + } + + let mut cursor = 0; + + for pair in components_pair.chunks_exact(2) { + let [component_program, component] = pair else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let mut size = core::mem::size_of::(); + + let len_bytes = &data[cursor..cursor + size]; + + let len = + u32::from_le_bytes(unsafe { (len_bytes.as_ptr() as *const [u8; 4]).read() }) as usize; + + size += len; + + bolt_cpi_interface::component::UpdateWithSession { + authority, + component, + component_program: component_program.key(), + instruction_data: &data[cursor..cursor + size], + instruction_sysvar_account, + session_token, + } + .invoke()?; + + cursor += size; + } + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/approve_system.rs b/crates/pinocchio/world/src/instructions/approve_system.rs new file mode 100644 index 00000000..564e742b --- /dev/null +++ b/crates/pinocchio/world/src/instructions/approve_system.rs @@ -0,0 +1,52 @@ +use crate::{error::WorldError, state::world::WorldMut}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; + +pub fn approve_system(accounts: &[AccountInfo]) -> ProgramResult { + let [authority, world_acct, system, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() { + return Err(WorldError::InvalidAuthority.into()); + } + + let mut world = WorldMut::from_account_info(world_acct)?; + + let authorities = world.authorities()?; + + if !authorities.contains(authority.key()) { + return Err(WorldError::InvalidAuthority.into()); + } + + let is_permissionless = world.is_permissionless()?; + + if *is_permissionless { + *is_permissionless = false + } + + world.add_system(system.key())?; + + let world_size = world.size()?; + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(world_size); + + let lamports_diff = new_minimum_balance.saturating_sub(world_acct.lamports()); + + if lamports_diff > 0 { + pinocchio_system::instructions::Transfer { + lamports: lamports_diff, + from: authority, + to: world_acct, + } + .invoke()?; + } + + world_acct.realloc(world_size, false)?; + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/destroy_component.rs b/crates/pinocchio/world/src/instructions/destroy_component.rs new file mode 100644 index 00000000..b0d19497 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/destroy_component.rs @@ -0,0 +1,21 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +pub fn destroy_component(accounts: &[AccountInfo]) -> ProgramResult { + let [authority, receiver, component_program, component_program_data, entity, component, instruction_sysvar_account, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + bolt_cpi_interface::component::Destroy { + authority, + component_program_data, + component, + component_program: component_program.key(), + receiver, + entity, + instruction_sysvar_account, + system_program, + } + .invoke() +} diff --git a/crates/pinocchio/world/src/instructions/initialize_component.rs b/crates/pinocchio/world/src/instructions/initialize_component.rs new file mode 100644 index 00000000..e7962891 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/initialize_component.rs @@ -0,0 +1,25 @@ +use crate::error::WorldError; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +pub fn initialize_component(accounts: &[AccountInfo]) -> ProgramResult { + let [payer, data, entity, component_program, authority, instruction_sysvar_account, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() && authority.key() != &crate::ID { + return Err(WorldError::InvalidAuthority.into()); + } + + bolt_cpi_interface::component::Initialize { + payer, + authority, + component_program: component_program.key(), + data, + entity, + instruction_sysvar_account, + system_program, + } + .invoke() +} diff --git a/crates/pinocchio/world/src/instructions/initialize_new_world.rs b/crates/pinocchio/world/src/instructions/initialize_new_world.rs new file mode 100644 index 00000000..53029bcf --- /dev/null +++ b/crates/pinocchio/world/src/instructions/initialize_new_world.rs @@ -0,0 +1,46 @@ +use crate::state::{ + registry::Registry, + transmutable::TransmutableMut, + world::{World, WorldMut}, +}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use pinocchio_system::instructions::CreateAccount; + +pub fn initialize_new_world(accounts: &[AccountInfo]) -> ProgramResult { + let [payer, world, registry, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let reg = unsafe { Registry::load_mut_unchecked(registry.borrow_mut_data_unchecked())? }; + + if &Registry::pda().0 != registry.key() { + return Err(ProgramError::InvalidSeeds); + } + + let (_, bump) = World::pda(®.worlds.to_be_bytes()); + + let lamports_needed = Rent::get()?.minimum_balance(World::INIT_SIZE); + + CreateAccount { + from: payer, + to: world, + lamports: lamports_needed, + space: World::INIT_SIZE as u64, + owner: &crate::ID, + } + .invoke_signed(&[World::signer(®.worlds.to_be_bytes(), &[bump]) + .as_slice() + .into()])?; + + let mut world = WorldMut::from_bytes(unsafe { world.borrow_mut_data_unchecked() })?; + world.init(reg.worlds)?; + + reg.worlds += 1; + + Ok(()) +} diff --git a/crates/pinocchio/world/src/instructions/initialize_registry.rs b/crates/pinocchio/world/src/instructions/initialize_registry.rs new file mode 100644 index 00000000..bb86236e --- /dev/null +++ b/crates/pinocchio/world/src/instructions/initialize_registry.rs @@ -0,0 +1,34 @@ +use crate::state::{ + registry::Registry, + transmutable::{Transmutable, TransmutableMut}, +}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use pinocchio_system::instructions::CreateAccount; + +pub fn initialize_registry(accounts: &[AccountInfo]) -> ProgramResult { + let [registry, payer, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let lamports_needed = Rent::get()?.minimum_balance(Registry::LEN); + + let (_, bump) = Registry::pda(); + + CreateAccount { + from: payer, + to: registry, + lamports: lamports_needed, + space: Registry::LEN as u64, + owner: &crate::ID, + } + .invoke_signed(&[Registry::signer(&[bump]).as_slice().into()])?; + + let reg = unsafe { Registry::load_mut_unchecked(registry.borrow_mut_data_unchecked())? }; + + reg.init() +} diff --git a/crates/pinocchio/world/src/instructions/mod.rs b/crates/pinocchio/world/src/instructions/mod.rs new file mode 100644 index 00000000..3b90ca6e --- /dev/null +++ b/crates/pinocchio/world/src/instructions/mod.rs @@ -0,0 +1,82 @@ +mod add_authority; +pub use add_authority::*; + +mod add_entity; +pub use add_entity::*; + +mod apply_system; +pub use apply_system::*; + +mod apply_system_session; +pub use apply_system_session::*; + +mod approve_system; +pub use approve_system::*; + +mod destroy_component; +pub use destroy_component::*; + +mod initialize_component; +pub use initialize_component::*; + +mod initialize_registry; +pub use initialize_registry::*; + +mod initialize_new_world; +pub use initialize_new_world::*; + +mod remove_authority; +pub use remove_authority::*; + +mod remove_system; +pub use remove_system::*; + +use pinocchio::program_error::ProgramError; + +pub const INITIALIZE_REGISTRY_DISCRIMINATOR: u64 = 4321548737212364221; +pub const INITIALIZE_NEW_WORLD_DISCRIMINATOR: u64 = 7118163274173538327; +pub const ADD_AUTHORIITY_DISCRIMINATOR: u64 = 13217455069452700133; +pub const REMOVE_AUTHORIITY_DISCRIMINATOR: u64 = 15585545156648003826; +pub const APPROVE_SYSTEM_DISCRIMINATOR: u64 = 8777308090533520754; +pub const REMOVE_SYSTEM_DISCRIMINATOR: u64 = 8688994685429436634; +pub const ADD_ENTITY_DISCRIMINATOR: u64 = 4121062988444201379; +pub const INITIALIZE_COMPONENT_DISCRIMINATOR: u64 = 2179155133888827172; +pub const DESTROY_COMPONENT_DISCRIMINATOR: u64 = 5321952129328727336; +pub const APPLY_DISCRIMINATOR: u64 = 16258613031726085112; +pub const APPLY_WITH_SESSION_DISCRIMINATOR: u64 = 7459768094276011477; + +#[repr(u64)] +pub enum WorldInstruction { + InitializeRegistry = INITIALIZE_REGISTRY_DISCRIMINATOR, + InitializeNewWorld = INITIALIZE_NEW_WORLD_DISCRIMINATOR, + AddAuthority = ADD_AUTHORIITY_DISCRIMINATOR, + RemoveAuthority = REMOVE_AUTHORIITY_DISCRIMINATOR, + ApproveSystem = APPROVE_SYSTEM_DISCRIMINATOR, + RemoveSystem = REMOVE_SYSTEM_DISCRIMINATOR, + AddEntity = ADD_ENTITY_DISCRIMINATOR, + InitilizeComponent = INITIALIZE_COMPONENT_DISCRIMINATOR, + DestroyComponent = DESTROY_COMPONENT_DISCRIMINATOR, + Apply = APPLY_DISCRIMINATOR, + ApplyWithSession = APPLY_WITH_SESSION_DISCRIMINATOR, +} + +impl TryFrom for WorldInstruction { + type Error = ProgramError; + + fn try_from(byte: u64) -> Result { + match byte { + INITIALIZE_REGISTRY_DISCRIMINATOR => Ok(WorldInstruction::InitializeRegistry), + INITIALIZE_NEW_WORLD_DISCRIMINATOR => Ok(WorldInstruction::InitializeNewWorld), + ADD_AUTHORIITY_DISCRIMINATOR => Ok(WorldInstruction::AddAuthority), + REMOVE_AUTHORIITY_DISCRIMINATOR => Ok(WorldInstruction::RemoveAuthority), + APPROVE_SYSTEM_DISCRIMINATOR => Ok(WorldInstruction::ApproveSystem), + REMOVE_SYSTEM_DISCRIMINATOR => Ok(WorldInstruction::RemoveSystem), + ADD_ENTITY_DISCRIMINATOR => Ok(WorldInstruction::AddEntity), + INITIALIZE_COMPONENT_DISCRIMINATOR => Ok(WorldInstruction::InitilizeComponent), + DESTROY_COMPONENT_DISCRIMINATOR => Ok(WorldInstruction::DestroyComponent), + APPLY_DISCRIMINATOR => Ok(WorldInstruction::Apply), + APPLY_WITH_SESSION_DISCRIMINATOR => Ok(WorldInstruction::ApplyWithSession), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} diff --git a/crates/pinocchio/world/src/instructions/remove_authority.rs b/crates/pinocchio/world/src/instructions/remove_authority.rs new file mode 100644 index 00000000..e8382c57 --- /dev/null +++ b/crates/pinocchio/world/src/instructions/remove_authority.rs @@ -0,0 +1,61 @@ +use crate::{ + error::WorldError, + state::world::{World, WorldMut}, +}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; + +pub fn remove_authority(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let [authority, authority_to_delete, world_acct, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let world_id = unsafe { (data.as_ptr() as *const u64).read_unaligned() }; + + // assert world pda + if &World::pda(&world_id.to_be_bytes()).0 != world_acct.key() { + return Err(ProgramError::InvalidSeeds); + } + + let mut world = WorldMut::from_bytes(unsafe { world_acct.borrow_mut_data_unchecked() })?; + + let authorities = world.authorities()?; + + if !authorities.contains(authority_to_delete.key()) { + return Err(WorldError::InvalidAuthority.into()); + } + + if let Some(index) = world + .authorities()? + .iter() + .position(|x| x == authority_to_delete.key()) + { + world.remove_authority(index)?; + let world_size = world.size()?; + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(world_size); + + let lamports_diff = world_acct.lamports().saturating_sub(new_minimum_balance); + + if lamports_diff > 0 { + unsafe { + *world_acct.borrow_mut_lamports_unchecked() -= lamports_diff; + *authority.borrow_mut_lamports_unchecked() += lamports_diff + } + } + + world_acct.realloc(world_size, false)?; + + Ok(()) + } else { + Err(WorldError::AuthorityNotFound.into()) + } +} diff --git a/crates/pinocchio/world/src/instructions/remove_system.rs b/crates/pinocchio/world/src/instructions/remove_system.rs new file mode 100644 index 00000000..fe3743cb --- /dev/null +++ b/crates/pinocchio/world/src/instructions/remove_system.rs @@ -0,0 +1,48 @@ +use crate::{error::WorldError, state::world::WorldMut, utils::assert_program_account}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; + +pub fn remove_system(accounts: &[AccountInfo]) -> ProgramResult { + let [authority, world_acct, system, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if !authority.is_signer() { + return Err(WorldError::InvalidAuthority.into()); + } + + assert_program_account(world_acct)?; + + let mut world = WorldMut::from_account_info(world_acct)?; + + let authorities = world.authorities()?; + + if !authorities.contains(authority.key()) { + return Err(WorldError::InvalidAuthority.into()); + } + + let size = world.remove_system(system.key())?; + + if size > 0 { + let world_size = world.size()?; + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(world_size); + + let lamports_diff = world_acct.lamports().saturating_sub(new_minimum_balance); + + if lamports_diff > 0 { + unsafe { + *world_acct.borrow_mut_lamports_unchecked() -= lamports_diff; + *authority.borrow_mut_lamports_unchecked() += lamports_diff + } + } + + world_acct.realloc(world_size, false)? + } + + Ok(()) +} diff --git a/crates/pinocchio/world/src/lib.rs b/crates/pinocchio/world/src/lib.rs new file mode 100644 index 00000000..34893413 --- /dev/null +++ b/crates/pinocchio/world/src/lib.rs @@ -0,0 +1,49 @@ +#![no_std] +#![allow(unexpected_cfgs)] + +mod consts; +mod error; +mod instructions; +mod state; +mod utils; + +use consts::DISCRIMATOR_LENGTH; +use instructions::*; +use pinocchio::{ + account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, + ProgramResult, +}; + +pinocchio_pubkey::declare_id!("WorLD15A7CrDwLcLy4fRqtaTb9fbd8o8iqiEMUDse2n"); + +pinocchio::nostd_panic_handler!(); + +pinocchio::entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if program_id != &crate::ID { + return Err(ProgramError::IncorrectProgramId); + } + + let (instruction_bytes, data) = instruction_data.split_at(DISCRIMATOR_LENGTH); + + let instruction = unsafe { (instruction_bytes.as_ptr() as *const u64).read() }; + + match WorldInstruction::try_from(instruction)? { + WorldInstruction::InitializeRegistry => initialize_registry(accounts), + WorldInstruction::InitializeNewWorld => initialize_new_world(accounts), + WorldInstruction::AddAuthority => add_authority(accounts, data), + WorldInstruction::RemoveAuthority => remove_authority(accounts, data), + WorldInstruction::InitilizeComponent => initialize_component(accounts), + WorldInstruction::DestroyComponent => destroy_component(accounts), + WorldInstruction::ApproveSystem => approve_system(accounts), + WorldInstruction::RemoveSystem => remove_system(accounts), + WorldInstruction::Apply => apply_system(accounts, data), + WorldInstruction::ApplyWithSession => apply_system_session(accounts, data), + WorldInstruction::AddEntity => add_entity(accounts, data), + } +} diff --git a/crates/pinocchio/world/src/state/account.rs b/crates/pinocchio/world/src/state/account.rs new file mode 100644 index 00000000..ff7aac53 --- /dev/null +++ b/crates/pinocchio/world/src/state/account.rs @@ -0,0 +1,13 @@ +use pinocchio::{account_info::AccountInfo, ProgramResult}; + +use crate::utils::assert_program_account_and_discriminator; + +pub trait AnchorAccount { + const DISCRIMINATOR: [u8; 8]; + + fn discriminator(&self) -> [u8; 8]; + + fn assert_account(&self, account_info: &AccountInfo) -> ProgramResult { + assert_program_account_and_discriminator(account_info, &self.discriminator()) + } +} diff --git a/crates/pinocchio/world/src/state/entity.rs b/crates/pinocchio/world/src/state/entity.rs new file mode 100644 index 00000000..7fd55cc5 --- /dev/null +++ b/crates/pinocchio/world/src/state/entity.rs @@ -0,0 +1,94 @@ +use super::{ + account::AnchorAccount, + transmutable::{Transmutable, TransmutableMut}, +}; +use pinocchio::{ + instruction::Seed, + program_error::ProgramError, + pubkey::{find_program_address, Pubkey}, +}; + +#[repr(C)] +pub struct Entity { + pub discriminator: [u8; 8], + pub id: u64, +} + +impl Entity { + pub fn seeds() -> &'static [u8] { + b"entity".as_ref() + } + + pub fn init(&mut self, id: u64) -> Result<(), ProgramError> { + self.discriminator = Self::DISCRIMINATOR; + self.id = id; + Ok(()) + } + + pub fn remaining_seeds<'a>( + world_entity: &'a [u8], + extra_seed: &'a [u8], // Option: Borsh vec + ) -> Result<(&'a [u8], &'a [u8]), ProgramError> { + let (is_extra_seeds_bytes, seeds_vec_bytes) = extra_seed + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + let is_extra_seeds = match is_extra_seeds_bytes { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ProgramError::InvalidInstructionData), + }?; + + let seeds: (&[u8], &[u8]) = if is_extra_seeds { + let (_, s) = seeds_vec_bytes.split_at(4); + ([0; 8].as_slice(), s) + } else { + (world_entity, &[]) + }; + + Ok(seeds) + } + + pub fn pda( + world_id: &[u8; 8], + world_entity: &[u8; 8], + extra_seed: &[u8], + ) -> Result<(Pubkey, u8), ProgramError> { + let seeds = Self::remaining_seeds(world_entity, extra_seed)?; + + Ok(find_program_address( + &[Entity::seeds(), world_id, seeds.0, seeds.1], + &crate::ID, + )) + } + + pub fn signer<'a>( + world_id: &'a [u8; 8], + world_entity: &'a [u8; 8], + extra_seed: &'a [u8], + bump: &'a [u8; 1], + ) -> Result<[Seed<'a>; 5], ProgramError> { + let seeds = Self::remaining_seeds(world_entity, extra_seed)?; + + Ok([ + Self::seeds().as_ref().into(), + world_id.as_ref().into(), + seeds.0.as_ref().into(), + seeds.1.as_ref().into(), + bump.as_ref().into(), + ]) + } +} + +impl TransmutableMut for Entity {} + +impl Transmutable for Entity { + const LEN: usize = core::mem::size_of::(); +} + +impl AnchorAccount for Entity { + const DISCRIMINATOR: [u8; 8] = [46, 157, 161, 161, 254, 46, 79, 24]; + + fn discriminator(&self) -> [u8; 8] { + self.discriminator + } +} diff --git a/crates/pinocchio/world/src/state/mod.rs b/crates/pinocchio/world/src/state/mod.rs new file mode 100644 index 00000000..afdb0e70 --- /dev/null +++ b/crates/pinocchio/world/src/state/mod.rs @@ -0,0 +1,6 @@ +pub mod account; +pub mod entity; +pub mod registry; +pub mod system_whitelist; +pub mod transmutable; +pub mod world; diff --git a/crates/pinocchio/world/src/state/registry.rs b/crates/pinocchio/world/src/state/registry.rs new file mode 100644 index 00000000..771c9b19 --- /dev/null +++ b/crates/pinocchio/world/src/state/registry.rs @@ -0,0 +1,54 @@ +use pinocchio::{instruction::Seed, program_error::ProgramError, pubkey::{find_program_address, Pubkey}}; + +use super::{ + account::AnchorAccount, + transmutable::{Transmutable, TransmutableMut}, +}; + +#[repr(C)] +pub struct Registry { + pub discriminator: [u8; 8], + pub worlds: u64, +} + +impl Registry { + pub fn seeds() -> &'static [u8] { + b"registry".as_ref() + } + + pub fn pda() -> (Pubkey, u8) { + find_program_address(&[Registry::seeds()], &crate::ID) + } + + pub fn signer(bump: &[u8; 1]) -> [Seed; 2] { + [Registry::seeds().as_ref().into(), bump.as_ref().into()] + } + + pub fn init(&mut self) -> Result<(), ProgramError> { + *self = Registry::default(); + Ok(()) + } +} + +impl Default for Registry { + fn default() -> Self { + Self { + discriminator: Self::DISCRIMINATOR, + worlds: 0 + } + } +} + +impl TransmutableMut for Registry {} + +impl Transmutable for Registry { + const LEN: usize = core::mem::size_of::(); +} + +impl AnchorAccount for Registry { + const DISCRIMINATOR: [u8; 8] = [47, 174, 110, 246, 184, 182, 252, 218]; + + fn discriminator(&self) -> [u8; 8] { + self.discriminator + } +} diff --git a/crates/pinocchio/world/src/state/system_whitelist.rs b/crates/pinocchio/world/src/state/system_whitelist.rs new file mode 100644 index 00000000..a9328bc4 --- /dev/null +++ b/crates/pinocchio/world/src/state/system_whitelist.rs @@ -0,0 +1,15 @@ +use super::transmutable::{Transmutable, TransmutableMut}; + +#[repr(C)] +#[derive(Default)] +pub struct SystemWhitelist { + pub discriminator: u64, +} + +impl SystemWhitelist {} + +impl TransmutableMut for SystemWhitelist {} + +impl Transmutable for SystemWhitelist { + const LEN: usize = core::mem::size_of::(); +} diff --git a/crates/pinocchio/world/src/state/transmutable.rs b/crates/pinocchio/world/src/state/transmutable.rs new file mode 100644 index 00000000..14b397e1 --- /dev/null +++ b/crates/pinocchio/world/src/state/transmutable.rs @@ -0,0 +1,29 @@ +use pinocchio::program_error::ProgramError; + +pub trait Transmutable: Sized { + const LEN: usize; + + /// # Safety + unsafe fn load_unchecked(bytes: &[u8]) -> Result<&Self, ProgramError> { + if bytes.len() != Self::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&*(bytes.as_ptr() as *const Self)) + } +} + +pub trait TransmutableMut: Transmutable { + /// Return a mutable `T` reference from the given bytes. + /// + /// # Safety + /// + /// The caller must ensure that `bytes` contains a valid representation of + /// `T`. + #[inline(always)] + unsafe fn load_mut_unchecked(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> { + if bytes.len() != Self::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&mut *(bytes.as_mut_ptr() as *mut Self)) + } +} diff --git a/crates/pinocchio/world/src/state/world.rs b/crates/pinocchio/world/src/state/world.rs new file mode 100644 index 00000000..f230ba37 --- /dev/null +++ b/crates/pinocchio/world/src/state/world.rs @@ -0,0 +1,338 @@ +use super::{ + account::AnchorAccount, + transmutable::{Transmutable, TransmutableMut}, +}; +use pinocchio::{ + account_info::AccountInfo, + instruction::Seed, + memory::sol_memmove, + program_error::ProgramError, + pubkey::{find_program_address, Pubkey}, +}; + +pub struct World; + +impl World { + pub const DISCRIMINATOR: [u8; 8] = [145, 45, 170, 174, 122, 32, 155, 124]; + + pub const INIT_SIZE: usize = 8 + 8 + 8 + 4 + 1 + 4; + + fn world_seed() -> &'static [u8] { + b"world" + } + + pub fn pda(id: &[u8; 8]) -> (Pubkey, u8) { + find_program_address(&[Self::world_seed(), id], &crate::ID) + } + + pub fn signer<'a>(world_id: &'a [u8; 8], bump: &'a [u8; 1]) -> [Seed<'a>; 3] { + [ + Self::world_seed().as_ref().into(), + world_id.as_ref().into(), + bump.as_ref().into(), + ] + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct WorldMetadata { + pub discriminator: [u8; 8], + pub id: u64, + pub entities: u64, +} + +impl TransmutableMut for WorldMetadata {} + +impl Transmutable for WorldMetadata { + const LEN: usize = core::mem::size_of::(); +} + +impl WorldMetadata { + pub fn new(id: u64) -> Self { + Self { + id, + ..Default::default() + } + } +} + +impl Default for WorldMetadata { + fn default() -> Self { + Self { + discriminator: World::DISCRIMINATOR, + entities: 0, + id: 0, + } + } +} + +#[allow(dead_code)] +pub struct WorldRef<'a> { + pub metadata: &'a WorldMetadata, + pub authorities_len: u32, + pub authorities: &'a [Pubkey], + pub permissionless: &'a u8, + pub systems_len: u32, + pub systems: &'a [Pubkey], +} + +impl<'a> WorldRef<'a> { + pub fn from_account_info(account_info: &'a AccountInfo) -> Result { + let data = Self::from_bytes(unsafe { account_info.borrow_data_unchecked() })?; + data.assert_account(account_info)?; + Ok(data) + } + + pub fn from_bytes(bytes: &'a [u8]) -> Result { + let world = unsafe { + let metadata = WorldMetadata::load_unchecked(&bytes[..WorldMetadata::LEN])?; + + let authorities_len_ptr = + bytes.as_ptr().add(WorldMetadata::LEN) as *const _ as *const u32; + + // aligned up to this point, shouldn't error + let authorities_len = authorities_len_ptr.read(); + let authorities_ptr = authorities_len_ptr.add(1) as *const Pubkey; + + let permissionless_ptr = authorities_ptr.add(authorities_len as usize) as *const u8; + + let systems_len_ptr = permissionless_ptr.add(1) as *const [u8; 4]; + let systems_len = u32::from_le_bytes(systems_len_ptr.read()); + + let systems_ptr = systems_len_ptr.add(1) as *const Pubkey; + + Self { + metadata, + authorities: core::slice::from_raw_parts(authorities_ptr, authorities_len as usize), + authorities_len, + permissionless: &*permissionless_ptr, + systems_len, + systems: core::slice::from_raw_parts( + systems_ptr, + systems_len as usize / core::mem::size_of::(), + ), + } + }; + + Ok(world) + } + + pub fn permissionless(&self) -> Result { + match self.permissionless { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ProgramError::InvalidAccountData), + } + } +} + +pub struct WorldMut<'a> { + pub metadata: &'a mut WorldMetadata, + pub data: &'a mut [u8], +} + +impl<'a> WorldMut<'a> { + pub fn from_account_info(account_info: &'a AccountInfo) -> Result { + let data = Self::from_bytes(unsafe { account_info.borrow_mut_data_unchecked() })?; + data.assert_account(account_info)?; + Ok(data) + } + + pub fn from_bytes(bytes: &'a mut [u8]) -> Result { + let world = unsafe { + let (metadata_bytes, data) = bytes.split_at_mut(WorldMetadata::LEN); + let metadata = WorldMetadata::load_mut_unchecked(metadata_bytes)?; + Self { metadata, data } + }; + + Ok(world) + } + + pub fn init(&mut self, id: u64) -> Result<(), ProgramError> { + *self.metadata = WorldMetadata::new(id); + *self.is_permissionless()? = true; + Ok(()) + } + + pub fn add_new_authority(&mut self, authority: &Pubkey) -> Result<(), ProgramError> { + let data_ptr = self.data as *mut _ as *mut u8; + let offset = self.authority_size()?; + + unsafe { + let new_authority_ptr = data_ptr.add(offset) as *mut Pubkey; + let permissionles_ptr = new_authority_ptr.add(1) as *mut u8; + let size = self.permissionless_len()? + self.systems_size()?; + sol_memmove(permissionles_ptr, new_authority_ptr as *mut u8, size); + *new_authority_ptr = *authority; + }; + + *self.authorities_len()? += 1; + + Ok(()) + } + + pub fn remove_authority(&mut self, index: usize) -> Result<(), ProgramError> { + self.data + .copy_within(authorities_size(index + 1).., authorities_size(index)); + + *self.authorities_len()? -= 1; + + Ok(()) + } + + pub fn authorities_len(&mut self) -> Result<&mut u32, ProgramError> { + Ok(unsafe { &mut *(self.data.as_mut_ptr() as *mut u32) }) + } + + pub fn permissionless_len(&mut self) -> Result { + Ok(core::mem::size_of::()) + } + + pub fn permissionless(&mut self) -> Result<&mut u8, ProgramError> { + let byte = &mut self.data[self.authority_size()?]; + Ok(byte) + } + + pub fn is_permissionless(&mut self) -> Result<&mut bool, ProgramError> { + let byte = self.permissionless()?; + match byte { + 0 | 1 => { + let ptr = byte as *mut u8 as *mut bool; + Ok(unsafe { &mut *ptr }) + } + _ => Err(ProgramError::InvalidAccountData), + } + } + + pub fn authority_size(&mut self) -> Result { + let authorities_len = *self.authorities_len()?; + Ok(authorities_size(authorities_len as usize)) + } + + pub fn authorities(&mut self) -> Result<&[Pubkey], ProgramError> { + let authorities_len = *self.authorities_len()?; + let authorities = unsafe { + let authorities_ptr = + self.data + .as_mut_ptr() + .add(core::mem::size_of::()) as *mut _ as *const Pubkey; + core::slice::from_raw_parts(authorities_ptr, authorities_len as usize) + }; + Ok(authorities) + } + + pub fn systems_size(&mut self) -> Result { + let systems_len = *self.systems_len()?; + Ok(systems_size(systems_len as usize)) + } + + pub fn systems_len(&mut self) -> Result<&mut u32, ProgramError> { + Ok(unsafe { + let permissionless_ptr = self.permissionless()? as *mut u8; + &mut *(permissionless_ptr.add(1) as *mut u32) + }) + } + + pub fn systems_pubkey_slice(&mut self) -> Result<&mut [Pubkey], ProgramError> { + let systems_len = self.systems_len()?; + + let systems_ptr = unsafe { (systems_len as *mut _ as *mut u8).add(4) as *mut Pubkey }; + + Ok(unsafe { + core::slice::from_raw_parts_mut( + systems_ptr, + *systems_len as usize / core::mem::size_of::(), + ) + }) + } + + pub fn add_system(&mut self, system: &Pubkey) -> Result { + let system_slice = self.systems_pubkey_slice()?; + + let insert_pos = match system_slice.binary_search(system) { + Ok(_) => return Ok(0), + Err(pos) => pos, + }; + + let shift = system_slice.len().saturating_sub(insert_pos); + + let size = core::mem::size_of::(); + + unsafe { + let src = system_slice.as_mut_ptr().add(insert_pos); + + if shift > 0 { + let dst = src.add(1); + sol_memmove(dst as *mut u8, src as *mut u8, shift * size); + }; + + *src = *system; + } + + *self.systems_len()? += size as u32; + + Ok(size) + } + + pub fn remove_system(&mut self, system: &Pubkey) -> Result { + match self.systems_pubkey_slice()?.binary_search(system) { + Ok(index) => self.remove_system_at_index(index), + Err(_) => Ok(0), + } + } + + fn remove_system_at_index(&mut self, index: usize) -> Result { + let authorities_len = self.systems_pubkey_slice()?.len(); + + let remaining_systems = authorities_len - index - 1; + + let size = core::mem::size_of::(); + + let size_to_move = remaining_systems * size; + + if remaining_systems > 0 { + unsafe { + let authorities_ptr = self.systems_pubkey_slice()?.as_mut_ptr(); + let src_ptr = authorities_ptr.add(index + 1); + let dst_ptr = authorities_ptr.add(index); + sol_memmove(dst_ptr as *mut u8, src_ptr as *mut u8, size_to_move); + }; + } + + *self.systems_len()? -= size as u32; + + Ok(size) + } + + pub fn size(&mut self) -> Result { + Ok(WorldMetadata::LEN + + self.authority_size()? + + self.permissionless_len()? + + self.systems_size()?) + } +} + +pub fn systems_size(count: usize) -> usize { + core::mem::size_of::() + count +} + +pub fn authorities_size(count: usize) -> usize { + core::mem::size_of::() + (count * core::mem::size_of::()) +} + +impl AnchorAccount for WorldRef<'_> { + const DISCRIMINATOR: [u8; 8] = World::DISCRIMINATOR; + + fn discriminator(&self) -> [u8; 8] { + self.metadata.discriminator + } +} + +impl AnchorAccount for WorldMut<'_> { + const DISCRIMINATOR: [u8; 8] = World::DISCRIMINATOR; + + fn discriminator(&self) -> [u8; 8] { + self.metadata.discriminator + } +} diff --git a/crates/pinocchio/world/src/utils.rs b/crates/pinocchio/world/src/utils.rs new file mode 100644 index 00000000..c3d588e3 --- /dev/null +++ b/crates/pinocchio/world/src/utils.rs @@ -0,0 +1,70 @@ +use crate::consts::DISCRIMATOR_LENGTH; +use core::mem::MaybeUninit; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +#[allow(clippy::type_complexity)] +pub fn init_execute_cpi_accounts<'a>( + remaining: &'a [AccountInfo], + ctx_accounts: &'a mut [MaybeUninit<&'a AccountInfo>], +) -> Result<(&'a [&'a AccountInfo], Option, &'a [&'a AccountInfo]), ProgramError> { + let mut separator_idx: Option = None; + let mut len = 0; + let mut component_len = 0; + + #[allow(clippy::needless_range_loop)] + for i in 0..remaining.len() { + if separator_idx.is_some() { + ctx_accounts[len].write(&remaining[i]); + len += 1; + } else if (i + 1) % 2 == 0 { + ctx_accounts[len].write(&remaining[i]); + component_len += 1; + len += 1; + } else if remaining[i].key() == &crate::ID { + separator_idx = Some(i); + } + } + + let maybe_components = &ctx_accounts[..component_len]; + let maybe_remaining_accounts = &ctx_accounts[component_len..len]; + + let components = unsafe { + core::slice::from_raw_parts( + maybe_components.as_ptr() as *const &AccountInfo, + maybe_components.len(), + ) + }; + let remaining_accounts = unsafe { + core::slice::from_raw_parts( + maybe_remaining_accounts.as_ptr() as *const &AccountInfo, + maybe_remaining_accounts.len(), + ) + }; + + Ok((components, separator_idx, remaining_accounts)) +} + +pub fn assert_program_account(account_info: &AccountInfo) -> ProgramResult { + if !account_info.is_owned_by(&crate::ID) { + return Err(ProgramError::IllegalOwner); + } + Ok(()) +} + +pub fn assert_discriminator(account_info: &AccountInfo, discriminator: &[u8; 8]) -> ProgramResult { + let disc = unsafe { &account_info.borrow_data_unchecked()[..DISCRIMATOR_LENGTH] }; + + if disc != discriminator { + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} + +pub fn assert_program_account_and_discriminator( + account_info: &AccountInfo, + discriminator: &[u8; 8], +) -> ProgramResult { + assert_program_account(account_info)?; + assert_discriminator(account_info, discriminator) +}