From 09d1674ee3a22335464dcb6fe495a9b311b0b7cd Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 21 Jan 2026 17:09:42 +0400 Subject: [PATCH 01/15] feat: implement ephemeral account creation --- Cargo.lock | 2 +- Cargo.toml | 4 +- magicblock-accounts-db/src/lib.rs | 8 +- magicblock-api/src/fund_account.rs | 18 +++++ magicblock-api/src/magic_validator.rs | 4 +- .../src/chainlink/blacklisted_accounts.rs | 1 + .../src/instruction.rs | 29 +++++++ magicblock-magic-program-api/src/lib.rs | 8 ++ magicblock-processor/src/executor/mod.rs | 4 +- .../magicblock/src/ephemeral_accounts/mod.rs | 14 ++++ .../src/ephemeral_accounts/process_close.rs | 56 ++++++++++++++ .../src/ephemeral_accounts/process_create.rs | 76 ++++++++++++++++++ .../src/ephemeral_accounts/process_resize.rs | 77 +++++++++++++++++++ .../src/ephemeral_accounts/processor.rs | 10 +++ .../src/ephemeral_accounts/validation.rs | 50 ++++++++++++ programs/magicblock/src/lib.rs | 1 + .../magicblock/src/magicblock_processor.rs | 13 ++++ .../process_schedule_commit.rs | 10 +++ programs/magicblock/src/test_utils/mod.rs | 8 ++ .../src/utils/instruction_context_frames.rs | 1 - programs/magicblock/src/utils/mod.rs | 1 - 21 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 programs/magicblock/src/ephemeral_accounts/mod.rs create mode 100644 programs/magicblock/src/ephemeral_accounts/process_close.rs create mode 100644 programs/magicblock/src/ephemeral_accounts/process_create.rs create mode 100644 programs/magicblock/src/ephemeral_accounts/process_resize.rs create mode 100644 programs/magicblock/src/ephemeral_accounts/processor.rs create mode 100644 programs/magicblock/src/ephemeral_accounts/validation.rs diff --git a/Cargo.lock b/Cargo.lock index 48ddb1189..3b45a1bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "solana-account" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/solana-account.git?rev=2246929#2246929c6614f60d9909fdd117aab3bc454a9775" +source = "git+https://github.com/magicblock-labs/solana-account.git?rev=6eae52b#6eae52bde25e90b3c79d4935ce2b267e35338945" dependencies = [ "bincode", "qualifier_attr", diff --git a/Cargo.toml b/Cargo.toml index a1844cc59..065ab9434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ serde_json = "1.0" serde_with = "3.16" serial_test = "3.2" sha3 = "0.10.8" -solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" } +solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-account-decoder = { version = "2.2" } solana-account-decoder-client-types = { version = "2.2" } solana-account-info = { version = "2.2" } @@ -229,7 +229,7 @@ version = "0.22.0" # some solana dependencies have solana-storage-proto as dependency # we need to patch them with our version, because they use protobuf-src v1.1.0 # and we use protobuf-src v2.1.1. Otherwise compilation fails -solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" } +solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-storage-proto = { path = "./storage-proto" } solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "3e9456ec4" } # Fork is used to enable `disable_manual_compaction` usage diff --git a/magicblock-accounts-db/src/lib.rs b/magicblock-accounts-db/src/lib.rs index 033617536..34f2ec232 100644 --- a/magicblock-accounts-db/src/lib.rs +++ b/magicblock-accounts-db/src/lib.rs @@ -20,7 +20,7 @@ pub type AccountsDbResult = Result; /// A global lock used to suspend all write operations during critical /// sections (like snapshots). -pub type GlobalWriteLock = Arc>; +pub type GlobalSyncLock = Arc>; pub const ACCOUNTSDB_DIR: &str = "accountsdb"; @@ -41,7 +41,7 @@ pub struct AccountsDb { /// Global lock ensures atomic snapshots by pausing writes. /// Note: Reads are generally wait-free/lock-free via mmap, /// unless they require index cursor stability. - write_lock: GlobalWriteLock, + write_lock: GlobalSyncLock, /// Configured interval (in slots) for creating snapshots. snapshot_frequency: u64, } @@ -86,7 +86,7 @@ impl AccountsDb { storage, index, snapshot_manager, - write_lock: GlobalWriteLock::default(), + write_lock: GlobalSyncLock::default(), snapshot_frequency: config.snapshot_frequency, }; @@ -366,7 +366,7 @@ impl AccountsDb { self.index.flush(); } - pub fn write_lock(&self) -> GlobalWriteLock { + pub fn write_lock(&self) -> GlobalSyncLock { self.write_lock.clone() } } diff --git a/magicblock-api/src/fund_account.rs b/magicblock-api/src/fund_account.rs index 8e609e270..fd2b96727 100644 --- a/magicblock-api/src/fund_account.rs +++ b/magicblock-api/src/fund_account.rs @@ -82,3 +82,21 @@ pub(crate) fn fund_magic_context(accountsdb: &AccountsDb) { let _ = accountsdb .insert_account(&magic_program::MAGIC_CONTEXT_PUBKEY, &magic_context); } + +pub(crate) fn fund_ephemeral_vault(accountsdb: &AccountsDb) { + // Only create vault if it doesn't exist (don't overwrite on restart) + if accountsdb + .get_account(&magic_program::EPHEMERAL_VAULT_PUBKEY) + .is_none() + { + // Create with 0 balance, will accumulate rent over time + fund_account(accountsdb, &magic_program::EPHEMERAL_VAULT_PUBKEY, 0); + let mut vault = accountsdb + .get_account(&magic_program::EPHEMERAL_VAULT_PUBKEY) + .unwrap(); + vault.set_delegated(true); + vault.set_ephemeral(true); + let _ = accountsdb + .insert_account(&magic_program::EPHEMERAL_VAULT_PUBKEY, &vault); + } +} diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 3d54f17ff..aca45c5eb 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -83,7 +83,8 @@ use crate::{ domain_registry_manager::DomainRegistryManager, errors::{ApiError, ApiResult}, fund_account::{ - fund_magic_context, funded_faucet, init_validator_identity, + fund_ephemeral_vault, fund_magic_context, funded_faucet, + init_validator_identity, }, genesis_utils::{create_genesis_config_with_leader, GenesisConfigInfo}, ledger::{ @@ -179,6 +180,7 @@ impl MagicValidator { init_validator_identity(&accountsdb, &validator_pubkey); fund_magic_context(&accountsdb); + fund_ephemeral_vault(&accountsdb); let faucet_keypair = funded_faucet(&accountsdb, ledger.ledger_path().as_path())?; diff --git a/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs b/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs index cdb1c090a..963debe48 100644 --- a/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs +++ b/magicblock-chainlink/src/chainlink/blacklisted_accounts.rs @@ -29,6 +29,7 @@ pub fn blacklisted_accounts( blacklisted_accounts.insert(magic_program::ID); blacklisted_accounts.insert(magic_program::MAGIC_CONTEXT_PUBKEY); + blacklisted_accounts.insert(magic_program::EPHEMERAL_VAULT_PUBKEY); blacklisted_accounts.insert(*validator_id); blacklisted_accounts.insert(*faucet_id); blacklisted_accounts diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 859cc9768..ae5cc5a58 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -124,6 +124,35 @@ pub enum MagicBlockInstruction { /// - **0.** `[SIGNER]` Validator authority EnableExecutableCheck, + /// Creates a new ephemeral account with rent paid by a sponsor. + /// The account is automatically owned by the calling program (CPI caller). + /// + /// # Account references + /// - **0.** `[WRITE]` Sponsor account (pays rent, can be PDA or oncurve) + /// - **1.** `[WRITE]` Ephemeral account to create (must have 0 lamports) + /// - **2.** `[WRITE]` Vault account (receives rent payment) + CreateEphemeralAccount { + /// Initial data length in bytes + data_len: usize, + }, + + /// Resizes an existing ephemeral account, adjusting rent accordingly. + /// + /// # Account references + /// - **0.** `[WRITE]` Sponsor account (pays/receives rent difference) + /// - **1.** `[WRITE]` Ephemeral account to resize + ResizeEphemeralAccount { + /// New data length in bytes + new_data_len: usize, + }, + + /// Closes an ephemeral account, refunding rent to the sponsor. + /// + /// # Account references + /// - **0.** `[WRITE]` Sponsor account (receives rent refund) + /// - **1.** `[WRITE]` Ephemeral account to close + CloseEphemeralAccount, + /// Noop instruction Noop(u64), diff --git a/magicblock-magic-program-api/src/lib.rs b/magicblock-magic-program-api/src/lib.rs index 79bf67ced..0a78964a2 100644 --- a/magicblock-magic-program-api/src/lib.rs +++ b/magicblock-magic-program-api/src/lib.rs @@ -8,9 +8,17 @@ declare_id!("Magic11111111111111111111111111111111111111"); pub const MAGIC_CONTEXT_PUBKEY: Pubkey = pubkey!("MagicContext1111111111111111111111111111111"); +/// Vault account that collects rent for ephemeral accounts. +pub const EPHEMERAL_VAULT_PUBKEY: Pubkey = + pubkey!("MagicVau1t999999999999999999999999999999999"); + /// We believe 5MB should be enough to store all scheduled commits within a /// slot. Once we store more data in the magic context we need to reconsicer /// this size. /// NOTE: the default max accumulated account size per transaction is 64MB. /// See: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES inside program-runtime/src/compute_budget_processor.rs pub const MAGIC_CONTEXT_SIZE: usize = 1024 * 1024 * 5; // 5 MB + +/// Rent rate for ephemeral accounts: 32 lamports per byte. +/// This is ~109x cheaper than Solana's base rent (3,480 lamports/byte). +pub const EPHEMERAL_RENT_PER_BYTE: u64 = 32; diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index 412c03b95..5cabbea94 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -3,7 +3,7 @@ use std::{ sync::{Arc, RwLock}, }; -use magicblock_accounts_db::{AccountsDb, GlobalWriteLock}; +use magicblock_accounts_db::{AccountsDb, GlobalSyncLock}; use magicblock_core::link::{ accounts::AccountUpdateTx, transactions::{ @@ -37,7 +37,7 @@ pub(super) struct TransactionExecutor { accountsdb: Arc, ledger: Arc, block: LatestBlock, - sync: GlobalWriteLock, + sync: GlobalSyncLock, // SVM Components processor: TransactionBatchProcessor, diff --git a/programs/magicblock/src/ephemeral_accounts/mod.rs b/programs/magicblock/src/ephemeral_accounts/mod.rs new file mode 100644 index 000000000..d6e84ab45 --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/mod.rs @@ -0,0 +1,14 @@ +//! Ephemeral account instruction processors +//! +//! Ephemeral accounts are zero-balance accounts with rent paid by a sponsor. +//! Rent is charged at 32 lamports/byte (109x cheaper than Solana base rent). + +mod process_close; +mod process_create; +mod process_resize; +mod processor; +mod validation; + +pub(crate) use process_create::process_create_ephemeral_account; +pub(crate) use process_close::process_close_ephemeral_account; +pub(crate) use process_resize::process_resize_ephemeral_account; diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs new file mode 100644 index 000000000..ba86a5496 --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -0,0 +1,56 @@ +//! Close ephemeral account instruction processor + +use magicblock_magic_program_api::id; +use solana_account::{ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_transaction_context::TransactionContext; + +use super::processor::rent_for; +use super::validation::{validate_cpi_only, validate_sponsor}; +use crate::utils::accounts; + +/// Closes an ephemeral account, refunding rent to the sponsor. +pub(crate) fn process_close_ephemeral_account( + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, +) -> Result<(), InstructionError> { + // Must be called via CPI (user programs mediate all access) + validate_cpi_only(transaction_context)?; + + // Validate sponsor (signer or PDA owned by caller) + validate_sponsor(transaction_context)?; + + // Validate vault is owned by magic program + let vault = accounts::get_instruction_account_with_idx(transaction_context, 2)?; + if *vault.borrow().owner() != id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let ephemeral = accounts::get_instruction_account_with_idx(transaction_context, 1)?; + + if !ephemeral.borrow().ephemeral() { + return Err(InstructionError::InvalidAccountData); + } + + let data_len = ephemeral.borrow().data().len(); + let refund = rent_for(data_len); + // Credit sponsor, debit vault + accounts::credit_instruction_account_at_index( + transaction_context, + 0, + refund, + )?; + accounts::debit_instruction_account_at_index(transaction_context, 2, refund)?; + + let mut acc = ephemeral.borrow_mut(); + acc.set_lamports(0); + acc.set_owner(solana_sdk_ids::system_program::id()); + acc.resize(0, 0); + acc.set_ephemeral(false); + acc.set_delegated(false); + + ic_msg!(invoke_context, "Closed ephemeral, refunded: {}", refund); + Ok(()) +} diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs new file mode 100644 index 000000000..2facf23a0 --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -0,0 +1,76 @@ +//! Create ephemeral account instruction processor + +use magicblock_magic_program_api::id; +use solana_account::{ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_transaction_context::TransactionContext; + +use super::processor::rent_for; +use super::validation::{validate_cpi_only, validate_sponsor}; +use crate::utils::accounts; + +/// Creates a new ephemeral account with rent paid by the sponsor. +/// The account is owned by the calling program (inferred from CPI context). +pub(crate) fn process_create_ephemeral_account( + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + data_len: usize, +) -> Result<(), InstructionError> { + use crate::utils::instruction_context_frames::InstructionContextFrames; + + // Must be called via CPI (user programs mediate all access) + validate_cpi_only(transaction_context)?; + + // Get caller program ID (will be the owner of the ephemeral account) + let frames = InstructionContextFrames::try_from(transaction_context)?; + let caller_program_id = frames + .find_program_id_of_parent_of_current_instruction() + .ok_or(InstructionError::IncorrectProgramId)?; + + // Validate sponsor (signer or PDA owned by caller) + validate_sponsor(transaction_context)?; + + // Validate vault is owned by magic program + let vault = + accounts::get_instruction_account_with_idx(transaction_context, 2)?; + if *vault.borrow().owner() != id() { + return Err(InstructionError::InvalidAccountOwner); + } + + // Validate: must have 0 lamports and not already ephemeral + let ephemeral = + accounts::get_instruction_account_with_idx(transaction_context, 1)?; + let acc = ephemeral.borrow(); + if acc.lamports() != 0 || acc.ephemeral() { + return Err(InstructionError::InvalidAccountData); + } + drop(acc); + + // Debit rent from sponsor, credit to vault + let rent = rent_for(data_len); + accounts::debit_instruction_account_at_index(transaction_context, 0, rent)?; + accounts::credit_instruction_account_at_index( + transaction_context, + 2, + rent, + )?; + + // Set up ephemeral account (owned by the calling program) + let mut acc = ephemeral.borrow_mut(); + acc.set_lamports(0); + acc.set_owner(*caller_program_id); + acc.resize(data_len, 0); + acc.set_ephemeral(true); + acc.set_delegated(true); + + ic_msg!( + invoke_context, + "Created ephemeral: {} bytes, {} rent, owner: {}", + data_len, + rent, + caller_program_id + ); + Ok(()) +} diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs new file mode 100644 index 000000000..b724f61a5 --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -0,0 +1,77 @@ +//! Resize ephemeral account instruction processor + +use magicblock_magic_program_api::id; +use solana_account::ReadableAccount; +use solana_instruction::error::InstructionError; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_transaction_context::TransactionContext; + +use super::processor::rent_for; +use super::validation::{validate_cpi_only, validate_sponsor}; +use crate::utils::accounts; + +/// Resizes an existing ephemeral account, adjusting rent accordingly. +pub(crate) fn process_resize_ephemeral_account( + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + new_data_len: usize, +) -> Result<(), InstructionError> { + // Must be called via CPI (user programs mediate all access) + validate_cpi_only(transaction_context)?; + + // Validate sponsor (signer or PDA owned by caller) + validate_sponsor(transaction_context)?; + + // Validate vault is owned by magic program + let vault = accounts::get_instruction_account_with_idx(transaction_context, 2)?; + if *vault.borrow().owner() != id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let ephemeral = accounts::get_instruction_account_with_idx(transaction_context, 1)?; + + if !ephemeral.borrow().ephemeral() { + return Err(InstructionError::InvalidAccountData); + } + + let old_len = ephemeral.borrow().data().len(); + let delta = rent_for(new_data_len) as i64 - rent_for(old_len) as i64; + + if delta > 0 { + // Debit sponsor, credit vault + accounts::debit_instruction_account_at_index( + transaction_context, + 0, + delta as u64, + )?; + accounts::credit_instruction_account_at_index( + transaction_context, + 2, + delta as u64, + )?; + } else { + // Credit sponsor, debit vault + accounts::credit_instruction_account_at_index( + transaction_context, + 0, + delta.unsigned_abs(), + )?; + accounts::debit_instruction_account_at_index( + transaction_context, + 2, + delta.unsigned_abs(), + )?; + } + + ephemeral.borrow_mut().resize(new_data_len, 0); + + ic_msg!( + invoke_context, + "Resized: {} -> {} bytes, delta: {}", + old_len, + new_data_len, + delta + ); + Ok(()) +} diff --git a/programs/magicblock/src/ephemeral_accounts/processor.rs b/programs/magicblock/src/ephemeral_accounts/processor.rs new file mode 100644 index 000000000..c48f9606d --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/processor.rs @@ -0,0 +1,10 @@ +//! Core processor utilities for ephemeral account instructions + +use magicblock_magic_program_api::EPHEMERAL_RENT_PER_BYTE; +use solana_account::AccountSharedData; + +/// Calculates rent for an ephemeral account based on its data length +pub(crate) const fn rent_for(data_len: usize) -> u64 { + let total_size = data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64; + total_size * EPHEMERAL_RENT_PER_BYTE +} diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs new file mode 100644 index 000000000..200623e32 --- /dev/null +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -0,0 +1,50 @@ +//! Validation utilities for ephemeral account instructions + +use solana_instruction::error::InstructionError; +use solana_transaction_context::TransactionContext; + +/// Validates the sponsor account according to MIMD-0016 spec: +/// - Oncurve accounts: must be a signer +/// - PDA accounts: must be owned by the calling program +pub(crate) fn validate_sponsor( + transaction_context: &TransactionContext, +) -> Result<(), InstructionError> { + use crate::utils::accounts; + use crate::utils::instruction_context_frames::InstructionContextFrames; + + let ix_ctx = transaction_context.get_current_instruction_context()?; + + // Check if sponsor is a signer + if !ix_ctx.is_instruction_account_signer(0)? { + // Not a signer, must be a PDA owned by the calling program + let frames = InstructionContextFrames::try_from(transaction_context)?; + let caller_program_id = frames + .find_program_id_of_parent_of_current_instruction() + .ok_or(InstructionError::InvalidAccountData)?; + + let sponsor_owner = accounts::get_instruction_account_owner_with_idx(transaction_context, 0)?; + if sponsor_owner != *caller_program_id { + return Err(InstructionError::InvalidAccountOwner); + } + } + Ok(()) +} + +/// Validates that the instruction is being called via CPI (not directly from a transaction). +/// This ensures all ephemeral account operations are mediated by user programs, +/// which implement their own access control and business logic. +pub(crate) fn validate_cpi_only( + transaction_context: &TransactionContext, +) -> Result<(), InstructionError> { + use crate::utils::instruction_context_frames::InstructionContextFrames; + + let frames = InstructionContextFrames::try_from(transaction_context)?; + let caller_program_id = frames.find_program_id_of_parent_of_current_instruction(); + + // If caller_program_id is None, we're at the top level (direct call, not CPI) + if caller_program_id.is_none() { + return Err(InstructionError::IncorrectProgramId); + } + + Ok(()) +} diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 76d21bde8..7d68ec5f7 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -1,4 +1,5 @@ pub mod errors; +mod ephemeral_accounts; mod magic_context; mod mutate_accounts; mod schedule_task; diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 30bbaec9a..dc9edb001 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -2,6 +2,10 @@ use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_program_runtime::declare_process_instruction; use crate::{ + ephemeral_accounts::{ + process_close_ephemeral_account, process_create_ephemeral_account, + process_resize_ephemeral_account, + }, mutate_accounts::process_mutate_accounts, process_scheduled_commit_sent, schedule_task::{process_cancel_task, process_schedule_task}, @@ -88,6 +92,15 @@ declare_process_instruction!( EnableExecutableCheck => { process_toggle_executable_check(signers, invoke_context, true) } + CreateEphemeralAccount { data_len } => { + process_create_ephemeral_account(invoke_context, transaction_context, data_len) + } + ResizeEphemeralAccount { new_data_len } => { + process_resize_ephemeral_account(invoke_context, transaction_context, new_data_len) + } + CloseEphemeralAccount => { + process_close_ephemeral_account(invoke_context, transaction_context) + } Noop(_) => Ok(()), } } diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index 77f57de3e..8817e5985 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -142,6 +142,16 @@ pub(crate) fn process_schedule_commit( return Err(InstructionError::InvalidAccountData); } + // Prevent ephemeral accounts from being committed to base chain + if acc.borrow().ephemeral() { + ic_msg!( + invoke_context, + "ScheduleCommit ERR: account {} is ephemeral and cannot be committed to base chain", + acc_pubkey + ); + return Err(InstructionError::InvalidAccountData); + } + { let is_delegated = acc.borrow().delegated(); diff --git a/programs/magicblock/src/test_utils/mod.rs b/programs/magicblock/src/test_utils/mod.rs index dafd8dc97..7929fc14a 100644 --- a/programs/magicblock/src/test_utils/mod.rs +++ b/programs/magicblock/src/test_utils/mod.rs @@ -9,6 +9,7 @@ use std::{ }; use magicblock_core::traits::PersistsAccountModData; +use magicblock_magic_program_api::{id, EPHEMERAL_VAULT_PUBKEY}; use solana_account::AccountSharedData; use solana_instruction::{error::InstructionError, AccountMeta}; use solana_log_collector::log::debug; @@ -30,6 +31,13 @@ pub fn ensure_started_validator(map: &mut HashMap) { AccountSharedData::new(AUTHORITY_BALANCE, 0, &system_program::id()) }); + // Ensure ephemeral vault account exists + map.entry(EPHEMERAL_VAULT_PUBKEY).or_insert_with(|| { + let mut vault = AccountSharedData::new(0, 0, &id()); + vault.set_ephemeral(true); + vault + }); + let stub = Arc::new(PersisterStub::default()); init_persister(stub); diff --git a/programs/magicblock/src/utils/instruction_context_frames.rs b/programs/magicblock/src/utils/instruction_context_frames.rs index 213ef9377..8196abd03 100644 --- a/programs/magicblock/src/utils/instruction_context_frames.rs +++ b/programs/magicblock/src/utils/instruction_context_frames.rs @@ -1,4 +1,3 @@ -#![cfg(not(test))] use solana_instruction::error::InstructionError; use solana_pubkey::Pubkey; use solana_transaction_context::{InstructionContext, TransactionContext}; diff --git a/programs/magicblock/src/utils/mod.rs b/programs/magicblock/src/utils/mod.rs index a05af586d..0af80b180 100644 --- a/programs/magicblock/src/utils/mod.rs +++ b/programs/magicblock/src/utils/mod.rs @@ -2,7 +2,6 @@ use solana_pubkey::Pubkey; pub mod account_actions; pub mod accounts; -#[cfg(not(test))] pub(crate) mod instruction_context_frames; pub mod instruction_utils; From f132bc9c31cc549aad94446cd8335415ab0d5a0a Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 28 Jan 2026 22:08:20 +0400 Subject: [PATCH 02/15] test: add test suite for ephemeral accounts --- Cargo.lock | 3 +- Cargo.toml | 4 +- magicblock-processor/Cargo.toml | 1 + .../tests/ephemeral_accounts.rs | 1126 +++++++++++++++++ programs/elfs/guinea.so | Bin 143720 -> 155264 bytes programs/guinea/src/lib.rs | 163 ++- .../src/ephemeral_accounts/process_close.rs | 30 +- .../src/ephemeral_accounts/process_create.rs | 1 - test-integration/Cargo.lock | 36 +- test-integration/Cargo.toml | 6 +- 10 files changed, 1348 insertions(+), 22 deletions(-) create mode 100644 magicblock-processor/tests/ephemeral_accounts.rs diff --git a/Cargo.lock b/Cargo.lock index 3b45a1bb4..3b4091d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3214,6 +3214,7 @@ dependencies = [ "magicblock-accounts-db", "magicblock-core", "magicblock-ledger", + "magicblock-magic-program-api", "magicblock-metrics", "magicblock-program", "parking_lot", @@ -7094,7 +7095,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4#3e9456ec4d5798ad8281537501c1e777d6888ba3" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85#e676441d85f91cc4a3f55029aaf12bc060bbd00f" dependencies = [ "ahash 0.8.12", "log", diff --git a/Cargo.toml b/Cargo.toml index 065ab9434..8acf190ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,7 +217,7 @@ spl-token-2022 = "7.0" [workspace.dependencies.solana-svm] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "3e9456ec4" +rev = "e676441d85" features = ["dev-context-only-utils"] [workspace.dependencies.rocksdb] @@ -231,7 +231,7 @@ version = "0.22.0" # and we use protobuf-src v2.1.1. Otherwise compilation fails solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-storage-proto = { path = "./storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "3e9456ec4" } +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "e676441d85" } # Fork is used to enable `disable_manual_compaction` usage # Fork is based on commit d4e9e16 of rocksdb (parent commit of 0.23.0 release) # without patching update isn't possible due to conflict with solana deps diff --git a/magicblock-processor/Cargo.toml b/magicblock-processor/Cargo.toml index 8344f8c54..4bb50e9d6 100644 --- a/magicblock-processor/Cargo.toml +++ b/magicblock-processor/Cargo.toml @@ -44,6 +44,7 @@ rustc-hash = { workspace = true } [dev-dependencies] guinea = { workspace = true } +magicblock-magic-program-api = { workspace = true } solana-keypair = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } diff --git a/magicblock-processor/tests/ephemeral_accounts.rs b/magicblock-processor/tests/ephemeral_accounts.rs new file mode 100644 index 000000000..7e7688306 --- /dev/null +++ b/magicblock-processor/tests/ephemeral_accounts.rs @@ -0,0 +1,1126 @@ +use guinea::GuineaInstruction; +use magicblock_accounts_db::traits::AccountsBank; +use magicblock_magic_program_api::{ + instruction::MagicBlockInstruction, EPHEMERAL_RENT_PER_BYTE, + EPHEMERAL_VAULT_PUBKEY, ID as MAGIC_PROGRAM_ID, +}; +use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; +use test_kit::{ExecutionTestEnv, Signer}; + +/// Calculates rent for an ephemeral account (same logic as magic program) +fn rent_for(data_len: usize) -> u64 { + (data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64) + * EPHEMERAL_RENT_PER_BYTE +} + +/// Test context with common setup +struct TestContext { + env: ExecutionTestEnv, + sponsor: Pubkey, + ephemeral: Pubkey, +} + +/// Sets up a test with vault, sponsor, and ephemeral account +fn setup_test() -> TestContext { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0).pubkey(); + + TestContext { + env, + sponsor, + ephemeral, + } +} + +/// Executes an instruction and returns the result +async fn execute_instruction( + env: &ExecutionTestEnv, + ix: Instruction, +) -> Result<(), solana_transaction_error::TransactionError> { + let txn = env.build_transaction(&[ix]); + env.execute_transaction(txn).await +} + +/// Helper to initialize the ephemeral vault account in the accounts database +fn init_vault(env: &ExecutionTestEnv) { + // Create vault with enough balance to be rent-exempt (covers the account overhead) + // Using a fixed amount that's sufficient for rent-exempt status + const VAULT_RENT_EXEMPT_BALANCE: u64 = 1_000_000; + env.fund_account_with_owner( + EPHEMERAL_VAULT_PUBKEY, + VAULT_RENT_EXEMPT_BALANCE, + MAGIC_PROGRAM_ID, + ); + let mut vault = env.get_account(EPHEMERAL_VAULT_PUBKEY); + vault.set_ephemeral(true); + vault.commit(); +} + +/// Helper to initialize the sponsor account as delegated +fn init_sponsor(env: &ExecutionTestEnv, sponsor: Pubkey) { + // Ensure sponsor is marked as delegated so it can be modified in gasless mode + let mut sponsor_acc = env.get_account(sponsor); + if !sponsor_acc.delegated() { + sponsor_acc.set_delegated(true); + sponsor_acc.commit(); + } +} + +/// Helper to create an instruction that calls guinea to create an ephemeral account +fn create_ephemeral_account_ix( + magic_program: Pubkey, + sponsor: Pubkey, + ephemeral: Pubkey, + vault: Pubkey, + data_len: usize, +) -> Instruction { + Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CreateEphemeralAccount { data_len }, + vec![ + AccountMeta::new_readonly(magic_program, false), + AccountMeta::new(sponsor, true), + AccountMeta::new(ephemeral, false), + AccountMeta::new(vault, false), + ], + ) +} + +/// Helper to create an instruction that calls guinea to resize an ephemeral account +fn resize_ephemeral_account_ix( + magic_program: Pubkey, + sponsor: Pubkey, + ephemeral: Pubkey, + vault: Pubkey, + new_data_len: usize, +) -> Instruction { + Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::ResizeEphemeralAccount { new_data_len }, + vec![ + AccountMeta::new_readonly(magic_program, false), + AccountMeta::new(sponsor, true), + AccountMeta::new(ephemeral, false), + AccountMeta::new(vault, false), + ], + ) +} + +/// Helper to create an instruction that calls guinea to close an ephemeral account +fn close_ephemeral_account_ix( + magic_program: Pubkey, + sponsor: Pubkey, + ephemeral: Pubkey, + vault: Pubkey, +) -> Instruction { + Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CloseEphemeralAccount, + vec![ + AccountMeta::new_readonly(magic_program, false), + AccountMeta::new(sponsor, true), + AccountMeta::new(ephemeral, false), + AccountMeta::new(vault, false), + ], + ) +} + +#[tokio::test] +async fn test_create_ephemeral_account_via_cpi() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use the payer (which is a signer) as the sponsor + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); // Must start with 0 lamports + + let data_len = 1000; + let expected_rent = rent_for(data_len); + + // Check balances BEFORE operation + let sponsor_before = env.get_account(sponsor); + let vault_before = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_before = env.get_account(ephemeral.pubkey()); + let total_before = sponsor_before.lamports() + + vault_before.lamports() + + ephemeral_before.lamports(); + + println!("=== BEFORE CREATE ==="); + println!("Sponsor: {}", sponsor_before.lamports()); + println!("Vault: {}", vault_before.lamports()); + println!("Ephemeral: {}", ephemeral_before.lamports()); + println!("Total: {}", total_before); + println!("Expected rent: {}", expected_rent); + + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + data_len, + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + if let Err(e) = &result { + println!("Error executing transaction: {:?}", e); + } + assert!(result.is_ok()); + + // Check balances AFTER operation + let sponsor_after = env.get_account(sponsor); + let vault_after = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_after = env.get_account(ephemeral.pubkey()); + let total_after = sponsor_after.lamports() + + vault_after.lamports() + + ephemeral_after.lamports(); + + println!("=== AFTER CREATE ==="); + println!("Sponsor: {}", sponsor_after.lamports()); + println!("Vault: {}", vault_after.lamports()); + println!("Ephemeral: {}", ephemeral_after.lamports()); + println!("Total: {}", total_after); + + // Verify the ephemeral account was created correctly + assert!( + ephemeral_after.ephemeral(), + "Account should be marked as ephemeral" + ); + assert!( + ephemeral_after.delegated(), + "Account should be marked as delegated" + ); + assert_eq!( + *ephemeral_after.owner(), + guinea::ID, + "Owner should be guinea program" + ); + assert_eq!( + ephemeral_after.data().len(), + data_len, + "Data length should match" + ); + assert_eq!( + ephemeral_after.lamports(), + 0, + "Ephemeral account should have 0 lamports" + ); + + // CONSERVATION CHECK: Total lamports should not change + assert_eq!( + total_after, total_before, + "Total lamports should be conserved" + ); + + // VAULT CHECK: Vault should receive exactly the rent amount + assert_eq!( + vault_after.lamports(), + vault_before.lamports() + expected_rent, + "Vault should receive exactly {} lamports", + expected_rent + ); + + // SPONSOR CHECK: Sponsor should be charged exactly the rent amount + assert_eq!( + sponsor_after.lamports(), + sponsor_before.lamports() - expected_rent, + "Sponsor should be charged exactly {} lamports", + expected_rent + ); +} + +#[tokio::test] +async fn test_resize_ephemeral_account_via_cpi() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use the payer (which is a signer) as the sponsor + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + let initial_data_len = 1000; + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + initial_data_len, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Check balances BEFORE resize + let sponsor_before_resize = env.get_account(sponsor); + let vault_before_resize = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_before_resize = env.get_account(ephemeral.pubkey()); + let total_before_resize = sponsor_before_resize.lamports() + + vault_before_resize.lamports() + + ephemeral_before_resize.lamports(); + + println!("=== BEFORE RESIZE (GROW) ==="); + println!("Sponsor: {}", sponsor_before_resize.lamports()); + println!("Vault: {}", vault_before_resize.lamports()); + println!("Ephemeral: {}", ephemeral_before_resize.lamports()); + println!("Total: {}", total_before_resize); + + // Resize the account + let new_data_len = 2000; + let initial_rent = rent_for(initial_data_len); + let new_rent = rent_for(new_data_len); + let rent_difference = new_rent - initial_rent; + + println!("Expected rent difference: {}", rent_difference); + + let ix = resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + new_data_len, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Check balances AFTER resize + let sponsor_after_resize = env.get_account(sponsor); + let vault_after_resize = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_after_resize = env.get_account(ephemeral.pubkey()); + let total_after_resize = sponsor_after_resize.lamports() + + vault_after_resize.lamports() + + ephemeral_after_resize.lamports(); + + println!("=== AFTER RESIZE (GROW) ==="); + println!("Sponsor: {}", sponsor_after_resize.lamports()); + println!("Vault: {}", vault_after_resize.lamports()); + println!("Ephemeral: {}", ephemeral_after_resize.lamports()); + println!("Total: {}", total_after_resize); + + // Verify the account was resized + assert_eq!( + ephemeral_after_resize.data().len(), + new_data_len, + "Data length should be updated" + ); + assert!( + ephemeral_after_resize.ephemeral(), + "Account should still be ephemeral" + ); + + // CONSERVATION CHECK: Total lamports should not change + assert_eq!( + total_after_resize, total_before_resize, + "Total lamports should be conserved" + ); + + // VAULT CHECK: Vault should receive exactly the rent difference + assert_eq!( + vault_after_resize.lamports(), + vault_before_resize.lamports() + rent_difference, + "Vault should receive exactly {} lamports", + rent_difference + ); + + // SPONSOR CHECK: Sponsor should be charged exactly the rent difference + assert_eq!( + sponsor_after_resize.lamports(), + sponsor_before_resize.lamports() - rent_difference, + "Sponsor should be charged exactly {} lamports", + rent_difference + ); +} + +#[tokio::test] +async fn test_close_ephemeral_account_via_cpi() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use the payer (which is a signer) as the sponsor + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + let data_len = 1000; + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + data_len, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let expected_rent = rent_for(data_len); + + // Check balances BEFORE close + let sponsor_before_close = env.get_account(sponsor); + let vault_before_close = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_before_close = env.get_account(ephemeral.pubkey()); + let total_before_close = sponsor_before_close.lamports() + + vault_before_close.lamports() + + ephemeral_before_close.lamports(); + + println!("=== BEFORE CLOSE ==="); + println!("Sponsor: {}", sponsor_before_close.lamports()); + println!("Vault: {}", vault_before_close.lamports()); + println!("Ephemeral: {}", ephemeral_before_close.lamports()); + println!("Total: {}", total_before_close); + println!("Expected refund: {}", expected_rent); + + // Close the ephemeral account + let ix = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + if let Err(e) = &result { + println!("Error closing account: {:?}", e); + } + assert!(result.is_ok()); + + // Check balances AFTER close + let sponsor_after_close = env.get_account(sponsor); + let vault_after_close = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_after_close = env.get_account(ephemeral.pubkey()); + let total_after_close = sponsor_after_close.lamports() + + vault_after_close.lamports() + + ephemeral_after_close.lamports(); + + println!("=== AFTER CLOSE ==="); + println!("Sponsor: {}", sponsor_after_close.lamports()); + println!("Vault: {}", vault_after_close.lamports()); + println!("Ephemeral: {}", ephemeral_after_close.lamports()); + println!("Total: {}", total_after_close); + + // Verify the account was closed (owned by system, data cleared) + // Note: ephemeral and delegated flags are NOT reset on close + assert_eq!( + *ephemeral_after_close.owner(), + solana_sdk_ids::system_program::id(), + "Owner should be system program" + ); + assert_eq!( + ephemeral_after_close.data().len(), + 0, + "Data should be empty" + ); + + // CONSERVATION CHECK: Total lamports should not change + assert_eq!( + total_after_close, total_before_close, + "Total lamports should be conserved" + ); + + // VAULT CHECK: Vault should pay out exactly the expected rent + assert_eq!( + vault_after_close.lamports(), + vault_before_close.lamports() - expected_rent, + "Vault should pay out exactly {} lamports", + expected_rent + ); + + // SPONSOR CHECK: Sponsor should receive exactly the expected refund + assert_eq!( + sponsor_after_close.lamports(), + sponsor_before_close.lamports() + expected_rent, + "Sponsor should receive exactly {} lamports refund", + expected_rent + ); +} + +#[tokio::test] +async fn test_resize_smaller_via_cpi() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use the payer (which is a signer) as the sponsor + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + let initial_data_len = 2000; + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + initial_data_len, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Check balances BEFORE resize (shrink) + let sponsor_before_resize = env.get_account(sponsor); + let vault_before_resize = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_before_resize = env.get_account(ephemeral.pubkey()); + let total_before_resize = sponsor_before_resize.lamports() + + vault_before_resize.lamports() + + ephemeral_before_resize.lamports(); + + println!("=== BEFORE RESIZE (SHRINK) ==="); + println!("Sponsor: {}", sponsor_before_resize.lamports()); + println!("Vault: {}", vault_before_resize.lamports()); + println!("Ephemeral: {}", ephemeral_before_resize.lamports()); + println!("Total: {}", total_before_resize); + + // Resize to smaller + let new_data_len = 1000; + let initial_rent = rent_for(initial_data_len); + let new_rent = rent_for(new_data_len); + let rent_refund = initial_rent - new_rent; + + println!("Expected refund: {}", rent_refund); + + let ix = resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + new_data_len, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Check balances AFTER resize + let sponsor_after_resize = env.get_account(sponsor); + let vault_after_resize = env.get_account(EPHEMERAL_VAULT_PUBKEY); + let ephemeral_after_resize = env.get_account(ephemeral.pubkey()); + let total_after_resize = sponsor_after_resize.lamports() + + vault_after_resize.lamports() + + ephemeral_after_resize.lamports(); + + println!("=== AFTER RESIZE (SHRINK) ==="); + println!("Sponsor: {}", sponsor_after_resize.lamports()); + println!("Vault: {}", vault_after_resize.lamports()); + println!("Ephemeral: {}", ephemeral_after_resize.lamports()); + println!("Total: {}", total_after_resize); + + // Verify the account was resized + assert_eq!( + ephemeral_after_resize.data().len(), + new_data_len, + "Data length should be updated" + ); + + // CONSERVATION CHECK: Total lamports should not change + assert_eq!( + total_after_resize, total_before_resize, + "Total lamports should be conserved" + ); + + // VAULT CHECK: Vault should pay out exactly the refund + assert_eq!( + vault_after_resize.lamports(), + vault_before_resize.lamports() - rent_refund, + "Vault should pay out exactly {} lamports", + rent_refund + ); + + // SPONSOR CHECK: Sponsor should receive exactly the refund + assert_eq!( + sponsor_after_resize.lamports(), + sponsor_before_resize.lamports() + rent_refund, + "Sponsor should receive exactly {} lamports refund", + rent_refund + ); +} + +// ============================================================================ +// Failure & Validation Tests +// ============================================================================ + +/// Helper to create a direct magic-program instruction (not via CPI) +fn direct_create_instruction( + sponsor: Pubkey, + ephemeral: Pubkey, + vault: Pubkey, + data_len: usize, +) -> Instruction { + Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::CreateEphemeralAccount { data_len }, + vec![ + AccountMeta::new(sponsor, true), + AccountMeta::new(ephemeral, false), + AccountMeta::new(vault, false), + ], + ) +} + +#[tokio::test] +async fn test_direct_call_rejected() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + // Call magic-program directly (NOT via CPI) - should fail + let ix = direct_create_instruction( + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 1000, + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + assert!(result.is_err(), "Direct call should be rejected"); +} + +#[tokio::test] +async fn test_create_with_non_zero_lamports_fails() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(100).pubkey(); // Non-zero lamports! + + let result = execute_instruction( + &env, + create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral, + EPHEMERAL_VAULT_PUBKEY, + 1000, + ), + ) + .await; + + assert!(result.is_err(), "Should fail with non-zero lamports"); +} + +#[tokio::test] +async fn test_create_already_ephemeral_fails() { + let ctx = setup_test(); + + // Mark as already ephemeral + let mut acc = ctx.env.get_account(ctx.ephemeral); + acc.set_ephemeral(true); + acc.commit(); + + let result = execute_instruction( + &ctx.env, + create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + ctx.sponsor, + ctx.ephemeral, + EPHEMERAL_VAULT_PUBKEY, + 1000, + ), + ) + .await; + + assert!(result.is_err(), "Should fail - already ephemeral"); +} + +#[tokio::test] +async fn test_create_with_wrong_vault_fails() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0).pubkey(); + + // Use wrong vault (not owned by magic-program) + let wrong_vault = env.create_account(1000).pubkey(); + + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CreateEphemeralAccount { data_len: 1000 }, + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(sponsor, true), + AccountMeta::new(ephemeral, false), + AccountMeta::new(wrong_vault, false), + ], + ); + + let result = execute_instruction(&env, ix).await; + assert!(result.is_err(), "Should fail with wrong vault"); +} + +#[tokio::test] +async fn test_resize_non_ephemeral_fails() { + let ctx = setup_test(); + let regular = ctx.env.create_account(0).pubkey(); + + // Try to resize a non-ephemeral account + let result = execute_instruction( + &ctx.env, + resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + ctx.sponsor, + regular, + EPHEMERAL_VAULT_PUBKEY, + 1000, + ), + ) + .await; + + assert!(result.is_err(), "Should fail - not ephemeral"); +} + +#[tokio::test] +async fn test_close_non_ephemeral_fails() { + let ctx = setup_test(); + let regular = ctx.env.create_account(0).pubkey(); + + // Try to close a non-ephemeral account + let result = execute_instruction( + &ctx.env, + close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + ctx.sponsor, + regular, + EPHEMERAL_VAULT_PUBKEY, + ), + ) + .await; + + assert!(result.is_err(), "Should fail - not ephemeral"); +} + +#[tokio::test] +async fn test_resize_to_zero_size() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + // Create with initial size + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 1000, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Resize to zero + let ix = resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 0, + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + assert!(result.is_ok(), "Should allow resize to zero"); + + let ephemeral_after = env.get_account(ephemeral.pubkey()); + assert_eq!(ephemeral_after.data().len(), 0); +} + +#[tokio::test] +async fn test_close_already_closed() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + // Create and close + let create_ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 1000, + ); + let txn = env.build_transaction(&[create_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let close_ix = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[close_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Try to close again - should fail because account is now owned by system-program + let close_ix2 = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[close_ix2]); + + let result = env.execute_transaction(txn).await; + assert!( + result.is_err(), + "Re-close should fail (account owned by system-program)" + ); +} + +#[tokio::test] +async fn test_close_already_closed_double_spend() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + // Create and close + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 1000, + ); + let txn = env.build_transaction(&[ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + // Track balances before first close + let sponsor_before = + env.accountsdb.get_account(&sponsor).unwrap().lamports(); + let vault_before = env + .accountsdb + .get_account(&EPHEMERAL_VAULT_PUBKEY) + .unwrap() + .lamports(); + + let close_ix = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[close_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let sponsor_after_close = + env.accountsdb.get_account(&sponsor).unwrap().lamports(); + let vault_after_close = env + .accountsdb + .get_account(&EPHEMERAL_VAULT_PUBKEY) + .unwrap() + .lamports(); + + let first_refund = sponsor_after_close - sponsor_before; + let first_vault_debit = vault_before - vault_after_close; + + println!( + "First close - Sponsor refund: {}, Vault debit: {}", + first_refund, first_vault_debit + ); + + let close_ix2 = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[close_ix2]); + let result = env.execute_transaction(txn).await; + + assert!( + result.is_err(), + "Re-close should fail (account not owned by magic-program)" + ); + + // Verify no additional refund was given + let sponsor_after_close2 = + env.accountsdb.get_account(&sponsor).unwrap().lamports(); + let vault_after_close2 = env + .accountsdb + .get_account(&EPHEMERAL_VAULT_PUBKEY) + .unwrap() + .lamports(); + + assert_eq!( + sponsor_after_close2, sponsor_after_close, + "Sponsor should not get second refund" + ); + assert_eq!( + vault_after_close2, vault_after_close, + "Vault should not be debited twice" + ); +} + +#[tokio::test] +async fn test_insufficient_balance_fails() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use the payer but give it very low balance + let sponsor = env.get_payer().pubkey; + + // Set sponsor balance to very low amount + let mut sponsor_acc = env.get_account(sponsor); + sponsor_acc.set_lamports(100); // Only 100 lamports + sponsor_acc.commit(); + + let ephemeral = env.create_account(0); + + let data_len = 1000; + let required_rent = rent_for(data_len); + + assert!(required_rent > 100, "Rent should exceed sponsor balance"); + + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + data_len, + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + assert!(result.is_err(), "Should fail - insufficient balance"); +} + +// NOTE: This test is IGNORED because testing `invoke_signed` with PDAs requires nested CPI, +// which cannot be tested directly from a client transaction. In production, user programs +// would call magic-program via CPI, and those programs would use `invoke_signed` for their PDAs. +// +// The guinea program DOES support the global PDA sponsor (via invoke_signed with proper seeds), +// but testing it requires a wrapper program for the CPI call. +// +// See: https://solana.stackexchange.com/questions/17627 for invoke_signed seed/bump format +#[tokio::test] +async fn test_create_with_pda_sponsor() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // 1. Derive the global sponsor PDA (same seed as in guinea program) + let (global_sponsor_pda, _bump) = + Pubkey::find_program_address(&[b"global_sponsor"], &guinea::ID); + + // 2. Insert into accountsdb with 1 SOL and 32 bytes data, owned by guinea + let mut account = AccountSharedData::new(1_000_000_000, 32, &guinea::ID); + account.set_delegated(true); + let _ = env.accountsdb.insert_account(&global_sponsor_pda, &account); + + let ephemeral = env.create_account(0); + + // 3. Try creating ephemeral account with PDA sponsor using the regular instruction + // guinea will detect the PDA and patch the instruction to use invoke_signed + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CreateEphemeralAccount { data_len: 1000 }, + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(global_sponsor_pda, false), // PDA (not a signer in transaction) + AccountMeta::new(ephemeral.pubkey(), false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + + // Should succeed - guinea patches the instruction and uses invoke_signed + match result { + Ok(_) => { + // Verify ephemeral account was created successfully + let ephemeral_acc = env.get_account(ephemeral.pubkey()); + assert!(ephemeral_acc.ephemeral(), "Account should be ephemeral"); + assert_eq!( + ephemeral_acc.data().len(), + 1000, + "Account should have 1000 bytes" + ); + } + Err(e) => { + panic!("PDA sponsor test failed with: {:?}", e); + } + } +} + +#[tokio::test] +async fn test_pda_wrong_owner_fails() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Create a PDA owned by system program (not guinea) + let (pda, _bump) = Pubkey::find_program_address( + &[b"wrong"], + &solana_sdk_ids::system_program::id(), + ); + let mut account = + AccountSharedData::new(0, 0, &solana_sdk_ids::system_program::id()); + account.set_delegated(true); + let _ = env.accountsdb.insert_account(&pda, &account); + + let ephemeral = env.create_account(0); + + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CreateEphemeralAccount { data_len: 1000 }, + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(pda, false), // NOT a signer, not owned by guinea + AccountMeta::new(ephemeral.pubkey(), false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + assert!(result.is_err(), "Should fail - PDA not owned by caller"); +} + +#[tokio::test] +async fn test_non_signer_oncurve_sponsor_fails() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Create oncurve account that is NOT a signer + let non_signer = env.create_account(1000); + let ephemeral = env.create_account(0); + + let ix = Instruction::new_with_bincode( + guinea::ID, + &GuineaInstruction::CreateEphemeralAccount { data_len: 1000 }, + vec![ + AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), + AccountMeta::new(non_signer.pubkey(), false), // NOT a signer + AccountMeta::new(ephemeral.pubkey(), false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + let txn = env.build_transaction(&[ix]); + + let result = env.execute_transaction(txn).await; + assert!(result.is_err(), "Should fail - non-signer oncurve account"); +} + +#[tokio::test] +async fn test_full_lifecycle() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + let sponsor = env.get_payer().pubkey; + init_sponsor(&env, sponsor); + let ephemeral = env.create_account(0); + + // Create → Resize (grow) → Resize (shrink) → Close + let create_ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 1000, + ); + let txn = env.build_transaction(&[create_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let grow_ix = resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 2000, + ); + let txn = env.build_transaction(&[grow_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let shrink_ix = resize_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + 500, + ); + let txn = env.build_transaction(&[shrink_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let close_ix = close_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + ephemeral.pubkey(), + EPHEMERAL_VAULT_PUBKEY, + ); + let txn = env.build_transaction(&[close_ix]); + assert!(env.execute_transaction(txn).await.is_ok()); + + let ephemeral_after = env.get_account(ephemeral.pubkey()); + assert_eq!( + *ephemeral_after.owner(), + solana_sdk_ids::system_program::id() + ); + assert_eq!(ephemeral_after.data().len(), 0); +} + +#[tokio::test] +async fn test_multiple_accounts_same_sponsor() { + let env = ExecutionTestEnv::new_with_config(0, 1, false); + init_vault(&env); + + // Use payer[0] as sponsor - need to be explicit about which payer + let sponsor = env.payers[0].pubkey(); + init_sponsor(&env, sponsor); + + let eph1 = env.create_account(0); + let eph2 = env.create_account(0); + let eph3 = env.create_account(0); + + let total_rent = rent_for(1000) + rent_for(2000) + rent_for(500); + + // Get initial balance directly from AccountsDB to avoid caching issues + let sponsor_balance_before = + env.accountsdb.get_account(&sponsor).unwrap().lamports(); + + // Create 3 accounts with different sizes + for (eph, len) in [ + (eph1.pubkey(), 1000), + (eph2.pubkey(), 2000), + (eph3.pubkey(), 500), + ] { + let ix = create_ephemeral_account_ix( + magicblock_magic_program_api::ID, + sponsor, + eph, + EPHEMERAL_VAULT_PUBKEY, + len, + ); + // Manually build transaction with same signer + let txn = solana_transaction::Transaction::new_signed_with_payer( + &[ix], + Some(&sponsor), + &[&env.payers[0]], + env.ledger.latest_blockhash(), + ); + assert!(env.execute_transaction(txn).await.is_ok()); + } + + // Get final balance directly from AccountsDB + let sponsor_balance_after = + env.accountsdb.get_account(&sponsor).unwrap().lamports(); + + assert_eq!( + sponsor_balance_before - sponsor_balance_after, + total_rent, + "Sponsor should be charged total rent for all accounts" + ); +} diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index 62c6cedb4b653ad5f28a2473eb54d35b33c51701..92917f89b5993c3684e19fa3d1b2e995d090b7ef 100755 GIT binary patch literal 155264 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 literal 143720 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 EKXIo (Pubkey, u8) { + Pubkey::find_program_address(&[GLOBAL_SPONSOR_SEED], &crate::ID) +} + +/// Helper to invoke or invoke_signed depending on sponsor type. +/// Takes ownership of the instruction and patches the signer flag for PDAs. +fn invoke_with_sponsor( + mut instruction: Instruction, + account_infos: &[AccountInfo], + sponsor_info: &AccountInfo, +) -> ProgramResult { + // Check if sponsor is the global PDA + let (expected_pda, bump) = global_sponsor_pda(); + if sponsor_info.key == &expected_pda { + // PDA sponsor: patch instruction and use invoke_signed + instruction.accounts[0].is_signer = true; + + let bump_bytes = &[bump]; + let seeds_for_signer: Vec<&[u8]> = vec![GLOBAL_SPONSOR_SEED, bump_bytes]; + let signer_seeds: &[&[&[u8]]] = &[&seeds_for_signer[..]]; + + invoke_signed(&instruction, account_infos, signer_seeds) + } else { + // Regular signer sponsor + invoke(&instruction, account_infos) + } +} + #[derive(Serialize, Deserialize)] pub enum GuineaInstruction { ComputeBalances, @@ -31,6 +64,9 @@ pub enum GuineaInstruction { Resize(usize), ScheduleTask(ScheduleTaskArgs), CancelTask(i64), + CreateEphemeralAccount { data_len: usize }, + ResizeEphemeralAccount { new_data_len: usize }, + CloseEphemeralAccount, } fn compute_balances(accounts: slice::Iter) { @@ -165,6 +201,122 @@ fn cancel_task( Ok(()) } +fn create_ephemeral_account( + mut accounts: slice::Iter, + data_len: usize, +) -> ProgramResult { + let magic_program_info = next_account_info(&mut accounts)?; + let sponsor_info = next_account_info(&mut accounts)?; + let ephemeral_info = next_account_info(&mut accounts)?; + let vault_info = next_account_info(&mut accounts)?; + + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + + if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { + return Err(ProgramError::InvalidAccountData); + } + + let account_infos = &[ + sponsor_info.clone(), + ephemeral_info.clone(), + vault_info.clone(), + ]; + + // Create instruction (signer flag will be patched by helper if needed) + let ix = Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::CreateEphemeralAccount { data_len }, + vec![ + AccountMeta::new(*sponsor_info.key, true), + AccountMeta::new(*ephemeral_info.key, false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + + invoke_with_sponsor(ix, account_infos, sponsor_info)?; + + Ok(()) +} + +fn resize_ephemeral_account( + mut accounts: slice::Iter, + new_data_len: usize, +) -> ProgramResult { + let magic_program_info = next_account_info(&mut accounts)?; + let sponsor_info = next_account_info(&mut accounts)?; + let ephemeral_info = next_account_info(&mut accounts)?; + let vault_info = next_account_info(&mut accounts)?; + + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + + if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { + return Err(ProgramError::InvalidAccountData); + } + + let account_infos = &[ + sponsor_info.clone(), + ephemeral_info.clone(), + vault_info.clone(), + ]; + + // Create instruction (signer flag will be patched by helper if needed) + let ix = Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::ResizeEphemeralAccount { new_data_len }, + vec![ + AccountMeta::new(*sponsor_info.key, true), + AccountMeta::new(*ephemeral_info.key, false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + + invoke_with_sponsor(ix, account_infos, sponsor_info)?; + + Ok(()) +} + +fn close_ephemeral_account( + mut accounts: slice::Iter, +) -> ProgramResult { + let magic_program_info = next_account_info(&mut accounts)?; + let sponsor_info = next_account_info(&mut accounts)?; + let ephemeral_info = next_account_info(&mut accounts)?; + let vault_info = next_account_info(&mut accounts)?; + + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + + if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { + return Err(ProgramError::InvalidAccountData); + } + + let account_infos = &[ + sponsor_info.clone(), + ephemeral_info.clone(), + vault_info.clone(), + ]; + + // Create instruction (signer flag will be patched by helper if needed) + let ix = Instruction::new_with_bincode( + magicblock_magic_program_api::ID, + &MagicBlockInstruction::CloseEphemeralAccount, + vec![ + AccountMeta::new(*sponsor_info.key, true), + AccountMeta::new(*ephemeral_info.key, false), + AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), + ], + ); + + invoke_with_sponsor(ix, account_infos, sponsor_info)?; + + Ok(()) +} + fn process_instruction( _program_id: &Pubkey, accounts: &[AccountInfo], @@ -194,6 +346,15 @@ fn process_instruction( GuineaInstruction::CancelTask(task_id) => { cancel_task(accounts, task_id)? } + GuineaInstruction::CreateEphemeralAccount { data_len } => { + create_ephemeral_account(accounts, data_len)? + } + GuineaInstruction::ResizeEphemeralAccount { new_data_len } => { + resize_ephemeral_account(accounts, new_data_len)? + } + GuineaInstruction::CloseEphemeralAccount => { + close_ephemeral_account(accounts)? + } } Ok(()) } diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index ba86a5496..ff62196e1 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -5,11 +5,12 @@ use solana_account::{ReadableAccount, WritableAccount}; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; +use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use super::processor::rent_for; use super::validation::{validate_cpi_only, validate_sponsor}; -use crate::utils::accounts; +use crate::utils::{accounts, instruction_context_frames::InstructionContextFrames}; /// Closes an ephemeral account, refunding rent to the sponsor. pub(crate) fn process_close_ephemeral_account( @@ -23,17 +24,30 @@ pub(crate) fn process_close_ephemeral_account( validate_sponsor(transaction_context)?; // Validate vault is owned by magic program - let vault = accounts::get_instruction_account_with_idx(transaction_context, 2)?; + let vault = + accounts::get_instruction_account_with_idx(transaction_context, 2)?; if *vault.borrow().owner() != id() { return Err(InstructionError::InvalidAccountOwner); } - let ephemeral = accounts::get_instruction_account_with_idx(transaction_context, 1)?; + let ephemeral = + accounts::get_instruction_account_with_idx(transaction_context, 1)?; if !ephemeral.borrow().ephemeral() { return Err(InstructionError::InvalidAccountData); } + // Prevent double-close: verify account is still owned by calling program + // After closing, account is owned by system_program, so this prevents double-spend + let frames = InstructionContextFrames::try_from(transaction_context)?; + let caller_program_id = frames + .find_program_id_of_parent_of_current_instruction() + .ok_or(InstructionError::IncorrectProgramId)?; + + if *ephemeral.borrow().owner() != *caller_program_id { + return Err(InstructionError::InvalidAccountOwner); + } + let data_len = ephemeral.borrow().data().len(); let refund = rent_for(data_len); // Credit sponsor, debit vault @@ -42,14 +56,16 @@ pub(crate) fn process_close_ephemeral_account( 0, refund, )?; - accounts::debit_instruction_account_at_index(transaction_context, 2, refund)?; + accounts::debit_instruction_account_at_index( + transaction_context, + 2, + refund, + )?; let mut acc = ephemeral.borrow_mut(); acc.set_lamports(0); - acc.set_owner(solana_sdk_ids::system_program::id()); + acc.set_owner(system_program::id()); acc.resize(0, 0); - acc.set_ephemeral(false); - acc.set_delegated(false); ic_msg!(invoke_context, "Closed ephemeral, refunded: {}", refund); Ok(()) diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index 2facf23a0..8766fc192 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -63,7 +63,6 @@ pub(crate) fn process_create_ephemeral_account( acc.set_owner(*caller_program_id); acc.resize(data_len, 0); acc.set_ephemeral(true); - acc.set_delegated(true); ic_msg!( invoke_context, diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 11c887e53..ab46181fb 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3712,7 +3712,7 @@ dependencies = [ "solana-pubkey", "solana-rent-collector", "solana-sdk-ids", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4)", + "solana-svm 2.2.1", "solana-svm-transaction", "solana-system-program", "solana-transaction", @@ -6225,7 +6225,7 @@ dependencies = [ [[package]] name = "solana-account" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/solana-account.git?rev=2246929#2246929c6614f60d9909fdd117aab3bc454a9775" +source = "git+https://github.com/magicblock-labs/solana-account.git?rev=6eae52b#6eae52bde25e90b3c79d4935ce2b267e35338945" dependencies = [ "bincode", "qualifier_attr", @@ -8613,13 +8613,11 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850baf834aba4a94a7558fa6cf6ca93fad284abf0363dec5fb9cab173a11fc4" dependencies = [ "ahash 0.8.12", - "itertools 0.12.1", "log", "percentage", + "qualifier_attr", "serde", "serde_derive", "solana-account", @@ -8644,6 +8642,7 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-rent-debits", + "solana-reserved-account-keys", "solana-sdk", "solana-sdk-ids", "solana-svm-rent-collector", @@ -8658,12 +8657,13 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4#3e9456ec4d5798ad8281537501c1e777d6888ba3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850baf834aba4a94a7558fa6cf6ca93fad284abf0363dec5fb9cab173a11fc4" dependencies = [ "ahash 0.8.12", + "itertools 0.12.1", "log", "percentage", - "qualifier_attr", "serde", "serde_derive", "solana-account", @@ -8688,7 +8688,6 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-rent-debits", - "solana-reserved-account-keys", "solana-sdk", "solana-sdk-ids", "solana-svm-rent-collector", @@ -10042,6 +10041,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "test-ephemeral-accounts" +version = "0.0.0" +dependencies = [ + "async-trait", + "borsh 1.6.0", + "ephemeral-rollups-sdk", + "magicblock-magic-program-api 0.6.1", + "magicblock-program", + "magicblock-rpc-client", + "program-flexi-counter", + "solana-account", + "solana-pubkey", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "test-kit", + "tokio", + "tracing", +] + [[package]] name = "test-kit" version = "0.6.2" diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 406a37b0b..e2ac524a5 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -20,6 +20,7 @@ members = [ "test-config", "test-schedule-intent", "test-task-scheduler", + "test-ephemeral-accounts", ] resolver = "2" @@ -78,7 +79,7 @@ rkyv = "0.7.45" schedulecommit-client = { path = "schedulecommit/client" } serde = "1.0.217" serial_test = "3.2.0" -solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" } +solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-loader-v2-interface = "2.2" solana-loader-v3-interface = "4.0" solana-loader-v4-interface = "2.1" @@ -104,6 +105,7 @@ test-kit = { path = "../test-kit" } tokio = "1.0" toml = "0.8.13" tracing = "0.1" +test-ephemeral-accounts = { path = "test-ephemeral-accounts" } ureq = "2.9.6" url = "2.5.0" @@ -114,4 +116,4 @@ url = "2.5.0" solana-storage-proto = { path = "../storage-proto" } # same reason as above rocksdb = { git = "https://github.com/magicblock-labs/rust-rocksdb.git", rev = "6d975197" } -solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" } +solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } From 80df2a33fc6ba0d7cd852f9df8f0044bffd7a056 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 28 Jan 2026 22:23:02 +0400 Subject: [PATCH 03/15] fix: fmt and clippy --- programs/guinea/src/lib.rs | 3 +- .../magicblock/src/ephemeral_accounts/mod.rs | 2 +- .../src/ephemeral_accounts/process_close.rs | 10 ++++-- .../src/ephemeral_accounts/process_create.rs | 6 ++-- .../src/ephemeral_accounts/process_resize.rs | 12 ++++--- .../src/ephemeral_accounts/processor.rs | 3 +- .../src/ephemeral_accounts/validation.rs | 13 ++++--- programs/magicblock/src/lib.rs | 2 +- .../magicblock/src/magicblock_processor.rs | 19 ++++++++--- test-integration/Cargo.lock | 34 ++++--------------- test-integration/Cargo.toml | 1 - 11 files changed, 55 insertions(+), 50 deletions(-) diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index 6e8726158..06fc9e8d2 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -44,7 +44,8 @@ fn invoke_with_sponsor( instruction.accounts[0].is_signer = true; let bump_bytes = &[bump]; - let seeds_for_signer: Vec<&[u8]> = vec![GLOBAL_SPONSOR_SEED, bump_bytes]; + let seeds_for_signer: Vec<&[u8]> = + vec![GLOBAL_SPONSOR_SEED, bump_bytes]; let signer_seeds: &[&[&[u8]]] = &[&seeds_for_signer[..]]; invoke_signed(&instruction, account_infos, signer_seeds) diff --git a/programs/magicblock/src/ephemeral_accounts/mod.rs b/programs/magicblock/src/ephemeral_accounts/mod.rs index d6e84ab45..e93fc4cb6 100644 --- a/programs/magicblock/src/ephemeral_accounts/mod.rs +++ b/programs/magicblock/src/ephemeral_accounts/mod.rs @@ -9,6 +9,6 @@ mod process_resize; mod processor; mod validation; -pub(crate) use process_create::process_create_ephemeral_account; pub(crate) use process_close::process_close_ephemeral_account; +pub(crate) use process_create::process_create_ephemeral_account; pub(crate) use process_resize::process_resize_ephemeral_account; diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index ff62196e1..63fedd370 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -8,9 +8,13 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; -use super::processor::rent_for; -use super::validation::{validate_cpi_only, validate_sponsor}; -use crate::utils::{accounts, instruction_context_frames::InstructionContextFrames}; +use super::{ + processor::rent_for, + validation::{validate_cpi_only, validate_sponsor}, +}; +use crate::utils::{ + accounts, instruction_context_frames::InstructionContextFrames, +}; /// Closes an ephemeral account, refunding rent to the sponsor. pub(crate) fn process_close_ephemeral_account( diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index 8766fc192..e4e58eba0 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -7,8 +7,10 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::processor::rent_for; -use super::validation::{validate_cpi_only, validate_sponsor}; +use super::{ + processor::rent_for, + validation::{validate_cpi_only, validate_sponsor}, +}; use crate::utils::accounts; /// Creates a new ephemeral account with rent paid by the sponsor. diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs index b724f61a5..ac7acfbeb 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_resize.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -7,8 +7,10 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::processor::rent_for; -use super::validation::{validate_cpi_only, validate_sponsor}; +use super::{ + processor::rent_for, + validation::{validate_cpi_only, validate_sponsor}, +}; use crate::utils::accounts; /// Resizes an existing ephemeral account, adjusting rent accordingly. @@ -24,12 +26,14 @@ pub(crate) fn process_resize_ephemeral_account( validate_sponsor(transaction_context)?; // Validate vault is owned by magic program - let vault = accounts::get_instruction_account_with_idx(transaction_context, 2)?; + let vault = + accounts::get_instruction_account_with_idx(transaction_context, 2)?; if *vault.borrow().owner() != id() { return Err(InstructionError::InvalidAccountOwner); } - let ephemeral = accounts::get_instruction_account_with_idx(transaction_context, 1)?; + let ephemeral = + accounts::get_instruction_account_with_idx(transaction_context, 1)?; if !ephemeral.borrow().ephemeral() { return Err(InstructionError::InvalidAccountData); diff --git a/programs/magicblock/src/ephemeral_accounts/processor.rs b/programs/magicblock/src/ephemeral_accounts/processor.rs index c48f9606d..b09d0f4c5 100644 --- a/programs/magicblock/src/ephemeral_accounts/processor.rs +++ b/programs/magicblock/src/ephemeral_accounts/processor.rs @@ -5,6 +5,7 @@ use solana_account::AccountSharedData; /// Calculates rent for an ephemeral account based on its data length pub(crate) const fn rent_for(data_len: usize) -> u64 { - let total_size = data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64; + let total_size = + data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64; total_size * EPHEMERAL_RENT_PER_BYTE } diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index 200623e32..cb21c4279 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -9,8 +9,9 @@ use solana_transaction_context::TransactionContext; pub(crate) fn validate_sponsor( transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { - use crate::utils::accounts; - use crate::utils::instruction_context_frames::InstructionContextFrames; + use crate::utils::{ + accounts, instruction_context_frames::InstructionContextFrames, + }; let ix_ctx = transaction_context.get_current_instruction_context()?; @@ -22,7 +23,10 @@ pub(crate) fn validate_sponsor( .find_program_id_of_parent_of_current_instruction() .ok_or(InstructionError::InvalidAccountData)?; - let sponsor_owner = accounts::get_instruction_account_owner_with_idx(transaction_context, 0)?; + let sponsor_owner = accounts::get_instruction_account_owner_with_idx( + transaction_context, + 0, + )?; if sponsor_owner != *caller_program_id { return Err(InstructionError::InvalidAccountOwner); } @@ -39,7 +43,8 @@ pub(crate) fn validate_cpi_only( use crate::utils::instruction_context_frames::InstructionContextFrames; let frames = InstructionContextFrames::try_from(transaction_context)?; - let caller_program_id = frames.find_program_id_of_parent_of_current_instruction(); + let caller_program_id = + frames.find_program_id_of_parent_of_current_instruction(); // If caller_program_id is None, we're at the top level (direct call, not CPI) if caller_program_id.is_none() { diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 7d68ec5f7..33888f27a 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -1,5 +1,5 @@ -pub mod errors; mod ephemeral_accounts; +pub mod errors; mod magic_context; mod mutate_accounts; mod schedule_task; diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index dc9edb001..640d08f2d 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -93,14 +93,23 @@ declare_process_instruction!( process_toggle_executable_check(signers, invoke_context, true) } CreateEphemeralAccount { data_len } => { - process_create_ephemeral_account(invoke_context, transaction_context, data_len) + process_create_ephemeral_account( + invoke_context, + transaction_context, + data_len, + ) } ResizeEphemeralAccount { new_data_len } => { - process_resize_ephemeral_account(invoke_context, transaction_context, new_data_len) - } - CloseEphemeralAccount => { - process_close_ephemeral_account(invoke_context, transaction_context) + process_resize_ephemeral_account( + invoke_context, + transaction_context, + new_data_len, + ) } + CloseEphemeralAccount => process_close_ephemeral_account( + invoke_context, + transaction_context, + ), Noop(_) => Ok(()), } } diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index ab46181fb..8310fd199 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3712,7 +3712,7 @@ dependencies = [ "solana-pubkey", "solana-rent-collector", "solana-sdk-ids", - "solana-svm 2.2.1", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85)", "solana-svm-transaction", "solana-system-program", "solana-transaction", @@ -8613,11 +8613,13 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850baf834aba4a94a7558fa6cf6ca93fad284abf0363dec5fb9cab173a11fc4" dependencies = [ "ahash 0.8.12", + "itertools 0.12.1", "log", "percentage", - "qualifier_attr", "serde", "serde_derive", "solana-account", @@ -8642,7 +8644,6 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-rent-debits", - "solana-reserved-account-keys", "solana-sdk", "solana-sdk-ids", "solana-svm-rent-collector", @@ -8657,13 +8658,12 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850baf834aba4a94a7558fa6cf6ca93fad284abf0363dec5fb9cab173a11fc4" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85#e676441d85f91cc4a3f55029aaf12bc060bbd00f" dependencies = [ "ahash 0.8.12", - "itertools 0.12.1", "log", "percentage", + "qualifier_attr", "serde", "serde_derive", "solana-account", @@ -8688,6 +8688,7 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-rent-debits", + "solana-reserved-account-keys", "solana-sdk", "solana-sdk-ids", "solana-svm-rent-collector", @@ -10041,27 +10042,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "test-ephemeral-accounts" -version = "0.0.0" -dependencies = [ - "async-trait", - "borsh 1.6.0", - "ephemeral-rollups-sdk", - "magicblock-magic-program-api 0.6.1", - "magicblock-program", - "magicblock-rpc-client", - "program-flexi-counter", - "solana-account", - "solana-pubkey", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-sdk", - "test-kit", - "tokio", - "tracing", -] - [[package]] name = "test-kit" version = "0.6.2" diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index e2ac524a5..890f5684c 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -20,7 +20,6 @@ members = [ "test-config", "test-schedule-intent", "test-task-scheduler", - "test-ephemeral-accounts", ] resolver = "2" From 7449c7b58ab3bccc00438168ef36601a88e77730 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 29 Jan 2026 22:22:16 +0400 Subject: [PATCH 04/15] fix: review fixes --- magicblock-api/src/fund_account.rs | 23 ++--- .../src/instruction.rs | 6 +- .../tests/ephemeral_accounts.rs | 29 +++--- programs/elfs/guinea.so | Bin 155264 -> 157912 bytes programs/guinea/src/lib.rs | 45 ++++----- .../src/ephemeral_accounts/process_close.rs | 50 ++++----- .../src/ephemeral_accounts/process_create.rs | 46 ++++----- .../src/ephemeral_accounts/process_resize.rs | 48 ++++----- .../src/ephemeral_accounts/processor.rs | 17 +++- .../src/ephemeral_accounts/validation.rs | 95 +++++++++++++----- 10 files changed, 186 insertions(+), 173 deletions(-) diff --git a/magicblock-api/src/fund_account.rs b/magicblock-api/src/fund_account.rs index fd2b96727..12583887b 100644 --- a/magicblock-api/src/fund_account.rs +++ b/magicblock-api/src/fund_account.rs @@ -6,6 +6,7 @@ use magicblock_program::MagicContext; use solana_account::{AccountSharedData, WritableAccount}; use solana_keypair::Keypair; use solana_pubkey::Pubkey; +use solana_rent::Rent; use solana_signer::Signer; use crate::{ @@ -84,19 +85,13 @@ pub(crate) fn fund_magic_context(accountsdb: &AccountsDb) { } pub(crate) fn fund_ephemeral_vault(accountsdb: &AccountsDb) { - // Only create vault if it doesn't exist (don't overwrite on restart) - if accountsdb + let lamports = Rent::default().minimum_balance(0); + fund_account(accountsdb, &magic_program::EPHEMERAL_VAULT_PUBKEY, lamports); + let mut vault = accountsdb .get_account(&magic_program::EPHEMERAL_VAULT_PUBKEY) - .is_none() - { - // Create with 0 balance, will accumulate rent over time - fund_account(accountsdb, &magic_program::EPHEMERAL_VAULT_PUBKEY, 0); - let mut vault = accountsdb - .get_account(&magic_program::EPHEMERAL_VAULT_PUBKEY) - .unwrap(); - vault.set_delegated(true); - vault.set_ephemeral(true); - let _ = accountsdb - .insert_account(&magic_program::EPHEMERAL_VAULT_PUBKEY, &vault); - } + .expect("vault should have been created"); + vault.set_ephemeral(true); + vault.set_owner(magic_program::ID); + let _ = accountsdb + .insert_account(&magic_program::EPHEMERAL_VAULT_PUBKEY, &vault); } diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index ae5cc5a58..268abacc9 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -133,7 +133,7 @@ pub enum MagicBlockInstruction { /// - **2.** `[WRITE]` Vault account (receives rent payment) CreateEphemeralAccount { /// Initial data length in bytes - data_len: usize, + data_len: u32, }, /// Resizes an existing ephemeral account, adjusting rent accordingly. @@ -141,9 +141,10 @@ pub enum MagicBlockInstruction { /// # Account references /// - **0.** `[WRITE]` Sponsor account (pays/receives rent difference) /// - **1.** `[WRITE]` Ephemeral account to resize + /// - **2.** `[WRITE]` Vault account (holds/receives lamports for rent transfer) ResizeEphemeralAccount { /// New data length in bytes - new_data_len: usize, + new_data_len: u32, }, /// Closes an ephemeral account, refunding rent to the sponsor. @@ -151,6 +152,7 @@ pub enum MagicBlockInstruction { /// # Account references /// - **0.** `[WRITE]` Sponsor account (receives rent refund) /// - **1.** `[WRITE]` Ephemeral account to close + /// - **2.** `[WRITE]` Vault account (source of rent refund) CloseEphemeralAccount, /// Noop instruction diff --git a/magicblock-processor/tests/ephemeral_accounts.rs b/magicblock-processor/tests/ephemeral_accounts.rs index 7e7688306..3f0becbdc 100644 --- a/magicblock-processor/tests/ephemeral_accounts.rs +++ b/magicblock-processor/tests/ephemeral_accounts.rs @@ -10,8 +10,8 @@ use solana_pubkey::Pubkey; use test_kit::{ExecutionTestEnv, Signer}; /// Calculates rent for an ephemeral account (same logic as magic program) -fn rent_for(data_len: usize) -> u64 { - (data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64) +fn rent_for(data_len: u32) -> u64 { + (u64::from(data_len) + AccountSharedData::ACCOUNT_STATIC_SIZE as u64) * EPHEMERAL_RENT_PER_BYTE } @@ -78,7 +78,7 @@ fn create_ephemeral_account_ix( sponsor: Pubkey, ephemeral: Pubkey, vault: Pubkey, - data_len: usize, + data_len: u32, ) -> Instruction { Instruction::new_with_bincode( guinea::ID, @@ -98,7 +98,7 @@ fn resize_ephemeral_account_ix( sponsor: Pubkey, ephemeral: Pubkey, vault: Pubkey, - new_data_len: usize, + new_data_len: u32, ) -> Instruction { Instruction::new_with_bincode( guinea::ID, @@ -204,7 +204,7 @@ async fn test_create_ephemeral_account_via_cpi() { ); assert_eq!( ephemeral_after.data().len(), - data_len, + data_len as usize, "Data length should match" ); assert_eq!( @@ -306,7 +306,7 @@ async fn test_resize_ephemeral_account_via_cpi() { // Verify the account was resized assert_eq!( ephemeral_after_resize.data().len(), - new_data_len, + new_data_len as usize, "Data length should be updated" ); assert!( @@ -510,7 +510,7 @@ async fn test_resize_smaller_via_cpi() { // Verify the account was resized assert_eq!( ephemeral_after_resize.data().len(), - new_data_len, + new_data_len as usize, "Data length should be updated" ); @@ -546,7 +546,7 @@ fn direct_create_instruction( sponsor: Pubkey, ephemeral: Pubkey, vault: Pubkey, - data_len: usize, + data_len: u32, ) -> Instruction { Instruction::new_with_bincode( magicblock_magic_program_api::ID, @@ -609,8 +609,9 @@ async fn test_create_with_non_zero_lamports_fails() { async fn test_create_already_ephemeral_fails() { let ctx = setup_test(); - // Mark as already ephemeral + // Simulate an existing ephemeral account (owned by a program, not system) let mut acc = ctx.env.get_account(ctx.ephemeral); + acc.set_owner(guinea::ID); acc.set_ephemeral(true); acc.commit(); @@ -899,14 +900,8 @@ async fn test_insufficient_balance_fails() { assert!(result.is_err(), "Should fail - insufficient balance"); } -// NOTE: This test is IGNORED because testing `invoke_signed` with PDAs requires nested CPI, -// which cannot be tested directly from a client transaction. In production, user programs -// would call magic-program via CPI, and those programs would use `invoke_signed` for their PDAs. -// -// The guinea program DOES support the global PDA sponsor (via invoke_signed with proper seeds), -// but testing it requires a wrapper program for the CPI call. -// -// See: https://solana.stackexchange.com/questions/17627 for invoke_signed seed/bump format +// Tests creating an ephemeral account with a PDA sponsor. +// The guinea program uses `invoke_signed` with proper seeds to sign for the PDA. #[tokio::test] async fn test_create_with_pda_sponsor() { let env = ExecutionTestEnv::new_with_config(0, 1, false); diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index 92917f89b5993c3684e19fa3d1b2e995d090b7ef..91103553a962271fc2e54c75f3bbef7ec4dfe261 100755 GIT binary patch delta 37685 zcmeHweOy&l*8e&CauLw<0$#jc1umd)C3z9w$P!UgkqXg7k<#!TvkbJ1<1H^a)sQtt z?Wsd=253faN|{1a3D48iP*cYAie*a8Ofgo(^kVo1zqR(>=iYm^FVpjRexJ|ppWm5@ zeZOn1z4qE`uf6s@`<#2Ncs;cJZ=qF=h)hjkS2iz{>QxUUPng=)qcUuiijA|88Vvt+ zpWuACY@NjAS|6w#5>YaqFyt>EcqcKUX&k5eOG?yuXMQ{3bap;SxF5mmow zl^RX2>={5C$Y|1c5j{fQB7YmUcb8>?g5sxlBh zHo|DI5?mDocx7;z(vOuJ^A!txXkH3`;OYm8`rvv>tz&f0_0`;ftu?&CTG4PsCX=_5 z6R1zo47%M#uO5EWs1_zYXao~d1{vL}Yc~-Eyr9ZS8*qt8MoiPdq zP9yvYfzqm^G0Rp4N5E%ADF}X^`33od8UJF&w+AOH1K^WA*sWXyCGt_;*WE}yAn1QK z1YTB6sfdpJH?tjMGvKONobVC%SDo-gM1Bz^M>r|7IXas z9x!Ow2&hlH6XLw{d5ebk`+>zrzzI5(@xY1|d@V!BP2*)h2!wq2h_;}UFcuo1o?U$a z`^@NWD7EpOY`hAr|7K+~HVl_l`GsLC8Z&?{c0EKkS?SNNL~@Ij4v))A<{|WPu$^^n zW$;AH>!I1|bTqRg{tB zP129vo|}}u0%~f{@LE>o0GOjF9|sR)A+R!dFdOQE;HL>j{ywr1tc!QJ5LlJ%Sb$CA z*|--3-($ge+DC5XLI|wNADECmI2UChc>YLm@b{5&6dFFls#Gxnh$<2oE$C1*l?m>Q zUot5*2u&-nObWvK+78jb1OSM|oWg22${r_=nSGXF!E|)OWupy&yMUVtGu*DMXYE>* zOGFS|X8}(@d&{l#%86)Li7@O=3x>0yKocHhg5V4K?vw!$R%HWooKGm6{&#F<(LXHA zW^&BJY&Pb7IFG}#;yvzO9C+s)eL=ZRPxAa2lZsF>h|0vXgx>KW$5=&$sOte{32LB4Kmu;Xev*Oht zdUeXgP?Z_)Np7W&rNpRfXl`nv3)?&j)y~%iT9xV;*8H$&&^L+iK98z*|L3ZB8amsm zSN?;lAhe2Jo|b4cd$ZZ!v0cAO#o@HX2w4Sa9-Kacd58WcB{!!h26H8}A;rxq-f^Ld zrDNi0*cgXgr}V;gCdu;GU06PzIx`&S_s>-qstp6rUA!~n0xq8Pn^asj_JS@>`b|nc z8hZgd2LC1%H;g-Ll*olDc8r^Je(!Zdy1CG_;oiG%{Aik=Iy5#QWgLZy=bEN8VnUV! zCuG~X1R!E2ecp=`(U+9NF<0^(H~YZtR>P++3fhKC8^ZN6!TTm=q5QiOFALhIUV4V!kU3(2Rrv=Kgluo2H(!zxf1agM zi*j)}OD}!(lGO8yxrNxt3yY--vEv|?<=i3mY~zI06Va!zysH1Ek4;L;*~buuRz7@c zp3a~$Xag@jllU&z%61{JXuzU^a?#^l}22;krw9##h_1{XE83z`~^7v zJGh$Wo9VNcCWX!trsUAiF8#z;$&<@1#i30PsHE+rA6$08Cl;uCaDW)-era;zImP;$ z#ZFF63eDlZ)MnG%=~?kkD)6heO~*GKqbph~O^IhP!zyyBETJ(PBnijoZ-Qy4zgp+biu;aWf@v zJeDtI4W6y9zJ@-QnH2s8V;cr*JL~C(nNNBrvArm6obehol9$V%t9aXnJ8}pY*g^1a z`sRtya36vrjkvsnxJF9kM!HX5)hql5wx@UG!1>A($}ZOdRM`x0%g6~!0;S~abqSrg%~muZh)aR5g>k#jNz>s+r~I1 zruaM}^!Obzi0i3s&t=`gZeWu>#Udj`CyeHbe{*;K0`dj$KfDYl`TQ&rZu1VtW;@Le z*oorAjz0;vX3Aa68mn>@6KDjhFv2zC_A&uy55I3kw>U70{8vTvbc>fkUxj!+(CBUS z+@J_9t=02gleVi`G#H2a*bVUrD6PsLPa!^g|Nmzs`#ToyVi?`W_y6wC7{S+bAz)W~ zINjG1`qk9T!U3#RZ5&@`22Eo!EH>~YM1#1)%O2FjtW1LL#sO}g!N|UOg}L`AxKS7U z*co*FG`IRIeQes)DZhiR+m+d(nf_i9g-_bpY=-R?J1e!F zVg}8~WH`y9D8=vz?*F}PX|b{+9Y){`5U2q+*xAh>D4c&Dx6Wt!&UR}yrw?-R@+`Ls zely`g22mpNm(ioKeAX_Ggf79w<~Wpa8_uwNthG22Tm+XtSc(}oq7>`neXUg)&gecl zZ)71C%wPp;2%V-ymNJ6f62(ei1$zm@0WRc=Ge#3!=l3M0YtTDScv8rFG2U5(&SQKy z+Y%897Nd-DhB62z1zgw@7NLS4JA%`jr}EP9q;ZegEaoZJgRt_)%-C|QaQQiA{(7bf z4KrtqN=CO^Ze*n|F^sIBkIk4!Rx0$P8RNaeOy3a`Xkq3;83*-Z1;2S3Q{o?Z-!XuT zG0HkP*e;Pd)}N^+%}AQ-YlFM(?6f|S z8woa=2ba$`yCsA1eVrOfADxj&lK)p@i)jrq7L0Y(Nz@(~k|BV7b4wf**;O&;e4Rll zJi?rs>=o0<@SX@G3>P+w{*(7cGEL|39;eHLYnY15CtkNilm6@@ZmfC|pjB~UO8#gF z-JBc4T#nERBjZ17yu6p*Hrws!g?idkd?Vv${&JGuHftQMaR!l}Ewn3rI6(@Ce|t{x zjhy-Sb25L&nV+ANxrH-7JSX#xlXS%FO!aNLcy=_|%q3gSDfuvGHeHC>a8Blrr&+)1 z&&$kp+iH;MJKqsC9to`B+ti;pO|Q&z&?~P9u|0?aKE`Q3<31BZIpeEww}}zN7;X%; zzFZK6_2kB}g%6`^ zAo@rMo7c zlll4?RHwm)mfm@l+hw$#g2H8@C7%_b35Loiln6q86W=yziZC=q`!vx7S0vIKt_rb5 znd%)qLEpV9eaJwRTPnAx(7#?aX0r91yngKXqJXSEfwdZ8_ke90w^^_+Vswq((H^_E zHSe&h9-vRG>O=JVxl`0X(3E+z)H~@d^AeUfCvi)f zc^YFQ;_d6}D^Q{tLlZat+R}7}&)yp2Z6~-V1BMdIF|1Hx<9NAGwbN61DG64#rMZw? z1jr3d!JZk*zehY4(w1d2W!h4{P+-(d{}pW1<8D?RV?~S2m{Z|u1Rvt&0i}u{=`}BB zWS@m_3e!gi%ySL78kt7-g!xwG8%pO*javY{HMU2n=(|+JXdGAEMSq!>M%oi-;?=3n z?9-fqOwS}oHqoc%@{?*1h%xLsU2gPgyucT}p3+zl-P#@OLi# ze&HN-7NbvL^ojKK!YOJRJp+G}=;TFHf@7h34=FT$0bd$1f(HU=BpfE5H%k6{mKuqwaoBn)f>Iyut^Cpl1_X7@Ro@f;KM`bPvFV+w}_Y%s!6Q-vn( zx2?+0eRSud#PA>bpsIokD3(^`d;0mJ2h{86jYVndK>9?{l<-dJ*5>ry?~Bq12~FP{ zvv>kg&(Jf=Qs|cJGBPS9!KJX7Y`z1V<-sBI7Z0YJmq*dl*VRYJtm%Vk-inxTnLL^P zv-rBRsA;rd>8EE=Bk1I1L(ZaJL{~1;)k?5=&KOyH7aTcV$lhc0_T|lIX>6lct%yB~ z`VPH)#qYxvDQ+97edVVSGWF3&nzbzIEa8Xf$W^O}?KIuCDoQ;?pISAG)SRMStD=Y_ zfd;RhN%ozhd8?yHVFFzR=<`!_H=yza`qt{0q5Y(}foB3Z&D!Xh)oG-T*UU_yGfQGD zt*2D|eVb~BXY;q0q>)6Jyd_Z))spQ$C9t{kJ(y4uHFOUItW`O|gOG(5*MEXWuUW0$ zM(3c>QV88$ov$rJfZ@Kkl8pc8W@DBk1NEqQbiaOh!|y=~FixRp-#V zZahjVPSaKEHmh#>&vmz|>*?Z~hAecAz<}i!ivJrn^~Qd%xt7y$*Io*#9r?ZB0toPR)VzrqvcR zMOs}6E3vbnzYp_qz*V8u@(yWKy*ZK|E=@@WuHT1B1)gN%WmOU}*CDsbcFa~?el7>~X9f4<36)%g@SvlU4Z3&@&und`U5N=;EsbG(+%2Z~`$3a6G zwg(Mo?G^-$Qjmcj3$V4qs*IyotoKfhXD3-tS!()laYHG7CI|pBnN_^BR1m~(xsV@l z0e?8{zage*BvwWvhOly2E@CQm%EcNNZ{0(Q&maWvY zVWQ)AK5eK~Tou|#`p$+zZ?=MsuO!#7mv-zVQ^h9nMwD+Mn0Q5V2`iNXl1^B14}Fc< z+z;AM2bMkbPwYJdLk{W1jNZuJL(rD-`dR`nG3i@pygULN>tn{J9)A98Oh!kaWO?#0u&?V8wqApe2=-)uwvgU2F1Sv;K&53W{tY2vWa4v&eBzB(z} zSj7q~DJ-7KmK8F&l?9}hzadfAaT+Ne@hXW{$K%D5EaqdKo5xWyp9`K%m3%%H@j2yj z@_CA5j2;qK1)GcXqPvp3uR*{Y<~4%(7z`*arCv|-2Uqj!(g*~zpF4ru! zRk-0Zh93Eo<9IzIo^cVs9Ox`bLGEj=7N_n>V!%Eotvt}lS3J8I4X&~o_!q3OI_Sl>%$m2JSu4kg9aN!B1*A*GSzr(Sk;S_`v`fVgMr_za z2M|YJp%J6 z@q{V7UU9zQhX|Y_u#Mvy5hN}~hT)75HkLAiMrW6g8rsPN?a@qD$K#U?q4$LR=FQ^K5nTMnyLCJjiN+q2JPb zE7H7QvGly5=u2L1?O>(n(RNnU~c>d=ZgY!<2N`P z1m?zjob?=gYlZdB8bIO}-=WB!Cx-N8N-5G$Jljoi)f zs!%5F?}?4%LvQTlaF+gsPWGg%IeXp4WfW&KOcpkq--A4;J;Uo;mBCEACzt1*^$t-%%{bodWC#hEdm+_>5C*df@R9UdGI}TTe1N=9NcH`+!pxI^g-3X`F3U zLRdvl5#KkCg6^PScvo@)c|M$k!cH_}Jw z?V+(lj9j-H_Q_gpH_y~{Jv8#pv|-hlWnfQoA1jTtWe?p-RQknj_r}=CADNBl3+8%; zaMOJY)S$cX8sRNvRSjQSm30g^LPEw1UfpF_cBcQ9<$51KFg*PFRxrLtEX{%bWF2$D zDzO!|mRg;-GpylRU#l&pYwu3<+Lp8W+S7bv+Uz#$DqwV@or2Iy*p3ffP|WO}%lNnq z;Dz>!-6+CGwmuPr&1I+@*ureJj}(8Eo3YWwbis@fyBPV+cEW^Z2)7vF&nZwYEBwQMEGe zQoFjD(fdKaks^nPRdI5D2;4qafq*^K0ejxt{-7%KoqL+d4=TNLYw9&qnULWCS&0XZ zvhaeFROX_xrG*R^s2zM|2CDEo()o=EeDD$a`BBz)h?Bo$s3gU9-zZK#2 z0&hj)p&cB6qi|!2L2($ItGR}w9WanpEI9hjgYJz$LUJV|7#%H8%h}#5a6%NHfx`yo z4zBojcydxx!s@|c11k&EGJ3_mu@QVrX; z!%IkbeY%@Q-k0R%$;3Di2{h#n3~(pke+JUmGdkag2GT1Sy`6PwQ&np3}5n7;TuwH9*+>oVl*hxiM?rdX< z*TgZlvex+#!|>FW8Kwz2kCW$IuNDXM6fPrsuZhDzR-hu!@@2JUEJ-hf(S`pjcJ(3A zFrPAOYIy@|orhUNOKZ6S%bo4K;ac91$H`YRhOio*T)wDn3gcE9vFma2Y~$I=cY0R#b`7{u)H7aU z7$i>PmhR|a`qu3H2P@Z3^eL4GFb!u*t<-u)O-^8W6#CuzR+_sTqZ(qgEO7uvqDd0PV<%-3*`sOjg6?1)u`7Jn<(XvT9oxY@rH$jEWe<9iy{M**XJbX*f8f&a z2P~MS>`FI#T4E3RBOUhOMDK1!-H;r@^*_b+d5RQ<*VJ)`tWD-Yu(XalY|1i_Q^Ul6aVPZeFLzolB zz2V7K=%o)$j1ydO&mLWo$XbcMGm!^NosH6m(!9LQ#qJ-@8UbThI~hH{laG`p+R|!- z=WBV0!;?~0e`#$y>-TbJlPDWlxu%w<-?h%y7+&jqnU$JXoVuMK~_yrL&rq{vqdv8AI-I@-Z}=YMgaz!f>z0$t!4e+!MI%5DPr| ztHP7a6}%U*h>hEtT49ir8}vW!ajoc}lXcJw+iJe@c#^pXJWkQGWZttiwW8-v-g9jy zKg`me?%}hmwv&~MhwzjgIEnD#3r?OEh#R9Gh7m4Qe1l34=mXSl`d2+x98r2ATr5ts zTHbF@1aA0nf9&E8_q{I7=awgePZ{G3&VxrNaILB3BfK_(zu2_2mPf+!h;+u+YFpXd z^hEF{^$-cXzn+LJAt3w^F_YmnwY>jpBlx`~P6M{SY~p_Ki*WLaVx>10u7ZA7un@%1!U(Y{@k|c=V{F~vV})_Lwh*lYM|(C^ zaLD4cj)jG<9O+T!6;C9U$LK|m->QB_?|OWq`XPP&@nOTYrz@Bl{`;)Y z7%m91_R`B9iE#$;?tzJXWcYpM3vs}{@A+u@z$0@Dx|s;>0@21zVV$fDj2wOkoX!kx zej0?}LXWe}-%f!ck}$&Xp+LiFRi6{2+TAa88KK2@$Xg**1u8}b_>6;I`>4aa3LL1N z=cXpwHk`F?^jE7dXZ^G)^{m_k)?geP?sVKpFLIG%a)1fKjOebTb9pt z6z6cc>7sh*2-b#YtAko<$0v9;@uk0+I~MMeY`i&sron?g(+dYdh2F^c_hNAMp4WTdo3YFx>L*h#}U~^zj{8V@kLW#LH!#Ox* z3p>KbVZ#39fOi9run6VK^gq!)O)q^m&3lrK(1s%Z9@vIWJ$wYk?|yi4J2_77;N{kKUOL-Y312K= zyJI6$!*~1y<{8^~F1*3nD&)D^wk%qs4vn}FAgQ`@igkC7%V2d6qD5+koB$X$A_TL;;6!^)baW? zV#51gW_Xg(o5#~*8{DC;9(*EcRbuEHFU5ss8w&N?hpR)JPjsR$?`QJpA{<)2+U$+l z3x{i+z4YMzR8qBMZ}0xELrBvI`gQYh(m7)9k(Pl$L>WoD{@hF2M(pi+Ggu{#k$aE7 zwOu9YBlkYsx{i>Xku>}S26R|uVQXfdf8Nw*(L zB`sglgNIg;7LIB@q21rW=oL zvy=m~U&mxpk;vYcf0Vt4fg_72YyS7;ixcsEdCT1(z2|DX<$fr?>G*}TB+?^a&LnM6 z`KxV~7gVyh@vB=1`;hj}87j$1q#M76*T)j+_OBC@tJxc0Tsc0egNJPFQA=hLZYBAJ zZ?F|6(JrXfnMk9)i6M?eI^ml{lAcJHd^7XBIuuEVD~TTbW*sR_qFJ2_EI$%L_0AcV zsTM+yc22g`BoX@l{uBbIZh8MowS)lK`>*kq7C~Yn+_Ov}{( z?*|Ai?)oKfr|78L)Q;#1colxa?D#`qqrv92r(ofSRzXGwBruG2(iZ1DyKq^D% zfu7+5_=<>|E5|4x>~gk;`lDcwakmVll4QF1L@!Bq)6FNdEJY!NHlJKfnl7d>rxp-* ze?89zJ-hFegLGc3cfG03-22_BPC|Mv-up>!R}is{*59(IcZ0m17o@HuEBosQg4C!8 zH~$tr3p$LsoY9S62dRlFDIVQ8Fj!3tCa?C_w-1M({3{===M7NvNY&`Z?E}=Agycu+ zUk_B5U^#OTsQ$UXesGYQMNaqE2il+v8Gtrrk>LaMZCGADpz)wh-QJ%x4$#+uMrVCM z+nAfj!A5O|M0926J|+@z76@8Nc@R{pP!;1Kz**1#v{?{gdpg7V^qAF*bjSj5K{3_ z{yHslCU)pCaA;tMswMtIf-gEm8dCKAqrlLf6#WR69VvQnGBoH;)f1Be+fwzs>YOr8e%qSlt#(l(fdI40W688=}fozpJoE zv*CU>utzl#f4blsL)0YkCkwt9B4?Y#pD6f8BKQz?rs})KLC+m&`oVEvd3u`OHSPi) zC{EL3#$Uh#Icbe~7YmCWMmsoHtH(#m_B&0p9aoc3|h~EcQX=WdM z3=n-VI#S;bDxCEa|9!zXnrKPWzn%hp=uXoIWB=;m0#ce!9M78Zcbx`rTM& zrZ?`MhQ@sqsjr#=uSt9iGi-*s8}Y+4)m6giAw$#62K^nA2mD7P_0(BlP=&<*LhxUT z)RzFCRHrv?oTUy_ou7)I1HgbS5Q~xnCQ&=|EC(z-6saG`ftmYhq~4nYaRT-%n5`Zs zha>g8TI$`sfd1es)d{FK>q_Vs=slRHjvm18 zRIw}2@{uc{<-?JUF;}U(RUt9~n*>IbK75Yq@PRAms5yoaq$6G5JqNtd5zslD0k; zuB##EEgGy(m=9*T#^@X81Fmn^56p*-q<^I!pAQ4JlBtYpystn_S4sXD`~lRNq~?ge z?vR?Ok6x&jkkT>w!wW$Ll%FhwzI&ibAq=~?YFIKmc z7g&ckl9vbT`pAB#ndhfveScy>hvFC;1bA6=2>ggZ0!B)u|V+ zP#-6+4c2>CK>uGFtVgW`jjxMQ`Cz5`C{wozh+NGlq?y-zxCDM>);hQmwB!-;;owGC z)nSxA1h2vJu0Ot3?MATe2DO)!H;Qr{#2yOW1hMPNXxx31nrA0Z*!A8!)ooaAy9-Rt z&u9$3TMf35#i#Xw_kd;H_Qu3})Zj2ulA)(ks9KTHxP+9+zF1a`B3lL3AQ!)=#A^1LKBw^Y23I=-DM#mWA&J4AvR!$SZv()EDU>1k-q&| zP}B5;eh^Dg*ZVAZvv91w`8h89)^p%Mq(2AhTQl^$=Q;i1=K%x#>*pb+D#teFy#OmS zX&S5V-lHZuo5uQw5z}}sJjlUlsu`cdbr9%)qJZUhLMUx5q>gk7(ws~BP6tDy3+ zXn)6WlH-9LF6AJY1$-vB;5A-z^J_5tWM4Gj&Vb6ho57bLd7xQ^-xh`sT`kZkHg@+L zkmWvz);Iqd%mV!IpSg+ye}=S)goAH_BOFlUO(eL->Ai13M`74uFd?x2q6U%p7=6`W zV4~-Y(|7#^2IYc6#p4?Hx2iKC&%tzf8=U#^;Kpt5sGAvK;JaWaFlN519>g;0J@qXV zIPxC)1_+zq=K}lRhs6T$sy0m4gBuUDsk;mfkmo*7oygt(0gO1R`H)Mk`w&+C*=g__G7`^Lr^%K%LR)6a-__l3qW7lDIH{6}}Hg5l?8XO8Aboh|b*xse? z2~sQcJAP8<=v6Hzl2MyJE?ae zu44Di@a3D6dW?faMOOvX`vXM%aG@~sIyiLDq05nB?Kg4SJ16yZP$l9cs1k3q3+3^X z`Vl9IBDtsZ-42pUdal(EI0(dxC~b>5Zn%b*t|h!|=VeCuVy>(V-Yw*BH7SYRxS{=`|=1CH=XDavhtVGMlN7QnCr^kg%79?J&07oU1rJv6D(llXgF~Ew{1<>PzR-{S-N^Sq{rS9# zQVHcS@!8YI9O`^OK>uvu8I8Z+rw<$k4nM@@EWHZRrwk*}))1JiM4vj046}y8$D!^M(o4jgZK>qMfn@p2bq#xC~y=6EcoHqVI+k#{7cUoN@CT2v5^~A z)5!%_L-r6~N@Ii_AnzRx74d^UoTT*6KE@@H@IDf30fc_hB*oi)f=hI>!!gK?KXUvY z=r$q%DW3oa8-3TO|AC(g&NDiMmE`&zP#-1`ycH}iS>Rg`fX$)rnri{CfW@TZls+(q z#9UM?z69R^r)Lf}RY=HbJr7tG#j5Jf4j7Sq;Hfy7BfU%Dej{+kt^pj_p=$R0Us0Z7 z?}sVMeo>YPdc7!H1^shTelNZK8Zxl;4Tc zYURmoxG3GC93#pQ@Hk$V3OrAgYgy@qQ$Rtu+Yo@j#OqOk_ldGq^z;pZ4~eo%lqWk>73CwMd`^_FiLzCc4$-r}34BM)zjVLDxi*FYAc2QP}l8W+KQSKMze~U6#)c;7}uSD6^ zkK|(e${9hhg>Vapi84`?V?~)I${bN%CCYsGN*j;$6x$#^dTpX~^KZK;DgE)C2LF`> zc(DE=tR!R}oWjJB)c6V^S_lJe%+X3P$jHVZH!vO&WII$flHQe|(Z~nw{q-#5R}COa zl4xlolty;1f)f*&k57^U_}gXve+TG?`Uhn`KJ({qNx|YdK<}0LnpuBpf@B~5!eBuA zOJqL&LgWb0ffwlCCiCw!w|_w9UkUtpULEUq$^3x$p-(%+QmiZS!`^NZz{v$H~1#|s@BPILRnd@iC{4dP?TPO4JmcR&0Xn(uR zzXA9G_8pY@KLDT4A@~^?IIcl{Ioxv_i}tGl_LsxSM54rh-OOJi^YQS&@ITbw2K?wi z{rD{s{7DwPBH=dG$rksF6!-`=eV2^eU{mAraTR4X>~oVy;*F(W@>=BqQxL`O1bssS zk`F-B&W4q|=@9HPTumA;zkoZ_B=Fi7b|QXVS5bCB z+eU-}Kh+KUJ~)I9eAz@91otd~`@T59$xJi8fnXSgBGVx225zu+06%c!`5zQ2&w#wKH$;81{1wn4aWby_j6b|*Y+9O!=y{{S zW6hu-8f(H82LD~mK0zKit#3wtRX`9F0ncdcu4sKX@N>V1+e#zPV%1={jWKQn5WfI} z3gPFwS^ygc-3L^|TP+Y=$s{VHUGiuG=!^H&R+Jfl4I7YrO^m)EnZy&@M14~-$s#4= z^*<%U9YpB_{j+4UU{rexuiVNDHkuL8s`0M}*)Hddp?cZHWJqJ_#iU&w(;+-!j7+^) zDGb~)lUFiuyRM~>Sa0i9W^(4WW?UlS3JJGJxLv|c;%>)iw#|&aN{^YKG>AJmqeA5h zvq8mc%(z0r`PWN)33rZun*Z1w&89DE~7^-+RJYqlUadV#;=+AIT9|BaF2vTUN_fI7uaj`prgesVEZpK?s?mc zyWcTm<$VdanQ_S9%((sohP^%)IzBcF*gi4i`Y+A6`ztec{oRc7CEOt44u--1h6i%~ zVOCHs;d%+TOE~?X=K94Fu99#I;>+Ma!vN)Lvx0O97fZNae`Oqrofq zB<%RvtT11~6%wwOaH|PVhku~ZL{JphFH!>%Zj*42en}<_WXW-JwMq$hOV}k2cZ~^D zB4J05SznHX+sqhp!3lGPLJ8MMxc;10t2CHPuei4o7*Ed{3?A~;7SYuSElyH-T zJ44JpYR5Z&xQzP6coGqS+a%m+#;_qa6BL)ttRN@KjEg1QAmJYNVA*d_;ZSq^4ikn! za5>Ea#S*TOaEF8|63z9iCEOt4)aD{}c{kR{tr!|2{%Z%Rl*$-?vb!7 z*Q`I=k6HUlDW0{$Wn{E!3AaeNe2!VZN5b{_X1;B%8M}RW7He0@_7ONPlyIqpDuQ%P)ii$reM9|5wr{2H@#$+c$OD&ZapD@)AsITEgvaGMz~S3<5c zE2xxkTd|q%SZc=g67H06{xZF28o04)nYmu8gl)^s{2U3l==V$`Xg&z8!Hm61{#vs@tAs0VF!Plg&A42`u61U9p@eNW1@a*WxJ?B3 z9-V|MN&_pv2BOT2dnBBV4}ah?JX&?L8CTyDfH4QvYz!bEzE#5IW?ZG2af^g)_nY}O z5_VMi`78&zsGoqAXG%Ex2{XS`!XdlN{D$3T+#z9QkB`rDaK4{_?ZZD!?h|0RvcZhg zUpC`%2{%Z%L&COwMt$DCQspDC_LO!B+x8n3IKNoJ^%CxpaIyIMj}a3suLymjeWlYN zaQ%w%Cv$`460VnUi-bEQYX;gskaxvp}naZ3oPJw}i7LTqxuJGS~0s*ndJ+ z^@drXNy4r-&HPLW=S#TcEwg-;ge%@gKKS1llWIor<2DH^e>E$te$R}n4w`Yjgxe(C z@jl4o_!|Z&ZDs|wznO8Ngv%w|Bw^PF=K3WPt^~~D&oH1>60m(_R!}2h*C%Fvp@iL^ zn)%%lt~#Xh`1cub$XucPGc#_JaQR^~zd^$8FUPX*2!-LxXR8%h14`lDzScSO|WElAd5T zFJHf>0IaT($PN12K%Uwyk&S<>%V^Fv!mQ2s*B%Uhrap85XfKq+O7u&RTq%*O^+iDT zHc8|*33o~u{+Sk*`;A5&5>A(Jj)aTNI6^5m6O<}FbRpg+NaSt_D~XaF5>A(Jj)co4 zTy4hsUlx+^@D?*sY10pZZE=nybK5Qn=j)dgl32aGkl55JU4zeK;9Gzuw>3x@z7A;O zx9N|didIlT6u16aA&Cihxy>@^`p`unld}k#sni$Y=Sujj*TbK<9!WT4l)17)KLiz2 zxBf3E!ZRgqwtfQ1`4YKM4=n<6i9{~dha$N`B3J68kX$X1YxGNy+#r#g^w~gGTlGa) zwCiiI=+reRB6=iQC0XBE1Rk*^%bj`zYq|Aju*lS3!6HY08wzisBwH-u5($?|xLm>& z60VeRl^J`LYBNEpk#N0)8zkH$;T8$EO1RC0VFhb95#WSR!krTCmT-@Rm5a^Z50S7f z5aSB!2qXY@N!TsnbO~olI9tLw63!36Yy~e2AmDT>mT-xL;iV1~^KE^~C0rrlN(oo_ zG2cN{`w2{OjfCqZ+#um53AahOUBVqc%-dHweFWCN(j#G;_`au+-m@i~BjI8RmrJA(Jj)aRPTrOeuUNx0L5G5*Gx$IKyNw}dk#oFn04371Q_ zG7w|?)qw=S^%8Ch;x~C=z{5|4o8S*zh%NA6@%@PWxavg-|Cbrp2k66J*ijtfg)CGU z1pjr5_oWQn5>Ns8trD($6Nqr3ycPcI{16ex!BEzJ#31A2GsO0GA5LO(JxAQ?=O{YuA#&;oT>=cZ~7}{mW}fT3AOMAJvXH-MN@d y2x}5fZ5pQ+Aubuu`S5=0qlilQvRKs_vel^_}R77o)43F_pI}?8@Ur$(z3^bL`}X6Dq?7RqR}h+(`Jp z^;<5NY2|pXSGlQnV2u3|!cd5A`fFN@HSB)i= z1L+(!ixd>n^+1Z+=|Oc0`Jxx?QqxG?K>frLF5LHJ7O8wspT1O0q}xdjdA%1sMqVQu zUHf-h##%_aYyYvxDSb%u!2M~CwJJFjy?;yWPL(_my?=@`OC<}V>DsuRWLh*$bWI^w zMAPZcczUlZnOqi4ce--PD9#~+lF23fMh#Af&+`V~ zm=mkmpmcb9I$3*S73_DbGJx53MBc;VQ5bnI@WAa&bkUHU#iJuosgPV6fx}MQ#Dj~& zBkZbJkaP!c>%$(#M!(UlRY_-jN2IFs1dOy7Pz*j&7DT!r=}{6BK~E>8k=_wBK6$zr zgi9l%S(m&Y*^jxpG}0|xe34)m9U$*U&o@656HBkc;Xl%p9MY=HZJ_|Oc6uRyJi!sN!!FH zghSnqF;z55ce{s=;IUBpx{(KNLsiFM)aXEy0S^{(Z6p&beF?QCxye=w9h#Id$jzBn z<$G2|Bjxx2naRRVd!-HVH;R&4Dux5<&F4y*5dje}ZHSu*4yXkY7#;uLa1;49!lZ6NfiG+Rs_0%#okk43FYZ~8`Bx}VRzMv*9b z2m?177(##sjfy106%K16tGF-<(=}L!e#!g!{CN$lKn(xmN5fu%^C#x;)53luU6>xH zwo+gE2%O*h(nqP6(|@MdEaZuz@nJLq?Hte3OaOWfhil{2jIH?QHZgNP8&5bmP_{vx znn*NS4d-h+xL)HUyqvbJX8&iNzU05Grtx}FU05}R-Bi|1H;mIl_+q7pgf8Sw&UOMxzQ{5j|zG_r{F(pxAq6#|5k<2KW2@KIp2kH-%Y&L zzRZ^M^yYCB+>MwY;6k&11G7eW8ml;;=8cR?*(pL5G`XIK_Lqz`{ny60zrX0&-isKs zfM@iH?6eqZOn_sw*YH$R>mK_h8>2^bYvbk-W*ZOul8rY|fEV}3S`^Ym&AuICv9*Yg}mK#;|=*%a1BuB5=vD)awj6prMWx|YllFrK*X3^^E=;FNeeuuf&TX}lZ zwl~nn^PVEvVB*qY^oH?v+h#X2e?T2hFBzX}+w2i=D2F^M2hf{9%Z?`d#`E-~L)5l^e)cznAUCQSg>ftbTf+Jy9MZy()yl4+|XYhQ03vk~Ut_5S9JG;H!&p+&} zW)v&J>kjf1`{aZHR};sLAs<5?2SfP6Nj?w7aA0vWira3!>n&y2;5*cv98*3I0)!@C znO*d6?!*|LA^Of_ItqG?U9M$w>`*$HUOK=e_`eZ`HeP*)aEjUJ6WA&0Kv6!}N_SuB zQ5Vp^T{+pWfk%Z(In&fO@$g;Pisf7``qW5SgT>zWfLg3VKrYx5F9O?mg;H1@>qc!G zZ_wZrg*R8z-AvIq1RStjLuXC7dTjRD^XDDPyA^u4aMeYH*X6jEfY7+15_C*-h*e4B7Up$pVFDM<>Q=aeH|d&gh1;0W zb;U)55HeT`oW@#k8L9Bd^#-#8a=)P;`=!_^ol7aSKsqkrRG*}}aFw&&I^I&Bu5R#6yaPNzT3 z%AhyTigKQX$AdP=>4UVFnhbte7w z4U^QV^wk@#QYX{ib5m&jya`ETv#~5h6E2*pZ)PDz%N!q}z2^@n6>&6oewM{^N~OR1 zDprl7>*wcY3^>KrP!;!VxUS>+pAg1JlXK}e^9M%#wZEd^$(`<*mzT7g)e@?;N~-l? zddq_A$i!3h@PgP(MOyZkr=g2H^)@D9?R*$5F3#KV!i1j)k(992dL|@a&;D6 zP@GPB^rt80$HKw#$>M==lRM1bW5P#od(pRwCndcLXM*sslt>QVw13!*166enz0x}= zVmcIkc=rJMgx8}^p|5%;MdbpAHTLHl^VQKzX=s>IDt&+9BsHEo7EMwI(85KNJh)n5 z0lYMkQXTAWB;4r(imhb^XD?3cclJCC5VjRVYE?SvgNrvo|0XOMuB!CfC6oG9NtyfG z{ZB3#PRP&0_a9j{mZ%+c@6E&L$Q2_;W=o2*VexFd2du({TP|5%Fo0&=5=-~2sEd(B zV+YW#TN3-p>}>kzO*dY^O{UhBA6&q-(r;D{ynw4v$IUv7>%BK$H$t}lDU7OL^7kKf z*e#70SbUkDyJheN+!yJvRS)#5lOnR4zPjpz7@7MJ45gK^7bsWK_itTJI#1Hv(pdEb zEiAo?RGy?;N@GcrlO8OcLY_QHy8tP1G=G}PdFl9 zrh8WpCmVRlHYYs;y7iJSgn{Yqr^9X=PWqgp#ka-AN+nR!7 z=SRL6_rnh7CcdU`-L_o4m5y06iNyD(8=oIf>(~5DPR7vUC0Q4YtS|O|bNeKfG{n)Y zwXywLrK$fQEnNEv+{GMo=O?7(6pbxgrw*czmffi?rM<{0R&Eq`&qkvz zhZ(NbHi$+vF^WA6>gRFRgGZSm zacVvt>J<0>D*FA$5;|9QS>3pz)3Gb_QCTOw!^958ml=@PZkE=C$IB>lh1j;$su;cb3~`oo)zOp z3}(HrjppaKa*Lgf7Hy21*>PAeWAfEkaDex}v#rE~91cP+)} zzc}?Lf z0HykNcBL8^QC*8kOGmK+a94*|c^J1Md`cmq=D} z{{Q1dQpmjsPb6_-gkF$HuAv`om^6dW#Q(iKaw%Q;;JBp!lt!}X3-=HI|2>Ob`@fk* zZ1mcPCXO7!<}e=9@iOMP!BP1yqt@z%1Bl)xSItOz8i5w zFJ@(p8D`8-M|6UP#&1kZKFP#5Q8A~9tHOB14zK=L=#q`@7(1-~!8*Vtc#ms}swVnz zZ{c$_vBZd*teir_2JVb`!bE;s*ap749X4)&jUBeHyj~<)p22bFj7^7~Gds_Qr8jy- zkA4F&Rbq;JuP=nJmhZ;3t#x$X?}iV;+dQ~kIl*YC^sHZS`I@@((D81m1RoTMixNhKu{)_k^ z3_E^2&h%utQI6JcvR%5FS5=E8VHJS5V&B3Jk1eQ;_j11RVhQ#+zA~fLmeTJwCHWtM zJ11JL&}EkR(pd@oMiFM?X_3z*^u+Br9$c>fA?U;Igx4wLSl7Rpea?TCty`!Uv_t8` zasGXm@LOEq_W7l3n+`s0<2u?_5%C>-xyNzi;!VQ86$>2v7^*GLVO(uB52-`tap?hV zK#n{_tjcoMT5YkyN1-;Fm1m09E1ye6RR8zVM;FZIb6gvZy9#dT>pvYmhM!-)^8?R< z**r?xXkIZ^wCUiN8&Ur7S20Hy%*JU54HO-$jpBO(hecIBd0~8BAYvpx8#*FJF^|vy zFT%=Iz`aMt53TP71lM=fO8-n7NIIc+J(A`B71J?#N^Y%>9}HpO#dBK_#g8G{nj4?) zVAg42J%R3TYDU24XkMM#} zAznM6dq<7p2V$hYayQd+C^xbA_-yub^jDi#WlUxvFt&#dC68ge0V)P1i_wc$Or{@g zPWSWtW!yao_i_Zz1#!j*=O;6M4y!zzpUwDA#xJ+==X=KHG+eJ4TJLiy{BDKc7S)CR zg)87B48J>}!0iy65dS!F{`~4%K9~#DeAa4hD|cjJ5(|>D*kxyiS0pj9veGqxVQnjS zOcQ!O7f%>lkHporyucb)6*pYV4fOgGox%Kh_YdIaYF`!cf`UcHsKZ%t+n zm)A@ddVHp7G22AfZ|5Ub+m%QE_+*@)ZTf7Shco2g@T^&@5jw(2e2)jnAhKZiyRQ zA?EIVP)Mt(ADA z5?`GCEHo9qOm5(4v0VVRk{Wh-h$%U!(X z+IBk+-L5)rpthP`vo+3-&kb;3e+FNtFm&)`Z(KHv2f^vkx&mkyyq(O-8gIQDFFyJ1 z^{`eO6Tuhb7MKUY7sEGUTCJByKL#p<3!>>43_jw6_qpJi0$wX>d4I2r;oHT9wH-`< zaZEOo8+ZJCG58(~8sv@l#pDVsq7pNO;T5$!7%OA&F(+(pdO&xvDO%2>;uAMK3nT7k zEww86jsP0wQyZoWkK)#PHULBYj#>(IyTase#bonxVqrE`n8F^{uyQ!gaJ*PklpLYR z=UC`WGlN+yjJW?C9l9gYbsw{f3oZ{8-?i>d0`Z=`3G|*FGsLzGw^7)na$7Z{jk}m3 zXe;cnu~WAR4E3^I({_QO({V5uosLyiOOL=tbb!28=tCu}yjYSO{r7PJZoSa~998(T zjCPT;M1o^9kL51jbXav+Y^?0N8H%!cu{iHq5}7@1o15wTjJvMeN(Ihm<^0AW#y38X zF?{9xoanRh3XHp_cwolI9@u%t1%4_TK6r$yaRjHh_#c9Z8D|OOHHBd#7BdCoeu%@m z2=Odt@Q|MuMuQFuS2R*LJ`M}-y@?}O!Qk^MJmLKc%3V=0N`qeh~Wlg-9bL$zs z9xPz}FumU1T_#y3I?s?V7>XA7kHX9LF^=RE(a) z03XA;FFG^>EKXL1e^;W=x`oG09PDu9gm-kIG)pWD){Q6z`V7-OrCPJdwDruW;Olj;C1t=nU*gpnX$F4T+5e~DX>Uq zNpRYQOWVq8@gInJ^jjf-*Xq>R@88M-|n$fm(FE6KJg4_^-}TFYp&?Vr3FIHXSe3f z=lBXn;69m~-~HN9K!4gj#^1=}1ZV1eUR`++r;iAI>j3WC(lW--R#Y&$+FQx#LjI_0 z_3{l2uPNsbkBo;#Yiwn#YkiNEayqw!)2Uvhe)tMg5o2hz1-xQcfxvw1haL*?2Ci`# z4~0bf5)XlV8*;eeOpy+2MT3lIKH?#faclB6TRuEVp<{a_vQ4U-)^VuAOyXG>K4@F{ zkinsAnqDzHF5&3|J0GXW17{%lL2AIuSi!7HCU70}jfufG zM8wUH`rrdXN20<3)>^smEf33P_{EeeQ`v> zZ=qv)3?7L6it(5U?SR{Ay!~*e{=sL`Get&9;*>U~EO!sg_hh-#P{OnaXRbm|P1cM4s5RrTIcG)>d(Q z*XI`2BNC})|G2muo{fy>b3+X206J^`q^q60?%;ENx4@O$_EZnAdr5`B*&KU$vIxEa z5pAI#?H`w`V}0Pe*wg(Dm0a$sU{tH+ zK8K#}bDxbw4f_aI>l)f`&p&4eC z-u7@an2XT7=ljsQWmR^w_!?VQyvL0ttx9btR;XP(x8<|s<`dH%4JUwMZ3pi#tMUjl zjC*5#a{$dL@BrcLS^S{Ycvu{*ievx0Kf%ka-|m0#`Ejb)6~XuQe_&d04FB5;lj09E zgmpk0&Ku5REGwKe>rf&AGW}3uZbewyw!&tU*Hpwju_{kBnU1kFz_G z=%JnXhy~Vh`q81J@$CaeP6i%!Rrv2{pgV28mdi@Nka_v9!*FF)X&P5~N?jmKb{gEw^Drs`<@AJ+Um9)C{ z?|XL@AssH7`2HCZHE4hA(ak-Hd(i%UpH5K?yR9F9-7L%OGw`jBFNe|lKg%XLgXr|n zvq&9CKF_4~&nJ))&Z&Hzt^)xU`8><=&G%5`^FmT7gztq5{3l*L`{zz zcanTo)Y9~ULj9i)GYr2Eh9`YFQ7YGym3z|i22g+dTjG6hk{?f({VR>Y>)advwbA1K zL8XJgnn*nH_zF6?{0F*fek_TK$5+q?W&MD$?>P(_#JCys~ znWdIyVLqxI#!Vvq)ej!aTOhjihrO1n(-6n+hmoAu==9F@*nLMkvk*o8I0xHS{NohM z51s1%T|ZhamNP#=y+4m3m$KLPi9L~a{hVm|7Py1YC6IzdntQI;@(sYJ&W#}>xKc*~ zJ#lVqSdrYWDZwJyiF9YzDpHzAW6x)~)Sp9r?Q=OhuzT2MD3KPQpW^!Ir;r$j0=ruQ zn@sfP{B$zr)T4dWvEbCBQ`94*Dp5bONKMq?eI`3`zkx43WD(~neXXh~bVn#=mAt1^4D zm!;U~V_)x>gAIk2v?PV49T=7!NrA3DYMM%-k^^aWH7$}XR`uzzYAj84*!A0E)jX1u z95@&YzuH4Oqx3GPT8y+f4(fVB)sM%ixn#Slr@4UEtLR;t&B=i?>FU@B zXnr7TsG8Q}qAl{I=&MGBwJ0w|-vaoeEhrNMKkvCN@M)}41s4P|Ai1PJOAsrunuh(uGW zemqxAbhSz?90MacJOWWeLUyo^lZD)fKu2m|*(CKKA^W5B#LJ-}?jeD(m#f7V?99zq zs(lEl7!ufdm3r*rF~}LBZ@C(Leoyi_)fmS^^dle-R#7rU@46b=P(4IXn+nmd9}<`{ z73MDK9HOtC27+cVh_r1;;I(P!++ET7y6e<+pFEG;(Z0`vYM&qY{6#zGWsbDx&p+Gr^U6qV=OQVQvCHb`}f- zsBqOR^$fW`T3ygEf&{Uf~y%@sK zoThI?Ty;c0S`6+&w(fHy^h0l3VB?J{?1?(l^xV(XvGmO^?f5HYi3`<@#F?(|TL^`L zo>-_ZbEViq?ioLDfbJ#P^i_+XmxnMKtM6H)x=BttE*(=y`v-dLVqW9)#p+s8!uS(( z+dU5RNwdq@Kf&g7_(+}RHZXty>j8D9N+e&pVnQzmNtc1bWoUX6G8QKiO$XmdZ z8$sz7bthS5)6-Ug6ANv6@hbJ8Ye`r$9bz2f))*?ux)qEs1$(!u2QAI%M89q+tbcnx zQ+w(4rRwd%WU`*WT78Zzx9MH0Ap(1&(XK1ty-giWh|fm1oP!wMu6DrZz?M6(y9D#l zTD3j+`OdJ;7`);_6jjv|r00)2L>JEKURM?bg^Brpt)?}JWZd*4Jo z^=UPmc!RRQ`lr$J$ligC_3BOwsrTq(pM%yy7p{8_qFGs>AAAn1)KL96(n{PU!%pIW z>Lp5szWD&xJ$3+fh3p{IQ0mc_;ScD6oqY#kf`S)a2VtURX9Si7V5k#shW^^0V6*`} z@h9l>{0u$vd1!h>jK1!9sOVw4zVCV9|I@C&_B^=1rw{UD^`DJ(QvzwcFC0XG{U&pHfw!zAA<IJHjN@-Z^qMks*oIBfr?sffq$PtT34Pi-u;HtGT`ztIH2&IO zU-u4Vi4M?w2RiB(m7+2ON8ZJJbGJ>8eGdYDw=J;qJ#`&ptosq*te+VrEdgMb`KY*_LJEnJA;OO7gorW2vaPVUo6=>lfTw(n`VC_T3 zvQM~$EuX+n0dQIyS6J7ko>7TA6B{`N2j1a-Lb}K^;br>QpQ%N}K0u%HIS7Hi_PM$x z#y%hvS0`+#fxq?(XkPUIJ@PobR9lsyA3F~5ErSmEQr!m!+0!~4TSf-jzg1zi^_~s@ z`ZPk{bxyrH;OSBirHpaH7jOA1M|?D;1LjKz$0NIw>$T_9fqHf_>1C@bD^eM?C zExGI)j?2QzZGaJ+%-Kp=hrThHz`kLQo|r;nM|zfV!Ly)myi@%Juy6x|*CF5=Wf33i z&`&^l>qz*@ZDa1+IX6eOg1b*^O&`v?CXX@+H$Y8aQi<>yLd1s?BMY-XW z-UV*>OCXW)Z?SQHCt%|?HO}4y&Q>mm`#}7*4QI>*Ebg8%#uC7I)-kS|0K@zaUbg}c zujg8*2cmOHP@x4BvV#ade6NaE$v89ad;M@KiFL&R--y>m3VF)+ zdg2g>KTwP#x$}E{I$+nGVfKnaS3Eye?%{gYooApveR2sIsMnuUBdqm+Nf>?>a9jvq zG8Nhp9;p6sZ=kpEp#}Wm-#~cnO-HdS!)MP|v3CtHaS5ttme+!0sdfGS#8R_Z3==>zaxDgosrf)f;wS&{_ zc24hrstmu7vkox08od1fLEZ*wA5w1clFL^@d6;~m(I6{OP=Hfa@cOZn^`bt_L!7-! zP6qjks!vWQ{d?#C$XU%Ga_UJbq(A96sZURaiPCBHxKKfvM5 z%M+{CwLfxhv7mOrFA%g;@b44!Q9-u~S}*9Ug1*D4U-?)tP6`@%mRIBuG)2&{g3cH8 z5kdC~`ih_}f_^Ayy>Rd|fzJxspWUtS!yZ%Iw#X86Jd9$zE*E%#pryjlN`dbYbhDtf zf(8VAMbNhe{kxzpVZR3LLH$aMxUG^R=tw~)3pz{CC4!a;+9Vv(1ioL;je^Eq`Y z*01akj0Qnp7W9aq9|?L~&<;Vr7c>Xn=wm~DKnDNHpweLFO|Y|f{S@VWc&yxy)y4lD zSM;t+AQhff^w^;!%T^-vl+*h3p^zHSg1(UsAA)3Vq^=58Ux)Gv*!c3deH8`Y$}%!U z4$=3aJO}Qtq>K9Q1Zu1YAU`I{eP(%|VN!W?%}AqAK3A5Po6E0~K5Ojhj^N2I>%>y;1r`S-u-&hW{Wx zEX((qm?K{+FSAm1X(OTzpE`4L$@7wm_X z?*h5?1+ec5E&Z3i4;ah@FBvz$p2r=O!tb=fNto@S^aZd}yQSy@Uf2yOEqt*Y3Tgwx zid_NN2of)!%+R*(Ihw7 z_<4UL=uT1Am?+Mn`p7Y4PIA*hE@&2V?By8n!uS^m>aGav9z)KkBkD!ej9w}b-Dlv+ z$=rg0Tl9Cvk-`4XDQ32NmKhgFxJ1Hr5^j=kjW{$J)i#*1UuiQllxlG}GzzrNH9J&) zqZzkIxW+5VC7ib~Tn;n9vnZSak3=M#zt}7=k!U4E^j+%m(u% zTqBRwvl6b7aJ__EB^>qd@Cv}bgd4&!)UULKGXUGaHXA6AaH)h_B^>pQR9?c$iI5!Y zSF*zxh#Mr_F5$dyQ7bgtD| zbH&OagX2yKd%iat%#(1Xgli<+p+A#{JJd5~GoByJxJbet_J(69&_xoil5m5By+4}l z`;{^?Ln%9JQcxNs?EJ|rFO_hOgj>4I`c3ew4S4Y^)?ZQb#Z4*BfT5;Oz5)W%Cb6R; z!^^=4?PbRK60VkTTOYH29^QBf`k^SzW(-TW-K>x=;YtZNOSmN7T)slW)e^2ZVVDKY zCI%!033p1^o?xypTf#*WE|qX)IL2966V3qKAmLUC+mp=>0{hqmI3)4c16F>IJjhE#Vvq7aTI{mkAu)*_1Sz6)Gg$ zAmR2VN&jUtE_=m{t0dgOacF1L{HnP?yM#MmGt2Y-V#Zzx*GM?3*{ol{FvQ=;(4~?> zy@WeoHyfz@s~JbVX~uaHw!dYTw<5;=HykK?+pJJ8VQ-6B-XLM`yJmT{gzbMb%S!;W z@n<-Y|GrtFLc&o;&2q1Vi(1X{8VS2UG|NjqRQdQf9Ps?ztWYQ6{Ey7?A_=!kxaniF ze!GOLzwnzCYQ#S)Za7pqkgy49;A#oiNw`76Z4x%V3~AW2`^DES4TkZpc>^2&golBR ze|5^hUa7qCuTU9s)U>m=M1 zj&c0AhBE+nNH}Vcc@VlKoFm~P371K@It;V>;oc`TxVcWKithS8*G+vtAwNYzn&c&B;jTWw@A2E!fk^IAOD5} z?UF)=ggYgyxXrUBO2T#tJ0VU&dJ5_Xy} zj6b)D0k7mpI9tLw63&xwzJv=TTojIR{CmS0fXgIYA>m31S4p@=!VMB`55xM<*4jf5NY8K5?zU1A&GA~&jYitk(-I9q=Pim6`xQzTV-zc~XEroRL1&F~F86&}zQ zL4ED|3?P#`B}Kc(T%Sk6c@i$rYtdS%{tS{T{T(Fr`lm=*^pil6I;0|s_`;?U0J}co z2DoYLy@A;ED*V%ge>(7wdoKJbz&{oErw0Et;h#46qekf?=3&8kD91lF@W)>xo_O0J zG2mIXPP}i27;aH3&immHTn4V%WyWOzGp-7=hvgOVwi=d?fd5;?%VYxkmAbG3s8BEA z(ieaU7Z$L>|1Ga0;y4m$)n6H8@*eQ9=|~7;%(DN%kKkAyap|Wac{I8d^$-2>W#k}f zIH8|iMzZ>~e#;9O^lAOl0Qw}9=J=%)bpa}MQl lgFc}rC6ht=>=H7-)*xi?2JvkrBsaQq9@ouhIv54z{{YJ}6m$Rp diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index 06fc9e8d2..8e01d3fbe 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -65,8 +65,8 @@ pub enum GuineaInstruction { Resize(usize), ScheduleTask(ScheduleTaskArgs), CancelTask(i64), - CreateEphemeralAccount { data_len: usize }, - ResizeEphemeralAccount { new_data_len: usize }, + CreateEphemeralAccount { data_len: u32 }, + ResizeEphemeralAccount { new_data_len: u32 }, CloseEphemeralAccount, } @@ -202,22 +202,29 @@ fn cancel_task( Ok(()) } +fn validate_ephemeral_accounts( + magic_program_info: &AccountInfo, + vault_info: &AccountInfo, +) -> ProgramResult { + if magic_program_info.key != &magicblock_magic_program_api::ID { + return Err(ProgramError::InvalidAccountData); + } + if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { + return Err(ProgramError::InvalidAccountData); + } + Ok(()) +} + fn create_ephemeral_account( mut accounts: slice::Iter, - data_len: usize, + data_len: u32, ) -> ProgramResult { let magic_program_info = next_account_info(&mut accounts)?; let sponsor_info = next_account_info(&mut accounts)?; let ephemeral_info = next_account_info(&mut accounts)?; let vault_info = next_account_info(&mut accounts)?; - if magic_program_info.key != &magicblock_magic_program_api::ID { - return Err(ProgramError::InvalidAccountData); - } - - if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { - return Err(ProgramError::InvalidAccountData); - } + validate_ephemeral_accounts(magic_program_info, vault_info)?; let account_infos = &[ sponsor_info.clone(), @@ -243,20 +250,14 @@ fn create_ephemeral_account( fn resize_ephemeral_account( mut accounts: slice::Iter, - new_data_len: usize, + new_data_len: u32, ) -> ProgramResult { let magic_program_info = next_account_info(&mut accounts)?; let sponsor_info = next_account_info(&mut accounts)?; let ephemeral_info = next_account_info(&mut accounts)?; let vault_info = next_account_info(&mut accounts)?; - if magic_program_info.key != &magicblock_magic_program_api::ID { - return Err(ProgramError::InvalidAccountData); - } - - if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { - return Err(ProgramError::InvalidAccountData); - } + validate_ephemeral_accounts(magic_program_info, vault_info)?; let account_infos = &[ sponsor_info.clone(), @@ -288,13 +289,7 @@ fn close_ephemeral_account( let ephemeral_info = next_account_info(&mut accounts)?; let vault_info = next_account_info(&mut accounts)?; - if magic_program_info.key != &magicblock_magic_program_api::ID { - return Err(ProgramError::InvalidAccountData); - } - - if *vault_info.key != EPHEMERAL_VAULT_PUBKEY { - return Err(ProgramError::InvalidAccountData); - } + validate_ephemeral_accounts(magic_program_info, vault_info)?; let account_infos = &[ sponsor_info.clone(), diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index 63fedd370..25c2163df 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -1,6 +1,5 @@ //! Close ephemeral account instruction processor -use magicblock_magic_program_api::id; use solana_account::{ReadableAccount, WritableAccount}; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; @@ -10,51 +9,35 @@ use solana_transaction_context::TransactionContext; use super::{ processor::rent_for, - validation::{validate_cpi_only, validate_sponsor}, -}; -use crate::utils::{ - accounts, instruction_context_frames::InstructionContextFrames, + validation::{ + get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, + validate_sponsor, validate_vault, + }, }; +use crate::utils::accounts; /// Closes an ephemeral account, refunding rent to the sponsor. pub(crate) fn process_close_ephemeral_account( invoke_context: &InvokeContext, transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { - // Must be called via CPI (user programs mediate all access) validate_cpi_only(transaction_context)?; - - // Validate sponsor (signer or PDA owned by caller) validate_sponsor(transaction_context)?; + validate_vault(transaction_context)?; - // Validate vault is owned by magic program - let vault = - accounts::get_instruction_account_with_idx(transaction_context, 2)?; - if *vault.borrow().owner() != id() { - return Err(InstructionError::InvalidAccountOwner); - } - + let caller_program_id = get_caller_program_id(transaction_context)?; let ephemeral = - accounts::get_instruction_account_with_idx(transaction_context, 1)?; - - if !ephemeral.borrow().ephemeral() { - return Err(InstructionError::InvalidAccountData); - } - - // Prevent double-close: verify account is still owned by calling program - // After closing, account is owned by system_program, so this prevents double-spend - let frames = InstructionContextFrames::try_from(transaction_context)?; - let caller_program_id = frames - .find_program_id_of_parent_of_current_instruction() - .ok_or(InstructionError::IncorrectProgramId)?; + validate_existing_ephemeral(transaction_context, &caller_program_id)?; - if *ephemeral.borrow().owner() != *caller_program_id { - return Err(InstructionError::InvalidAccountOwner); - } + let data_len: u32 = ephemeral + .borrow() + .data() + .len() + .try_into() + .map_err(|_| InstructionError::ArithmeticOverflow)?; - let data_len = ephemeral.borrow().data().len(); - let refund = rent_for(data_len); - // Credit sponsor, debit vault + // Transfer rent from vault back to sponsor + let refund = rent_for(data_len)?; accounts::credit_instruction_account_at_index( transaction_context, 0, @@ -66,6 +49,7 @@ pub(crate) fn process_close_ephemeral_account( refund, )?; + // Reset account to empty state let mut acc = ephemeral.borrow_mut(); acc.set_lamports(0); acc.set_owner(system_program::id()); diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index e4e58eba0..bf87e704c 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -1,15 +1,18 @@ //! Create ephemeral account instruction processor -use magicblock_magic_program_api::id; use solana_account::{ReadableAccount, WritableAccount}; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; +use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use super::{ - processor::rent_for, - validation::{validate_cpi_only, validate_sponsor}, + processor::{rent_for, MAX_DATA_LEN}, + validation::{ + get_caller_program_id, validate_cpi_only, validate_sponsor, + validate_vault, + }, }; use crate::utils::accounts; @@ -18,40 +21,29 @@ use crate::utils::accounts; pub(crate) fn process_create_ephemeral_account( invoke_context: &InvokeContext, transaction_context: &TransactionContext, - data_len: usize, + data_len: u32, ) -> Result<(), InstructionError> { - use crate::utils::instruction_context_frames::InstructionContextFrames; + if data_len > MAX_DATA_LEN { + return Err(InstructionError::InvalidArgument); + } - // Must be called via CPI (user programs mediate all access) validate_cpi_only(transaction_context)?; - - // Get caller program ID (will be the owner of the ephemeral account) - let frames = InstructionContextFrames::try_from(transaction_context)?; - let caller_program_id = frames - .find_program_id_of_parent_of_current_instruction() - .ok_or(InstructionError::IncorrectProgramId)?; - - // Validate sponsor (signer or PDA owned by caller) validate_sponsor(transaction_context)?; + validate_vault(transaction_context)?; - // Validate vault is owned by magic program - let vault = - accounts::get_instruction_account_with_idx(transaction_context, 2)?; - if *vault.borrow().owner() != id() { - return Err(InstructionError::InvalidAccountOwner); - } + let caller_program_id = get_caller_program_id(transaction_context)?; - // Validate: must have 0 lamports and not already ephemeral + // Target account must be empty (0 lamports, not owned by system program) let ephemeral = accounts::get_instruction_account_with_idx(transaction_context, 1)?; let acc = ephemeral.borrow(); - if acc.lamports() != 0 || acc.ephemeral() { + if acc.lamports() != 0 || *acc.owner() != system_program::ID { return Err(InstructionError::InvalidAccountData); } drop(acc); - // Debit rent from sponsor, credit to vault - let rent = rent_for(data_len); + // Transfer rent from sponsor to vault + let rent = rent_for(data_len)?; accounts::debit_instruction_account_at_index(transaction_context, 0, rent)?; accounts::credit_instruction_account_at_index( transaction_context, @@ -59,11 +51,11 @@ pub(crate) fn process_create_ephemeral_account( rent, )?; - // Set up ephemeral account (owned by the calling program) + // Initialize ephemeral account let mut acc = ephemeral.borrow_mut(); acc.set_lamports(0); - acc.set_owner(*caller_program_id); - acc.resize(data_len, 0); + acc.set_owner(caller_program_id); + acc.resize(data_len as usize, 0); acc.set_ephemeral(true); ic_msg!( diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs index ac7acfbeb..bcad104bf 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_resize.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -1,6 +1,5 @@ //! Resize ephemeral account instruction processor -use magicblock_magic_program_api::id; use solana_account::ReadableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; @@ -8,8 +7,11 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; use super::{ - processor::rent_for, - validation::{validate_cpi_only, validate_sponsor}, + processor::{rent_for, MAX_DATA_LEN}, + validation::{ + get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, + validate_sponsor, validate_vault, + }, }; use crate::utils::accounts; @@ -17,33 +19,33 @@ use crate::utils::accounts; pub(crate) fn process_resize_ephemeral_account( invoke_context: &InvokeContext, transaction_context: &TransactionContext, - new_data_len: usize, + new_data_len: u32, ) -> Result<(), InstructionError> { - // Must be called via CPI (user programs mediate all access) - validate_cpi_only(transaction_context)?; + if new_data_len > MAX_DATA_LEN { + return Err(InstructionError::InvalidArgument); + } - // Validate sponsor (signer or PDA owned by caller) + validate_cpi_only(transaction_context)?; validate_sponsor(transaction_context)?; + validate_vault(transaction_context)?; - // Validate vault is owned by magic program - let vault = - accounts::get_instruction_account_with_idx(transaction_context, 2)?; - if *vault.borrow().owner() != id() { - return Err(InstructionError::InvalidAccountOwner); - } - + let caller_program_id = get_caller_program_id(transaction_context)?; let ephemeral = - accounts::get_instruction_account_with_idx(transaction_context, 1)?; + validate_existing_ephemeral(transaction_context, &caller_program_id)?; - if !ephemeral.borrow().ephemeral() { - return Err(InstructionError::InvalidAccountData); - } + let old_len: u32 = ephemeral + .borrow() + .data() + .len() + .try_into() + .map_err(|_| InstructionError::ArithmeticOverflow)?; - let old_len = ephemeral.borrow().data().len(); - let delta = rent_for(new_data_len) as i64 - rent_for(old_len) as i64; + let new_rent = rent_for(new_data_len)?; + let old_rent = rent_for(old_len)?; + let delta = new_rent as i64 - old_rent as i64; if delta > 0 { - // Debit sponsor, credit vault + // Growing: debit sponsor, credit vault accounts::debit_instruction_account_at_index( transaction_context, 0, @@ -55,7 +57,7 @@ pub(crate) fn process_resize_ephemeral_account( delta as u64, )?; } else { - // Credit sponsor, debit vault + // Shrinking: credit sponsor, debit vault accounts::credit_instruction_account_at_index( transaction_context, 0, @@ -68,7 +70,7 @@ pub(crate) fn process_resize_ephemeral_account( )?; } - ephemeral.borrow_mut().resize(new_data_len, 0); + ephemeral.borrow_mut().resize(new_data_len as usize, 0); ic_msg!( invoke_context, diff --git a/programs/magicblock/src/ephemeral_accounts/processor.rs b/programs/magicblock/src/ephemeral_accounts/processor.rs index b09d0f4c5..5d7824da4 100644 --- a/programs/magicblock/src/ephemeral_accounts/processor.rs +++ b/programs/magicblock/src/ephemeral_accounts/processor.rs @@ -2,10 +2,17 @@ use magicblock_magic_program_api::EPHEMERAL_RENT_PER_BYTE; use solana_account::AccountSharedData; +use solana_instruction::error::InstructionError; -/// Calculates rent for an ephemeral account based on its data length -pub(crate) const fn rent_for(data_len: usize) -> u64 { - let total_size = - data_len as u64 + AccountSharedData::ACCOUNT_STATIC_SIZE as u64; - total_size * EPHEMERAL_RENT_PER_BYTE +/// Maximum allowed data length for ephemeral accounts (10 MB, matching Solana's limit) +pub(crate) const MAX_DATA_LEN: u32 = 10 * 1024 * 1024; + +/// Calculates rent for an ephemeral account based on its data length. +pub(crate) fn rent_for(data_len: u32) -> Result { + let total_size = u64::from(data_len) + .checked_add(AccountSharedData::ACCOUNT_STATIC_SIZE as u64) + .ok_or(InstructionError::ArithmeticOverflow)?; + total_size + .checked_mul(EPHEMERAL_RENT_PER_BYTE) + .ok_or(InstructionError::ArithmeticOverflow) } diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index cb21c4279..7d7c037c5 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -1,55 +1,96 @@ //! Validation utilities for ephemeral account instructions +use std::cell::RefCell; + +use magicblock_magic_program_api::EPHEMERAL_VAULT_PUBKEY; +use solana_account::{AccountSharedData, ReadableAccount}; use solana_instruction::error::InstructionError; +use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -/// Validates the sponsor account according to MIMD-0016 spec: -/// - Oncurve accounts: must be a signer -/// - PDA accounts: must be owned by the calling program -pub(crate) fn validate_sponsor( +use crate::utils::{ + accounts, instruction_context_frames::InstructionContextFrames, +}; + +/// Returns the caller program ID (the program that invoked us via CPI). +pub(crate) fn get_caller_program_id( + transaction_context: &TransactionContext, +) -> Result { + let frames = InstructionContextFrames::try_from(transaction_context)?; + frames + .find_program_id_of_parent_of_current_instruction() + .copied() + .ok_or(InstructionError::IncorrectProgramId) +} + +/// Validates that the instruction is called via CPI (not directly from a transaction). +pub(crate) fn validate_cpi_only( transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { - use crate::utils::{ - accounts, instruction_context_frames::InstructionContextFrames, - }; + let frames = InstructionContextFrames::try_from(transaction_context)?; + if frames + .find_program_id_of_parent_of_current_instruction() + .is_none() + { + return Err(InstructionError::IncorrectProgramId); + } + Ok(()) +} +/// Validates the sponsor account (index 0): +/// - Oncurve accounts must be a signer +/// - PDA accounts must be owned by the calling program +pub(crate) fn validate_sponsor( + transaction_context: &TransactionContext, +) -> Result<(), InstructionError> { let ix_ctx = transaction_context.get_current_instruction_context()?; - // Check if sponsor is a signer if !ix_ctx.is_instruction_account_signer(0)? { - // Not a signer, must be a PDA owned by the calling program - let frames = InstructionContextFrames::try_from(transaction_context)?; - let caller_program_id = frames - .find_program_id_of_parent_of_current_instruction() - .ok_or(InstructionError::InvalidAccountData)?; - + // Not a signer - must be a PDA owned by the calling program + let caller_program_id = get_caller_program_id(transaction_context)?; let sponsor_owner = accounts::get_instruction_account_owner_with_idx( transaction_context, 0, )?; - if sponsor_owner != *caller_program_id { + if sponsor_owner != caller_program_id { return Err(InstructionError::InvalidAccountOwner); } } Ok(()) } -/// Validates that the instruction is being called via CPI (not directly from a transaction). -/// This ensures all ephemeral account operations are mediated by user programs, -/// which implement their own access control and business logic. -pub(crate) fn validate_cpi_only( +/// Validates the vault account (index 2) matches the expected pubkey. +pub(crate) fn validate_vault( transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { - use crate::utils::instruction_context_frames::InstructionContextFrames; + let vault_pubkey = + accounts::get_instruction_pubkey_with_idx(transaction_context, 2)?; + if *vault_pubkey != EPHEMERAL_VAULT_PUBKEY { + return Err(InstructionError::InvalidAccountOwner); + } + Ok(()) +} - let frames = InstructionContextFrames::try_from(transaction_context)?; - let caller_program_id = - frames.find_program_id_of_parent_of_current_instruction(); +/// Validates an existing ephemeral account (index 1): +/// - Must be marked as ephemeral +/// - Must be owned by the caller program +/// +/// Returns the account for further operations. +pub(crate) fn validate_existing_ephemeral<'a>( + transaction_context: &'a TransactionContext, + caller_program_id: &Pubkey, +) -> Result<&'a RefCell, InstructionError> { + let ephemeral = + accounts::get_instruction_account_with_idx(transaction_context, 1)?; - // If caller_program_id is None, we're at the top level (direct call, not CPI) - if caller_program_id.is_none() { - return Err(InstructionError::IncorrectProgramId); + let acc = ephemeral.borrow(); + if !acc.ephemeral() { + return Err(InstructionError::InvalidAccountData); + } + if acc.owner() != caller_program_id { + return Err(InstructionError::InvalidAccountOwner); } + drop(acc); - Ok(()) + Ok(ephemeral) } From 30041f646bf09d3f4d4e3fce7e12e1f3ad394a6b Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 30 Jan 2026 19:04:42 +0400 Subject: [PATCH 05/15] fix: add signer check for ephmeral account --- .../tests/ephemeral_accounts.rs | 69 +++++++++--------- programs/elfs/guinea.so | Bin 157912 -> 158832 bytes programs/guinea/src/lib.rs | 10 ++- .../src/ephemeral_accounts/process_create.rs | 8 +- test-kit/src/lib.rs | 20 +++++ 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/magicblock-processor/tests/ephemeral_accounts.rs b/magicblock-processor/tests/ephemeral_accounts.rs index 3f0becbdc..fd37a3bea 100644 --- a/magicblock-processor/tests/ephemeral_accounts.rs +++ b/magicblock-processor/tests/ephemeral_accounts.rs @@ -5,6 +5,7 @@ use magicblock_magic_program_api::{ EPHEMERAL_VAULT_PUBKEY, ID as MAGIC_PROGRAM_ID, }; use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; +use solana_keypair::Keypair; use solana_program::instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; use test_kit::{ExecutionTestEnv, Signer}; @@ -19,7 +20,7 @@ fn rent_for(data_len: u32) -> u64 { struct TestContext { env: ExecutionTestEnv, sponsor: Pubkey, - ephemeral: Pubkey, + ephemeral: Keypair, } /// Sets up a test with vault, sponsor, and ephemeral account @@ -29,7 +30,7 @@ fn setup_test() -> TestContext { let sponsor = env.get_payer().pubkey; init_sponsor(&env, sponsor); - let ephemeral = env.create_account(0).pubkey(); + let ephemeral = env.create_account(0); TestContext { env, @@ -47,6 +48,16 @@ async fn execute_instruction( env.execute_transaction(txn).await } +/// Executes an instruction with additional signers +async fn execute_instruction_with_signers( + env: &ExecutionTestEnv, + ix: Instruction, + signers: &[&Keypair], +) -> Result<(), solana_transaction_error::TransactionError> { + let txn = env.build_transaction_with_signers(&[ix], signers); + env.execute_transaction(txn).await +} + /// Helper to initialize the ephemeral vault account in the accounts database fn init_vault(env: &ExecutionTestEnv) { // Create vault with enough balance to be rent-exempt (covers the account overhead) @@ -86,7 +97,7 @@ fn create_ephemeral_account_ix( vec![ AccountMeta::new_readonly(magic_program, false), AccountMeta::new(sponsor, true), - AccountMeta::new(ephemeral, false), + AccountMeta::new(ephemeral, true), // Must be signer to prevent squatting AccountMeta::new(vault, false), ], ) @@ -166,7 +177,7 @@ async fn test_create_ephemeral_account_via_cpi() { EPHEMERAL_VAULT_PUBKEY, data_len, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); let result = env.execute_transaction(txn).await; if let Err(e) = &result { @@ -254,7 +265,7 @@ async fn test_resize_ephemeral_account_via_cpi() { EPHEMERAL_VAULT_PUBKEY, initial_data_len, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); // Check balances BEFORE resize @@ -355,7 +366,7 @@ async fn test_close_ephemeral_account_via_cpi() { EPHEMERAL_VAULT_PUBKEY, data_len, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); let expected_rent = rent_for(data_len); @@ -458,7 +469,7 @@ async fn test_resize_smaller_via_cpi() { EPHEMERAL_VAULT_PUBKEY, initial_data_len, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); // Check balances BEFORE resize (shrink) @@ -588,17 +599,18 @@ async fn test_create_with_non_zero_lamports_fails() { let sponsor = env.get_payer().pubkey; init_sponsor(&env, sponsor); - let ephemeral = env.create_account(100).pubkey(); // Non-zero lamports! + let ephemeral = env.create_account(100); // Non-zero lamports! - let result = execute_instruction( + let result = execute_instruction_with_signers( &env, create_ephemeral_account_ix( magicblock_magic_program_api::ID, sponsor, - ephemeral, + ephemeral.pubkey(), EPHEMERAL_VAULT_PUBKEY, 1000, ), + &[&ephemeral], ) .await; @@ -610,20 +622,21 @@ async fn test_create_already_ephemeral_fails() { let ctx = setup_test(); // Simulate an existing ephemeral account (owned by a program, not system) - let mut acc = ctx.env.get_account(ctx.ephemeral); + let mut acc = ctx.env.get_account(ctx.ephemeral.pubkey()); acc.set_owner(guinea::ID); acc.set_ephemeral(true); acc.commit(); - let result = execute_instruction( + let result = execute_instruction_with_signers( &ctx.env, create_ephemeral_account_ix( magicblock_magic_program_api::ID, ctx.sponsor, - ctx.ephemeral, + ctx.ephemeral.pubkey(), EPHEMERAL_VAULT_PUBKEY, 1000, ), + &[&ctx.ephemeral], ) .await; @@ -715,7 +728,7 @@ async fn test_resize_to_zero_size() { EPHEMERAL_VAULT_PUBKEY, 1000, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); // Resize to zero @@ -752,7 +765,7 @@ async fn test_close_already_closed() { EPHEMERAL_VAULT_PUBKEY, 1000, ); - let txn = env.build_transaction(&[create_ix]); + let txn = env.build_transaction_with_signers(&[create_ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); let close_ix = close_ephemeral_account_ix( @@ -797,7 +810,7 @@ async fn test_close_already_closed_double_spend() { EPHEMERAL_VAULT_PUBKEY, 1000, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); // Track balances before first close @@ -894,7 +907,7 @@ async fn test_insufficient_balance_fails() { EPHEMERAL_VAULT_PUBKEY, data_len, ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); let result = env.execute_transaction(txn).await; assert!(result.is_err(), "Should fail - insufficient balance"); @@ -926,11 +939,11 @@ async fn test_create_with_pda_sponsor() { vec![ AccountMeta::new_readonly(magicblock_magic_program_api::ID, false), AccountMeta::new(global_sponsor_pda, false), // PDA (not a signer in transaction) - AccountMeta::new(ephemeral.pubkey(), false), + AccountMeta::new(ephemeral.pubkey(), true), // Ephemeral must sign AccountMeta::new(EPHEMERAL_VAULT_PUBKEY, false), ], ); - let txn = env.build_transaction(&[ix]); + let txn = env.build_transaction_with_signers(&[ix], &[&ephemeral]); let result = env.execute_transaction(txn).await; @@ -1027,7 +1040,7 @@ async fn test_full_lifecycle() { EPHEMERAL_VAULT_PUBKEY, 1000, ); - let txn = env.build_transaction(&[create_ix]); + let txn = env.build_transaction_with_signers(&[create_ix], &[&ephemeral]); assert!(env.execute_transaction(txn).await.is_ok()); let grow_ix = resize_ephemeral_account_ix( @@ -1087,25 +1100,15 @@ async fn test_multiple_accounts_same_sponsor() { env.accountsdb.get_account(&sponsor).unwrap().lamports(); // Create 3 accounts with different sizes - for (eph, len) in [ - (eph1.pubkey(), 1000), - (eph2.pubkey(), 2000), - (eph3.pubkey(), 500), - ] { + for (eph, len) in [(&eph1, 1000), (&eph2, 2000), (&eph3, 500)] { let ix = create_ephemeral_account_ix( magicblock_magic_program_api::ID, sponsor, - eph, + eph.pubkey(), EPHEMERAL_VAULT_PUBKEY, len, ); - // Manually build transaction with same signer - let txn = solana_transaction::Transaction::new_signed_with_payer( - &[ix], - Some(&sponsor), - &[&env.payers[0]], - env.ledger.latest_blockhash(), - ); + let txn = env.build_transaction_with_signers(&[ix], &[eph]); assert!(env.execute_transaction(txn).await.is_ok()); } diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index 91103553a962271fc2e54c75f3bbef7ec4dfe261..2702f2e35e8426712dfc47174fbdd2142f27ac9c 100755 GIT binary patch delta 21258 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 delta 19903 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 v Transaction { + let payer = { + let index = self.payer_index.fetch_add(1, Ordering::Relaxed); + &self.payers[index % self.payers.len()] + }; + let mut signers: Vec<&Keypair> = vec![payer]; + signers.extend(additional_signers); + Transaction::new_signed_with_payer( + ixs, + Some(&payer.pubkey()), + &signers, + self.ledger.latest_blockhash(), + ) + } + /// Submits a transaction for execution and waits for its result. #[instrument(skip(self, txn))] pub async fn execute_transaction( From 123278eca2303899d87eea66338398c4a664793a Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 30 Jan 2026 19:15:18 +0400 Subject: [PATCH 06/15] fix: remove test-integration dep --- test-integration/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 890f5684c..823294242 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -104,7 +104,6 @@ test-kit = { path = "../test-kit" } tokio = "1.0" toml = "0.8.13" tracing = "0.1" -test-ephemeral-accounts = { path = "test-ephemeral-accounts" } ureq = "2.9.6" url = "2.5.0" From 3a4cbe85e04ef6f95589d66b4565821a23f9eded Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 30 Jan 2026 22:07:41 +0400 Subject: [PATCH 07/15] fix: allow undelegating accounts to be writeable --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b4091d4b..383ea07fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7095,7 +7095,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85#e676441d85f91cc4a3f55029aaf12bc060bbd00f" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=517a597#517a5975f527e717bf490f52a75a5da1c19e6fe0" dependencies = [ "ahash 0.8.12", "log", diff --git a/Cargo.toml b/Cargo.toml index 8acf190ae..5bf54229a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,7 +217,7 @@ spl-token-2022 = "7.0" [workspace.dependencies.solana-svm] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "e676441d85" +rev = "517a597" features = ["dev-context-only-utils"] [workspace.dependencies.rocksdb] @@ -231,7 +231,7 @@ version = "0.22.0" # and we use protobuf-src v2.1.1. Otherwise compilation fails solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-storage-proto = { path = "./storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "e676441d85" } +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "517a597" } # Fork is used to enable `disable_manual_compaction` usage # Fork is based on commit d4e9e16 of rocksdb (parent commit of 0.23.0 release) # without patching update isn't possible due to conflict with solana deps From d5234e0be1115b5eb3c0dc2a9a42949a477a07be Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 30 Jan 2026 22:42:57 +0400 Subject: [PATCH 08/15] fix: update svm --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 383ea07fc..641567fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7095,7 +7095,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=517a597#517a5975f527e717bf490f52a75a5da1c19e6fe0" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bdbaac86#bdbaac86043180fd946cd2fb9a88cf23dea47f97" dependencies = [ "ahash 0.8.12", "log", diff --git a/Cargo.toml b/Cargo.toml index 5bf54229a..b316a4ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,7 +217,7 @@ spl-token-2022 = "7.0" [workspace.dependencies.solana-svm] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "517a597" +rev = "bdbaac86" features = ["dev-context-only-utils"] [workspace.dependencies.rocksdb] @@ -231,7 +231,7 @@ version = "0.22.0" # and we use protobuf-src v2.1.1. Otherwise compilation fails solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "6eae52b" } solana-storage-proto = { path = "./storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "517a597" } +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "bdbaac86" } # Fork is used to enable `disable_manual_compaction` usage # Fork is based on commit d4e9e16 of rocksdb (parent commit of 0.23.0 release) # without patching update isn't possible due to conflict with solana deps From de11a54e2a76840562c4b7e5b3abeefff6959699 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Mon, 2 Feb 2026 18:22:32 +0800 Subject: [PATCH 09/15] fix: correct instruction ordering --- .../src/instruction.rs | 48 +++++++++--------- programs/elfs/guinea.so | Bin 158832 -> 158856 bytes test-integration/Cargo.lock | 4 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 268abacc9..73e86aa25 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -124,6 +124,30 @@ pub enum MagicBlockInstruction { /// - **0.** `[SIGNER]` Validator authority EnableExecutableCheck, + /// Noop instruction + Noop(u64), + + /// Schedules execution of a *bundle* of intents in a single instruction. + /// + /// A "intent bundle" is an atomic unit of work executed by the validator on the Base layer, + /// such as: + /// - standalone base actions + /// - an optional `Commit` + /// - an optional `CommitAndUndelegate` + /// + /// This is the recommended scheduling path when the caller wants to submit multiple + /// independent intents while paying account overhead only once. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Payer requesting the bundle to be scheduled + /// - **1.** `[WRITE]` Magic Context account + /// - **2..n** `[]` All accounts referenced by any intent in the bundle + /// + /// # Data + /// The embedded [`MagicIntentBundleArgs`] encodes account references by indices into the + /// accounts array. + ScheduleIntentBundle(MagicIntentBundleArgs), + /// Creates a new ephemeral account with rent paid by a sponsor. /// The account is automatically owned by the calling program (CPI caller). /// @@ -154,30 +178,6 @@ pub enum MagicBlockInstruction { /// - **1.** `[WRITE]` Ephemeral account to close /// - **2.** `[WRITE]` Vault account (source of rent refund) CloseEphemeralAccount, - - /// Noop instruction - Noop(u64), - - /// Schedules execution of a *bundle* of intents in a single instruction. - /// - /// A "intent bundle" is an atomic unit of work executed by the validator on the Base layer, - /// such as: - /// - standalone base actions - /// - an optional `Commit` - /// - an optional `CommitAndUndelegate` - /// - /// This is the recommended scheduling path when the caller wants to submit multiple - /// independent intents while paying account overhead only once. - /// - /// # Account references - /// - **0.** `[WRITE, SIGNER]` Payer requesting the bundle to be scheduled - /// - **1.** `[WRITE]` Magic Context account - /// - **2..n** `[]` All accounts referenced by any intent in the bundle - /// - /// # Data - /// The embedded [`MagicIntentBundleArgs`] encodes account references by indices into the - /// accounts array. - ScheduleIntentBundle(MagicIntentBundleArgs), } impl MagicBlockInstruction { diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index 2702f2e35e8426712dfc47174fbdd2142f27ac9c..cea822f0844d6ccbb2fa162165829571e5d8c16c 100755 GIT binary patch delta 17683 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 delta 18485 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 L^VJaEaOHmiz`U{I diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 8310fd199..a1edff65e 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3712,7 +3712,7 @@ dependencies = [ "solana-pubkey", "solana-rent-collector", "solana-sdk-ids", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bdbaac86)", "solana-svm-transaction", "solana-system-program", "solana-transaction", @@ -8658,7 +8658,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=e676441d85#e676441d85f91cc4a3f55029aaf12bc060bbd00f" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=bdbaac86#bdbaac86043180fd946cd2fb9a88cf23dea47f97" dependencies = [ "ahash 0.8.12", "log", From 96e1cabd8a76a302a14d2d6c5c275d7bbae8149d Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Tue, 3 Feb 2026 15:47:55 +0800 Subject: [PATCH 10/15] fix: remove processor module, cleanup --- .../magicblock/src/ephemeral_accounts/mod.rs | 17 ++++++++++++++++- .../src/ephemeral_accounts/process_close.rs | 2 +- .../src/ephemeral_accounts/process_create.rs | 13 ++++++------- .../src/ephemeral_accounts/process_resize.rs | 14 +++++++------- .../src/ephemeral_accounts/processor.rs | 18 ------------------ .../src/ephemeral_accounts/validation.rs | 10 +--------- 6 files changed, 31 insertions(+), 43 deletions(-) delete mode 100644 programs/magicblock/src/ephemeral_accounts/processor.rs diff --git a/programs/magicblock/src/ephemeral_accounts/mod.rs b/programs/magicblock/src/ephemeral_accounts/mod.rs index e93fc4cb6..2817a7768 100644 --- a/programs/magicblock/src/ephemeral_accounts/mod.rs +++ b/programs/magicblock/src/ephemeral_accounts/mod.rs @@ -6,9 +6,24 @@ mod process_close; mod process_create; mod process_resize; -mod processor; mod validation; +use magicblock_magic_program_api::EPHEMERAL_RENT_PER_BYTE; pub(crate) use process_close::process_close_ephemeral_account; pub(crate) use process_create::process_create_ephemeral_account; pub(crate) use process_resize::process_resize_ephemeral_account; +use solana_account::AccountSharedData; +use solana_instruction::error::InstructionError; + +/// Maximum allowed data length for ephemeral accounts (10 MB, matching Solana's limit) +pub(crate) const MAX_DATA_LEN: u32 = 10 * 1024 * 1024; + +/// Calculates rent for an ephemeral account based on its data length. +pub(crate) fn rent_for(data_len: u32) -> Result { + let total_size = u64::from(data_len) + .checked_add(AccountSharedData::ACCOUNT_STATIC_SIZE as u64) + .ok_or(InstructionError::ArithmeticOverflow)?; + total_size + .checked_mul(EPHEMERAL_RENT_PER_BYTE) + .ok_or(InstructionError::ArithmeticOverflow) +} diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index 25c2163df..7088db6bb 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -8,7 +8,7 @@ use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use super::{ - processor::rent_for, + rent_for, validation::{ get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, validate_sponsor, validate_vault, diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index 29c1ee329..53dc56258 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -7,14 +7,13 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; -use super::{ - processor::{rent_for, MAX_DATA_LEN}, - validation::{ - get_caller_program_id, validate_cpi_only, validate_sponsor, - validate_vault, - }, +use super::validation::{ + get_caller_program_id, validate_cpi_only, validate_sponsor, validate_vault, +}; +use crate::{ + ephemeral_accounts::{rent_for, MAX_DATA_LEN}, + utils::accounts, }; -use crate::utils::accounts; /// Creates a new ephemeral account with rent paid by the sponsor. /// The account is owned by the calling program (inferred from CPI context). diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs index bcad104bf..f58804fa7 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_resize.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -6,14 +6,14 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::{ - processor::{rent_for, MAX_DATA_LEN}, - validation::{ - get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, - validate_sponsor, validate_vault, - }, +use super::validation::{ + get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, + validate_sponsor, validate_vault, +}; +use crate::{ + ephemeral_accounts::{rent_for, MAX_DATA_LEN}, + utils::accounts, }; -use crate::utils::accounts; /// Resizes an existing ephemeral account, adjusting rent accordingly. pub(crate) fn process_resize_ephemeral_account( diff --git a/programs/magicblock/src/ephemeral_accounts/processor.rs b/programs/magicblock/src/ephemeral_accounts/processor.rs deleted file mode 100644 index 5d7824da4..000000000 --- a/programs/magicblock/src/ephemeral_accounts/processor.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Core processor utilities for ephemeral account instructions - -use magicblock_magic_program_api::EPHEMERAL_RENT_PER_BYTE; -use solana_account::AccountSharedData; -use solana_instruction::error::InstructionError; - -/// Maximum allowed data length for ephemeral accounts (10 MB, matching Solana's limit) -pub(crate) const MAX_DATA_LEN: u32 = 10 * 1024 * 1024; - -/// Calculates rent for an ephemeral account based on its data length. -pub(crate) fn rent_for(data_len: u32) -> Result { - let total_size = u64::from(data_len) - .checked_add(AccountSharedData::ACCOUNT_STATIC_SIZE as u64) - .ok_or(InstructionError::ArithmeticOverflow)?; - total_size - .checked_mul(EPHEMERAL_RENT_PER_BYTE) - .ok_or(InstructionError::ArithmeticOverflow) -} diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index 7d7c037c5..d03c183d3 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -46,15 +46,7 @@ pub(crate) fn validate_sponsor( let ix_ctx = transaction_context.get_current_instruction_context()?; if !ix_ctx.is_instruction_account_signer(0)? { - // Not a signer - must be a PDA owned by the calling program - let caller_program_id = get_caller_program_id(transaction_context)?; - let sponsor_owner = accounts::get_instruction_account_owner_with_idx( - transaction_context, - 0, - )?; - if sponsor_owner != caller_program_id { - return Err(InstructionError::InvalidAccountOwner); - } + return Err(InstructionError::MissingRequiredSignature); } Ok(()) } From 90bfcd8e36abc537829a5ee2e7248a5344784549 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 4 Feb 2026 15:14:17 +0800 Subject: [PATCH 11/15] fix: don't overwrite ephemeral accounts --- magicblock-accounts-db/src/lib.rs | 5 ++++ .../src/executor/processing.rs | 29 ++++++++++--------- .../tests/ephemeral_accounts.rs | 2 +- .../src/ephemeral_accounts/validation.rs | 4 +-- .../process_mutate_accounts.rs | 7 ++++- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/magicblock-accounts-db/src/lib.rs b/magicblock-accounts-db/src/lib.rs index 34f2ec232..01af7e9a3 100644 --- a/magicblock-accounts-db/src/lib.rs +++ b/magicblock-accounts-db/src/lib.rs @@ -169,6 +169,11 @@ impl AccountsDb { } }; } + // The ephemeral account has been closed, remove it from DB + if account.ephemeral() && account.data().is_empty() { + self.index.remove(pubkey, txn!())?; + return Ok(()); + } match account { AccountSharedData::Borrowed(acc) => { if acc.owner_changed() { diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 2ba085764..e8a753e71 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -383,20 +383,21 @@ impl super::TransactionExecutor { // 2. Confined Account Integrity Check // Confined accounts must not have their lamport balance changed. for (pubkey, acc) in &txn.accounts { - if acc.confined() { - let lamports_changed = acc - .as_borrowed() - .map(|a| a.lamports_changed()) - .unwrap_or(true); - - if lamports_changed { - executed.execution_details.status = - Err(TransactionError::UnbalancedTransaction); - logs.push(format!( - "Confined account {pubkey} has been illegally modified" - )); - break; - } + if !acc.confined() { + continue; + } + let lamports_changed = acc + .as_borrowed() + .map(|a| a.lamports_changed()) + .unwrap_or(true); + + if lamports_changed { + executed.execution_details.status = + Err(TransactionError::UnbalancedTransaction); + logs.push(format!( + "Confined account {pubkey} has been illegally modified" + )); + break; } } } diff --git a/magicblock-processor/tests/ephemeral_accounts.rs b/magicblock-processor/tests/ephemeral_accounts.rs index fd37a3bea..a75fc1d02 100644 --- a/magicblock-processor/tests/ephemeral_accounts.rs +++ b/magicblock-processor/tests/ephemeral_accounts.rs @@ -97,7 +97,7 @@ fn create_ephemeral_account_ix( vec![ AccountMeta::new_readonly(magic_program, false), AccountMeta::new(sponsor, true), - AccountMeta::new(ephemeral, true), // Must be signer to prevent squatting + AccountMeta::new(ephemeral, true), AccountMeta::new(vault, false), ], ) diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index d03c183d3..69e11f17e 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -37,9 +37,7 @@ pub(crate) fn validate_cpi_only( Ok(()) } -/// Validates the sponsor account (index 0): -/// - Oncurve accounts must be a signer -/// - PDA accounts must be owned by the calling program +/// Validates the sponsor account (index 0) is a signer pub(crate) fn validate_sponsor( transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 475a77f9e..4f91d4ebc 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -107,6 +107,11 @@ pub(crate) fn process_mutate_accounts( .get_index_of_instruction_account_in_transaction(account_idx)?; let account = transaction_context .get_account_at_index(account_transaction_index)?; + // we do not allow for account modification if the + // account is ephemeral (i.e. exists locally on ER) + if account.borrow().ephemeral() { + continue; + } let account_key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; @@ -305,7 +310,7 @@ pub(crate) fn process_mutate_accounts( // Now it is super unlikely for the transaction to fail since all checks passed. // The only option would be if another instruction runs after it which at this point - // is impossible since we create/send them from insider our validator. + // is impossible since we create/send them from inside of our validator. // Thus we can persist the applied data mods to make them available for ledger replay. for resolved_data in memory_data_mods { resolved_data From f800bba1578b95d269d25c3fd63e117b8a4d2ea0 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sat, 7 Feb 2026 12:55:52 +0800 Subject: [PATCH 12/15] fix: post review cleanup --- magicblock-accounts-db/src/lib.rs | 2 +- .../tests/ephemeral_accounts.rs | 37 +++---- .../magicblock/src/ephemeral_accounts/mod.rs | 54 ++++++++- .../src/ephemeral_accounts/process_close.rs | 38 +------ .../src/ephemeral_accounts/process_create.rs | 41 ++----- .../src/ephemeral_accounts/process_resize.rs | 51 +-------- .../src/ephemeral_accounts/validation.rs | 103 ++++++++++++------ .../process_mutate_accounts.rs | 9 +- .../src/utils/instruction_context_frames.rs | 3 +- test-kit/src/lib.rs | 10 ++ 10 files changed, 171 insertions(+), 177 deletions(-) diff --git a/magicblock-accounts-db/src/lib.rs b/magicblock-accounts-db/src/lib.rs index 01af7e9a3..b1437a24a 100644 --- a/magicblock-accounts-db/src/lib.rs +++ b/magicblock-accounts-db/src/lib.rs @@ -170,7 +170,7 @@ impl AccountsDb { }; } // The ephemeral account has been closed, remove it from DB - if account.ephemeral() && account.data().is_empty() { + if account.ephemeral() && account.owner() == &Pubkey::default() { self.index.remove(pubkey, txn!())?; return Ok(()); } diff --git a/magicblock-processor/tests/ephemeral_accounts.rs b/magicblock-processor/tests/ephemeral_accounts.rs index a75fc1d02..b072ca8f5 100644 --- a/magicblock-processor/tests/ephemeral_accounts.rs +++ b/magicblock-processor/tests/ephemeral_accounts.rs @@ -404,31 +404,23 @@ async fn test_close_ephemeral_account_via_cpi() { // Check balances AFTER close let sponsor_after_close = env.get_account(sponsor); let vault_after_close = env.get_account(EPHEMERAL_VAULT_PUBKEY); - let ephemeral_after_close = env.get_account(ephemeral.pubkey()); - let total_after_close = sponsor_after_close.lamports() - + vault_after_close.lamports() - + ephemeral_after_close.lamports(); + + // Closed ephemeral accounts are removed from the DB + assert!( + env.try_get_account(ephemeral.pubkey()).is_none(), + "Closed ephemeral account should be removed from DB" + ); + + let total_after_close = + sponsor_after_close.lamports() + vault_after_close.lamports(); println!("=== AFTER CLOSE ==="); println!("Sponsor: {}", sponsor_after_close.lamports()); println!("Vault: {}", vault_after_close.lamports()); - println!("Ephemeral: {}", ephemeral_after_close.lamports()); println!("Total: {}", total_after_close); - // Verify the account was closed (owned by system, data cleared) - // Note: ephemeral and delegated flags are NOT reset on close - assert_eq!( - *ephemeral_after_close.owner(), - solana_sdk_ids::system_program::id(), - "Owner should be system program" - ); - assert_eq!( - ephemeral_after_close.data().len(), - 0, - "Data should be empty" - ); - // CONSERVATION CHECK: Total lamports should not change + // (ephemeral had 0 lamports, so sponsor + vault should equal prior total) assert_eq!( total_after_close, total_before_close, "Total lamports should be conserved" @@ -1072,12 +1064,11 @@ async fn test_full_lifecycle() { let txn = env.build_transaction(&[close_ix]); assert!(env.execute_transaction(txn).await.is_ok()); - let ephemeral_after = env.get_account(ephemeral.pubkey()); - assert_eq!( - *ephemeral_after.owner(), - solana_sdk_ids::system_program::id() + // Closed ephemeral accounts are removed from the DB + assert!( + env.try_get_account(ephemeral.pubkey()).is_none(), + "Closed ephemeral account should be removed from DB" ); - assert_eq!(ephemeral_after.data().len(), 0); } #[tokio::test] diff --git a/programs/magicblock/src/ephemeral_accounts/mod.rs b/programs/magicblock/src/ephemeral_accounts/mod.rs index 2817a7768..ff6151b02 100644 --- a/programs/magicblock/src/ephemeral_accounts/mod.rs +++ b/programs/magicblock/src/ephemeral_accounts/mod.rs @@ -8,18 +8,34 @@ mod process_create; mod process_resize; mod validation; +use std::cell::RefCell; + use magicblock_magic_program_api::EPHEMERAL_RENT_PER_BYTE; pub(crate) use process_close::process_close_ephemeral_account; pub(crate) use process_create::process_create_ephemeral_account; pub(crate) use process_resize::process_resize_ephemeral_account; -use solana_account::AccountSharedData; +use solana_account::{AccountSharedData, ReadableAccount}; use solana_instruction::error::InstructionError; +use solana_transaction_context::TransactionContext; + +use crate::utils::accounts; + +// ----- Account indices shared by validation and processors ----- + +/// Instruction account index for the sponsor (rent payer). +const SPONSOR_IDX: u16 = 0; +/// Instruction account index for the ephemeral account. +const EPHEMERAL_IDX: u16 = 1; +/// Instruction account index for the vault. +const VAULT_IDX: u16 = 2; + +// ----- Shared helpers ----- /// Maximum allowed data length for ephemeral accounts (10 MB, matching Solana's limit) -pub(crate) const MAX_DATA_LEN: u32 = 10 * 1024 * 1024; +const MAX_DATA_LEN: u32 = 10 * 1024 * 1024; /// Calculates rent for an ephemeral account based on its data length. -pub(crate) fn rent_for(data_len: u32) -> Result { +fn rent_for(data_len: u32) -> Result { let total_size = u64::from(data_len) .checked_add(AccountSharedData::ACCOUNT_STATIC_SIZE as u64) .ok_or(InstructionError::ArithmeticOverflow)?; @@ -27,3 +43,35 @@ pub(crate) fn rent_for(data_len: u32) -> Result { .checked_mul(EPHEMERAL_RENT_PER_BYTE) .ok_or(InstructionError::ArithmeticOverflow) } + +/// Returns the data length of an ephemeral account as a `u32`. +fn get_ephemeral_data_len( + ephemeral: &RefCell, +) -> Result { + ephemeral + .borrow() + .data() + .len() + .try_into() + .map_err(|_| InstructionError::ArithmeticOverflow) +} + +/// Transfers rent between sponsor and vault. +/// +/// Positive `amount` moves lamports from sponsor to vault (creation / growth). +/// Negative `amount` moves lamports from vault to sponsor (close / shrink). +fn transfer_rent( + tc: &TransactionContext, + amount: i64, +) -> Result<(), InstructionError> { + if amount > 0 { + let abs = amount as u64; + accounts::debit_instruction_account_at_index(tc, SPONSOR_IDX, abs)?; + accounts::credit_instruction_account_at_index(tc, VAULT_IDX, abs)?; + } else { + let abs = amount.unsigned_abs(); + accounts::credit_instruction_account_at_index(tc, SPONSOR_IDX, abs)?; + accounts::debit_instruction_account_at_index(tc, VAULT_IDX, abs)?; + } + Ok(()) +} diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index 7088db6bb..9fc6121bb 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -1,53 +1,27 @@ //! Close ephemeral account instruction processor -use solana_account::{ReadableAccount, WritableAccount}; +use solana_account::WritableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; -use super::{ - rent_for, - validation::{ - get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, - validate_sponsor, validate_vault, - }, -}; -use crate::utils::accounts; +use super::validation::{validate_common, validate_existing_ephemeral}; +use super::{get_ephemeral_data_len, rent_for, transfer_rent}; /// Closes an ephemeral account, refunding rent to the sponsor. pub(crate) fn process_close_ephemeral_account( invoke_context: &InvokeContext, transaction_context: &TransactionContext, ) -> Result<(), InstructionError> { - validate_cpi_only(transaction_context)?; - validate_sponsor(transaction_context)?; - validate_vault(transaction_context)?; - - let caller_program_id = get_caller_program_id(transaction_context)?; + let caller_program_id = validate_common(transaction_context)?; let ephemeral = validate_existing_ephemeral(transaction_context, &caller_program_id)?; - let data_len: u32 = ephemeral - .borrow() - .data() - .len() - .try_into() - .map_err(|_| InstructionError::ArithmeticOverflow)?; - - // Transfer rent from vault back to sponsor + let data_len = get_ephemeral_data_len(ephemeral)?; let refund = rent_for(data_len)?; - accounts::credit_instruction_account_at_index( - transaction_context, - 0, - refund, - )?; - accounts::debit_instruction_account_at_index( - transaction_context, - 2, - refund, - )?; + transfer_rent(transaction_context, -(refund as i64))?; // Reset account to empty state let mut acc = ephemeral.borrow_mut(); diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index 53dc56258..5a9fede14 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -1,19 +1,15 @@ //! Create ephemeral account instruction processor -use solana_account::{ReadableAccount, WritableAccount}; +use solana_account::WritableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; -use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use super::validation::{ - get_caller_program_id, validate_cpi_only, validate_sponsor, validate_vault, -}; -use crate::{ - ephemeral_accounts::{rent_for, MAX_DATA_LEN}, - utils::accounts, + validate_common, validate_ephemeral_signer, validate_new_ephemeral, }; +use super::{rent_for, transfer_rent, MAX_DATA_LEN}; /// Creates a new ephemeral account with rent paid by the sponsor. /// The account is owned by the calling program (inferred from CPI context). @@ -26,35 +22,12 @@ pub(crate) fn process_create_ephemeral_account( return Err(InstructionError::InvalidArgument); } - validate_cpi_only(transaction_context)?; - validate_sponsor(transaction_context)?; - validate_vault(transaction_context)?; - - // Ephemeral account must be a signer (prevents pubkey squatting) - let ix_ctx = transaction_context.get_current_instruction_context()?; - if !ix_ctx.is_instruction_account_signer(1)? { - return Err(InstructionError::MissingRequiredSignature); - } - - let caller_program_id = get_caller_program_id(transaction_context)?; + let caller_program_id = validate_common(transaction_context)?; + validate_ephemeral_signer(transaction_context)?; + let ephemeral = validate_new_ephemeral(transaction_context)?; - // Target account must be empty (0 lamports, owned by system program) - let ephemeral = - accounts::get_instruction_account_with_idx(transaction_context, 1)?; - let acc = ephemeral.borrow(); - if acc.lamports() != 0 || *acc.owner() != system_program::ID { - return Err(InstructionError::InvalidAccountData); - } - drop(acc); - - // Transfer rent from sponsor to vault let rent = rent_for(data_len)?; - accounts::debit_instruction_account_at_index(transaction_context, 0, rent)?; - accounts::credit_instruction_account_at_index( - transaction_context, - 2, - rent, - )?; + transfer_rent(transaction_context, rent as i64)?; // Initialize ephemeral account let mut acc = ephemeral.borrow_mut(); diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs index f58804fa7..a59378bbd 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_resize.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -1,19 +1,12 @@ //! Resize ephemeral account instruction processor -use solana_account::ReadableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::validation::{ - get_caller_program_id, validate_cpi_only, validate_existing_ephemeral, - validate_sponsor, validate_vault, -}; -use crate::{ - ephemeral_accounts::{rent_for, MAX_DATA_LEN}, - utils::accounts, -}; +use super::validation::{validate_common, validate_existing_ephemeral}; +use super::{get_ephemeral_data_len, rent_for, transfer_rent, MAX_DATA_LEN}; /// Resizes an existing ephemeral account, adjusting rent accordingly. pub(crate) fn process_resize_ephemeral_account( @@ -25,50 +18,16 @@ pub(crate) fn process_resize_ephemeral_account( return Err(InstructionError::InvalidArgument); } - validate_cpi_only(transaction_context)?; - validate_sponsor(transaction_context)?; - validate_vault(transaction_context)?; - - let caller_program_id = get_caller_program_id(transaction_context)?; + let caller_program_id = validate_common(transaction_context)?; let ephemeral = validate_existing_ephemeral(transaction_context, &caller_program_id)?; - let old_len: u32 = ephemeral - .borrow() - .data() - .len() - .try_into() - .map_err(|_| InstructionError::ArithmeticOverflow)?; - + let old_len = get_ephemeral_data_len(ephemeral)?; let new_rent = rent_for(new_data_len)?; let old_rent = rent_for(old_len)?; let delta = new_rent as i64 - old_rent as i64; - if delta > 0 { - // Growing: debit sponsor, credit vault - accounts::debit_instruction_account_at_index( - transaction_context, - 0, - delta as u64, - )?; - accounts::credit_instruction_account_at_index( - transaction_context, - 2, - delta as u64, - )?; - } else { - // Shrinking: credit sponsor, debit vault - accounts::credit_instruction_account_at_index( - transaction_context, - 0, - delta.unsigned_abs(), - )?; - accounts::debit_instruction_account_at_index( - transaction_context, - 2, - delta.unsigned_abs(), - )?; - } + transfer_rent(transaction_context, delta)?; ephemeral.borrow_mut().resize(new_data_len as usize, 0); diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index 69e11f17e..44097ea44 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -1,4 +1,4 @@ -//! Validation utilities for ephemeral account instructions +//! Validation helpers for ephemeral account instructions. use std::cell::RefCell; @@ -6,72 +6,103 @@ use magicblock_magic_program_api::EPHEMERAL_VAULT_PUBKEY; use solana_account::{AccountSharedData, ReadableAccount}; use solana_instruction::error::InstructionError; use solana_pubkey::Pubkey; +use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; +use super::{EPHEMERAL_IDX, SPONSOR_IDX, VAULT_IDX}; use crate::utils::{ accounts, instruction_context_frames::InstructionContextFrames, }; -/// Returns the caller program ID (the program that invoked us via CPI). -pub(crate) fn get_caller_program_id( - transaction_context: &TransactionContext, +/// Returns the program ID of the CPI caller. +/// +/// Fails with [`InstructionError::IncorrectProgramId`] when invoked +/// outside of CPI, so this implicitly rejects direct top-level calls. +fn get_caller_program_id( + tc: &TransactionContext, ) -> Result { - let frames = InstructionContextFrames::try_from(transaction_context)?; + let frames = InstructionContextFrames::try_from(tc)?; frames .find_program_id_of_parent_of_current_instruction() .copied() .ok_or(InstructionError::IncorrectProgramId) } -/// Validates that the instruction is called via CPI (not directly from a transaction). -pub(crate) fn validate_cpi_only( - transaction_context: &TransactionContext, -) -> Result<(), InstructionError> { - let frames = InstructionContextFrames::try_from(transaction_context)?; - if frames - .find_program_id_of_parent_of_current_instruction() - .is_none() - { - return Err(InstructionError::IncorrectProgramId); - } - Ok(()) -} - -/// Validates the sponsor account (index 0) is a signer -pub(crate) fn validate_sponsor( - transaction_context: &TransactionContext, +/// Validates that the sponsor account is a signer. +/// PDAs may satisfy this via `invoke_signed`. +fn validate_sponsor( + tc: &TransactionContext, ) -> Result<(), InstructionError> { - let ix_ctx = transaction_context.get_current_instruction_context()?; - - if !ix_ctx.is_instruction_account_signer(0)? { + let ix_ctx = tc.get_current_instruction_context()?; + if !ix_ctx.is_instruction_account_signer(SPONSOR_IDX)? { return Err(InstructionError::MissingRequiredSignature); } Ok(()) } -/// Validates the vault account (index 2) matches the expected pubkey. -pub(crate) fn validate_vault( - transaction_context: &TransactionContext, +/// Validates the vault account matches the expected pubkey. +fn validate_vault( + tc: &TransactionContext, ) -> Result<(), InstructionError> { let vault_pubkey = - accounts::get_instruction_pubkey_with_idx(transaction_context, 2)?; + accounts::get_instruction_pubkey_with_idx(tc, VAULT_IDX)?; if *vault_pubkey != EPHEMERAL_VAULT_PUBKEY { return Err(InstructionError::InvalidAccountOwner); } Ok(()) } -/// Validates an existing ephemeral account (index 1): -/// - Must be marked as ephemeral -/// - Must be owned by the caller program +// ----- Public helpers consumed by the three processors ----- + +/// Common validation sequence shared by all ephemeral account instructions. /// -/// Returns the account for further operations. -pub(crate) fn validate_existing_ephemeral<'a>( - transaction_context: &'a TransactionContext, +/// Checks CPI context, sponsor signature, and vault identity. +/// Returns the caller program ID on success. +pub(super) fn validate_common( + tc: &TransactionContext, +) -> Result { + let caller_program_id = get_caller_program_id(tc)?; + validate_sponsor(tc)?; + validate_vault(tc)?; + Ok(caller_program_id) +} + +/// Validates that the ephemeral account is a signer (prevents pubkey squatting). +/// Only required for account creation. +pub(super) fn validate_ephemeral_signer( + tc: &TransactionContext, +) -> Result<(), InstructionError> { + let ix_ctx = tc.get_current_instruction_context()?; + if !ix_ctx.is_instruction_account_signer(EPHEMERAL_IDX)? { + return Err(InstructionError::MissingRequiredSignature); + } + Ok(()) +} + +/// Validates that the account at [`EPHEMERAL_IDX`] is an empty system-owned +/// account (0 lamports, system program owner). Returns the account for +/// initialization. +pub(super) fn validate_new_ephemeral( + tc: &TransactionContext, +) -> Result<&RefCell, InstructionError> { + let ephemeral = + accounts::get_instruction_account_with_idx(tc, EPHEMERAL_IDX)?; + let acc = ephemeral.borrow(); + if acc.lamports() != 0 || *acc.owner() != system_program::ID { + return Err(InstructionError::InvalidAccountData); + } + drop(acc); + Ok(ephemeral) +} + +/// Validates an existing ephemeral account is marked ephemeral and owned by +/// the caller program. Returns the account for further operations. +pub(super) fn validate_existing_ephemeral<'a>( + tc: &'a TransactionContext, caller_program_id: &Pubkey, ) -> Result<&'a RefCell, InstructionError> { let ephemeral = - accounts::get_instruction_account_with_idx(transaction_context, 1)?; + accounts::get_instruction_account_with_idx(tc, EPHEMERAL_IDX)?; let acc = ephemeral.borrow(); if !acc.ephemeral() { diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 4f91d4ebc..9b7deec93 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -38,7 +38,7 @@ pub(crate) fn process_mutate_accounts( ic_msg!( invoke_context, "Validator identity '{}' not in signers", - &validator_authority_id.to_string() + validator_authority_id ); return Err(InstructionError::MissingRequiredSignature); } @@ -110,6 +110,13 @@ pub(crate) fn process_mutate_accounts( // we do not allow for account modification if the // account is ephemeral (i.e. exists locally on ER) if account.borrow().ephemeral() { + let key = transaction_context + .get_key_of_account_at_index(account_transaction_index)?; + ic_msg!( + invoke_context, + "MutateAccounts: skipping ephemeral account {}", + key + ); continue; } let account_key = transaction_context diff --git a/programs/magicblock/src/utils/instruction_context_frames.rs b/programs/magicblock/src/utils/instruction_context_frames.rs index 8196abd03..d405a0924 100644 --- a/programs/magicblock/src/utils/instruction_context_frames.rs +++ b/programs/magicblock/src/utils/instruction_context_frames.rs @@ -143,7 +143,8 @@ impl<'a> TryFrom<&'a TransactionContext> for InstructionContextFrames<'a> { frames.push(frame); } - let current_frame_idx = current_frame_idx.expect("current frame not found in frames which is invalid validator behavior"); + let current_frame_idx = current_frame_idx + .ok_or(InstructionError::InvalidAccountData)?; Ok(InstructionContextFrames::new(frames, current_frame_idx)) } } diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index bcd42278c..85977a954 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -342,6 +342,16 @@ impl ExecutionTestEnv { } } + pub fn try_get_account(&self, pubkey: Pubkey) -> Option> { + self.accountsdb.get_account(&pubkey).map(|account| { + CommitableAccount { + pubkey, + account, + db: &self.accountsdb, + } + }) + } + pub fn get_payer(&self) -> CommitableAccount<'_> { let payer = { let index = self.payer_index.load(Ordering::Relaxed); From e6c37f746fa484f79ddcbc70bdc8f3b241c4814f Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sat, 7 Feb 2026 12:59:03 +0800 Subject: [PATCH 13/15] fix: fmt --- .../src/ephemeral_accounts/process_close.rs | 6 ++++-- .../src/ephemeral_accounts/process_create.rs | 9 ++++++--- .../src/ephemeral_accounts/process_resize.rs | 7 +++++-- .../magicblock/src/ephemeral_accounts/validation.rs | 8 ++------ .../src/utils/instruction_context_frames.rs | 4 ++-- test-kit/src/lib.rs | 13 ++++++++----- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/programs/magicblock/src/ephemeral_accounts/process_close.rs b/programs/magicblock/src/ephemeral_accounts/process_close.rs index 9fc6121bb..1cab8e3e6 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_close.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_close.rs @@ -7,8 +7,10 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; -use super::validation::{validate_common, validate_existing_ephemeral}; -use super::{get_ephemeral_data_len, rent_for, transfer_rent}; +use super::{ + get_ephemeral_data_len, rent_for, transfer_rent, + validation::{validate_common, validate_existing_ephemeral}, +}; /// Closes an ephemeral account, refunding rent to the sponsor. pub(crate) fn process_close_ephemeral_account( diff --git a/programs/magicblock/src/ephemeral_accounts/process_create.rs b/programs/magicblock/src/ephemeral_accounts/process_create.rs index 5a9fede14..e6958296d 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_create.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_create.rs @@ -6,10 +6,13 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::validation::{ - validate_common, validate_ephemeral_signer, validate_new_ephemeral, +use super::{ + rent_for, transfer_rent, + validation::{ + validate_common, validate_ephemeral_signer, validate_new_ephemeral, + }, + MAX_DATA_LEN, }; -use super::{rent_for, transfer_rent, MAX_DATA_LEN}; /// Creates a new ephemeral account with rent paid by the sponsor. /// The account is owned by the calling program (inferred from CPI context). diff --git a/programs/magicblock/src/ephemeral_accounts/process_resize.rs b/programs/magicblock/src/ephemeral_accounts/process_resize.rs index a59378bbd..9481d8d4b 100644 --- a/programs/magicblock/src/ephemeral_accounts/process_resize.rs +++ b/programs/magicblock/src/ephemeral_accounts/process_resize.rs @@ -5,8 +5,11 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_transaction_context::TransactionContext; -use super::validation::{validate_common, validate_existing_ephemeral}; -use super::{get_ephemeral_data_len, rent_for, transfer_rent, MAX_DATA_LEN}; +use super::{ + get_ephemeral_data_len, rent_for, transfer_rent, + validation::{validate_common, validate_existing_ephemeral}, + MAX_DATA_LEN, +}; /// Resizes an existing ephemeral account, adjusting rent accordingly. pub(crate) fn process_resize_ephemeral_account( diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index 44097ea44..17df8f1a1 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -30,9 +30,7 @@ fn get_caller_program_id( /// Validates that the sponsor account is a signer. /// PDAs may satisfy this via `invoke_signed`. -fn validate_sponsor( - tc: &TransactionContext, -) -> Result<(), InstructionError> { +fn validate_sponsor(tc: &TransactionContext) -> Result<(), InstructionError> { let ix_ctx = tc.get_current_instruction_context()?; if !ix_ctx.is_instruction_account_signer(SPONSOR_IDX)? { return Err(InstructionError::MissingRequiredSignature); @@ -41,9 +39,7 @@ fn validate_sponsor( } /// Validates the vault account matches the expected pubkey. -fn validate_vault( - tc: &TransactionContext, -) -> Result<(), InstructionError> { +fn validate_vault(tc: &TransactionContext) -> Result<(), InstructionError> { let vault_pubkey = accounts::get_instruction_pubkey_with_idx(tc, VAULT_IDX)?; if *vault_pubkey != EPHEMERAL_VAULT_PUBKEY { diff --git a/programs/magicblock/src/utils/instruction_context_frames.rs b/programs/magicblock/src/utils/instruction_context_frames.rs index d405a0924..d6b038dbf 100644 --- a/programs/magicblock/src/utils/instruction_context_frames.rs +++ b/programs/magicblock/src/utils/instruction_context_frames.rs @@ -143,8 +143,8 @@ impl<'a> TryFrom<&'a TransactionContext> for InstructionContextFrames<'a> { frames.push(frame); } - let current_frame_idx = current_frame_idx - .ok_or(InstructionError::InvalidAccountData)?; + let current_frame_idx = + current_frame_idx.ok_or(InstructionError::InvalidAccountData)?; Ok(InstructionContextFrames::new(frames, current_frame_idx)) } } diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index 85977a954..aadf1520c 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -342,14 +342,17 @@ impl ExecutionTestEnv { } } - pub fn try_get_account(&self, pubkey: Pubkey) -> Option> { - self.accountsdb.get_account(&pubkey).map(|account| { - CommitableAccount { + pub fn try_get_account( + &self, + pubkey: Pubkey, + ) -> Option> { + self.accountsdb + .get_account(&pubkey) + .map(|account| CommitableAccount { pubkey, account, db: &self.accountsdb, - } - }) + }) } pub fn get_payer(&self) -> CommitableAccount<'_> { From f42459d8c207c345aebab7adb9df050cad5e16d9 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sat, 7 Feb 2026 13:28:23 +0800 Subject: [PATCH 14/15] fix: consistent error/cleanup --- programs/magicblock/src/ephemeral_accounts/validation.rs | 2 +- .../magicblock/src/mutate_accounts/process_mutate_accounts.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/magicblock/src/ephemeral_accounts/validation.rs b/programs/magicblock/src/ephemeral_accounts/validation.rs index 17df8f1a1..a8d153ebd 100644 --- a/programs/magicblock/src/ephemeral_accounts/validation.rs +++ b/programs/magicblock/src/ephemeral_accounts/validation.rs @@ -43,7 +43,7 @@ fn validate_vault(tc: &TransactionContext) -> Result<(), InstructionError> { let vault_pubkey = accounts::get_instruction_pubkey_with_idx(tc, VAULT_IDX)?; if *vault_pubkey != EPHEMERAL_VAULT_PUBKEY { - return Err(InstructionError::InvalidAccountOwner); + return Err(InstructionError::InvalidAccountData); } Ok(()) } diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 9b7deec93..17f29ccb7 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -112,6 +112,7 @@ pub(crate) fn process_mutate_accounts( if account.borrow().ephemeral() { let key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; + account_mods.remove(key); ic_msg!( invoke_context, "MutateAccounts: skipping ephemeral account {}", From d840bb24d38b7c6ab2327a74c545923c32058c2e Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 18 Feb 2026 12:51:07 +0400 Subject: [PATCH 15/15] chore: bump version to 0.7.0 --- .github/packages/npm-package/package.json | 10 +-- .../packages/npm-package/package.json.tmpl | 2 +- Cargo.lock | 48 +++++++------- Cargo.toml | 2 +- test-integration/Cargo.lock | 66 +++++++++---------- 5 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.github/packages/npm-package/package.json b/.github/packages/npm-package/package.json index eac4b3ef5..cbdb165a5 100644 --- a/.github/packages/npm-package/package.json +++ b/.github/packages/npm-package/package.json @@ -1,6 +1,6 @@ { "name": "@magicblock-labs/ephemeral-validator", - "version": "0.6.2", + "version": "0.7.0", "description": "MagicBlock Ephemeral Validator", "homepage": "https://github.com/magicblock-labs/magicblock-validator#readme", "bugs": { @@ -30,10 +30,10 @@ "typescript": "^4.9.4" }, "optionalDependencies": { - "@magicblock-labs/ephemeral-validator-darwin-arm64": "0.6.2", - "@magicblock-labs/ephemeral-validator-darwin-x64": "0.6.2", - "@magicblock-labs/ephemeral-validator-linux-arm64": "0.6.2", - "@magicblock-labs/ephemeral-validator-linux-x64": "0.6.2", + "@magicblock-labs/ephemeral-validator-darwin-arm64": "0.7.0", + "@magicblock-labs/ephemeral-validator-darwin-x64": "0.7.0", + "@magicblock-labs/ephemeral-validator-linux-arm64": "0.7.0", + "@magicblock-labs/ephemeral-validator-linux-x64": "0.7.0", "@magicblock-labs/vrf-oracle-linux-x64": "0.2.3", "@magicblock-labs/vrf-oracle-linux-arm64": "0.2.3", "@magicblock-labs/vrf-oracle-darwin-x64": "0.2.3", diff --git a/.github/packages/npm-package/package.json.tmpl b/.github/packages/npm-package/package.json.tmpl index 88ea285e3..cee3228d4 100644 --- a/.github/packages/npm-package/package.json.tmpl +++ b/.github/packages/npm-package/package.json.tmpl @@ -1,7 +1,7 @@ { "name": "@magicblock-labs/${node_pkg}", "description": "Ephemeral Validator (${node_pkg})", - "version": "0.6.2", + "version": "0.7.0", "repository": { "type": "git", "url": "git+https://github.com/magicblock-labs/magicblock-validator.git" diff --git a/Cargo.lock b/Cargo.lock index 641567fb0..895be58f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,7 +1836,7 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "guinea" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "magicblock-magic-program-api", @@ -2778,7 +2778,7 @@ dependencies = [ [[package]] name = "magicblock-account-cloner" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "bincode", @@ -2810,7 +2810,7 @@ dependencies = [ [[package]] name = "magicblock-accounts" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "magicblock-account-cloner", @@ -2832,7 +2832,7 @@ dependencies = [ [[package]] name = "magicblock-accounts-db" -version = "0.6.2" +version = "0.7.0" dependencies = [ "lmdb-rkv", "magicblock-config", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "magicblock-aperture" -version = "0.6.2" +version = "0.7.0" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -2902,7 +2902,7 @@ dependencies = [ [[package]] name = "magicblock-api" -version = "0.6.2" +version = "0.7.0" dependencies = [ "anyhow", "borsh 1.6.0", @@ -2957,7 +2957,7 @@ dependencies = [ [[package]] name = "magicblock-chainlink" -version = "0.6.2" +version = "0.7.0" dependencies = [ "arc-swap", "assert_matches", @@ -3013,7 +3013,7 @@ dependencies = [ [[package]] name = "magicblock-committor-program" -version = "0.6.2" +version = "0.7.0" dependencies = [ "borsh 1.6.0", "paste", @@ -3025,7 +3025,7 @@ dependencies = [ [[package]] name = "magicblock-committor-service" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "base64 0.21.7", @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "magicblock-config" -version = "0.6.2" +version = "0.7.0" dependencies = [ "clap 4.5.53", "derive_more", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "magicblock-core" -version = "0.6.2" +version = "0.7.0" dependencies = [ "flume", "magicblock-magic-program-api", @@ -3141,7 +3141,7 @@ dependencies = [ [[package]] name = "magicblock-ledger" -version = "0.6.2" +version = "0.7.0" dependencies = [ "arc-swap", "bincode", @@ -3184,7 +3184,7 @@ dependencies = [ [[package]] name = "magicblock-magic-program-api" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "serde", @@ -3193,7 +3193,7 @@ dependencies = [ [[package]] name = "magicblock-metrics" -version = "0.6.2" +version = "0.7.0" dependencies = [ "http-body-util", "hyper 1.8.1", @@ -3207,7 +3207,7 @@ dependencies = [ [[package]] name = "magicblock-processor" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "guinea", @@ -3249,7 +3249,7 @@ dependencies = [ [[package]] name = "magicblock-program" -version = "0.6.2" +version = "0.7.0" dependencies = [ "assert_matches", "bincode", @@ -3285,7 +3285,7 @@ dependencies = [ [[package]] name = "magicblock-rpc-client" -version = "0.6.2" +version = "0.7.0" dependencies = [ "solana-account", "solana-account-decoder-client-types", @@ -3307,7 +3307,7 @@ dependencies = [ [[package]] name = "magicblock-table-mania" -version = "0.6.2" +version = "0.7.0" dependencies = [ "ed25519-dalek", "magicblock-metrics", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "magicblock-task-scheduler" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "chrono", @@ -3359,7 +3359,7 @@ dependencies = [ [[package]] name = "magicblock-validator" -version = "0.6.2" +version = "0.7.0" dependencies = [ "console-subscriber", "magicblock-api", @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "magicblock-validator-admin" -version = "0.6.2" +version = "0.7.0" dependencies = [ "magicblock-delegation-program", "magicblock-program", @@ -3391,7 +3391,7 @@ dependencies = [ [[package]] name = "magicblock-version" -version = "0.6.2" +version = "0.7.0" dependencies = [ "git-version", "rustc_version", @@ -7071,7 +7071,7 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "bs58", @@ -8120,7 +8120,7 @@ dependencies = [ [[package]] name = "test-kit" -version = "0.6.2" +version = "0.7.0" dependencies = [ "guinea", "magicblock-accounts-db", diff --git a/Cargo.toml b/Cargo.toml index b316a4ae1..c40f0df6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ resolver = "2" [workspace.package] # Solana Version (2.2.x) -version = "0.6.2" +version = "0.7.0" authors = ["MagicBlock Maintainers "] repository = "https://github.com/magicblock-labs/ephemeral-validator" homepage = "https://www.magicblock.xyz" diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index a1edff65e..4c98c8b78 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -2241,10 +2241,10 @@ dependencies = [ [[package]] name = "guinea" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "serde", "solana-program", ] @@ -3257,7 +3257,7 @@ dependencies = [ [[package]] name = "magicblock-account-cloner" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "bincode", @@ -3267,7 +3267,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "magicblock-program", "magicblock-rpc-client", "rand 0.9.2", @@ -3289,7 +3289,7 @@ dependencies = [ [[package]] name = "magicblock-accounts" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "magicblock-account-cloner", @@ -3311,7 +3311,7 @@ dependencies = [ [[package]] name = "magicblock-accounts-db" -version = "0.6.2" +version = "0.7.0" dependencies = [ "lmdb-rkv", "magicblock-config", @@ -3326,7 +3326,7 @@ dependencies = [ [[package]] name = "magicblock-aperture" -version = "0.6.2" +version = "0.7.0" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "magicblock-api" -version = "0.6.2" +version = "0.7.0" dependencies = [ "anyhow", "borsh 1.6.0", @@ -3389,7 +3389,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3429,7 +3429,7 @@ dependencies = [ [[package]] name = "magicblock-chainlink" -version = "0.6.2" +version = "0.7.0" dependencies = [ "arc-swap", "async-trait", @@ -3441,7 +3441,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "magicblock-metrics", "parking_lot", "scc", @@ -3483,7 +3483,7 @@ dependencies = [ [[package]] name = "magicblock-committor-program" -version = "0.6.2" +version = "0.7.0" dependencies = [ "borsh 1.6.0", "paste", @@ -3495,7 +3495,7 @@ dependencies = [ [[package]] name = "magicblock-committor-service" -version = "0.6.2" +version = "0.7.0" dependencies = [ "async-trait", "base64 0.21.7", @@ -3539,7 +3539,7 @@ dependencies = [ [[package]] name = "magicblock-config" -version = "0.6.2" +version = "0.7.0" dependencies = [ "clap", "derive_more", @@ -3557,10 +3557,10 @@ dependencies = [ [[package]] name = "magicblock-core" -version = "0.6.2" +version = "0.7.0" dependencies = [ "flume", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "solana-account", "solana-account-decoder", "solana-hash", @@ -3616,7 +3616,7 @@ dependencies = [ [[package]] name = "magicblock-ledger" -version = "0.6.2" +version = "0.7.0" dependencies = [ "arc-swap", "bincode", @@ -3666,7 +3666,7 @@ dependencies = [ [[package]] name = "magicblock-magic-program-api" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "serde", @@ -3675,7 +3675,7 @@ dependencies = [ [[package]] name = "magicblock-metrics" -version = "0.6.2" +version = "0.7.0" dependencies = [ "http-body-util", "hyper 1.8.1", @@ -3689,7 +3689,7 @@ dependencies = [ [[package]] name = "magicblock-processor" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "magicblock-accounts-db", @@ -3725,12 +3725,12 @@ dependencies = [ [[package]] name = "magicblock-program" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "lazy_static", "magicblock-core", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "num-derive", "num-traits", "parking_lot", @@ -3757,7 +3757,7 @@ dependencies = [ [[package]] name = "magicblock-rpc-client" -version = "0.6.2" +version = "0.7.0" dependencies = [ "solana-account", "solana-account-decoder-client-types", @@ -3779,7 +3779,7 @@ dependencies = [ [[package]] name = "magicblock-table-mania" -version = "0.6.2" +version = "0.7.0" dependencies = [ "ed25519-dalek", "magicblock-metrics", @@ -3805,7 +3805,7 @@ dependencies = [ [[package]] name = "magicblock-task-scheduler" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "chrono", @@ -3831,7 +3831,7 @@ dependencies = [ [[package]] name = "magicblock-validator-admin" -version = "0.6.2" +version = "0.7.0" dependencies = [ "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", "magicblock-program", @@ -3848,7 +3848,7 @@ dependencies = [ [[package]] name = "magicblock-version" -version = "0.6.2" +version = "0.7.0" dependencies = [ "git-version", "rustc_version", @@ -4748,7 +4748,7 @@ dependencies = [ "bincode", "borsh 1.6.0", "ephemeral-rollups-sdk", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "serde", "solana-program", ] @@ -4772,7 +4772,7 @@ dependencies = [ "borsh 1.6.0", "ephemeral-rollups-sdk", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "rkyv 0.7.45", "solana-program", "static_assertions", @@ -5806,7 +5806,7 @@ dependencies = [ "ephemeral-rollups-sdk", "integration-test-tools", "magicblock-core", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "program-schedulecommit", "rand 0.8.5", "schedulecommit-client", @@ -5824,7 +5824,7 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "magicblock-core", - "magicblock-magic-program-api 0.6.2", + "magicblock-magic-program-api 0.7.0", "program-schedulecommit", "program-schedulecommit-security", "schedulecommit-client", @@ -8543,7 +8543,7 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "0.6.2" +version = "0.7.0" dependencies = [ "bincode", "bs58", @@ -10044,7 +10044,7 @@ dependencies = [ [[package]] name = "test-kit" -version = "0.6.2" +version = "0.7.0" dependencies = [ "guinea", "magicblock-accounts-db",