From 34f740a76ff70b4ca46ae277168aa7f048ca95fe Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 18 Feb 2026 12:46:46 +0400 Subject: [PATCH 01/16] feat: modify account cloning to use transactions instead of using side storage (stashing accounts in the ledger and then resolving them in runtime) we now embed accounts to clone directly into transactions thus greatly simplifying the whole cloning flow, and enabling full replication of ledger --- Cargo.lock | 2 + .../src/bpf_loader_v1.rs | 80 -- magicblock-account-cloner/src/lib.rs | 866 +++++++++++------- magicblock-account-cloner/src/util.rs | 12 + magicblock-api/src/magic_sys_adapter.rs | 27 +- magicblock-api/src/magic_validator.rs | 1 - magicblock-chainlink/src/cloner/errors.rs | 3 + .../src/database/cf_descriptors.rs | 1 - magicblock-ledger/src/database/columns.rs | 46 - .../src/database/ledger_column.rs | 1 - magicblock-ledger/src/database/meta.rs | 10 - magicblock-ledger/src/store/api.rs | 33 +- .../src/instruction.rs | 96 +- magicblock-metrics/src/metrics/mod.rs | 8 - programs/magicblock/Cargo.toml | 2 + .../magicblock/src/clone_account/common.rs | 78 ++ programs/magicblock/src/clone_account/mod.rs | 69 ++ .../src/clone_account/process_cleanup.rs | 55 ++ .../src/clone_account/process_clone.rs | 56 ++ .../clone_account/process_clone_continue.rs | 79 ++ .../src/clone_account/process_clone_init.rs | 88 ++ .../clone_account/process_finalize_buffer.rs | 122 +++ .../process_finalize_v1_buffer.rs | 152 +++ .../clone_account/process_set_authority.rs | 74 ++ programs/magicblock/src/errors.rs | 25 +- programs/magicblock/src/lib.rs | 1 + programs/magicblock/src/magic_sys.rs | 7 +- .../magicblock/src/magicblock_processor.rs | 75 ++ .../src/mutate_accounts/account_mod_data.rs | 188 ---- .../magicblock/src/mutate_accounts/mod.rs | 2 - .../process_mutate_accounts.rs | 66 +- programs/magicblock/src/test_utils/mod.rs | 2 +- .../magicblock/src/utils/instruction_utils.rs | 9 +- tools/ledger-stats/src/counts.rs | 9 - 34 files changed, 1514 insertions(+), 831 deletions(-) delete mode 100644 magicblock-account-cloner/src/bpf_loader_v1.rs create mode 100644 programs/magicblock/src/clone_account/common.rs create mode 100644 programs/magicblock/src/clone_account/mod.rs create mode 100644 programs/magicblock/src/clone_account/process_cleanup.rs create mode 100644 programs/magicblock/src/clone_account/process_clone.rs create mode 100644 programs/magicblock/src/clone_account/process_clone_continue.rs create mode 100644 programs/magicblock/src/clone_account/process_clone_init.rs create mode 100644 programs/magicblock/src/clone_account/process_finalize_buffer.rs create mode 100644 programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs create mode 100644 programs/magicblock/src/clone_account/process_set_authority.rs delete mode 100644 programs/magicblock/src/mutate_accounts/account_mod_data.rs diff --git a/Cargo.lock b/Cargo.lock index 8f7d1c7ef..40f2b8295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3420,6 +3420,8 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-keypair", + "solana-loader-v3-interface", + "solana-loader-v4-interface", "solana-log-collector", "solana-program-runtime", "solana-pubkey", diff --git a/magicblock-account-cloner/src/bpf_loader_v1.rs b/magicblock-account-cloner/src/bpf_loader_v1.rs deleted file mode 100644 index b71b5c501..000000000 --- a/magicblock-account-cloner/src/bpf_loader_v1.rs +++ /dev/null @@ -1,80 +0,0 @@ -use magicblock_chainlink::{ - cloner::errors::ClonerResult, - remote_account_provider::program_account::LoadedProgram, -}; -use magicblock_magic_program_api::instruction::AccountModification; -use solana_loader_v3_interface::state::UpgradeableLoaderState; -use solana_pubkey::Pubkey; -use solana_sdk_ids::bpf_loader_upgradeable; -use solana_sysvar::rent::Rent; -pub struct BpfUpgradableProgramModifications { - pub program_id_modification: AccountModification, - pub program_data_modification: AccountModification, -} - -fn create_loader_data( - loaded_program: &LoadedProgram, - deploy_slot: u64, -) -> ClonerResult> { - let loader_state = UpgradeableLoaderState::ProgramData { - slot: deploy_slot, - upgrade_authority_address: Some(loaded_program.authority), - }; - let mut loader_data = bincode::serialize(&loader_state)?; - loader_data.extend_from_slice(&loaded_program.program_data); - Ok(loader_data) -} - -impl BpfUpgradableProgramModifications { - pub fn try_from( - loaded_program: &LoadedProgram, - deploy_slot: u64, - ) -> ClonerResult { - let (program_data_address, _) = Pubkey::find_program_address( - &[loaded_program.program_id.as_ref()], - &bpf_loader_upgradeable::id(), - ); - - // 1. Create and store the ProgramData account (which holds the program data). - let program_data_modification = { - let loader_data = create_loader_data(loaded_program, deploy_slot)?; - AccountModification { - pubkey: program_data_address, - lamports: Some( - Rent::default().minimum_balance(loader_data.len()), - ), - data: Some(loader_data), - owner: Some(bpf_loader_upgradeable::id()), - executable: Some(false), - delegated: Some(false), - confined: Some(false), - remote_slot: Some(loaded_program.remote_slot), - } - }; - - // 2. Create and store the executable Program account. - let program_id_modification = { - let state = UpgradeableLoaderState::Program { - programdata_address: program_data_address, - }; - let exec_bytes = bincode::serialize(&state)?; - AccountModification { - pubkey: loaded_program.program_id, - lamports: Some( - Rent::default().minimum_balance(exec_bytes.len()).max(1), - ), - data: Some(exec_bytes), - owner: Some(bpf_loader_upgradeable::id()), - executable: Some(true), - delegated: Some(false), - confined: Some(false), - remote_slot: Some(loaded_program.remote_slot), - } - }; - - Ok(BpfUpgradableProgramModifications { - program_id_modification, - program_data_modification, - }) - } -} diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index 456ad08db..b4a5edb9f 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -1,7 +1,32 @@ -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; +//! Chainlink cloner - clones accounts from remote chain to ephemeral validator. +//! +//! # Account Cloning +//! +//! Accounts are cloned via direct encoding in transactions: +//! - Small accounts (<63KB): Single `CloneAccount` instruction +//! - Large accounts (>=63KB): `CloneAccountInit` → `CloneAccountContinue`* sequence +//! +//! # Program Cloning +//! +//! Programs use a buffer-based approach to handle loader-specific logic: +//! +//! ## V1 Programs (bpf_loader) +//! Converted to V3 (upgradeable loader) format: +//! 1. Clone ELF to buffer account +//! 2. `FinalizeV1ProgramFromBuffer` creates program + program_data accounts +//! +//! ## V4 Programs (loader_v4) +//! 1. Clone ELF to buffer account +//! 2. `FinalizeProgramFromBuffer` creates program account with LoaderV4 header +//! 3. `LoaderV4::Deploy` is called +//! 4. `SetProgramAuthority` sets the chain's authority +//! +//! # Buffer Account +//! +//! The buffer is a temporary account that holds the raw ELF data during cloning. +//! It's derived as a PDA: `["buffer", program_id]` owned by validator authority. + +use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use magicblock_accounts_db::AccountsDb; @@ -11,44 +36,45 @@ use magicblock_chainlink::{ AccountCloneRequest, Cloner, }, remote_account_provider::program_account::{ - DeployableV4Program, LoadedProgram, RemoteProgramLoader, + LoadedProgram, RemoteProgramLoader, }, }; -use magicblock_committor_service::{ - error::{CommittorServiceError, CommittorServiceResult}, - BaseIntentCommittor, CommittorService, -}; +use magicblock_committor_service::{BaseIntentCommittor, CommittorService}; use magicblock_config::config::ChainLinkConfig; use magicblock_core::link::transactions::TransactionSchedulerHandle; use magicblock_ledger::LatestBlock; -use magicblock_magic_program_api::instruction::AccountModification; -use magicblock_program::{ +use magicblock_magic_program_api::{ args::ScheduleTaskArgs, - instruction::MagicBlockInstruction, + instruction::{AccountCloneFields, MagicBlockInstruction}, + MAGIC_CONTEXT_PUBKEY, +}; +use magicblock_program::{ instruction_utils::InstructionUtils, validator::{validator_authority, validator_authority_id}, - MAGIC_CONTEXT_PUBKEY, }; use solana_account::ReadableAccount; use solana_hash::Hash; use solana_instruction::{AccountMeta, Instruction}; -use solana_loader_v4_interface::state::LoaderV4Status; +use solana_loader_v4_interface::{ + instruction::LoaderV4Instruction, + state::LoaderV4Status, +}; use solana_pubkey::Pubkey; -use solana_sdk_ids::loader_v4; +use solana_sdk_ids::{bpf_loader_upgradeable, loader_v4}; use solana_signature::Signature; -use solana_signer::{Signer, SignerError}; +use solana_signer::Signer; use solana_sysvar::rent::Rent; use solana_transaction::Transaction; -use tokio::sync::oneshot; use tracing::*; -use crate::bpf_loader_v1::BpfUpgradableProgramModifications; +/// Max data that fits in a single transaction (~63KB) +pub const MAX_INLINE_DATA_SIZE: usize = 63 * 1024; mod account_cloner; -mod bpf_loader_v1; mod util; pub use account_cloner::*; +pub use util::derive_buffer_pubkey; pub struct ChainlinkCloner { changeset_committor: Option>, @@ -66,389 +92,539 @@ impl ChainlinkCloner { accounts_db: Arc, block: LatestBlock, ) -> Self { - Self { - changeset_committor, - config, - tx_scheduler, - accounts_db, - block, - } + Self { changeset_committor, config, tx_scheduler, accounts_db, block } } - async fn send_transaction( - &self, - tx: Transaction, - ) -> ClonerResult { + // ----------------- + // Transaction Helpers + // ----------------- + + async fn send_tx(&self, tx: Transaction) -> ClonerResult { let sig = tx.signatures[0]; self.tx_scheduler.execute(tx).await?; Ok(sig) } - fn build_clone_message(request: &AccountCloneRequest) -> Option { - if request.account.delegated() { - // Account is delegated to us - None - } else if let Some(delegated_to_other) = request.delegated_to_other { - // Account is delegated to another validator - Some(format!( - "account is delegated to another validator: {}", - delegated_to_other - )) - } else { - Some("account is not delegated to any validator".to_string()) + fn sign_tx(&self, ixs: &[Instruction], blockhash: Hash) -> Transaction { + let kp = validator_authority(); + Transaction::new_signed_with_payer(ixs, Some(&kp.pubkey()), &[&kp], blockhash) + } + + // ----------------- + // Instruction Builders + // ----------------- + + fn clone_ix(pubkey: Pubkey, data: Vec, fields: AccountCloneFields) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::CloneAccount { pubkey, data, fields }, + clone_account_metas(pubkey), + ) + } + + fn clone_init_ix( + pubkey: Pubkey, + total_len: u32, + initial_data: Vec, + fields: AccountCloneFields, + ) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::CloneAccountInit { + pubkey, + total_data_len: total_len, + initial_data, + fields, + }, + clone_account_metas(pubkey), + ) + } + + fn clone_continue_ix( + pubkey: Pubkey, + offset: u32, + data: Vec, + is_last: bool, + ) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::CloneAccountContinue { pubkey, offset, data, is_last }, + clone_account_metas(pubkey), + ) + } + + fn cleanup_ix(pubkey: Pubkey) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::CleanupPartialClone { pubkey }, + clone_account_metas(pubkey), + ) + } + + fn finalize_program_ix(program: Pubkey, buffer: Pubkey, slot: u64) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::FinalizeProgramFromBuffer { slot }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + AccountMeta::new(buffer, false), + ], + ) + } + + fn set_authority_ix(program: Pubkey, authority: Pubkey) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::SetProgramAuthority { authority }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + ], + ) + } + + // ----------------- + // Clone Fields Helper + // ----------------- + + fn clone_fields(request: &AccountCloneRequest) -> AccountCloneFields { + AccountCloneFields { + lamports: request.account.lamports(), + owner: *request.account.owner(), + executable: request.account.executable(), + delegated: request.account.delegated(), + confined: request.account.confined(), + remote_slot: request.account.remote_slot(), } } - fn transaction_to_clone_regular_account( + // ----------------- + // Account Cloning + // ----------------- + + fn build_small_account_tx( &self, request: &AccountCloneRequest, - recent_blockhash: Hash, - ) -> Result { - let account_modification = AccountModification { - pubkey: request.pubkey, - lamports: Some(request.account.lamports()), - owner: Some(*request.account.owner()), - data: Some(request.account.data().to_owned()), - executable: Some(request.account.executable()), - delegated: Some(request.account.delegated()), - confined: Some(request.account.confined()), - remote_slot: Some(request.account.remote_slot()), - }; + blockhash: Hash, + ) -> Transaction { + let fields = Self::clone_fields(request); + let clone_ix = Self::clone_ix(request.pubkey, request.account.data().to_vec(), fields); + + // TODO(#625): Re-enable frequency commits when proper limits are in place: + // 1. Allow configuring a higher minimum frequency + // 2. Stop committing accounts if they have been committed more than X times + // where X corresponds to what we can charge + // + // To re-enable, uncomment the following and use `ixs` instead of `[clone_ix]`: + // let ixs = self.maybe_add_crank_commits_ix(request, clone_ix); + let ixs = vec![clone_ix]; - let message = Self::build_clone_message(request); + self.sign_tx(&ixs, blockhash) + } - let modify_ix = InstructionUtils::modify_accounts_instruction( - vec![account_modification], - message, + /// Builds crank commits instruction for periodic account commits. + /// Currently disabled - see https://github.com/magicblock-labs/magicblock-validator/issues/625 + #[allow(dead_code)] + fn build_crank_commits_ix(pubkey: Pubkey, commit_frequency_ms: i64) -> Instruction { + let task_id: i64 = rand::random(); + let schedule_commit_ix = Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::ScheduleCommit, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + AccountMeta::new_readonly(pubkey, false), + ], ); - // Defined positive commit frequency means commits should be scheduled - let ixs = match request.commit_frequency_ms { - // TODO(GabrielePicco): Hotfix. Do not schedule frequency commits until we impose limits. - // 1. Allow configuring a higher minimum. - // 2. Stop committing accounts if they have been committed more than X times, - // where X corresponds to what we can charge. - #[allow(clippy::overly_complex_bool_expr)] - Some(commit_frequency_ms) if commit_frequency_ms > 0 && false => { - // The task ID is randomly generated to avoid conflicts with other tasks - // TODO: remove once the program handles generating tasks instead of the client - // https://github.com/magicblock-labs/magicblock-validator/issues/625 - let task_id = rand::random(); - let schedule_commit_ix = Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::ScheduleCommit, - vec![ - AccountMeta::new(validator_authority_id(), true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - AccountMeta::new_readonly(request.pubkey, false), - ], - ); - let crank_commits_ix = - InstructionUtils::schedule_task_instruction( - &validator_authority_id(), - ScheduleTaskArgs { - task_id, - execution_interval_millis: commit_frequency_ms - as i64, - iterations: i64::MAX, - instructions: vec![schedule_commit_ix.clone()], - }, - &[ - request.pubkey, - MAGIC_CONTEXT_PUBKEY, - validator_authority_id(), - ], - ); - vec![modify_ix, crank_commits_ix] - } - _ => vec![modify_ix], - }; + InstructionUtils::schedule_task_instruction( + &validator_authority_id(), + ScheduleTaskArgs { + task_id, + execution_interval_millis: commit_frequency_ms, + iterations: i64::MAX, + instructions: vec![schedule_commit_ix], + }, + &[pubkey, MAGIC_CONTEXT_PUBKEY, validator_authority_id()], + ) + } + + fn build_large_account_txs( + &self, + request: &AccountCloneRequest, + blockhash: Hash, + ) -> Vec { + let data = request.account.data(); + let fields = Self::clone_fields(request); + let mut txs = Vec::new(); + + // Init tx with first chunk + let first_chunk = data[..MAX_INLINE_DATA_SIZE.min(data.len())].to_vec(); + let init_ix = Self::clone_init_ix(request.pubkey, data.len() as u32, first_chunk, fields); + txs.push(self.sign_tx(&[init_ix], blockhash)); + + // Continue txs for remaining chunks + let mut offset = MAX_INLINE_DATA_SIZE; + while offset < data.len() { + let end = (offset + MAX_INLINE_DATA_SIZE).min(data.len()); + let chunk = data[offset..end].to_vec(); + let is_last = end == data.len(); - let mut tx = - Transaction::new_with_payer(&ixs, Some(&validator_authority_id())); - tx.try_sign(&[&validator_authority()], recent_blockhash)?; - Ok(tx) + let continue_ix = Self::clone_continue_ix(request.pubkey, offset as u32, chunk, is_last); + txs.push(self.sign_tx(&[continue_ix], blockhash)); + offset = end; + } + + txs + } + + async fn send_cleanup(&self, pubkey: Pubkey) { + let blockhash = self.block.load().blockhash; + let tx = self.sign_tx(&[Self::cleanup_ix(pubkey)], blockhash); + if let Err(e) = self.send_tx(tx).await { + error!(pubkey = %pubkey, error = ?e, "Failed to cleanup partial clone"); + } } - /// Creates a transaction to clone the given program into the validator. - /// Handles the initial (and only) clone of a BPF Loader V1 program which is just - /// cloned as is without running an upgrade instruction. - /// Also see [magicblock_chainlink::chainlink::fetch_cloner::FetchCloner::handle_executable_sub_update] - /// For all other loaders we use the LoaderV4 and run a deploy instruction. - /// Returns None if the program is currently retracted on chain. - fn try_transaction_to_clone_program( + // ----------------- + // Program Cloning + // ----------------- + + fn build_program_txs( &self, program: LoadedProgram, - recent_blockhash: Hash, - ) -> ClonerResult> { - use RemoteProgramLoader::*; + blockhash: Hash, + ) -> ClonerResult>> { match program.loader { - V1 => { - // NOTE: we don't support modifying this kind of program once it was - // deployed into our validator once. - // By nature of being immutable on chain this should never happen. - // Thus we avoid having to run the upgrade instruction and get - // away with just directly modifying the program and program data accounts. - debug!(program_id = %program.program_id, "Loading V1 program"); - let validator_kp = validator_authority(); - - // BPF Loader (non-upgradeable) cannot be loaded via newer loaders, - // thus we just copy the account as is. It won't be upgradeable. - // For these programs, we use a slot that's earlier than the current slot to simulate - // that the program was deployed earlier and is ready to be used. - let deploy_slot = - self.accounts_db.slot().saturating_sub(5).max(1); - let modifications = - BpfUpgradableProgramModifications::try_from( - &program, - deploy_slot, - )?; - let mod_ix = InstructionUtils::modify_accounts_instruction( - vec![ - modifications.program_id_modification, - modifications.program_data_modification, - ], - None, - ); + RemoteProgramLoader::V1 => self.build_v1_program_txs(program, blockhash), + _ => self.build_v4_program_txs(program, blockhash), + } + } - Ok(Some(Transaction::new_signed_with_payer( - &[mod_ix], - Some(&validator_kp.pubkey()), - &[&validator_kp], - recent_blockhash, - ))) - } - _ => { - let validator_kp = validator_authority(); - // All other versions are loaded via the LoaderV4, no matter what - // the original loader was. We do this via a proper deploy instruction. - let program_id = program.program_id; - - // We don't allow users to retract the program in the ER, since in that case any - // accounts of that program still in the ER could never be committed nor - // undelegated - if matches!(program.loader_status, LoaderV4Status::Retracted) { - debug!( - program_id = %program.program_id, - "Program is retracted on chain" - ); - return Ok(None); - } - debug!( - program_id = %program.program_id, - "Deploying program with V4 loader" - ); + /// V1 programs are converted to V3 (upgradeable loader) format. + /// Supports programs of any size via multi-transaction cloning. + fn build_v1_program_txs( + &self, + program: LoadedProgram, + blockhash: Hash, + ) -> ClonerResult>> { + let program_id = program.program_id; + let chain_authority = program.authority; - // Create and initialize the program account in retracted state - // and then deploy it and finally set the authority to match the - // one on chain - let slot = self.accounts_db.slot(); - let program_remote_slot = program.remote_slot; - let DeployableV4Program { - pre_deploy_loader_state, - deploy_instruction, - post_deploy_loader_state, - } = program.try_into_deploy_data_and_ixs_v4( - slot, - validator_kp.pubkey(), - )?; - - let lamports = Rent::default() - .minimum_balance(pre_deploy_loader_state.len()); - - let disable_executable_check_instruction = - InstructionUtils::disable_executable_check_instruction( - &validator_kp.pubkey(), - ); - - // Programs aren't marked as confined since they are also never delegated - let pre_deploy_mod_instruction = { - let pre_deploy_mods = vec![AccountModification { - pubkey: program_id, - lamports: Some(lamports), - owner: Some(loader_v4::id()), - executable: Some(true), - data: Some(pre_deploy_loader_state), - confined: Some(false), - remote_slot: Some(program_remote_slot), - ..Default::default() - }]; - InstructionUtils::modify_accounts_instruction( - pre_deploy_mods, - None, - ) - }; - - let post_deploy_mod_instruction = { - let post_deploy_mods = vec![AccountModification { - pubkey: program_id, - data: Some(post_deploy_loader_state), - confined: Some(false), - remote_slot: Some(program_remote_slot), - ..Default::default() - }]; - InstructionUtils::modify_accounts_instruction( - post_deploy_mods, - None, - ) - }; - - let enable_executable_check_instruction = - InstructionUtils::enable_executable_check_instruction( - &validator_kp.pubkey(), - ); - - let ixs = vec![ - disable_executable_check_instruction, - pre_deploy_mod_instruction, - deploy_instruction, - post_deploy_mod_instruction, - enable_executable_check_instruction, - ]; - let tx = Transaction::new_signed_with_payer( - &ixs, - Some(&validator_kp.pubkey()), - &[&validator_kp], - recent_blockhash, - ); + debug!(program_id = %program_id, "Loading V1 program as V3 format"); - Ok(Some(tx)) - } - } + let slot = self.accounts_db.slot().saturating_sub(5).max(1); + let elf_data = program.program_data; + let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); + let (program_data_addr, _) = Pubkey::find_program_address( + &[program_id.as_ref()], + &bpf_loader_upgradeable::id(), + ); + + // Buffer is a dummy account owned by system program, just holds raw ELF data + let lamports = Rent::default().minimum_balance(elf_data.len()); + let buffer_fields = AccountCloneFields { + lamports, + owner: solana_sdk_ids::system_program::id(), + ..Default::default() + }; + + // Finalization instruction + // Must wrap in disable/enable executable check since finalize sets executable=true + let finalize_ixs = vec![ + InstructionUtils::disable_executable_check_instruction(&validator_authority_id()), + Self::finalize_v1_program_ix( + program_id, + program_data_addr, + buffer_pubkey, + slot, + chain_authority, + ), + InstructionUtils::enable_executable_check_instruction(&validator_authority_id()), + ]; + + // Build transactions based on ELF size + let txs = if elf_data.len() <= MAX_INLINE_DATA_SIZE { + // Small: single transaction with clone + finalize + let ixs = vec![ + Self::clone_ix(buffer_pubkey, elf_data, buffer_fields), + ] + .into_iter() + .chain(finalize_ixs) + .collect::>(); + vec![self.sign_tx(&ixs, blockhash)] + } else { + // Large: multi-transaction flow + self.build_large_program_txs( + buffer_pubkey, + elf_data, + buffer_fields, + finalize_ixs, + blockhash, + ) + }; + + Ok(Some(txs)) } - #[instrument(skip(committor), fields(pubkey = %pubkey, owner = %owner))] - async fn reserve_lookup_tables( - pubkey: Pubkey, - owner: Pubkey, - committor: Arc, - ) { - match Self::map_committor_request_result( - committor.reserve_pubkeys_for_committee(pubkey, owner), - &committor, + /// Builds finalize instruction for V1 programs (creates V3 accounts from buffer). + fn finalize_v1_program_ix( + program: Pubkey, + program_data: Pubkey, + buffer: Pubkey, + slot: u64, + authority: Pubkey, + ) -> Instruction { + Instruction::new_with_bincode( + magicblock_program::ID, + &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { slot, authority }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + AccountMeta::new(program_data, false), + AccountMeta::new(buffer, false), + ], ) - .await - { - Ok(initiated) => { - trace!( - duration_ms = initiated.elapsed().as_millis() as u64, - "Lookup table reservation completed" + } + + /// V2/V3/V4 programs use LoaderV4 with proper deploy flow. + /// Supports programs of any size via multi-transaction cloning. + fn build_v4_program_txs( + &self, + program: LoadedProgram, + blockhash: Hash, + ) -> ClonerResult>> { + let program_id = program.program_id; + let chain_authority = program.authority; + + // Skip retracted programs + if matches!(program.loader_status, LoaderV4Status::Retracted) { + debug!(program_id = %program_id, "Program is retracted on chain"); + return Ok(None); + } + + debug!(program_id = %program_id, "Deploying program with V4 loader"); + + let slot = self.accounts_db.slot(); + let program_data = program.program_data; + let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); + + // Buffer is a dummy account owned by system program, just holds raw ELF data + let lamports = Rent::default().minimum_balance(program_data.len()); + let buffer_fields = AccountCloneFields { + lamports, + owner: solana_sdk_ids::system_program::id(), + ..Default::default() + }; + + let deploy_ix = Instruction { + program_id: loader_v4::id(), + accounts: vec![ + AccountMeta::new(program_id, false), + AccountMeta::new_readonly(validator_authority_id(), true), + ], + data: bincode::serialize(&LoaderV4Instruction::Deploy) + .map_err(|e| ClonerError::SerializationError(e.to_string()))?, + }; + + // Finalization instructions (always in last tx) + // Must wrap in disable/enable executable check since finalize sets executable=true + let finalize_ixs = vec![ + InstructionUtils::disable_executable_check_instruction(&validator_authority_id()), + Self::finalize_program_ix(program_id, buffer_pubkey, slot), + deploy_ix, + Self::set_authority_ix(program_id, chain_authority), + InstructionUtils::enable_executable_check_instruction(&validator_authority_id()), + ]; + + // Build transactions based on program_data size + let txs = if program_data.len() <= MAX_INLINE_DATA_SIZE { + // Small: single transaction + let ixs = vec![ + Self::clone_ix(buffer_pubkey, program_data, buffer_fields), + ] + .into_iter() + .chain(finalize_ixs) + .collect::>(); + vec![self.sign_tx(&ixs, blockhash)] + } else { + // Large: multi-transaction flow + self.build_large_program_txs( + buffer_pubkey, + program_data, + buffer_fields, + finalize_ixs, + blockhash, + ) + }; + + Ok(Some(txs)) + } + + /// Builds multi-transaction flow for large programs (any loader). + fn build_large_program_txs( + &self, + buffer_pubkey: Pubkey, + program_data: Vec, + fields: AccountCloneFields, + finalize_ixs: Vec, + blockhash: Hash, + ) -> Vec { + let mut txs = Vec::new(); + let total_len = program_data.len() as u32; + let num_chunks = (total_len as usize).div_ceil(MAX_INLINE_DATA_SIZE); + + info!( + buffer = %buffer_pubkey, + total_len, + num_chunks, + "Building large program clone transactions" + ); + + // First chunk via Init + let first_chunk = program_data[..MAX_INLINE_DATA_SIZE.min(program_data.len())].to_vec(); + let init_ix = Self::clone_init_ix(buffer_pubkey, total_len, first_chunk, fields); + txs.push(self.sign_tx(&[init_ix], blockhash)); + + // Continue chunks + let mut offset = MAX_INLINE_DATA_SIZE; + let mut chunk_num = 1; + while offset < program_data.len() { + let end = (offset + MAX_INLINE_DATA_SIZE).min(program_data.len()); + let chunk = program_data[offset..end].to_vec(); + let is_last = end == program_data.len(); + chunk_num += 1; + + if is_last { + // Last chunk + finalize instructions in single tx + info!( + buffer = %buffer_pubkey, + chunk = chunk_num, + num_chunks, + finalize_ixs_count = finalize_ixs.len(), + "Building final transaction with continue + finalize" ); + let continue_ix = Self::clone_continue_ix(buffer_pubkey, offset as u32, chunk, true); + let ixs = vec![continue_ix] + .into_iter() + .chain(finalize_ixs.clone()) + .collect::>(); + txs.push(self.sign_tx(&ixs, blockhash)); + } else { + let continue_ix = Self::clone_continue_ix(buffer_pubkey, offset as u32, chunk, false); + txs.push(self.sign_tx(&[continue_ix], blockhash)); } - Err(err) => { - error!(error = ?err, "Failed to reserve lookup tables"); - } - }; + offset = end; + } + + txs } + // ----------------- + // Lookup Tables + // ----------------- + fn maybe_prepare_lookup_tables(&self, pubkey: Pubkey, owner: Pubkey) { - // Allow the committer service to reserve pubkeys in lookup tables - // that could be needed when we commit this account if let Some(committor) = self.changeset_committor.as_ref() { if self.config.prepare_lookup_tables { let committor = committor.clone(); - tokio::spawn(Self::reserve_lookup_tables( - pubkey, owner, committor, - )); - } - } - } - - async fn map_committor_request_result( - res: oneshot::Receiver>, - committor: &Arc, - ) -> ClonerResult { - match res.await.map_err(|err| { - // Send request error - ClonerError::CommittorServiceError(format!( - "error sending request {err:?}" - )) - })? { - Ok(val) => Ok(val), - Err(err) => { - // Commit error - match err { - CommittorServiceError::TableManiaError(table_mania_err) => { - let Some(sig) = table_mania_err.signature() else { - return Err(ClonerError::CommittorServiceError( - format!("{:?}", table_mania_err), - )); - }; - let (logs, cus) = - crate::util::get_tx_diagnostics(&sig, committor) - .await; - - let cus_str = cus - .map(|cus| format!("{:?}", cus)) - .unwrap_or("N/A".to_string()); - let logs_str = logs - .map(|logs| format!("{:#?}", logs)) - .unwrap_or("N/A".to_string()); - Err(ClonerError::CommittorServiceError(format!( - "{:?}\nCUs: {cus_str}\nLogs: {logs_str}", - table_mania_err - ))) + tokio::spawn(async move { + if let Err(e) = committor.reserve_pubkeys_for_committee(pubkey, owner).await { + error!(error = ?e, "Failed to reserve lookup tables"); } - _ => Err(ClonerError::CommittorServiceError(format!( - "{:?}", - err - ))), - } + }); } } } } +/// Shared account metas for clone instructions. +fn clone_account_metas(pubkey: Pubkey) -> Vec { + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(pubkey, false), + ] +} + #[async_trait] impl Cloner for ChainlinkCloner { async fn clone_account( &self, request: AccountCloneRequest, ) -> ClonerResult { - let recent_blockhash = self.block.load().blockhash; - let tx = self - .transaction_to_clone_regular_account(&request, recent_blockhash)?; + let blockhash = self.block.load().blockhash; + let data_len = request.account.data().len(); + if request.account.delegated() { - self.maybe_prepare_lookup_tables( - request.pubkey, - *request.account.owner(), - ); + self.maybe_prepare_lookup_tables(request.pubkey, *request.account.owner()); } - self.send_transaction(tx).await.map_err(|err| { - ClonerError::FailedToCloneRegularAccount( - request.pubkey, - Box::new(err), - ) - }) + + // Small account: single tx + if data_len <= MAX_INLINE_DATA_SIZE { + let tx = self.build_small_account_tx(&request, blockhash); + return self.send_tx(tx).await.map_err(|e| { + ClonerError::FailedToCloneRegularAccount(request.pubkey, Box::new(e)) + }); + } + + // Large account: multi-tx with cleanup on failure + let txs = self.build_large_account_txs(&request, blockhash); + + let mut last_sig = Signature::default(); + for tx in txs { + match self.send_tx(tx).await { + Ok(sig) => last_sig = sig, + Err(e) => { + self.send_cleanup(request.pubkey).await; + return Err(ClonerError::FailedToCloneRegularAccount( + request.pubkey, + Box::new(e), + )); + } + } + } + + Ok(last_sig) } async fn clone_program( &self, program: LoadedProgram, ) -> ClonerResult { - let recent_blockhash = self.block.load().blockhash; + let blockhash = self.block.load().blockhash; let program_id = program.program_id; - if let Some(tx) = self - .try_transaction_to_clone_program(program, recent_blockhash) - .map_err(|err| { - ClonerError::FailedToCreateCloneProgramTransaction( - program_id, - Box::new(err), - ) - })? - { - let res = self.send_transaction(tx).await.map_err(|err| { - ClonerError::FailedToCloneProgram(program_id, Box::new(err)) - })?; - // After cloning a program we need to wait at least one slot for it to become - // usable, so we do that here - let current_slot = self.accounts_db.slot(); - while self.accounts_db.slot() == current_slot { - tokio::time::sleep(Duration::from_millis(25)).await; + + let Some(txs) = self.build_program_txs(program, blockhash).map_err(|e| { + ClonerError::FailedToCreateCloneProgramTransaction(program_id, Box::new(e)) + })? + else { + // Program was retracted + return Ok(Signature::default()); + }; + + // Both V1 and V4 use buffer_pubkey for multi-tx cloning + let buffer_pubkey = derive_buffer_pubkey(&program_id).0; + + let mut last_sig = Signature::default(); + for tx in txs { + match self.send_tx(tx).await { + Ok(sig) => last_sig = sig, + Err(e) => { + self.send_cleanup(buffer_pubkey).await; + return Err(ClonerError::FailedToCloneProgram( + program_id, + Box::new(e), + )); + } } - Ok(res) - } else { - // No-op, program was retracted - Ok(Signature::default()) } + + // Wait one slot for program to become usable + let current_slot = self.accounts_db.slot(); + while self.accounts_db.slot() == current_slot { + tokio::time::sleep(Duration::from_millis(25)).await; + } + + Ok(last_sig) } } diff --git a/magicblock-account-cloner/src/util.rs b/magicblock-account-cloner/src/util.rs index 515c05603..992508faa 100644 --- a/magicblock-account-cloner/src/util.rs +++ b/magicblock-account-cloner/src/util.rs @@ -1,9 +1,21 @@ use std::sync::Arc; use magicblock_committor_service::BaseIntentCommittor; +use magicblock_program::validator::validator_authority_id; use magicblock_rpc_client::MagicblockRpcClient; +use solana_pubkey::Pubkey; use solana_signature::Signature; +/// Seed for deriving buffer account PDA +const BUFFER_SEED: &[u8] = b"buffer"; + +/// Derives a deterministic buffer account pubkey for program cloning. +/// Uses validator_authority as owner so it works for any loader type. +pub fn derive_buffer_pubkey(program_pubkey: &Pubkey) -> (Pubkey, u8) { + let seeds: &[&[u8]] = &[BUFFER_SEED, program_pubkey.as_ref()]; + Pubkey::find_program_address(seeds, &validator_authority_id()) +} + pub(crate) async fn get_tx_diagnostics( sig: &Signature, committor: &Arc, diff --git a/magicblock-api/src/magic_sys_adapter.rs b/magicblock-api/src/magic_sys_adapter.rs index 9515eea16..8935c95f3 100644 --- a/magicblock-api/src/magic_sys_adapter.rs +++ b/magicblock-api/src/magic_sys_adapter.rs @@ -2,15 +2,13 @@ use std::{collections::HashMap, error::Error, sync::Arc, time::Duration}; use magicblock_committor_service::CommittorService; use magicblock_core::{intent::CommittedAccount, traits::MagicSys}; -use magicblock_ledger::Ledger; use magicblock_metrics::metrics; use solana_instruction::error::InstructionError; use solana_pubkey::Pubkey; -use tracing::{enabled, error, trace, Level}; +use tracing::{error, trace}; #[derive(Clone)] pub struct MagicSysAdapter { - ledger: Arc, committor_service: Option>, } @@ -26,34 +24,19 @@ impl MagicSysAdapter { const FETCH_TIMEOUT: Duration = Duration::from_secs(30); - pub fn new( - ledger: Arc, - committor_service: Option>, - ) -> Self { - Self { - ledger, - committor_service, - } + pub fn new(committor_service: Option>) -> Self { + Self { committor_service } } } impl MagicSys for MagicSysAdapter { fn persist(&self, id: u64, data: Vec) -> Result<(), Box> { trace!(id, data_len = data.len(), "Persisting data"); - self.ledger.write_account_mod_data(id, &data.into())?; Ok(()) } - fn load(&self, id: u64) -> Result>, Box> { - let data = self.ledger.read_account_mod_data(id)?.map(|x| x.data); - if enabled!(Level::TRACE) { - if let Some(data) = &data { - trace!(id, data_len = data.len(), "Loading data"); - } else { - trace!(id, found = false, "Loading data"); - } - } - Ok(data) + fn load(&self, _id: u64) -> Result>, Box> { + Ok(None) } fn fetch_current_commit_nonces( diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index fd3d1e62c..0efcb51be 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -214,7 +214,6 @@ impl MagicValidator { let committor_service = Self::init_committor_service(&config).await?; log_timing("startup", "committor_service_init", step_start); init_magic_sys(Arc::new(MagicSysAdapter::new( - ledger.clone(), committor_service.clone(), ))); diff --git a/magicblock-chainlink/src/cloner/errors.rs b/magicblock-chainlink/src/cloner/errors.rs index 32bc8f1c7..febff28ca 100644 --- a/magicblock-chainlink/src/cloner/errors.rs +++ b/magicblock-chainlink/src/cloner/errors.rs @@ -28,4 +28,7 @@ pub enum ClonerError { #[error("Failed to clone program {0} : {1:?}")] FailedToCloneProgram(Pubkey, Box), + + #[error("Serialization error: {0}")] + SerializationError(String), } diff --git a/magicblock-ledger/src/database/cf_descriptors.rs b/magicblock-ledger/src/database/cf_descriptors.rs index c113c1506..7dc63a5bb 100644 --- a/magicblock-ledger/src/database/cf_descriptors.rs +++ b/magicblock-ledger/src/database/cf_descriptors.rs @@ -42,7 +42,6 @@ pub fn cf_descriptors( new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), - new_cf_descriptor::(options, oldest_slot), ]; // If the access type is Secondary, we don't need to open all of the diff --git a/magicblock-ledger/src/database/columns.rs b/magicblock-ledger/src/database/columns.rs index 1732b3816..4eae81b87 100644 --- a/magicblock-ledger/src/database/columns.rs +++ b/magicblock-ledger/src/database/columns.rs @@ -23,8 +23,6 @@ const CONFIRMED_TRANSACTION_CF: &str = "confirmed_transaction"; const TRANSACTION_MEMOS_CF: &str = "transaction_memos"; /// Column family for Performance Samples const PERF_SAMPLES_CF: &str = "perf_samples"; -/// Column family for AccountModDatas -const ACCOUNT_MOD_DATAS_CF: &str = "account_mod_datas"; #[derive(Debug)] /// The transaction status column @@ -91,12 +89,6 @@ pub struct TransactionMemos; /// * value type: [`crate::database::meta::PerfSample`] pub struct PerfSamples; -/// The AccountModData column -/// -/// * index type: `u64` -/// * value type: [`crate::database::meta::AccountModData`] -pub struct AccountModDatas; - // When adding a new column ... // - Add struct below and implement `Column` and `ColumnName` traits // - Add descriptor in Rocks::cf_descriptors() and name in Rocks::columns() @@ -114,7 +106,6 @@ pub fn columns() -> Vec<&'static str> { Transaction::NAME, TransactionMemos::NAME, PerfSamples::NAME, - AccountModDatas::NAME, ] } @@ -630,43 +621,6 @@ impl ColumnName for PerfSamples { const NAME: &'static str = PERF_SAMPLES_CF; } -// ----------------- -// AccountModDatas -// ----------------- -impl ColumnName for AccountModDatas { - const NAME: &'static str = ACCOUNT_MOD_DATAS_CF; -} - -impl Column for AccountModDatas { - type Index = u64; - - fn key(id: Self::Index) -> Vec { - id.to_le_bytes().to_vec() - } - - fn index(key: &[u8]) -> Self::Index { - Self::Index::from_le_bytes(key.try_into().unwrap()) - } - - fn slot(index: Self::Index) -> Slot { - index as Slot - } - - // AccountModDatas column is not keyed by slot so this method is meaningless - fn as_index(slot: Slot) -> Self::Index { - slot - } - - /// We don't clean AccountModData on compaction as it isn't slot based - fn keep_all_on_compaction() -> bool { - true - } -} - -impl TypedColumn for AccountModDatas { - type Type = meta::AccountModData; -} - // ----------------- // Column Configuration // ----------------- diff --git a/magicblock-ledger/src/database/ledger_column.rs b/magicblock-ledger/src/database/ledger_column.rs index 6e657397c..02d8b44a0 100644 --- a/magicblock-ledger/src/database/ledger_column.rs +++ b/magicblock-ledger/src/database/ledger_column.rs @@ -293,7 +293,6 @@ where crate::database::columns::Blocktime::NAME | crate::database::columns::Blockhash::NAME | crate::database::columns::PerfSamples::NAME - | crate::database::columns::AccountModDatas::NAME ) } diff --git a/magicblock-ledger/src/database/meta.rs b/magicblock-ledger/src/database/meta.rs index 8df218f91..e2e907979 100644 --- a/magicblock-ledger/src/database/meta.rs +++ b/magicblock-ledger/src/database/meta.rs @@ -13,13 +13,3 @@ pub struct PerfSample { pub sample_period_secs: u16, pub num_non_vote_transactions: u64, } - -#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct AccountModData { - pub data: Vec, -} -impl From> for AccountModData { - fn from(data: Vec) -> Self { - Self { data } - } -} diff --git a/magicblock-ledger/src/store/api.rs b/magicblock-ledger/src/store/api.rs index 0ad1ae509..42d58f784 100644 --- a/magicblock-ledger/src/store/api.rs +++ b/magicblock-ledger/src/store/api.rs @@ -39,7 +39,7 @@ use crate::{ db::Database, iterator::IteratorMode, ledger_column::{try_increase_entry_counter, LedgerColumn}, - meta::{AccountModData, AddressSignatureMeta, PerfSample}, + meta::{AddressSignatureMeta, PerfSample}, options::LedgerOptions, }, errors::{LedgerError, LedgerResult}, @@ -67,7 +67,6 @@ pub struct Ledger { transaction_cf: LedgerColumn, transaction_memos_cf: LedgerColumn, perf_samples_cf: LedgerColumn, - account_mod_datas_cf: LedgerColumn, transaction_successful_status_count: AtomicI64, transaction_failed_status_count: AtomicI64, @@ -143,8 +142,6 @@ impl Ledger { let transaction_memos_cf = db.column(); let perf_samples_cf = db.column(); - let account_mod_datas_cf = db.column(); - let db = Arc::new(db); // NOTE: left out max root @@ -165,7 +162,6 @@ impl Ledger { transaction_cf, transaction_memos_cf, perf_samples_cf, - account_mod_datas_cf, transaction_successful_status_count: AtomicI64::new(DIRTY_COUNT), transaction_failed_status_count: AtomicI64::new(DIRTY_COUNT), @@ -196,7 +192,6 @@ impl Ledger { self.transaction_cf.submit_rocksdb_cf_metrics(); self.transaction_memos_cf.submit_rocksdb_cf_metrics(); self.perf_samples_cf.submit_rocksdb_cf_metrics(); - self.account_mod_datas_cf.submit_rocksdb_cf_metrics(); } // ----------------- @@ -1187,30 +1182,6 @@ impl Ledger { self.perf_samples_cf.count_column_using_cache() } - // ----------------- - // AccountModDatas - // ----------------- - pub fn write_account_mod_data( - &self, - id: u64, - data: &AccountModData, - ) -> LedgerResult<()> { - self.account_mod_datas_cf.put(id, data)?; - self.account_mod_datas_cf.try_increase_entry_counter(1); - Ok(()) - } - - pub fn read_account_mod_data( - &self, - id: u64, - ) -> LedgerResult> { - self.account_mod_datas_cf.get(id) - } - - pub fn count_account_mod_data(&self) -> LedgerResult { - self.account_mod_datas_cf.count_column_using_cache() - } - pub fn read_slot_signature( &self, index: (Slot, u32), @@ -1272,7 +1243,6 @@ impl Ledger { self.transaction_cf.handle(), self.transaction_memos_cf.handle(), self.perf_samples_cf.handle(), - self.account_mod_datas_cf.handle(), ]; self.db @@ -1344,7 +1314,6 @@ impl_has_column!(Blockhash, blockhash_cf); impl_has_column!(Transaction, transaction_cf); impl_has_column!(TransactionMemos, transaction_memos_cf); impl_has_column!(PerfSamples, perf_samples_cf); -impl_has_column!(AccountModDatas, account_mod_datas_cf); struct MeasureGuard { measure: Measure, diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index a3aa000cf..19b4d31b0 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -194,6 +194,87 @@ pub enum MagicBlockInstruction { /// the scheduled commits /// - **2..n** `[]` Accounts to be committed ScheduleCommitFinalize { request_undelegation: bool }, + /// Clone a single account that fits in one transaction (<63KB data). + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Account to clone + CloneAccount { + pubkey: Pubkey, + data: Vec, + fields: AccountCloneFields, + }, + + /// Initialize a multi-transaction clone for a large account. + /// Adds the pubkey to PENDING_CLONES. Must be followed by CloneAccountContinue + /// with is_last=true to complete. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Account to clone + CloneAccountInit { + pubkey: Pubkey, + total_data_len: u32, + initial_data: Vec, + fields: AccountCloneFields, + }, + + /// Continue a multi-transaction clone with the next data chunk. + /// If is_last=true, removes the pubkey from PENDING_CLONES. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Account being cloned + CloneAccountContinue { + pubkey: Pubkey, + offset: u32, + data: Vec, + is_last: bool, + }, + + /// Cleanup a partial clone on failure. Removes from PENDING_CLONES + /// and deletes the account. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Account to cleanup + CleanupPartialClone { pubkey: Pubkey }, + + /// Finalize program deployment from a buffer account. + /// Does the following: + /// 1. Copies data from buffer account to program account + /// 2. Sets loader header with Retracted status and validator authority + /// 3. Closes buffer account + /// + /// After this, LoaderV4::Deploy must be called, then SetProgramAuthority. + /// + /// # Account references + /// - **0.** `[SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Program account + /// - **2.** `[WRITE]` Buffer account (closed after) + FinalizeProgramFromBuffer { slot: u64 }, + + /// Finalize V1 program deployment from a buffer account. + /// V1 programs are converted to V3 (upgradeable loader) format. + /// Does the following: + /// 1. Creates program_data account with V3 ProgramData header + ELF + /// 2. Creates program account with V3 Program header + /// 3. Closes buffer account + /// + /// # Account references + /// - **0.** `[SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Program account + /// - **2.** `[WRITE]` Program data account + /// - **3.** `[WRITE]` Buffer account (closed after) + FinalizeV1ProgramFromBuffer { slot: u64, authority: Pubkey }, + + /// Update the authority in a LoaderV4 program header. + /// Used after Deploy to set the final chain authority. + /// + /// # Account references + /// - **0.** `[SIGNER]` Validator Authority + /// - **1.** `[WRITE]` Program account + SetProgramAuthority { authority: Pubkey }, } impl MagicBlockInstruction { @@ -219,8 +300,21 @@ pub struct AccountModificationForInstruction { pub lamports: Option, pub owner: Option, pub executable: Option, - pub data_key: Option, + pub data: Option>, pub delegated: Option, pub confined: Option, pub remote_slot: Option, } + +/// Common fields for cloning an account. +#[derive( + Default, Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, +)] +pub struct AccountCloneFields { + pub lamports: u64, + pub owner: Pubkey, + pub executable: bool, + pub delegated: bool, + pub confined: bool, + pub remote_slot: u64, +} diff --git a/magicblock-metrics/src/metrics/mod.rs b/magicblock-metrics/src/metrics/mod.rs index f93c8d679..a91a65038 100644 --- a/magicblock-metrics/src/metrics/mod.rs +++ b/magicblock-metrics/src/metrics/mod.rs @@ -87,9 +87,6 @@ lazy_static::lazy_static! { static ref LEDGER_PERF_SAMPLES_GAUGE: IntGauge = IntGauge::new( "ledger_perf_samples_gauge", "Ledger Perf Samples Gauge", ).unwrap(); - static ref LEDGER_ACCOUNT_MOD_DATA_GAUGE: IntGauge = IntGauge::new( - "ledger_account_mod_data_gauge", "Ledger Account Mod Data Gauge", - ).unwrap(); pub static ref LEDGER_COLUMNS_COUNT_DURATION_SECONDS: Histogram = Histogram::with_opts( HistogramOpts::new( "ledger_columns_count_duration_seconds", @@ -548,7 +545,6 @@ pub(crate) fn register() { register!(LEDGER_TRANSACTIONS_GAUGE); register!(LEDGER_TRANSACTION_MEMOS_GAUGE); register!(LEDGER_PERF_SAMPLES_GAUGE); - register!(LEDGER_ACCOUNT_MOD_DATA_GAUGE); register!(LEDGER_COLUMNS_COUNT_DURATION_SECONDS); register!(LEDGER_TRUNCATOR_COMPACTION_SECONDS); register!(LEDGER_TRUNCATOR_DELETE_SECONDS); @@ -663,10 +659,6 @@ pub fn set_ledger_perf_samples_count(count: i64) { LEDGER_PERF_SAMPLES_GAUGE.set(count); } -pub fn set_ledger_account_mod_data_count(count: i64) { - LEDGER_ACCOUNT_MOD_DATA_GAUGE.set(count); -} - pub fn observe_columns_count_duration(f: F) -> T where F: FnOnce() -> T, diff --git a/programs/magicblock/Cargo.toml b/programs/magicblock/Cargo.toml index a24516966..567506c20 100644 --- a/programs/magicblock/Cargo.toml +++ b/programs/magicblock/Cargo.toml @@ -23,6 +23,8 @@ solana-fee-calculator = { workspace = true } solana-hash = { workspace = true } solana-instruction = { workspace = true } solana-keypair = { workspace = true } +solana-loader-v3-interface = { workspace = true } +solana-loader-v4-interface = { workspace = true } solana-log-collector = { workspace = true } solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs new file mode 100644 index 000000000..a4bb890e4 --- /dev/null +++ b/programs/magicblock/src/clone_account/common.rs @@ -0,0 +1,78 @@ +//! Shared utilities for clone account instruction processing. + +use std::{cell::RefCell, collections::HashSet}; + +use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_pubkey::Pubkey; +use solana_transaction_context::TransactionContext; + +use crate::validator::validator_authority_id; + +/// Validates that the validator authority has signed the transaction. +/// All clone instructions require this signature for authorization. +pub fn validate_authority( + signers: &HashSet, + invoke_context: &InvokeContext, +) -> Result<(), InstructionError> { + let auth = validator_authority_id(); + if signers.contains(&auth) { + return Ok(()); + } + ic_msg!(invoke_context, "Validator authority '{auth}' not in signers"); + Err(InstructionError::MissingRequiredSignature) +} + +/// Validates that the account at `ix_index` matches `expected` pubkey. +/// Returns the transaction-level index on success. +pub fn validate_and_get_index( + transaction_context: &TransactionContext, + ix_index: u16, + expected: &Pubkey, + name: &str, + invoke_context: &InvokeContext, +) -> Result { + let ctx = transaction_context.get_current_instruction_context()?; + let tx_idx = ctx.get_index_of_instruction_account_in_transaction(ix_index)?; + let key = transaction_context.get_key_of_account_at_index(tx_idx)?; + if *key == *expected { + return Ok(tx_idx); + } + ic_msg!(invoke_context, "{}: key mismatch, expected {}, got {}", name, expected, key); + Err(InstructionError::InvalidArgument) +} + +/// Adjusts validator authority lamports to maintain balanced transactions. +/// +/// # Lamports Flow +/// +/// When cloning an account, we set the target account's lamports to the required rent-exempt +/// amount. The difference (delta) between new and old lamports is debited/credited from the +/// validator authority account: +/// +/// - `delta > 0`: Account gained lamports → debit from authority +/// - `delta < 0`: Account lost lamports → credit to authority +/// +/// This ensures the runtime doesn't complain about unbalanced transactions. +pub fn adjust_authority_lamports( + auth_acc: &RefCell, + delta: i64, +) -> Result<(), InstructionError> { + if delta == 0 { + return Ok(()); + } + let auth_lamports = auth_acc.borrow().lamports(); + let adjusted = if delta > 0 { + auth_lamports + .checked_sub(delta as u64) + .ok_or(InstructionError::InsufficientFunds)? + } else { + auth_lamports + .checked_add((-delta) as u64) + .ok_or(InstructionError::ArithmeticOverflow)? + }; + auth_acc.borrow_mut().set_lamports(adjusted); + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/mod.rs b/programs/magicblock/src/clone_account/mod.rs new file mode 100644 index 000000000..09535c782 --- /dev/null +++ b/programs/magicblock/src/clone_account/mod.rs @@ -0,0 +1,69 @@ +//! Account cloning instructions for the ephemeral validator. +//! +//! # Overview +//! +//! Accounts are cloned from the remote chain via direct encoding in transactions. +//! Large accounts (>63KB) are split across multiple sequential transactions. +//! +//! # Flow for Regular Accounts +//! +//! 1. Small (<63KB): Single `CloneAccount` instruction +//! 2. Large (>=63KB): `CloneAccountInit` → `CloneAccountContinue`* → (complete) +//! +//! # Flow for Program Accounts +//! +//! Programs require a buffer-based approach to handle loader-specific logic: +//! +//! 1. Clone ELF data to a buffer account (dummy account owned by system program) +//! 2. `FinalizeProgramFromBuffer` or `FinalizeV1ProgramFromBuffer` creates the +//! actual program accounts with proper loader headers +//! 3. For V4: `LoaderV4::Deploy` is called, then `SetProgramAuthority` +//! +//! # Lamports Accounting +//! +//! All instructions track lamports delta and adjust the validator authority account +//! to maintain balanced transactions. The validator authority is funded at startup. + +mod common; +mod process_cleanup; +mod process_clone; +mod process_clone_continue; +mod process_clone_init; +mod process_finalize_buffer; +mod process_finalize_v1_buffer; +mod process_set_authority; + +use std::collections::HashSet; + +use lazy_static::lazy_static; +use parking_lot::RwLock; +use solana_pubkey::Pubkey; + +pub(crate) use common::*; +pub(crate) use process_cleanup::process_cleanup_partial_clone; +pub(crate) use process_clone::process_clone_account; +pub(crate) use process_clone_continue::process_clone_account_continue; +pub(crate) use process_clone_init::process_clone_account_init; +pub(crate) use process_finalize_buffer::process_finalize_program_from_buffer; +pub(crate) use process_finalize_v1_buffer::process_finalize_v1_program_from_buffer; +pub(crate) use process_set_authority::process_set_program_authority; + +lazy_static! { + /// Tracks in-progress multi-transaction clones. + /// - `CloneAccountInit` adds the pubkey + /// - `CloneAccountContinue(is_last=true)` removes it + /// - `CleanupPartialClone` removes it on failure + static ref PENDING_CLONES: RwLock> = RwLock::new(HashSet::new()); +} + +pub fn is_pending_clone(pubkey: &Pubkey) -> bool { + PENDING_CLONES.read().contains(pubkey) +} + +pub fn add_pending_clone(pubkey: Pubkey) -> bool { + PENDING_CLONES.write().insert(pubkey) +} + +pub fn remove_pending_clone(pubkey: &Pubkey) -> bool { + PENDING_CLONES.write().remove(pubkey) +} diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs new file mode 100644 index 000000000..44320f9f9 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -0,0 +1,55 @@ +//! Cleanup for failed multi-transaction clones. + +use std::collections::HashSet; + +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_pubkey::Pubkey; +use solana_transaction_context::TransactionContext; + +use super::{adjust_authority_lamports, remove_pending_clone, validate_and_get_index, validate_authority}; + +/// Cleans up a failed multi-transaction clone. +/// +/// Called when any transaction in the clone sequence fails. +/// Removes from `PENDING_CLONES` and resets the account to default state, +/// returning its lamports to the validator authority. +pub(crate) fn process_cleanup_partial_clone( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + pubkey: Pubkey, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + remove_pending_clone(&pubkey); + + let ctx = transaction_context.get_current_instruction_context()?; + let auth_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(0)?, + )?; + + let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CleanupPartialClone", invoke_context)?; + let account = transaction_context.get_account_at_index(tx_idx)?; + + ic_msg!(invoke_context, "CleanupPartialClone: cleaning up '{}'", pubkey); + + let current_lamports = account.borrow().lamports(); + let lamports_delta = -(current_lamports as i64); + + { + let mut acc = account.borrow_mut(); + acc.set_lamports(0); + acc.set_data_from_slice(&[]); + acc.set_executable(false); + acc.set_owner(Pubkey::default()); + acc.set_delegated(false); + acc.set_confined(false); + acc.set_remote_slot(0); + acc.set_undelegating(false); + } + + adjust_authority_lamports(auth_acc, lamports_delta)?; + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/process_clone.rs b/programs/magicblock/src/clone_account/process_clone.rs new file mode 100644 index 000000000..cbb103af3 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -0,0 +1,56 @@ +//! Single-transaction account cloning for accounts <63KB. + +use std::collections::HashSet; + +use magicblock_magic_program_api::instruction::AccountCloneFields; +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_pubkey::Pubkey; +use solana_transaction_context::TransactionContext; + +use super::{adjust_authority_lamports, validate_and_get_index, validate_authority}; + +/// Clones an account atomically in a single transaction. +/// +/// Used for accounts that fit within transaction size limits (<63KB data). +/// Sets all account fields atomically: lamports, owner, data, executable, delegated, etc. +pub(crate) fn process_clone_account( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + pubkey: Pubkey, + data: Vec, + fields: AccountCloneFields, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + let ctx = transaction_context.get_current_instruction_context()?; + let auth_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(0)?, + )?; + + let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccount", invoke_context)?; + let account = transaction_context.get_account_at_index(tx_idx)?; + + ic_msg!(invoke_context, "CloneAccount: cloning '{}', data_len={}", pubkey, data.len()); + + let current_lamports = account.borrow().lamports(); + let lamports_delta = fields.lamports as i64 - current_lamports as i64; + + { + let mut acc = account.borrow_mut(); + acc.set_lamports(fields.lamports); + acc.set_owner(fields.owner); + acc.set_data_from_slice(&data); + acc.set_executable(fields.executable); + acc.set_delegated(fields.delegated); + acc.set_confined(fields.confined); + acc.set_remote_slot(fields.remote_slot); + acc.set_undelegating(false); + } + + adjust_authority_lamports(auth_acc, lamports_delta)?; + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/process_clone_continue.rs b/programs/magicblock/src/clone_account/process_clone_continue.rs new file mode 100644 index 000000000..02fb4b73a --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone_continue.rs @@ -0,0 +1,79 @@ +//! Continuation of multi-transaction cloning for large accounts. + +use std::collections::HashSet; + +use solana_account::WritableAccount; +use solana_instruction::error::InstructionError; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_pubkey::Pubkey; +use solana_transaction_context::TransactionContext; + +use super::{is_pending_clone, remove_pending_clone, validate_and_get_index, validate_authority}; +use crate::errors::MagicBlockProgramError; + +/// Writes a data chunk to a pending multi-transaction clone. +/// +/// # Flow +/// +/// 1. Verifies pubkey is in `PENDING_CLONES` +/// 2. Writes `data` at `offset` in the account's data buffer +/// 3. If `is_last=true`, removes from `PENDING_CLONES` (clone complete) +/// +/// No lamports adjustment needed - account already has correct lamports from Init. +pub(crate) fn process_clone_account_continue( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + pubkey: Pubkey, + offset: u32, + data: Vec, + is_last: bool, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + if !is_pending_clone(&pubkey) { + ic_msg!(invoke_context, "CloneAccountContinue: no pending clone for {}", pubkey); + return Err(MagicBlockProgramError::NoPendingClone.into()); + } + + let tx_idx = validate_and_get_index( + transaction_context, + 1, + &pubkey, + "CloneAccountContinue", + invoke_context, + )?; + let account = transaction_context.get_account_at_index(tx_idx)?; + + ic_msg!( + invoke_context, + "CloneAccountContinue: '{}' offset={} len={} is_last={}", + pubkey, offset, data.len(), is_last + ); + + // Write data at offset + { + let mut acc = account.borrow_mut(); + let account_data = acc.data_as_mut_slice(); + let start = offset as usize; + let end = start + data.len(); + + if end > account_data.len() { + ic_msg!( + invoke_context, + "CloneAccountContinue: offset + len ({}) exceeds account len ({})", + end, account_data.len() + ); + return Err(InstructionError::InvalidArgument); + } + account_data[start..end].copy_from_slice(&data); + } + + if is_last { + remove_pending_clone(&pubkey); + ic_msg!(invoke_context, "CloneAccountContinue: clone complete for '{}'", pubkey); + } + + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/process_clone_init.rs b/programs/magicblock/src/clone_account/process_clone_init.rs new file mode 100644 index 000000000..775d8bd64 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -0,0 +1,88 @@ +//! First step of multi-transaction cloning for large accounts (>=63KB). + +use std::collections::HashSet; + +use magicblock_magic_program_api::instruction::AccountCloneFields; +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_pubkey::Pubkey; +use solana_transaction_context::TransactionContext; + +use super::{add_pending_clone, adjust_authority_lamports, is_pending_clone, validate_and_get_index, validate_authority}; +use crate::errors::MagicBlockProgramError; + +/// Initializes a multi-transaction clone for a large account. +/// +/// # Flow +/// +/// 1. Creates account with zeroed buffer of `total_data_len` size +/// 2. Copies `initial_data` to offset 0 +/// 3. Sets metadata fields (lamports, owner, etc.) +/// 4. Registers pubkey in `PENDING_CLONES` +/// +/// Must be followed by `CloneAccountContinue` instructions until `is_last=true`. +pub(crate) fn process_clone_account_init( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + pubkey: Pubkey, + total_data_len: u32, + initial_data: Vec, + fields: AccountCloneFields, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + if is_pending_clone(&pubkey) { + ic_msg!(invoke_context, "CloneAccountInit: account {} already has pending clone", pubkey); + return Err(MagicBlockProgramError::CloneAlreadyPending.into()); + } + + if initial_data.len() > total_data_len as usize { + ic_msg!( + invoke_context, + "CloneAccountInit: initial_data len {} exceeds total_data_len {}", + initial_data.len(), + total_data_len + ); + return Err(InstructionError::InvalidArgument); + } + + let ctx = transaction_context.get_current_instruction_context()?; + let auth_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(0)?, + )?; + + let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccountInit", invoke_context)?; + let account = transaction_context.get_account_at_index(tx_idx)?; + + ic_msg!( + invoke_context, + "CloneAccountInit: initializing '{}', total_len={}, initial_len={}", + pubkey, total_data_len, initial_data.len() + ); + + let current_lamports = account.borrow().lamports(); + let lamports_delta = fields.lamports as i64 - current_lamports as i64; + + // Pre-allocate full buffer and copy initial chunk + let mut data = vec![0u8; total_data_len as usize]; + data[..initial_data.len()].copy_from_slice(&initial_data); + + { + let mut acc = account.borrow_mut(); + acc.set_lamports(fields.lamports); + acc.set_owner(fields.owner); + acc.set_data_from_slice(&data); + acc.set_executable(fields.executable); + acc.set_delegated(fields.delegated); + acc.set_confined(fields.confined); + acc.set_remote_slot(fields.remote_slot); + acc.set_undelegating(false); + } + + adjust_authority_lamports(auth_acc, lamports_delta)?; + add_pending_clone(pubkey); + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/process_finalize_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_buffer.rs new file mode 100644 index 000000000..68c384804 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -0,0 +1,122 @@ +//! Finalizes V4 program deployment from a buffer account. +//! +//! This is part of the program cloning flow for LoaderV4 programs (V2/V3/V4 on remote chain). + +use std::collections::HashSet; + +use solana_account::{ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status}; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_pubkey::Pubkey; +use solana_sdk_ids::loader_v4; +use solana_sysvar::rent::Rent; +use solana_transaction_context::TransactionContext; + +use super::{adjust_authority_lamports, validate_authority}; +use crate::validator::validator_authority_id; + +/// Finalizes a LoaderV4 program from a buffer account. +/// +/// # Flow +/// +/// 1. Reads ELF data from buffer account +/// 2. Prepends LoaderV4State header with Retracted status +/// 3. Sets program account as executable with proper lamports +/// 4. Closes buffer account +/// +/// After this instruction, the caller must invoke `LoaderV4::Deploy` then `SetProgramAuthority`. +/// +/// # Slot Trick +/// +/// We use `slot - 5` for the deploy slot to bypass LoaderV4's cooldown mechanism. +/// LoaderV4 requires programs to wait N slots after deployment before certain operations. +/// By setting the slot to 5 slots ago, we simulate that the program was deployed earlier. +/// +/// # Lamports Accounting +/// +/// The program account gets rent-exempt lamports for (header + ELF). +/// The buffer's lamports are returned to the validator authority. +pub(crate) fn process_finalize_program_from_buffer( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + slot: u64, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + let ctx = transaction_context.get_current_instruction_context()?; + let auth_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(0)?, + )?; + let prog_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(1)?, + )?; + let buf_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(2)?, + )?; + + let prog_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(1)?, + )?; + let buf_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(2)?, + )?; + + let buf_data = buf_acc.borrow().data().to_vec(); + let buf_lamports = buf_acc.borrow().lamports(); + let prog_current_lamports = prog_acc.borrow().lamports(); + + ic_msg!(invoke_context, "FinalizeV4: prog={} buf={} len={}", prog_key, buf_key, buf_data.len()); + + // Build LoaderV4 account data: header + ELF + let deploy_slot = slot.saturating_sub(5).max(1); // Bypass cooldown + let state = LoaderV4State { + slot: deploy_slot, + authority_address_or_next_version: validator_authority_id(), + status: LoaderV4Status::Retracted, // Deploy instruction will activate it + }; + let program_data = build_loader_v4_data(&state, &buf_data); + + // Calculate rent-exempt lamports for full program account + let prog_lamports = Rent::default().minimum_balance(program_data.len()); + let lamports_delta = prog_lamports as i64 - prog_current_lamports as i64 - buf_lamports as i64; + + // Set up program account + { + let mut prog = prog_acc.borrow_mut(); + prog.set_lamports(prog_lamports); + prog.set_owner(loader_v4::id()); + prog.set_executable(true); + prog.set_data_from_slice(&program_data); + prog.set_remote_slot(slot); + prog.set_undelegating(false); + } + + // Close buffer account + { + let mut buf = buf_acc.borrow_mut(); + buf.set_lamports(0); + buf.set_data_from_slice(&[]); + buf.set_executable(false); + buf.set_owner(Pubkey::default()); + } + + adjust_authority_lamports(auth_acc, lamports_delta)?; + ic_msg!(invoke_context, "FinalizeV4: finalized {}, closed {}", prog_key, buf_key); + Ok(()) +} + +/// Builds LoaderV4 account data by prepending the state header to the program data. +fn build_loader_v4_data(state: &LoaderV4State, program_data: &[u8]) -> Vec { + let header_size = LoaderV4State::program_data_offset(); + let mut data = Vec::with_capacity(header_size + program_data.len()); + // SAFETY: LoaderV4State is POD with no uninitialized padding + let header: &[u8] = unsafe { + std::slice::from_raw_parts((state as *const LoaderV4State) as *const u8, header_size) + }; + data.extend_from_slice(header); + data.extend_from_slice(program_data); + data +} diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs new file mode 100644 index 000000000..8e31ebb67 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -0,0 +1,152 @@ +//! Finalizes V1 program deployment from a buffer account. +//! +//! V1 programs (legacy bpf_loader) are converted to V3 (bpf_loader_upgradeable) format +//! since the ephemeral validator only supports upgradeable loader for V1 programs. + +use std::collections::HashSet; + +use solana_account::{ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_loader_v3_interface::state::UpgradeableLoaderState; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_pubkey::Pubkey; +use solana_sdk_ids::bpf_loader_upgradeable; +use solana_sysvar::rent::Rent; +use solana_transaction_context::TransactionContext; + +use super::{adjust_authority_lamports, validate_authority}; + +/// Finalizes a V1 program from a buffer account, converting to V3 (upgradeable loader) format. +/// +/// # Why V3? +/// +/// The ephemeral validator doesn't support legacy bpf_loader (V1). We convert V1 programs +/// to the upgradeable loader format (V3) which is backwards compatible - the program ID +/// remains the same, and the program_data account is derived from it. +/// +/// # Flow +/// +/// 1. Reads ELF data from buffer account +/// 2. Creates program_data account with V3 `ProgramData` header + ELF +/// 3. Creates program account with V3 `Program` header (points to program_data) +/// 4. Closes buffer account +/// +/// # Account Structure +/// +/// ```text +/// program_account (executable): +/// - owner: bpf_loader_upgradeable +/// - data: [Program { programdata_address }] +/// +/// program_data_account: +/// - owner: bpf_loader_upgradeable +/// - data: [ProgramData { slot, authority }] + [ELF] +/// ``` +/// +/// # Lamports Accounting +/// +/// Both program and program_data accounts get rent-exempt lamports. +/// The buffer's lamports are returned to the validator authority. +pub(crate) fn process_finalize_v1_program_from_buffer( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + slot: u64, + authority: Pubkey, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + let ctx = transaction_context.get_current_instruction_context()?; + let auth_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(0)?, + )?; + let prog_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(1)?, + )?; + let data_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(2)?, + )?; + let buf_acc = transaction_context.get_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(3)?, + )?; + + let prog_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(1)?, + )?; + let data_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(2)?, + )?; + let buf_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(3)?, + )?; + + let elf_data = buf_acc.borrow().data().to_vec(); + ic_msg!(invoke_context, "FinalizeV1: prog={} data={} buf={} len={}", prog_key, data_key, buf_key, elf_data.len()); + + // Build V3 program_data account: ProgramData header + ELF + let program_data_content = { + let state = UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: Some(authority), + }; + let mut data = bincode::serialize(&state).map_err(|_| InstructionError::InvalidAccountData)?; + data.extend_from_slice(&elf_data); + data + }; + + // Build V3 program account: Program header pointing to program_data + let program_content = { + let state = UpgradeableLoaderState::Program { + programdata_address: data_key, + }; + bincode::serialize(&state).map_err(|_| InstructionError::InvalidAccountData)? + }; + + // Calculate rent-exempt lamports for both accounts + let rent = Rent::default(); + let data_lamports = rent.minimum_balance(program_data_content.len()); + let prog_lamports = rent.minimum_balance(program_content.len()).max(1); + + let prog_current = prog_acc.borrow().lamports(); + let data_current = data_acc.borrow().lamports(); + let buf_current = buf_acc.borrow().lamports(); + let lamports_delta = (prog_lamports as i64 - prog_current as i64) + + (data_lamports as i64 - data_current as i64) + - buf_current as i64; + + // Set up program_data account + { + let mut acc = data_acc.borrow_mut(); + acc.set_lamports(data_lamports); + acc.set_owner(bpf_loader_upgradeable::id()); + acc.set_executable(false); + acc.set_data_from_slice(&program_data_content); + acc.set_remote_slot(slot); + acc.set_undelegating(false); + } + + // Set up program account (executable) + { + let mut acc = prog_acc.borrow_mut(); + acc.set_lamports(prog_lamports); + acc.set_owner(bpf_loader_upgradeable::id()); + acc.set_executable(true); + acc.set_data_from_slice(&program_content); + acc.set_remote_slot(slot); + acc.set_undelegating(false); + } + + // Close buffer account + { + let mut buf = buf_acc.borrow_mut(); + buf.set_lamports(0); + buf.set_data_from_slice(&[]); + buf.set_executable(false); + buf.set_owner(Pubkey::default()); + } + + adjust_authority_lamports(auth_acc, lamports_delta)?; + ic_msg!(invoke_context, "FinalizeV1: created {} and {}, closed {}", prog_key, data_key, buf_key); + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/process_set_authority.rs b/programs/magicblock/src/clone_account/process_set_authority.rs new file mode 100644 index 000000000..d7ffc4f44 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_set_authority.rs @@ -0,0 +1,74 @@ +//! Updates the authority in a LoaderV4 program header. + +use std::collections::HashSet; + +use solana_account::{ReadableAccount, WritableAccount}; +use solana_instruction::error::InstructionError; +use solana_loader_v4_interface::state::LoaderV4State; +use solana_log_collector::ic_msg; +use solana_program_runtime::invoke_context::InvokeContext; +use solana_pubkey::Pubkey; +use solana_sdk_ids::loader_v4; +use solana_transaction_context::TransactionContext; + +use super::validate_authority; + +/// Updates the authority field in a LoaderV4 program's header. +/// +/// Called after `LoaderV4::Deploy` to set the final authority (from the remote chain). +/// The validator authority is set temporarily during finalize, then replaced with the +/// chain's authority so that the program behaves identically to the remote version. +pub(crate) fn process_set_program_authority( + signers: &HashSet, + invoke_context: &InvokeContext, + transaction_context: &TransactionContext, + new_authority: Pubkey, +) -> Result<(), InstructionError> { + validate_authority(signers, invoke_context)?; + + let ctx = transaction_context.get_current_instruction_context()?; + let idx = ctx.get_index_of_instruction_account_in_transaction(1)?; + let account = transaction_context.get_account_at_index(idx)?; + let key = *transaction_context.get_key_of_account_at_index(idx)?; + + // Verify loader v4 ownership + { + let acc = account.borrow(); + if acc.owner() != &loader_v4::id() { + ic_msg!(invoke_context, "SetProgramAuthority: {} not owned by loader_v4", key); + return Err(InstructionError::InvalidAccountOwner); + } + } + + // Update authority in header + let mut acc = account.borrow_mut(); + let data = acc.data(); + let header_size = LoaderV4State::program_data_offset(); + + if data.len() < header_size { + ic_msg!(invoke_context, "SetProgramAuthority: account data too small"); + return Err(InstructionError::InvalidAccountData); + } + + // SAFETY: LoaderV4State is POD + let current: LoaderV4State = unsafe { + std::ptr::read_unaligned(data.as_ptr() as *const LoaderV4State) + }; + + let new_state = LoaderV4State { + slot: current.slot, + authority_address_or_next_version: new_authority, + status: current.status, + }; + + let header: &[u8] = unsafe { + std::slice::from_raw_parts( + (&new_state as *const LoaderV4State) as *const u8, + header_size, + ) + }; + acc.data_as_mut_slice()[..header_size].copy_from_slice(header); + + ic_msg!(invoke_context, "SetProgramAuthority: {} authority -> {}", key, new_authority); + Ok(()) +} diff --git a/programs/magicblock/src/errors.rs b/programs/magicblock/src/errors.rs index 60e9156cd..55d59569f 100644 --- a/programs/magicblock/src/errors.rs +++ b/programs/magicblock/src/errors.rs @@ -30,23 +30,17 @@ pub enum MagicBlockProgramError { #[error("MagicBlock authority needs to be owned by system program")] MagicBlockAuthorityNeedsToBeOwnedBySystemProgram, - #[error("The account resolution for the provided key failed.")] - AccountDataResolutionFailed, - - #[error("The account data for the provided key is missing both from in-memory and ledger storage.")] + #[error("The account data for the provided key is missing.")] AccountDataMissing, - #[error("The account data for the provided key is missing from in-memory and we are not replaying the ledger.")] - AccountDataMissingFromMemory, - - #[error("Tried to persist data that could not be resolved.")] - AttemptedToPersistUnresolvedData, + #[error("Account already has a pending clone in progress")] + CloneAlreadyPending, - #[error("Tried to persist data that was resolved from storage.")] - AttemptedToPersistDataFromStorage, + #[error("No pending clone found for account")] + NoPendingClone, - #[error("Encountered an error when persisting account modification data.")] - FailedToPersistAccountModData, + #[error("Clone offset mismatch")] + CloneOffsetMismatch, #[error("The account is delegated and not currently undelegating.")] AccountIsDelegatedAndNotUndelegating, @@ -55,4 +49,9 @@ pub enum MagicBlockProgramError { "Remote slot updates cannot be older than the current remote slot." )] IncomingRemoteSlotIsOlderThanCurrentRemoteSlot, + #[error("Buffer account not found for program finalization")] + BufferAccountNotFound, + + #[error("Failed to deploy program via LoaderV4")] + ProgramDeployFailed, } diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 725ec69fd..12b1995ca 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -1,5 +1,6 @@ mod ephemeral_accounts; pub mod errors; +mod clone_account; mod magic_context; pub mod magic_sys; mod mutate_accounts; diff --git a/programs/magicblock/src/magic_sys.rs b/programs/magicblock/src/magic_sys.rs index 0fed11e79..830e4d731 100644 --- a/programs/magicblock/src/magic_sys.rs +++ b/programs/magicblock/src/magic_sys.rs @@ -31,7 +31,7 @@ pub fn init_magic_sys(magic_sys: Arc) { .replace(magic_sys); } -pub(crate) fn load_data(id: u64) -> Result>, Box> { +pub fn load_data(id: u64) -> Result>, Box> { MAGIC_SYS .read() .expect(MAGIC_SYS_POISONED_MSG) @@ -40,10 +40,7 @@ pub(crate) fn load_data(id: u64) -> Result>, Box> { .load(id) } -pub(crate) fn persist_data( - id: u64, - data: Vec, -) -> Result<(), Box> { +pub fn persist_data(id: u64, data: Vec) -> Result<(), Box> { MAGIC_SYS .read() .expect(MAGIC_SYS_POISONED_MSG) diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 7070aadc4..4077eab78 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -2,6 +2,12 @@ use magicblock_magic_program_api::instruction::MagicBlockInstruction; use solana_program_runtime::declare_process_instruction; use crate::{ + clone_account::{ + process_cleanup_partial_clone, process_clone_account, + process_clone_account_continue, process_clone_account_init, + process_finalize_program_from_buffer, + process_finalize_v1_program_from_buffer, process_set_program_authority, + }, ephemeral_accounts::{ process_close_ephemeral_account, process_create_ephemeral_account, process_resize_ephemeral_account, @@ -118,6 +124,75 @@ declare_process_instruction!( transaction_context, ), Noop(_) => Ok(()), + CloneAccount { + pubkey, + data, + fields, + } => process_clone_account( + &signers, + invoke_context, + transaction_context, + pubkey, + data, + fields, + ), + CloneAccountInit { + pubkey, + total_data_len, + initial_data, + fields, + } => process_clone_account_init( + &signers, + invoke_context, + transaction_context, + pubkey, + total_data_len, + initial_data, + fields, + ), + CloneAccountContinue { + pubkey, + offset, + data, + is_last, + } => process_clone_account_continue( + &signers, + invoke_context, + transaction_context, + pubkey, + offset, + data, + is_last, + ), + CleanupPartialClone { pubkey } => process_cleanup_partial_clone( + &signers, + invoke_context, + transaction_context, + pubkey, + ), + FinalizeProgramFromBuffer { slot } => { + process_finalize_program_from_buffer( + &signers, + invoke_context, + transaction_context, + slot, + ) + } + SetProgramAuthority { authority } => process_set_program_authority( + &signers, + invoke_context, + transaction_context, + authority, + ), + FinalizeV1ProgramFromBuffer { slot, authority } => { + process_finalize_v1_program_from_buffer( + &signers, + invoke_context, + transaction_context, + slot, + authority, + ) + } } } ); diff --git a/programs/magicblock/src/mutate_accounts/account_mod_data.rs b/programs/magicblock/src/mutate_accounts/account_mod_data.rs deleted file mode 100644 index 2df4cf6ae..000000000 --- a/programs/magicblock/src/mutate_accounts/account_mod_data.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::{ - collections::HashMap, - sync::{ - atomic::{AtomicU64, Ordering}, - Mutex, - }, -}; - -use lazy_static::lazy_static; -use solana_log_collector::ic_msg; -use solana_program_runtime::invoke_context::InvokeContext; - -use crate::{ - errors::MagicBlockProgramError, - magic_sys::{load_data, persist_data}, - validator, -}; - -lazy_static! { - /// In order to modify large data chunks we cannot include all the data as part of the - /// transaction. - /// Instead we register data here _before_ invoking the actual instruction and when it is - /// processed it resolved that data from the key that we provide in its place. - static ref DATA_MODS: Mutex>> = Mutex::default(); - - static ref DATA_MOD_ID: AtomicU64 = AtomicU64::new(0); - - static ref MAX_REPLAY_DATA_MOD_ID: Mutex> = Mutex::default(); -} - -/// We capture the max id we see during ledger replay and use it to assign -/// it to the [DATA_MOD_ID] once the validator starts running. -/// As a result once the [DATA_MOD_ID] has the same value as it did -/// when the initial validator instance stopped. -fn update_max_seen_data_id(next_id: u64) { - let mut max_id_lock = MAX_REPLAY_DATA_MOD_ID - .lock() - .expect("MAX_REPLAY_DATA_MOD_ID Mutex poisoned"); - let max = match *max_id_lock { - None => next_id, - Some(current_max) => current_max.max(next_id), - }; - max_id_lock.replace(max); -} - -pub fn get_account_mod_data_id() -> u64 { - { - // NOTE: we keep the lock while we update the DATA_MOD_ID in order to prevent another - // thread seeing `MAX_REPLAY_DATA_MOD_ID` as `None` and continuing to use the original - // `DATA_MOD_ID` value. - let mut max_id_lock = MAX_REPLAY_DATA_MOD_ID - .lock() - .expect("MAX_REPLAY_DATA_MOD_ID Mutex poisoned"); - if let Some(max_mod_id) = max_id_lock.take() { - DATA_MOD_ID.store(max_mod_id + 1, Ordering::SeqCst); - } - } - - DATA_MOD_ID.fetch_add(1, Ordering::SeqCst) -} - -pub(crate) fn set_account_mod_data(data: Vec) -> u64 { - let id = get_account_mod_data_id(); - DATA_MODS - .lock() - .expect("DATA_MODS poisoned") - .insert(id, data); - id -} - -pub(super) fn get_data(id: u64) -> Option> { - DATA_MODS.lock().expect("DATA_MODS poisoned").remove(&id) -} - -/// The resolved data including an indication about how it was resolved. -pub(super) enum ResolvedAccountModData { - /// The data was resolved from memory while the validator was processing - /// mutation transactions. - FromMemory { id: u64, data: Vec }, - /// The data was resolved from the ledger while replaying transactions. - FromStorage { id: u64, data: Vec }, - /// The data was not found in either memory or storage which means the - /// transaction is invalid. - NotFound { id: u64 }, -} - -impl ResolvedAccountModData { - pub fn id(&self) -> u64 { - use ResolvedAccountModData::*; - match self { - FromMemory { id, .. } => *id, - FromStorage { id, .. } => *id, - NotFound { id } => *id, - } - } - - pub fn data(&self) -> Option<&[u8]> { - use ResolvedAccountModData::*; - match self { - FromMemory { data, .. } => Some(data), - FromStorage { data, .. } => Some(data), - NotFound { .. } => None, - } - } - - pub fn persist( - self, - invoke_context: &InvokeContext, - ) -> Result<(), MagicBlockProgramError> { - use ResolvedAccountModData::*; - let (id, data) = match self { - FromMemory { id, data } => (id, data), - FromStorage { id, .. } => { - ic_msg!( - invoke_context, - "MutateAccounts: trying to persist data that came from storage with id: {}", - id - ); - return Err( - MagicBlockProgramError::AttemptedToPersistDataFromStorage, - ); - } - // Even though it is a developer error to call this method on NotFound - // we don't panic here, but let the mutate transaction fail by returning - // an error result. - NotFound { id } => { - ic_msg!( - invoke_context, - "MutateAccounts: trying to persist unresolved with id: {}", - id - ); - return Err( - MagicBlockProgramError::AttemptedToPersistUnresolvedData, - ); - } - }; - - persist_data(id, data).map_err(|err| { - ic_msg!( - invoke_context, - "MutateAccounts: failed to persist account mod data: {}", - err.to_string() - ); - MagicBlockProgramError::FailedToPersistAccountModData - })?; - - Ok(()) - } - - pub fn is_from_memory(&self) -> bool { - matches!(self, ResolvedAccountModData::FromMemory { .. }) - } -} - -pub(super) fn resolve_account_mod_data( - id: u64, - invoke_context: &InvokeContext, -) -> Result { - if let Some(data) = get_data(id) { - Ok(ResolvedAccountModData::FromMemory { id, data }) - } else if validator::is_starting_up() { - match load_data(id).map_err(|err| { - ic_msg!( - invoke_context, - "MutateAccounts: failed to load account mod data: {}", - err.to_string() - ); - MagicBlockProgramError::AccountDataResolutionFailed - })? { - Some(data) => { - update_max_seen_data_id(id); - Ok(ResolvedAccountModData::FromStorage { id, data }) - } - None => Ok(ResolvedAccountModData::NotFound { id }), - } - } else { - // We only load account data from the ledger while we are replaying transactions - // from that ledger. - // Afterwards the data needs to be added to the memory map before running the - // transaction. - ic_msg!( - invoke_context, - "MutateAccounts: failed to load account mod data: {} from memory after validator started up", - id, - ); - Err(MagicBlockProgramError::AccountDataMissingFromMemory) - } -} diff --git a/programs/magicblock/src/mutate_accounts/mod.rs b/programs/magicblock/src/mutate_accounts/mod.rs index 530cd1986..8ddc71c93 100644 --- a/programs/magicblock/src/mutate_accounts/mod.rs +++ b/programs/magicblock/src/mutate_accounts/mod.rs @@ -1,4 +1,2 @@ -mod account_mod_data; mod process_mutate_accounts; -pub(crate) use account_mod_data::*; pub(crate) use process_mutate_accounts::process_mutate_accounts; diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 3b93d7b2d..d402f2230 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -10,9 +10,7 @@ use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use crate::{ - errors::MagicBlockProgramError, - mutate_accounts::account_mod_data::resolve_account_mod_data, - validator::validator_authority_id, + errors::MagicBlockProgramError, validator::validator_authority_id, }; pub(crate) fn process_mutate_accounts( @@ -99,7 +97,6 @@ pub(crate) fn process_mutate_accounts( let mut lamports_to_debit: i128 = 0; // 2. Apply account modifications - let mut memory_data_mods = Vec::new(); for idx in 0..account_mods_len { // NOTE: first account is the MagicBlock authority, account mods start at second account let account_idx = (idx + 1) as u16; @@ -209,46 +206,13 @@ pub(crate) fn process_mutate_accounts( ); account.borrow_mut().set_executable(executable); } - if let Some(data_key) = modification.data_key.take() { - let resolved_data = resolve_account_mod_data( - data_key, + if let Some(data) = modification.data.take() { + ic_msg!( invoke_context, - ).inspect_err(|err| { - ic_msg!( - invoke_context, - "MutateAccounts: an error occurred when resolving account mod data for the provided key {}. Error: {:?}", - data_key, - err - ); - })?; - if let Some(data) = resolved_data.data() { - ic_msg!( - invoke_context, - "MutateAccounts: resolved data from id {}", - resolved_data.id() - ); - ic_msg!( - invoke_context, - "MutateAccounts: setting data to len {}", - data.len() - ); - account.borrow_mut().set_data_from_slice(data); - } else { - ic_msg!( - invoke_context, - "MutateAccounts: account data for the provided key {} is missing", - data_key - ); - return Err(MagicBlockProgramError::AccountDataMissing.into()); - } - - // We track resolved data mods in order to persist them at the end - // of the transaction. - // NOTE: that during ledger replay all mods came from storage, so we - // don't persist them again. - if resolved_data.is_from_memory() { - memory_data_mods.push(resolved_data); - } + "MutateAccounts: setting data to len {}", + data.len() + ); + account.borrow_mut().set_data_from_slice(&data); } if let Some(delegated) = modification.delegated { ic_msg!( @@ -316,22 +280,6 @@ 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 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 - .persist(invoke_context) - .inspect_err(|err| { - ic_msg!( - invoke_context, - "MutateAccounts: an error occurred when persisting account mod data. Error: {:?}", - err - ); - })?; - } - Ok(()) } diff --git a/programs/magicblock/src/test_utils/mod.rs b/programs/magicblock/src/test_utils/mod.rs index 0bf1b5a55..8f3dcada9 100644 --- a/programs/magicblock/src/test_utils/mod.rs +++ b/programs/magicblock/src/test_utils/mod.rs @@ -1,7 +1,7 @@ +use core::fmt; use std::{ collections::HashMap, error::Error, - fmt, sync::{ atomic::{AtomicU64, Ordering}, Arc, diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index eb42ab0ba..d86375636 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -18,10 +18,7 @@ use solana_pubkey::Pubkey; use solana_signer::Signer; use solana_transaction::Transaction; -use crate::{ - mutate_accounts::set_account_mod_data, - validator::{validator_authority, validator_authority_id}, -}; +use crate::validator::{validator_authority, validator_authority_id}; pub struct InstructionUtils; impl InstructionUtils { @@ -178,9 +175,7 @@ impl InstructionUtils { lamports: account_modification.lamports, owner: account_modification.owner, executable: account_modification.executable, - data_key: account_modification - .data - .map(set_account_mod_data), + data: account_modification.data, delegated: account_modification.delegated, confined: account_modification.confined, remote_slot: account_modification.remote_slot, diff --git a/tools/ledger-stats/src/counts.rs b/tools/ledger-stats/src/counts.rs index a00b7153e..c87f2ec6b 100644 --- a/tools/ledger-stats/src/counts.rs +++ b/tools/ledger-stats/src/counts.rs @@ -43,10 +43,6 @@ pub(crate) fn print_counts(ledger: &Ledger) { .count_perf_samples() .expect("Failed to count perf samples") .to_formatted_string(&Locale::en); - let account_mod_data_count = ledger - .count_account_mod_data() - .expect("Failed to count account mod datas") - .to_formatted_string(&Locale::en); let table = Table::new("{:<} {:>}") .with_row(Row::new().with_cell("Column").with_cell("Count")) @@ -90,11 +86,6 @@ pub(crate) fn print_counts(ledger: &Ledger) { .with_cell("SlotSignatures") .with_cell(slot_signatures_count), ) - .with_row( - Row::new() - .with_cell("AccountModDatas") - .with_cell(account_mod_data_count), - ) .with_row( Row::new() .with_cell("AddressSignatures") From 837247fc02fa66f103408d8653c9bae3d8b0caec Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 18 Feb 2026 18:34:40 +0400 Subject: [PATCH 02/16] fix: add race condition guards those should prevent account overwrites due to racy account udpates from chainlink --- .../magicblock/src/clone_account/common.rs | 76 +++++++++++++++---- .../src/clone_account/process_clone.rs | 7 +- .../src/clone_account/process_clone_init.rs | 7 +- programs/magicblock/src/errors.rs | 20 +++-- .../process_mutate_accounts.rs | 67 ++++++---------- 5 files changed, 106 insertions(+), 71 deletions(-) diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index a4bb890e4..fa4a32e4f 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -1,4 +1,4 @@ -//! Shared utilities for clone account instruction processing. +//! Shared utilities for clone account and mutate account instruction processing. use std::{cell::RefCell, collections::HashSet}; @@ -9,10 +9,10 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; +use crate::errors::MagicBlockProgramError; use crate::validator::validator_authority_id; /// Validates that the validator authority has signed the transaction. -/// All clone instructions require this signature for authorization. pub fn validate_authority( signers: &HashSet, invoke_context: &InvokeContext, @@ -44,18 +44,66 @@ pub fn validate_and_get_index( Err(InstructionError::InvalidArgument) } -/// Adjusts validator authority lamports to maintain balanced transactions. -/// -/// # Lamports Flow -/// -/// When cloning an account, we set the target account's lamports to the required rent-exempt -/// amount. The difference (delta) between new and old lamports is debited/credited from the -/// validator authority account: -/// -/// - `delta > 0`: Account gained lamports → debit from authority -/// - `delta < 0`: Account lost lamports → credit to authority -/// -/// This ensures the runtime doesn't complain about unbalanced transactions. +/// Returns true if account is ephemeral (exists locally on ER only). +pub fn is_ephemeral(account: &RefCell) -> bool { + account.borrow().ephemeral() +} + +/// Validates that a delegated account is undelegating (mutation allowed). +pub fn validate_not_delegated( + account: &RefCell, + pubkey: &Pubkey, + invoke_context: &InvokeContext, +) -> Result<(), InstructionError> { + let (is_delegated, is_undelegating) = { + let acc = account.borrow(); + (acc.delegated(), acc.undelegating()) + }; + if is_delegated && !is_undelegating { + ic_msg!(invoke_context, "Account {} is delegated and not undelegating", pubkey); + return Err(MagicBlockProgramError::AccountIsDelegated.into()); + } + Ok(()) +} + +/// Validates that the account can be mutated (not ephemeral, not active delegated). +pub fn validate_mutable( + account: &RefCell, + pubkey: &Pubkey, + invoke_context: &InvokeContext, +) -> Result<(), InstructionError> { + if is_ephemeral(account) { + ic_msg!(invoke_context, "Account {} is ephemeral and cannot be mutated", pubkey); + return Err(MagicBlockProgramError::AccountIsEphemeral.into()); + } + validate_not_delegated(account, pubkey, invoke_context) +} + +/// Validates that incoming remote_slot is not older than current. +/// Skips check if incoming_remote_slot is None. +pub fn validate_remote_slot( + account: &RefCell, + pubkey: &Pubkey, + incoming_remote_slot: Option, + invoke_context: &InvokeContext, +) -> Result<(), InstructionError> { + let Some(incoming) = incoming_remote_slot else { + return Ok(()); + }; + let current = account.borrow().remote_slot(); + if incoming < current { + ic_msg!( + invoke_context, + "Account {} incoming remote_slot {} is older than current {}; rejected", + pubkey, incoming, current + ); + return Err(MagicBlockProgramError::OutOfOrderUpdate.into()); + } + Ok(()) +} + +/// Adjusts validator authority lamports by delta. +/// Positive delta = debit, negative delta = credit. pub fn adjust_authority_lamports( auth_acc: &RefCell, delta: i64, diff --git a/programs/magicblock/src/clone_account/process_clone.rs b/programs/magicblock/src/clone_account/process_clone.rs index cbb103af3..5b46a1674 100644 --- a/programs/magicblock/src/clone_account/process_clone.rs +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -10,7 +10,7 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{adjust_authority_lamports, validate_and_get_index, validate_authority}; +use super::{adjust_authority_lamports, validate_and_get_index, validate_authority, validate_mutable, validate_remote_slot}; /// Clones an account atomically in a single transaction. /// @@ -34,6 +34,11 @@ pub(crate) fn process_clone_account( let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccount", invoke_context)?; let account = transaction_context.get_account_at_index(tx_idx)?; + // Prevent overwriting ephemeral or active delegated accounts + validate_mutable(&account, &pubkey, invoke_context)?; + // Prevent stale updates from overwriting fresher data + validate_remote_slot(&account, &pubkey, Some(fields.remote_slot), invoke_context)?; + ic_msg!(invoke_context, "CloneAccount: cloning '{}', data_len={}", pubkey, data.len()); let current_lamports = account.borrow().lamports(); diff --git a/programs/magicblock/src/clone_account/process_clone_init.rs b/programs/magicblock/src/clone_account/process_clone_init.rs index 775d8bd64..43e31ec19 100644 --- a/programs/magicblock/src/clone_account/process_clone_init.rs +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -10,7 +10,7 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{add_pending_clone, adjust_authority_lamports, is_pending_clone, validate_and_get_index, validate_authority}; +use super::{add_pending_clone, adjust_authority_lamports, is_pending_clone, validate_and_get_index, validate_authority, validate_mutable, validate_remote_slot}; use crate::errors::MagicBlockProgramError; /// Initializes a multi-transaction clone for a large account. @@ -57,6 +57,11 @@ pub(crate) fn process_clone_account_init( let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccountInit", invoke_context)?; let account = transaction_context.get_account_at_index(tx_idx)?; + // Prevent overwriting ephemeral or active delegated accounts + validate_mutable(&account, &pubkey, invoke_context)?; + // Prevent stale updates from overwriting fresher data + validate_remote_slot(&account, &pubkey, Some(fields.remote_slot), invoke_context)?; + ic_msg!( invoke_context, "CloneAccountInit: initializing '{}', total_len={}, initial_len={}", diff --git a/programs/magicblock/src/errors.rs b/programs/magicblock/src/errors.rs index 55d59569f..6062e53ce 100644 --- a/programs/magicblock/src/errors.rs +++ b/programs/magicblock/src/errors.rs @@ -21,7 +21,7 @@ pub enum MagicBlockProgramError { #[error("number of accounts to modify needs to match number of account modifications")] AccountsToModifyNotMatchingAccountModifications, - #[error("The account modification for the provided key is missing.")] + #[error("The account modification for the provided key is missing")] AccountModificationMissing, #[error("first account needs to be MagicBlock authority")] @@ -30,7 +30,7 @@ pub enum MagicBlockProgramError { #[error("MagicBlock authority needs to be owned by system program")] MagicBlockAuthorityNeedsToBeOwnedBySystemProgram, - #[error("The account data for the provided key is missing.")] + #[error("The account data for the provided key is missing")] AccountDataMissing, #[error("Account already has a pending clone in progress")] @@ -42,16 +42,14 @@ pub enum MagicBlockProgramError { #[error("Clone offset mismatch")] CloneOffsetMismatch, - #[error("The account is delegated and not currently undelegating.")] - AccountIsDelegatedAndNotUndelegating, + #[error("The account is delegated and not currently undelegating")] + AccountIsDelegated, - #[error( - "Remote slot updates cannot be older than the current remote slot." - )] - IncomingRemoteSlotIsOlderThanCurrentRemoteSlot, + #[error("The account is ephemeral and cannot be mutated")] + AccountIsEphemeral, + + #[error("Updates cannot be older than the current remote slot")] + OutOfOrderUpdate, #[error("Buffer account not found for program finalization")] BufferAccountNotFound, - - #[error("Failed to deploy program via LoaderV4")] - ProgramDeployFailed, } diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index d402f2230..4cfe1a6dc 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -10,7 +10,11 @@ use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use crate::{ - errors::MagicBlockProgramError, validator::validator_authority_id, + clone_account::{ + is_ephemeral, validate_not_delegated, validate_remote_slot, + }, + errors::MagicBlockProgramError, + validator::validator_authority_id, }; pub(crate) fn process_mutate_accounts( @@ -104,9 +108,9 @@ 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() { + + // Skip ephemeral accounts (exist locally on ER only) + if is_ephemeral(&account) { let key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; account_mods.remove(key); @@ -117,6 +121,7 @@ pub(crate) fn process_mutate_accounts( ); continue; } + let account_key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; @@ -132,51 +137,26 @@ pub(crate) fn process_mutate_accounts( ic_msg!( invoke_context, "MutateAccounts: modifying '{}'.", - account_key, + account_key ); - // If provided log the extra message to give more context to the user, i.e. - // why an account is not cloned as delegated, etc. + // If provided log the extra message to give more context to the user if let Some(ref msg) = message { ic_msg!(invoke_context, "MutateAccounts: {}", msg); } - let (is_delegated, is_undelegating) = { - let account_ref = account.borrow(); - (account_ref.delegated(), account_ref.undelegating()) - }; - if is_delegated && !is_undelegating { - ic_msg!( - invoke_context, - "MutateAccounts: account {} is delegated and not undelegating; mutation is forbidden", - account_key - ); - return Err( - MagicBlockProgramError::AccountIsDelegatedAndNotUndelegating - .into(), - ); - } - - let current_remote_slot = account.borrow().remote_slot(); - if let Some(incoming_remote_slot) = modification.remote_slot { - if incoming_remote_slot < current_remote_slot { - ic_msg!( - invoke_context, - "MutateAccounts: account {} incoming remote_slot {} is older than current remote_slot {}; mutation is forbidden", - account_key, - incoming_remote_slot, - current_remote_slot - ); - return Err( - MagicBlockProgramError::IncomingRemoteSlotIsOlderThanCurrentRemoteSlot - .into(), - ); - } - } + // Validate account is mutable + validate_not_delegated(&account, account_key, invoke_context)?; + validate_remote_slot( + &account, + account_key, + modification.remote_slot, + invoke_context, + )?; // While an account is undelegating and the delegation is not completed, // we will never clone/mutate it. Thus we can safely untoggle this flag - // here. + // here AFTER validation passes. account.borrow_mut().set_undelegating(false); if let Some(lamports) = modification.lamports { @@ -261,7 +241,7 @@ pub(crate) fn process_mutate_accounts( .map_err(|err| { ic_msg!( invoke_context, - "MutateAccounts: too much lamports in authority to credit: {}", + "MutateAccounts: too many lamports in authority to credit: {}", err ); err @@ -517,8 +497,7 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Err(MagicBlockProgramError::AccountIsDelegatedAndNotUndelegating - .into()), + Err(MagicBlockProgramError::AccountIsDelegated.into()), ); } @@ -813,7 +792,7 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Err(MagicBlockProgramError::IncomingRemoteSlotIsOlderThanCurrentRemoteSlot.into()), + Err(MagicBlockProgramError::OutOfOrderUpdate.into()), ); } From 365aa735a1ed97681f451fdd59a0ec7d25892d58 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 18 Feb 2026 18:46:29 +0400 Subject: [PATCH 03/16] chore: fixes fmt and clippy --- magicblock-account-cloner/src/lib.rs | 158 ++++++++++++++---- .../magicblock/src/clone_account/common.rs | 30 +++- programs/magicblock/src/clone_account/mod.rs | 5 +- .../src/clone_account/process_cleanup.rs | 19 ++- .../src/clone_account/process_clone.rs | 29 +++- .../clone_account/process_clone_continue.rs | 22 ++- .../src/clone_account/process_clone_init.rs | 33 +++- .../clone_account/process_finalize_buffer.rs | 24 ++- .../process_finalize_v1_buffer.rs | 23 ++- .../clone_account/process_set_authority.rs | 18 +- programs/magicblock/src/lib.rs | 2 +- .../process_mutate_accounts.rs | 6 +- 12 files changed, 289 insertions(+), 80 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index b4a5edb9f..af0d45b17 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -56,8 +56,7 @@ use solana_account::ReadableAccount; use solana_hash::Hash; use solana_instruction::{AccountMeta, Instruction}; use solana_loader_v4_interface::{ - instruction::LoaderV4Instruction, - state::LoaderV4Status, + instruction::LoaderV4Instruction, state::LoaderV4Status, }; use solana_pubkey::Pubkey; use solana_sdk_ids::{bpf_loader_upgradeable, loader_v4}; @@ -92,7 +91,13 @@ impl ChainlinkCloner { accounts_db: Arc, block: LatestBlock, ) -> Self { - Self { changeset_committor, config, tx_scheduler, accounts_db, block } + Self { + changeset_committor, + config, + tx_scheduler, + accounts_db, + block, + } } // ----------------- @@ -107,17 +112,30 @@ impl ChainlinkCloner { fn sign_tx(&self, ixs: &[Instruction], blockhash: Hash) -> Transaction { let kp = validator_authority(); - Transaction::new_signed_with_payer(ixs, Some(&kp.pubkey()), &[&kp], blockhash) + Transaction::new_signed_with_payer( + ixs, + Some(&kp.pubkey()), + &[&kp], + blockhash, + ) } // ----------------- // Instruction Builders // ----------------- - fn clone_ix(pubkey: Pubkey, data: Vec, fields: AccountCloneFields) -> Instruction { + fn clone_ix( + pubkey: Pubkey, + data: Vec, + fields: AccountCloneFields, + ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, - &MagicBlockInstruction::CloneAccount { pubkey, data, fields }, + &MagicBlockInstruction::CloneAccount { + pubkey, + data, + fields, + }, clone_account_metas(pubkey), ) } @@ -148,7 +166,12 @@ impl ChainlinkCloner { ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, - &MagicBlockInstruction::CloneAccountContinue { pubkey, offset, data, is_last }, + &MagicBlockInstruction::CloneAccountContinue { + pubkey, + offset, + data, + is_last, + }, clone_account_metas(pubkey), ) } @@ -161,7 +184,11 @@ impl ChainlinkCloner { ) } - fn finalize_program_ix(program: Pubkey, buffer: Pubkey, slot: u64) -> Instruction { + fn finalize_program_ix( + program: Pubkey, + buffer: Pubkey, + slot: u64, + ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, &MagicBlockInstruction::FinalizeProgramFromBuffer { slot }, @@ -209,7 +236,11 @@ impl ChainlinkCloner { blockhash: Hash, ) -> Transaction { let fields = Self::clone_fields(request); - let clone_ix = Self::clone_ix(request.pubkey, request.account.data().to_vec(), fields); + let clone_ix = Self::clone_ix( + request.pubkey, + request.account.data().to_vec(), + fields, + ); // TODO(#625): Re-enable frequency commits when proper limits are in place: // 1. Allow configuring a higher minimum frequency @@ -226,7 +257,10 @@ impl ChainlinkCloner { /// Builds crank commits instruction for periodic account commits. /// Currently disabled - see https://github.com/magicblock-labs/magicblock-validator/issues/625 #[allow(dead_code)] - fn build_crank_commits_ix(pubkey: Pubkey, commit_frequency_ms: i64) -> Instruction { + fn build_crank_commits_ix( + pubkey: Pubkey, + commit_frequency_ms: i64, + ) -> Instruction { let task_id: i64 = rand::random(); let schedule_commit_ix = Instruction::new_with_bincode( magicblock_program::ID, @@ -260,7 +294,12 @@ impl ChainlinkCloner { // Init tx with first chunk let first_chunk = data[..MAX_INLINE_DATA_SIZE.min(data.len())].to_vec(); - let init_ix = Self::clone_init_ix(request.pubkey, data.len() as u32, first_chunk, fields); + let init_ix = Self::clone_init_ix( + request.pubkey, + data.len() as u32, + first_chunk, + fields, + ); txs.push(self.sign_tx(&[init_ix], blockhash)); // Continue txs for remaining chunks @@ -270,7 +309,12 @@ impl ChainlinkCloner { let chunk = data[offset..end].to_vec(); let is_last = end == data.len(); - let continue_ix = Self::clone_continue_ix(request.pubkey, offset as u32, chunk, is_last); + let continue_ix = Self::clone_continue_ix( + request.pubkey, + offset as u32, + chunk, + is_last, + ); txs.push(self.sign_tx(&[continue_ix], blockhash)); offset = end; } @@ -296,7 +340,9 @@ impl ChainlinkCloner { blockhash: Hash, ) -> ClonerResult>> { match program.loader { - RemoteProgramLoader::V1 => self.build_v1_program_txs(program, blockhash), + RemoteProgramLoader::V1 => { + self.build_v1_program_txs(program, blockhash) + } _ => self.build_v4_program_txs(program, blockhash), } } @@ -332,7 +378,9 @@ impl ChainlinkCloner { // Finalization instruction // Must wrap in disable/enable executable check since finalize sets executable=true let finalize_ixs = vec![ - InstructionUtils::disable_executable_check_instruction(&validator_authority_id()), + InstructionUtils::disable_executable_check_instruction( + &validator_authority_id(), + ), Self::finalize_v1_program_ix( program_id, program_data_addr, @@ -340,18 +388,19 @@ impl ChainlinkCloner { slot, chain_authority, ), - InstructionUtils::enable_executable_check_instruction(&validator_authority_id()), + InstructionUtils::enable_executable_check_instruction( + &validator_authority_id(), + ), ]; // Build transactions based on ELF size let txs = if elf_data.len() <= MAX_INLINE_DATA_SIZE { // Small: single transaction with clone + finalize - let ixs = vec![ - Self::clone_ix(buffer_pubkey, elf_data, buffer_fields), - ] - .into_iter() - .chain(finalize_ixs) - .collect::>(); + let ixs = + vec![Self::clone_ix(buffer_pubkey, elf_data, buffer_fields)] + .into_iter() + .chain(finalize_ixs) + .collect::>(); vec![self.sign_tx(&ixs, blockhash)] } else { // Large: multi-transaction flow @@ -377,7 +426,10 @@ impl ChainlinkCloner { ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, - &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { slot, authority }, + &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { + slot, + authority, + }, vec![ AccountMeta::new_readonly(validator_authority_id(), true), AccountMeta::new(program, false), @@ -430,19 +482,25 @@ impl ChainlinkCloner { // Finalization instructions (always in last tx) // Must wrap in disable/enable executable check since finalize sets executable=true let finalize_ixs = vec![ - InstructionUtils::disable_executable_check_instruction(&validator_authority_id()), + InstructionUtils::disable_executable_check_instruction( + &validator_authority_id(), + ), Self::finalize_program_ix(program_id, buffer_pubkey, slot), deploy_ix, Self::set_authority_ix(program_id, chain_authority), - InstructionUtils::enable_executable_check_instruction(&validator_authority_id()), + InstructionUtils::enable_executable_check_instruction( + &validator_authority_id(), + ), ]; // Build transactions based on program_data size let txs = if program_data.len() <= MAX_INLINE_DATA_SIZE { // Small: single transaction - let ixs = vec![ - Self::clone_ix(buffer_pubkey, program_data, buffer_fields), - ] + let ixs = vec![Self::clone_ix( + buffer_pubkey, + program_data, + buffer_fields, + )] .into_iter() .chain(finalize_ixs) .collect::>(); @@ -482,8 +540,11 @@ impl ChainlinkCloner { ); // First chunk via Init - let first_chunk = program_data[..MAX_INLINE_DATA_SIZE.min(program_data.len())].to_vec(); - let init_ix = Self::clone_init_ix(buffer_pubkey, total_len, first_chunk, fields); + let first_chunk = program_data + [..MAX_INLINE_DATA_SIZE.min(program_data.len())] + .to_vec(); + let init_ix = + Self::clone_init_ix(buffer_pubkey, total_len, first_chunk, fields); txs.push(self.sign_tx(&[init_ix], blockhash)); // Continue chunks @@ -504,14 +565,24 @@ impl ChainlinkCloner { finalize_ixs_count = finalize_ixs.len(), "Building final transaction with continue + finalize" ); - let continue_ix = Self::clone_continue_ix(buffer_pubkey, offset as u32, chunk, true); + let continue_ix = Self::clone_continue_ix( + buffer_pubkey, + offset as u32, + chunk, + true, + ); let ixs = vec![continue_ix] .into_iter() .chain(finalize_ixs.clone()) .collect::>(); txs.push(self.sign_tx(&ixs, blockhash)); } else { - let continue_ix = Self::clone_continue_ix(buffer_pubkey, offset as u32, chunk, false); + let continue_ix = Self::clone_continue_ix( + buffer_pubkey, + offset as u32, + chunk, + false, + ); txs.push(self.sign_tx(&[continue_ix], blockhash)); } offset = end; @@ -529,7 +600,10 @@ impl ChainlinkCloner { if self.config.prepare_lookup_tables { let committor = committor.clone(); tokio::spawn(async move { - if let Err(e) = committor.reserve_pubkeys_for_committee(pubkey, owner).await { + if let Err(e) = committor + .reserve_pubkeys_for_committee(pubkey, owner) + .await + { error!(error = ?e, "Failed to reserve lookup tables"); } }); @@ -556,14 +630,20 @@ impl Cloner for ChainlinkCloner { let data_len = request.account.data().len(); if request.account.delegated() { - self.maybe_prepare_lookup_tables(request.pubkey, *request.account.owner()); + self.maybe_prepare_lookup_tables( + request.pubkey, + *request.account.owner(), + ); } // Small account: single tx if data_len <= MAX_INLINE_DATA_SIZE { let tx = self.build_small_account_tx(&request, blockhash); return self.send_tx(tx).await.map_err(|e| { - ClonerError::FailedToCloneRegularAccount(request.pubkey, Box::new(e)) + ClonerError::FailedToCloneRegularAccount( + request.pubkey, + Box::new(e), + ) }); } @@ -594,9 +674,13 @@ impl Cloner for ChainlinkCloner { let blockhash = self.block.load().blockhash; let program_id = program.program_id; - let Some(txs) = self.build_program_txs(program, blockhash).map_err(|e| { - ClonerError::FailedToCreateCloneProgramTransaction(program_id, Box::new(e)) - })? + let Some(txs) = + self.build_program_txs(program, blockhash).map_err(|e| { + ClonerError::FailedToCreateCloneProgramTransaction( + program_id, + Box::new(e), + ) + })? else { // Program was retracted return Ok(Signature::default()); diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index fa4a32e4f..f8aa593db 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -9,8 +9,9 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use crate::errors::MagicBlockProgramError; -use crate::validator::validator_authority_id; +use crate::{ + errors::MagicBlockProgramError, validator::validator_authority_id, +}; /// Validates that the validator authority has signed the transaction. pub fn validate_authority( @@ -21,7 +22,7 @@ pub fn validate_authority( if signers.contains(&auth) { return Ok(()); } - ic_msg!(invoke_context, "Validator authority '{auth}' not in signers"); + ic_msg!(invoke_context, "Validator authority not in signers",); Err(InstructionError::MissingRequiredSignature) } @@ -35,12 +36,19 @@ pub fn validate_and_get_index( invoke_context: &InvokeContext, ) -> Result { let ctx = transaction_context.get_current_instruction_context()?; - let tx_idx = ctx.get_index_of_instruction_account_in_transaction(ix_index)?; + let tx_idx = + ctx.get_index_of_instruction_account_in_transaction(ix_index)?; let key = transaction_context.get_key_of_account_at_index(tx_idx)?; if *key == *expected { return Ok(tx_idx); } - ic_msg!(invoke_context, "{}: key mismatch, expected {}, got {}", name, expected, key); + ic_msg!( + invoke_context, + "{}: key mismatch, expected {}, got {}", + name, + expected, + key + ); Err(InstructionError::InvalidArgument) } @@ -60,7 +68,11 @@ pub fn validate_not_delegated( (acc.delegated(), acc.undelegating()) }; if is_delegated && !is_undelegating { - ic_msg!(invoke_context, "Account {} is delegated and not undelegating", pubkey); + ic_msg!( + invoke_context, + "Account {} is delegated and not undelegating", + pubkey + ); return Err(MagicBlockProgramError::AccountIsDelegated.into()); } Ok(()) @@ -73,7 +85,11 @@ pub fn validate_mutable( invoke_context: &InvokeContext, ) -> Result<(), InstructionError> { if is_ephemeral(account) { - ic_msg!(invoke_context, "Account {} is ephemeral and cannot be mutated", pubkey); + ic_msg!( + invoke_context, + "Account {} is ephemeral and cannot be mutated", + pubkey + ); return Err(MagicBlockProgramError::AccountIsEphemeral.into()); } validate_not_delegated(account, pubkey, invoke_context) diff --git a/programs/magicblock/src/clone_account/mod.rs b/programs/magicblock/src/clone_account/mod.rs index 09535c782..063d0fc50 100644 --- a/programs/magicblock/src/clone_account/mod.rs +++ b/programs/magicblock/src/clone_account/mod.rs @@ -35,11 +35,9 @@ mod process_set_authority; use std::collections::HashSet; +pub(crate) use common::*; use lazy_static::lazy_static; use parking_lot::RwLock; -use solana_pubkey::Pubkey; - -pub(crate) use common::*; pub(crate) use process_cleanup::process_cleanup_partial_clone; pub(crate) use process_clone::process_clone_account; pub(crate) use process_clone_continue::process_clone_account_continue; @@ -47,6 +45,7 @@ pub(crate) use process_clone_init::process_clone_account_init; pub(crate) use process_finalize_buffer::process_finalize_program_from_buffer; pub(crate) use process_finalize_v1_buffer::process_finalize_v1_program_from_buffer; pub(crate) use process_set_authority::process_set_program_authority; +use solana_pubkey::Pubkey; lazy_static! { /// Tracks in-progress multi-transaction clones. diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs index 44320f9f9..1bf17d8d5 100644 --- a/programs/magicblock/src/clone_account/process_cleanup.rs +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -9,7 +9,10 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{adjust_authority_lamports, remove_pending_clone, validate_and_get_index, validate_authority}; +use super::{ + adjust_authority_lamports, remove_pending_clone, validate_and_get_index, + validate_authority, +}; /// Cleans up a failed multi-transaction clone. /// @@ -30,10 +33,20 @@ pub(crate) fn process_cleanup_partial_clone( ctx.get_index_of_instruction_account_in_transaction(0)?, )?; - let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CleanupPartialClone", invoke_context)?; + let tx_idx = validate_and_get_index( + transaction_context, + 1, + &pubkey, + "CleanupPartialClone", + invoke_context, + )?; let account = transaction_context.get_account_at_index(tx_idx)?; - ic_msg!(invoke_context, "CleanupPartialClone: cleaning up '{}'", pubkey); + ic_msg!( + invoke_context, + "CleanupPartialClone: cleaning up '{}'", + pubkey + ); let current_lamports = account.borrow().lamports(); let lamports_delta = -(current_lamports as i64); diff --git a/programs/magicblock/src/clone_account/process_clone.rs b/programs/magicblock/src/clone_account/process_clone.rs index 5b46a1674..1d9833d33 100644 --- a/programs/magicblock/src/clone_account/process_clone.rs +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -10,7 +10,10 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{adjust_authority_lamports, validate_and_get_index, validate_authority, validate_mutable, validate_remote_slot}; +use super::{ + adjust_authority_lamports, validate_and_get_index, validate_authority, + validate_mutable, validate_remote_slot, +}; /// Clones an account atomically in a single transaction. /// @@ -31,15 +34,31 @@ pub(crate) fn process_clone_account( ctx.get_index_of_instruction_account_in_transaction(0)?, )?; - let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccount", invoke_context)?; + let tx_idx = validate_and_get_index( + transaction_context, + 1, + &pubkey, + "CloneAccount", + invoke_context, + )?; let account = transaction_context.get_account_at_index(tx_idx)?; // Prevent overwriting ephemeral or active delegated accounts - validate_mutable(&account, &pubkey, invoke_context)?; + validate_mutable(account, &pubkey, invoke_context)?; // Prevent stale updates from overwriting fresher data - validate_remote_slot(&account, &pubkey, Some(fields.remote_slot), invoke_context)?; + validate_remote_slot( + account, + &pubkey, + Some(fields.remote_slot), + invoke_context, + )?; - ic_msg!(invoke_context, "CloneAccount: cloning '{}', data_len={}", pubkey, data.len()); + ic_msg!( + invoke_context, + "CloneAccount: cloning '{}', data_len={}", + pubkey, + data.len() + ); let current_lamports = account.borrow().lamports(); let lamports_delta = fields.lamports as i64 - current_lamports as i64; diff --git a/programs/magicblock/src/clone_account/process_clone_continue.rs b/programs/magicblock/src/clone_account/process_clone_continue.rs index 02fb4b73a..de8b7cf12 100644 --- a/programs/magicblock/src/clone_account/process_clone_continue.rs +++ b/programs/magicblock/src/clone_account/process_clone_continue.rs @@ -9,7 +9,10 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{is_pending_clone, remove_pending_clone, validate_and_get_index, validate_authority}; +use super::{ + is_pending_clone, remove_pending_clone, validate_and_get_index, + validate_authority, +}; use crate::errors::MagicBlockProgramError; /// Writes a data chunk to a pending multi-transaction clone. @@ -33,7 +36,11 @@ pub(crate) fn process_clone_account_continue( validate_authority(signers, invoke_context)?; if !is_pending_clone(&pubkey) { - ic_msg!(invoke_context, "CloneAccountContinue: no pending clone for {}", pubkey); + ic_msg!( + invoke_context, + "CloneAccountContinue: no pending clone for {}", + pubkey + ); return Err(MagicBlockProgramError::NoPendingClone.into()); } @@ -49,7 +56,10 @@ pub(crate) fn process_clone_account_continue( ic_msg!( invoke_context, "CloneAccountContinue: '{}' offset={} len={} is_last={}", - pubkey, offset, data.len(), is_last + pubkey, + offset, + data.len(), + is_last ); // Write data at offset @@ -72,7 +82,11 @@ pub(crate) fn process_clone_account_continue( if is_last { remove_pending_clone(&pubkey); - ic_msg!(invoke_context, "CloneAccountContinue: clone complete for '{}'", pubkey); + ic_msg!( + invoke_context, + "CloneAccountContinue: clone complete for '{}'", + pubkey + ); } Ok(()) diff --git a/programs/magicblock/src/clone_account/process_clone_init.rs b/programs/magicblock/src/clone_account/process_clone_init.rs index 43e31ec19..6320570f3 100644 --- a/programs/magicblock/src/clone_account/process_clone_init.rs +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -10,7 +10,11 @@ use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; -use super::{add_pending_clone, adjust_authority_lamports, is_pending_clone, validate_and_get_index, validate_authority, validate_mutable, validate_remote_slot}; +use super::{ + add_pending_clone, adjust_authority_lamports, is_pending_clone, + validate_and_get_index, validate_authority, validate_mutable, + validate_remote_slot, +}; use crate::errors::MagicBlockProgramError; /// Initializes a multi-transaction clone for a large account. @@ -35,7 +39,11 @@ pub(crate) fn process_clone_account_init( validate_authority(signers, invoke_context)?; if is_pending_clone(&pubkey) { - ic_msg!(invoke_context, "CloneAccountInit: account {} already has pending clone", pubkey); + ic_msg!( + invoke_context, + "CloneAccountInit: account {} already has pending clone", + pubkey + ); return Err(MagicBlockProgramError::CloneAlreadyPending.into()); } @@ -54,18 +62,31 @@ pub(crate) fn process_clone_account_init( ctx.get_index_of_instruction_account_in_transaction(0)?, )?; - let tx_idx = validate_and_get_index(transaction_context, 1, &pubkey, "CloneAccountInit", invoke_context)?; + let tx_idx = validate_and_get_index( + transaction_context, + 1, + &pubkey, + "CloneAccountInit", + invoke_context, + )?; let account = transaction_context.get_account_at_index(tx_idx)?; // Prevent overwriting ephemeral or active delegated accounts - validate_mutable(&account, &pubkey, invoke_context)?; + validate_mutable(account, &pubkey, invoke_context)?; // Prevent stale updates from overwriting fresher data - validate_remote_slot(&account, &pubkey, Some(fields.remote_slot), invoke_context)?; + validate_remote_slot( + account, + &pubkey, + Some(fields.remote_slot), + invoke_context, + )?; ic_msg!( invoke_context, "CloneAccountInit: initializing '{}', total_len={}, initial_len={}", - pubkey, total_data_len, initial_data.len() + pubkey, + total_data_len, + initial_data.len() ); let current_lamports = account.borrow().lamports(); diff --git a/programs/magicblock/src/clone_account/process_finalize_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_buffer.rs index 68c384804..18716a802 100644 --- a/programs/magicblock/src/clone_account/process_finalize_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -68,7 +68,13 @@ pub(crate) fn process_finalize_program_from_buffer( let buf_lamports = buf_acc.borrow().lamports(); let prog_current_lamports = prog_acc.borrow().lamports(); - ic_msg!(invoke_context, "FinalizeV4: prog={} buf={} len={}", prog_key, buf_key, buf_data.len()); + ic_msg!( + invoke_context, + "FinalizeV4: prog={} buf={} len={}", + prog_key, + buf_key, + buf_data.len() + ); // Build LoaderV4 account data: header + ELF let deploy_slot = slot.saturating_sub(5).max(1); // Bypass cooldown @@ -81,7 +87,9 @@ pub(crate) fn process_finalize_program_from_buffer( // Calculate rent-exempt lamports for full program account let prog_lamports = Rent::default().minimum_balance(program_data.len()); - let lamports_delta = prog_lamports as i64 - prog_current_lamports as i64 - buf_lamports as i64; + let lamports_delta = prog_lamports as i64 + - prog_current_lamports as i64 + - buf_lamports as i64; // Set up program account { @@ -104,7 +112,12 @@ pub(crate) fn process_finalize_program_from_buffer( } adjust_authority_lamports(auth_acc, lamports_delta)?; - ic_msg!(invoke_context, "FinalizeV4: finalized {}, closed {}", prog_key, buf_key); + ic_msg!( + invoke_context, + "FinalizeV4: finalized {}, closed {}", + prog_key, + buf_key + ); Ok(()) } @@ -114,7 +127,10 @@ fn build_loader_v4_data(state: &LoaderV4State, program_data: &[u8]) -> Vec { let mut data = Vec::with_capacity(header_size + program_data.len()); // SAFETY: LoaderV4State is POD with no uninitialized padding let header: &[u8] = unsafe { - std::slice::from_raw_parts((state as *const LoaderV4State) as *const u8, header_size) + std::slice::from_raw_parts( + (state as *const LoaderV4State) as *const u8, + header_size, + ) }; data.extend_from_slice(header); data.extend_from_slice(program_data); diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs index 8e31ebb67..51c0e62fb 100644 --- a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -82,7 +82,14 @@ pub(crate) fn process_finalize_v1_program_from_buffer( )?; let elf_data = buf_acc.borrow().data().to_vec(); - ic_msg!(invoke_context, "FinalizeV1: prog={} data={} buf={} len={}", prog_key, data_key, buf_key, elf_data.len()); + ic_msg!( + invoke_context, + "FinalizeV1: prog={} data={} buf={} len={}", + prog_key, + data_key, + buf_key, + elf_data.len() + ); // Build V3 program_data account: ProgramData header + ELF let program_data_content = { @@ -90,7 +97,8 @@ pub(crate) fn process_finalize_v1_program_from_buffer( slot, upgrade_authority_address: Some(authority), }; - let mut data = bincode::serialize(&state).map_err(|_| InstructionError::InvalidAccountData)?; + let mut data = bincode::serialize(&state) + .map_err(|_| InstructionError::InvalidAccountData)?; data.extend_from_slice(&elf_data); data }; @@ -100,7 +108,8 @@ pub(crate) fn process_finalize_v1_program_from_buffer( let state = UpgradeableLoaderState::Program { programdata_address: data_key, }; - bincode::serialize(&state).map_err(|_| InstructionError::InvalidAccountData)? + bincode::serialize(&state) + .map_err(|_| InstructionError::InvalidAccountData)? }; // Calculate rent-exempt lamports for both accounts @@ -147,6 +156,12 @@ pub(crate) fn process_finalize_v1_program_from_buffer( } adjust_authority_lamports(auth_acc, lamports_delta)?; - ic_msg!(invoke_context, "FinalizeV1: created {} and {}, closed {}", prog_key, data_key, buf_key); + ic_msg!( + invoke_context, + "FinalizeV1: created {} and {}, closed {}", + prog_key, + data_key, + buf_key + ); Ok(()) } diff --git a/programs/magicblock/src/clone_account/process_set_authority.rs b/programs/magicblock/src/clone_account/process_set_authority.rs index d7ffc4f44..56d3badcb 100644 --- a/programs/magicblock/src/clone_account/process_set_authority.rs +++ b/programs/magicblock/src/clone_account/process_set_authority.rs @@ -35,7 +35,11 @@ pub(crate) fn process_set_program_authority( { let acc = account.borrow(); if acc.owner() != &loader_v4::id() { - ic_msg!(invoke_context, "SetProgramAuthority: {} not owned by loader_v4", key); + ic_msg!( + invoke_context, + "SetProgramAuthority: {} not owned by loader_v4", + key + ); return Err(InstructionError::InvalidAccountOwner); } } @@ -46,7 +50,10 @@ pub(crate) fn process_set_program_authority( let header_size = LoaderV4State::program_data_offset(); if data.len() < header_size { - ic_msg!(invoke_context, "SetProgramAuthority: account data too small"); + ic_msg!( + invoke_context, + "SetProgramAuthority: account data too small" + ); return Err(InstructionError::InvalidAccountData); } @@ -69,6 +76,11 @@ pub(crate) fn process_set_program_authority( }; acc.data_as_mut_slice()[..header_size].copy_from_slice(header); - ic_msg!(invoke_context, "SetProgramAuthority: {} authority -> {}", key, new_authority); + ic_msg!( + invoke_context, + "SetProgramAuthority: {} authority -> {}", + key, + new_authority + ); Ok(()) } diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 12b1995ca..255b9bbb2 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -1,6 +1,6 @@ +mod clone_account; mod ephemeral_accounts; pub mod errors; -mod clone_account; mod magic_context; pub mod magic_sys; mod mutate_accounts; diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 4cfe1a6dc..a04029667 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -110,7 +110,7 @@ pub(crate) fn process_mutate_accounts( .get_account_at_index(account_transaction_index)?; // Skip ephemeral accounts (exist locally on ER only) - if is_ephemeral(&account) { + if is_ephemeral(account) { let key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; account_mods.remove(key); @@ -146,9 +146,9 @@ pub(crate) fn process_mutate_accounts( } // Validate account is mutable - validate_not_delegated(&account, account_key, invoke_context)?; + validate_not_delegated(account, account_key, invoke_context)?; validate_remote_slot( - &account, + account, account_key, modification.remote_slot, invoke_context, From 0e8620ab09bc27645bc20ea9ad17fddec23848b3 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 19 Feb 2026 12:06:26 +0400 Subject: [PATCH 04/16] fix: proper buffer account closing --- magicblock-account-cloner/src/lib.rs | 10 ++-------- .../magicblock/src/clone_account/process_cleanup.rs | 9 +++------ .../src/clone_account/process_finalize_buffer.rs | 9 +++++---- .../src/clone_account/process_finalize_v1_buffer.rs | 10 ++++++---- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index af0d45b17..2e2077de8 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -26,7 +26,7 @@ //! The buffer is a temporary account that holds the raw ELF data during cloning. //! It's derived as a PDA: `["buffer", program_id]` owned by validator authority. -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use async_trait::async_trait; use magicblock_accounts_db::AccountsDb; @@ -359,7 +359,7 @@ impl ChainlinkCloner { debug!(program_id = %program_id, "Loading V1 program as V3 format"); - let slot = self.accounts_db.slot().saturating_sub(5).max(1); + let slot = self.accounts_db.slot(); let elf_data = program.program_data; let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); let (program_data_addr, _) = Pubkey::find_program_address( @@ -703,12 +703,6 @@ impl Cloner for ChainlinkCloner { } } - // Wait one slot for program to become usable - let current_slot = self.accounts_db.slot(); - while self.accounts_db.slot() == current_slot { - tokio::time::sleep(Duration::from_millis(25)).await; - } - Ok(last_sig) } } diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs index 1bf17d8d5..5996f70ac 100644 --- a/programs/magicblock/src/clone_account/process_cleanup.rs +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -54,13 +54,10 @@ pub(crate) fn process_cleanup_partial_clone( { let mut acc = account.borrow_mut(); acc.set_lamports(0); - acc.set_data_from_slice(&[]); - acc.set_executable(false); - acc.set_owner(Pubkey::default()); + acc.resize(0, 0); + // this hack allows us to close the account and remove it from accountsdb + acc.set_ephemeral(true); acc.set_delegated(false); - acc.set_confined(false); - acc.set_remote_slot(0); - acc.set_undelegating(false); } adjust_authority_lamports(auth_acc, lamports_delta)?; diff --git a/programs/magicblock/src/clone_account/process_finalize_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_buffer.rs index 18716a802..8064f1566 100644 --- a/programs/magicblock/src/clone_account/process_finalize_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -77,7 +77,7 @@ pub(crate) fn process_finalize_program_from_buffer( ); // Build LoaderV4 account data: header + ELF - let deploy_slot = slot.saturating_sub(5).max(1); // Bypass cooldown + let deploy_slot = slot.saturating_sub(5); // Bypass cooldown let state = LoaderV4State { slot: deploy_slot, authority_address_or_next_version: validator_authority_id(), @@ -106,9 +106,10 @@ pub(crate) fn process_finalize_program_from_buffer( { let mut buf = buf_acc.borrow_mut(); buf.set_lamports(0); - buf.set_data_from_slice(&[]); - buf.set_executable(false); - buf.set_owner(Pubkey::default()); + buf.resize(0, 0); + // this hack allows us to close the account and remove it from accountsdb + buf.set_ephemeral(true); + buf.set_delegated(false); } adjust_authority_lamports(auth_acc, lamports_delta)?; diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs index 51c0e62fb..146bce0a6 100644 --- a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -92,9 +92,10 @@ pub(crate) fn process_finalize_v1_program_from_buffer( ); // Build V3 program_data account: ProgramData header + ELF + let deploy_slot = slot.saturating_sub(5); // Bypass cooldown let program_data_content = { let state = UpgradeableLoaderState::ProgramData { - slot, + slot: deploy_slot, upgrade_authority_address: Some(authority), }; let mut data = bincode::serialize(&state) @@ -150,9 +151,10 @@ pub(crate) fn process_finalize_v1_program_from_buffer( { let mut buf = buf_acc.borrow_mut(); buf.set_lamports(0); - buf.set_data_from_slice(&[]); - buf.set_executable(false); - buf.set_owner(Pubkey::default()); + buf.resize(0, 0); + // this hack allows us to close the account and remove it from accountsdb + buf.set_ephemeral(true); + buf.set_delegated(false); } adjust_authority_lamports(auth_acc, lamports_delta)?; From 9c4ac57e43839e03468ac0f79b632b839246b269 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 19 Feb 2026 13:39:05 +0400 Subject: [PATCH 05/16] fix: post review issues --- magicblock-account-cloner/src/lib.rs | 248 ++++++++---------- magicblock-api/src/magic_validator.rs | 1 - magicblock-chainlink/src/cloner/errors.rs | 3 - .../src/instruction.rs | 4 +- .../magicblock/src/clone_account/common.rs | 62 ++++- .../src/clone_account/process_cleanup.rs | 15 +- .../src/clone_account/process_clone.rs | 18 +- .../src/clone_account/process_clone_init.rs | 18 +- .../clone_account/process_finalize_buffer.rs | 59 ++--- .../process_finalize_v1_buffer.rs | 46 ++-- .../clone_account/process_set_authority.rs | 11 +- .../magicblock/src/magicblock_processor.rs | 23 +- 12 files changed, 233 insertions(+), 275 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index 2e2077de8..107695ba0 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -29,7 +29,6 @@ use std::sync::Arc; use async_trait::async_trait; -use magicblock_accounts_db::AccountsDb; use magicblock_chainlink::{ cloner::{ errors::{ClonerError, ClonerResult}, @@ -79,7 +78,6 @@ pub struct ChainlinkCloner { changeset_committor: Option>, config: ChainLinkConfig, tx_scheduler: TransactionSchedulerHandle, - accounts_db: Arc, block: LatestBlock, } @@ -88,14 +86,12 @@ impl ChainlinkCloner { changeset_committor: Option>, config: ChainLinkConfig, tx_scheduler: TransactionSchedulerHandle, - accounts_db: Arc, block: LatestBlock, ) -> Self { Self { changeset_committor, config, tx_scheduler, - accounts_db, block, } } @@ -187,11 +183,11 @@ impl ChainlinkCloner { fn finalize_program_ix( program: Pubkey, buffer: Pubkey, - slot: u64, + remote_slot: u64, ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, - &MagicBlockInstruction::FinalizeProgramFromBuffer { slot }, + &MagicBlockInstruction::FinalizeProgramFromBuffer { remote_slot }, vec![ AccountMeta::new_readonly(validator_authority_id(), true), AccountMeta::new(program, false), @@ -347,6 +343,47 @@ impl ChainlinkCloner { } } + /// Helper to build buffer fields for program cloning. + fn buffer_fields(data_len: usize) -> AccountCloneFields { + let lamports = Rent::default().minimum_balance(data_len); + AccountCloneFields { + lamports, + owner: solana_sdk_ids::system_program::id(), + ..Default::default() + } + } + + /// Helper to build program transactions (shared between V1 and V4). + fn build_program_txs_from_finalize( + &self, + buffer_pubkey: Pubkey, + program_data: Vec, + finalize_ixs: Vec, + blockhash: Hash, + ) -> Vec { + let buffer_fields = Self::buffer_fields(program_data.len()); + + if program_data.len() <= MAX_INLINE_DATA_SIZE { + // Small: single transaction with clone + finalize + let mut ixs = vec![Self::clone_ix( + buffer_pubkey, + program_data, + buffer_fields, + )]; + ixs.extend(finalize_ixs); + vec![self.sign_tx(&ixs, blockhash)] + } else { + // Large: multi-transaction flow + self.build_large_program_txs( + buffer_pubkey, + program_data, + buffer_fields, + finalize_ixs, + blockhash, + ) + } + } + /// V1 programs are converted to V3 (upgradeable loader) format. /// Supports programs of any size via multi-transaction cloning. fn build_v1_program_txs( @@ -356,10 +393,10 @@ impl ChainlinkCloner { ) -> ClonerResult>> { let program_id = program.program_id; let chain_authority = program.authority; + let remote_slot = program.remote_slot; debug!(program_id = %program_id, "Loading V1 program as V3 format"); - let slot = self.accounts_db.slot(); let elf_data = program.program_data; let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); let (program_data_addr, _) = Pubkey::find_program_address( @@ -367,14 +404,6 @@ impl ChainlinkCloner { &bpf_loader_upgradeable::id(), ); - // Buffer is a dummy account owned by system program, just holds raw ELF data - let lamports = Rent::default().minimum_balance(elf_data.len()); - let buffer_fields = AccountCloneFields { - lamports, - owner: solana_sdk_ids::system_program::id(), - ..Default::default() - }; - // Finalization instruction // Must wrap in disable/enable executable check since finalize sets executable=true let finalize_ixs = vec![ @@ -385,7 +414,7 @@ impl ChainlinkCloner { program_id, program_data_addr, buffer_pubkey, - slot, + remote_slot, chain_authority, ), InstructionUtils::enable_executable_check_instruction( @@ -393,25 +422,12 @@ impl ChainlinkCloner { ), ]; - // Build transactions based on ELF size - let txs = if elf_data.len() <= MAX_INLINE_DATA_SIZE { - // Small: single transaction with clone + finalize - let ixs = - vec![Self::clone_ix(buffer_pubkey, elf_data, buffer_fields)] - .into_iter() - .chain(finalize_ixs) - .collect::>(); - vec![self.sign_tx(&ixs, blockhash)] - } else { - // Large: multi-transaction flow - self.build_large_program_txs( - buffer_pubkey, - elf_data, - buffer_fields, - finalize_ixs, - blockhash, - ) - }; + let txs = self.build_program_txs_from_finalize( + buffer_pubkey, + elf_data, + finalize_ixs, + blockhash, + ); Ok(Some(txs)) } @@ -421,13 +437,13 @@ impl ChainlinkCloner { program: Pubkey, program_data: Pubkey, buffer: Pubkey, - slot: u64, + remote_slot: u64, authority: Pubkey, ) -> Instruction { Instruction::new_with_bincode( magicblock_program::ID, &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { - slot, + remote_slot, authority, }, vec![ @@ -448,6 +464,7 @@ impl ChainlinkCloner { ) -> ClonerResult>> { let program_id = program.program_id; let chain_authority = program.authority; + let remote_slot = program.remote_slot; // Skip retracted programs if matches!(program.loader_status, LoaderV4Status::Retracted) { @@ -457,26 +474,16 @@ impl ChainlinkCloner { debug!(program_id = %program_id, "Deploying program with V4 loader"); - let slot = self.accounts_db.slot(); let program_data = program.program_data; let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); - // Buffer is a dummy account owned by system program, just holds raw ELF data - let lamports = Rent::default().minimum_balance(program_data.len()); - let buffer_fields = AccountCloneFields { - lamports, - owner: solana_sdk_ids::system_program::id(), - ..Default::default() - }; - let deploy_ix = Instruction { program_id: loader_v4::id(), accounts: vec![ AccountMeta::new(program_id, false), AccountMeta::new_readonly(validator_authority_id(), true), ], - data: bincode::serialize(&LoaderV4Instruction::Deploy) - .map_err(|e| ClonerError::SerializationError(e.to_string()))?, + data: bincode::serialize(&LoaderV4Instruction::Deploy)?, }; // Finalization instructions (always in last tx) @@ -485,7 +492,7 @@ impl ChainlinkCloner { InstructionUtils::disable_executable_check_instruction( &validator_authority_id(), ), - Self::finalize_program_ix(program_id, buffer_pubkey, slot), + Self::finalize_program_ix(program_id, buffer_pubkey, remote_slot), deploy_ix, Self::set_authority_ix(program_id, chain_authority), InstructionUtils::enable_executable_check_instruction( @@ -493,28 +500,12 @@ impl ChainlinkCloner { ), ]; - // Build transactions based on program_data size - let txs = if program_data.len() <= MAX_INLINE_DATA_SIZE { - // Small: single transaction - let ixs = vec![Self::clone_ix( - buffer_pubkey, - program_data, - buffer_fields, - )] - .into_iter() - .chain(finalize_ixs) - .collect::>(); - vec![self.sign_tx(&ixs, blockhash)] - } else { - // Large: multi-transaction flow - self.build_large_program_txs( - buffer_pubkey, - program_data, - buffer_fields, - finalize_ixs, - blockhash, - ) - }; + let txs = self.build_program_txs_from_finalize( + buffer_pubkey, + program_data, + finalize_ixs, + blockhash, + ); Ok(Some(txs)) } @@ -528,66 +519,47 @@ impl ChainlinkCloner { finalize_ixs: Vec, blockhash: Hash, ) -> Vec { - let mut txs = Vec::new(); let total_len = program_data.len() as u32; - let num_chunks = (total_len as usize).div_ceil(MAX_INLINE_DATA_SIZE); + let num_chunks = + total_len.div_ceil(MAX_INLINE_DATA_SIZE as u32) as usize; - info!( - buffer = %buffer_pubkey, + // First chunk via Init + let first_chunk = + &program_data[..MAX_INLINE_DATA_SIZE.min(program_data.len())]; + let init_ix = Self::clone_init_ix( + buffer_pubkey, total_len, - num_chunks, - "Building large program clone transactions" + first_chunk.to_vec(), + fields, ); - - // First chunk via Init - let first_chunk = program_data - [..MAX_INLINE_DATA_SIZE.min(program_data.len())] - .to_vec(); - let init_ix = - Self::clone_init_ix(buffer_pubkey, total_len, first_chunk, fields); - txs.push(self.sign_tx(&[init_ix], blockhash)); - - // Continue chunks - let mut offset = MAX_INLINE_DATA_SIZE; - let mut chunk_num = 1; - while offset < program_data.len() { - let end = (offset + MAX_INLINE_DATA_SIZE).min(program_data.len()); - let chunk = program_data[offset..end].to_vec(); - let is_last = end == program_data.len(); - chunk_num += 1; - - if is_last { - // Last chunk + finalize instructions in single tx - info!( - buffer = %buffer_pubkey, - chunk = chunk_num, - num_chunks, - finalize_ixs_count = finalize_ixs.len(), - "Building final transaction with continue + finalize" - ); - let continue_ix = Self::clone_continue_ix( - buffer_pubkey, - offset as u32, - chunk, - true, - ); - let ixs = vec![continue_ix] - .into_iter() - .chain(finalize_ixs.clone()) - .collect::>(); - txs.push(self.sign_tx(&ixs, blockhash)); - } else { - let continue_ix = Self::clone_continue_ix( - buffer_pubkey, - offset as u32, - chunk, - false, - ); - txs.push(self.sign_tx(&[continue_ix], blockhash)); - } - offset = end; + let mut txs = vec![self.sign_tx(&[init_ix], blockhash)]; + + // Middle chunks (all except last) + let last_offset = (num_chunks - 1) * MAX_INLINE_DATA_SIZE; + for offset in + (MAX_INLINE_DATA_SIZE..last_offset).step_by(MAX_INLINE_DATA_SIZE) + { + let chunk = &program_data[offset..offset + MAX_INLINE_DATA_SIZE]; + let continue_ix = Self::clone_continue_ix( + buffer_pubkey, + offset as u32, + chunk.to_vec(), + false, + ); + txs.push(self.sign_tx(&[continue_ix], blockhash)); } + // Last chunk with finalize instructions + let last_chunk = &program_data[last_offset..]; + let mut ixs = vec![Self::clone_continue_ix( + buffer_pubkey, + last_offset as u32, + last_chunk.to_vec(), + true, + )]; + ixs.extend(finalize_ixs); + txs.push(self.sign_tx(&ixs, blockhash)); + txs } @@ -596,19 +568,21 @@ impl ChainlinkCloner { // ----------------- fn maybe_prepare_lookup_tables(&self, pubkey: Pubkey, owner: Pubkey) { - if let Some(committor) = self.changeset_committor.as_ref() { - if self.config.prepare_lookup_tables { - let committor = committor.clone(); - tokio::spawn(async move { - if let Err(e) = committor - .reserve_pubkeys_for_committee(pubkey, owner) - .await - { - error!(error = ?e, "Failed to reserve lookup tables"); - } - }); + let Some(committor) = self + .config + .prepare_lookup_tables + .then_some(self.changeset_committor.as_ref()) + .flatten() + .cloned() + else { + return; + }; + tokio::spawn(async move { + let result = committor.reserve_pubkeys_for_committee(pubkey, owner); + if let Err(e) = result.await { + error!(error = ?e, "Failed to reserve lookup tables"); } - } + }); } } diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 0efcb51be..7be891bd7 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -431,7 +431,6 @@ impl MagicValidator { committor_service, config.chainlink.clone(), transaction_scheduler.clone(), - accountsdb.clone(), latest_block.clone(), ); let cloner = Arc::new(cloner); diff --git a/magicblock-chainlink/src/cloner/errors.rs b/magicblock-chainlink/src/cloner/errors.rs index febff28ca..32bc8f1c7 100644 --- a/magicblock-chainlink/src/cloner/errors.rs +++ b/magicblock-chainlink/src/cloner/errors.rs @@ -28,7 +28,4 @@ pub enum ClonerError { #[error("Failed to clone program {0} : {1:?}")] FailedToCloneProgram(Pubkey, Box), - - #[error("Serialization error: {0}")] - SerializationError(String), } diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index 19b4d31b0..e790c9daa 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -252,7 +252,7 @@ pub enum MagicBlockInstruction { /// - **0.** `[SIGNER]` Validator Authority /// - **1.** `[WRITE]` Program account /// - **2.** `[WRITE]` Buffer account (closed after) - FinalizeProgramFromBuffer { slot: u64 }, + FinalizeProgramFromBuffer { remote_slot: u64 }, /// Finalize V1 program deployment from a buffer account. /// V1 programs are converted to V3 (upgradeable loader) format. @@ -266,7 +266,7 @@ pub enum MagicBlockInstruction { /// - **1.** `[WRITE]` Program account /// - **2.** `[WRITE]` Program data account /// - **3.** `[WRITE]` Buffer account (closed after) - FinalizeV1ProgramFromBuffer { slot: u64, authority: Pubkey }, + FinalizeV1ProgramFromBuffer { remote_slot: u64, authority: Pubkey }, /// Update the authority in a LoaderV4 program header. /// Used after Deploy to set the final chain authority. diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index f8aa593db..d429b4c69 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -2,8 +2,10 @@ use std::{cell::RefCell, collections::HashSet}; +use magicblock_magic_program_api::instruction::AccountCloneFields; use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; use solana_instruction::error::InstructionError; +use solana_loader_v4_interface::state::LoaderV4State; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; @@ -13,6 +15,23 @@ use crate::{ errors::MagicBlockProgramError, validator::validator_authority_id, }; +/// Converts a LoaderV4State reference to a byte slice. +/// +/// # Safety +/// +/// LoaderV4State is a POD type with no uninitialized padding bytes, +/// making it safe to reinterpret as a raw byte slice. +pub fn loader_v4_state_to_bytes(state: &LoaderV4State) -> &[u8] { + let header_size = LoaderV4State::program_data_offset(); + // SAFETY: LoaderV4State is POD with no uninitialized padding + unsafe { + std::slice::from_raw_parts( + (state as *const LoaderV4State) as *const u8, + header_size, + ) + } +} + /// Validates that the validator authority has signed the transaction. pub fn validate_authority( signers: &HashSet, @@ -107,7 +126,7 @@ pub fn validate_remote_slot( return Ok(()); }; let current = account.borrow().remote_slot(); - if incoming < current { + if incoming <= current { ic_msg!( invoke_context, "Account {} incoming remote_slot {} is older than current {}; rejected", @@ -134,9 +153,48 @@ pub fn adjust_authority_lamports( .ok_or(InstructionError::InsufficientFunds)? } else { auth_lamports - .checked_add((-delta) as u64) + .checked_add(delta.unsigned_abs()) .ok_or(InstructionError::ArithmeticOverflow)? }; auth_acc.borrow_mut().set_lamports(adjusted); Ok(()) } + +/// Closes a buffer/temporary account by resetting it to default state. +/// The account will be removed from accountsdb due to the ephemeral flag. +pub fn close_buffer_account(account: &RefCell) { + let mut acc = account.borrow_mut(); + acc.set_lamports(0); + acc.resize(0, 0); + // this hack allows us to close the account and remove it from accountsdb + acc.set_ephemeral(true); + acc.set_delegated(false); +} + +/// Returns the deploy slot for program cloning (current_slot - 5). +/// This bypasses LoaderV4's cooldown mechanism by simulating the program +/// was deployed 5 slots ago. +pub fn get_deploy_slot(invoke_context: &InvokeContext) -> u64 { + invoke_context + .get_sysvar_cache() + .get_clock() + .map(|clock| clock.slot.saturating_sub(5)) + .unwrap_or(0) +} + +/// Sets account fields from AccountCloneFields and data. +pub fn set_account_from_fields( + account: &RefCell, + data: &[u8], + fields: &AccountCloneFields, +) { + let mut acc = account.borrow_mut(); + acc.set_lamports(fields.lamports); + acc.set_owner(fields.owner); + acc.set_data_from_slice(data); + acc.set_executable(fields.executable); + acc.set_delegated(fields.delegated); + acc.set_confined(fields.confined); + acc.set_remote_slot(fields.remote_slot); + acc.set_undelegating(false); +} diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs index 5996f70ac..bdf35b53e 100644 --- a/programs/magicblock/src/clone_account/process_cleanup.rs +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use solana_account::{ReadableAccount, WritableAccount}; +use solana_account::ReadableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; @@ -10,8 +10,8 @@ use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; use super::{ - adjust_authority_lamports, remove_pending_clone, validate_and_get_index, - validate_authority, + adjust_authority_lamports, close_buffer_account, remove_pending_clone, + validate_and_get_index, validate_authority, }; /// Cleans up a failed multi-transaction clone. @@ -51,14 +51,7 @@ pub(crate) fn process_cleanup_partial_clone( let current_lamports = account.borrow().lamports(); let lamports_delta = -(current_lamports as i64); - { - let mut acc = account.borrow_mut(); - acc.set_lamports(0); - acc.resize(0, 0); - // this hack allows us to close the account and remove it from accountsdb - acc.set_ephemeral(true); - acc.set_delegated(false); - } + close_buffer_account(account); adjust_authority_lamports(auth_acc, lamports_delta)?; Ok(()) diff --git a/programs/magicblock/src/clone_account/process_clone.rs b/programs/magicblock/src/clone_account/process_clone.rs index 1d9833d33..63236b5b7 100644 --- a/programs/magicblock/src/clone_account/process_clone.rs +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use magicblock_magic_program_api::instruction::AccountCloneFields; -use solana_account::{ReadableAccount, WritableAccount}; +use solana_account::ReadableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; @@ -11,8 +11,8 @@ use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; use super::{ - adjust_authority_lamports, validate_and_get_index, validate_authority, - validate_mutable, validate_remote_slot, + adjust_authority_lamports, set_account_from_fields, validate_and_get_index, + validate_authority, validate_mutable, validate_remote_slot, }; /// Clones an account atomically in a single transaction. @@ -63,17 +63,7 @@ pub(crate) fn process_clone_account( let current_lamports = account.borrow().lamports(); let lamports_delta = fields.lamports as i64 - current_lamports as i64; - { - let mut acc = account.borrow_mut(); - acc.set_lamports(fields.lamports); - acc.set_owner(fields.owner); - acc.set_data_from_slice(&data); - acc.set_executable(fields.executable); - acc.set_delegated(fields.delegated); - acc.set_confined(fields.confined); - acc.set_remote_slot(fields.remote_slot); - acc.set_undelegating(false); - } + set_account_from_fields(account, &data, &fields); adjust_authority_lamports(auth_acc, lamports_delta)?; Ok(()) diff --git a/programs/magicblock/src/clone_account/process_clone_init.rs b/programs/magicblock/src/clone_account/process_clone_init.rs index 6320570f3..8037ee531 100644 --- a/programs/magicblock/src/clone_account/process_clone_init.rs +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use magicblock_magic_program_api::instruction::AccountCloneFields; -use solana_account::{ReadableAccount, WritableAccount}; +use solana_account::ReadableAccount; use solana_instruction::error::InstructionError; use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; @@ -12,8 +12,8 @@ use solana_transaction_context::TransactionContext; use super::{ add_pending_clone, adjust_authority_lamports, is_pending_clone, - validate_and_get_index, validate_authority, validate_mutable, - validate_remote_slot, + set_account_from_fields, validate_and_get_index, validate_authority, + validate_mutable, validate_remote_slot, }; use crate::errors::MagicBlockProgramError; @@ -96,17 +96,7 @@ pub(crate) fn process_clone_account_init( let mut data = vec![0u8; total_data_len as usize]; data[..initial_data.len()].copy_from_slice(&initial_data); - { - let mut acc = account.borrow_mut(); - acc.set_lamports(fields.lamports); - acc.set_owner(fields.owner); - acc.set_data_from_slice(&data); - acc.set_executable(fields.executable); - acc.set_delegated(fields.delegated); - acc.set_confined(fields.confined); - acc.set_remote_slot(fields.remote_slot); - acc.set_undelegating(false); - } + set_account_from_fields(account, &data, &fields); adjust_authority_lamports(auth_acc, lamports_delta)?; add_pending_clone(pubkey); diff --git a/programs/magicblock/src/clone_account/process_finalize_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_buffer.rs index 8064f1566..e734d22bb 100644 --- a/programs/magicblock/src/clone_account/process_finalize_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -14,7 +14,10 @@ use solana_sdk_ids::loader_v4; use solana_sysvar::rent::Rent; use solana_transaction_context::TransactionContext; -use super::{adjust_authority_lamports, validate_authority}; +use super::{ + adjust_authority_lamports, close_buffer_account, get_deploy_slot, + loader_v4_state_to_bytes, validate_authority, +}; use crate::validator::validator_authority_id; /// Finalizes a LoaderV4 program from a buffer account. @@ -30,7 +33,7 @@ use crate::validator::validator_authority_id; /// /// # Slot Trick /// -/// We use `slot - 5` for the deploy slot to bypass LoaderV4's cooldown mechanism. +/// We use `current_slot - 5` for the deploy slot to bypass LoaderV4's cooldown mechanism. /// LoaderV4 requires programs to wait N slots after deployment before certain operations. /// By setting the slot to 5 slots ago, we simulate that the program was deployed earlier. /// @@ -42,7 +45,7 @@ pub(crate) fn process_finalize_program_from_buffer( signers: &HashSet, invoke_context: &InvokeContext, transaction_context: &TransactionContext, - slot: u64, + remote_slot: u64, ) -> Result<(), InstructionError> { validate_authority(signers, invoke_context)?; @@ -57,27 +60,13 @@ pub(crate) fn process_finalize_program_from_buffer( ctx.get_index_of_instruction_account_in_transaction(2)?, )?; - let prog_key = *transaction_context.get_key_of_account_at_index( - ctx.get_index_of_instruction_account_in_transaction(1)?, - )?; - let buf_key = *transaction_context.get_key_of_account_at_index( - ctx.get_index_of_instruction_account_in_transaction(2)?, - )?; - let buf_data = buf_acc.borrow().data().to_vec(); let buf_lamports = buf_acc.borrow().lamports(); let prog_current_lamports = prog_acc.borrow().lamports(); - ic_msg!( - invoke_context, - "FinalizeV4: prog={} buf={} len={}", - prog_key, - buf_key, - buf_data.len() - ); + let deploy_slot = get_deploy_slot(invoke_context); // Build LoaderV4 account data: header + ELF - let deploy_slot = slot.saturating_sub(5); // Bypass cooldown let state = LoaderV4State { slot: deploy_slot, authority_address_or_next_version: validator_authority_id(), @@ -85,6 +74,14 @@ pub(crate) fn process_finalize_program_from_buffer( }; let program_data = build_loader_v4_data(&state, &buf_data); + ic_msg!( + invoke_context, + "FinalizeProgram: elf_len={} remote_slot={} deploy_slot={}", + buf_data.len(), + remote_slot, + deploy_slot + ); + // Calculate rent-exempt lamports for full program account let prog_lamports = Rent::default().minimum_balance(program_data.len()); let lamports_delta = prog_lamports as i64 @@ -98,27 +95,14 @@ pub(crate) fn process_finalize_program_from_buffer( prog.set_owner(loader_v4::id()); prog.set_executable(true); prog.set_data_from_slice(&program_data); - prog.set_remote_slot(slot); + prog.set_remote_slot(remote_slot); prog.set_undelegating(false); } // Close buffer account - { - let mut buf = buf_acc.borrow_mut(); - buf.set_lamports(0); - buf.resize(0, 0); - // this hack allows us to close the account and remove it from accountsdb - buf.set_ephemeral(true); - buf.set_delegated(false); - } + close_buffer_account(buf_acc); adjust_authority_lamports(auth_acc, lamports_delta)?; - ic_msg!( - invoke_context, - "FinalizeV4: finalized {}, closed {}", - prog_key, - buf_key - ); Ok(()) } @@ -126,14 +110,7 @@ pub(crate) fn process_finalize_program_from_buffer( fn build_loader_v4_data(state: &LoaderV4State, program_data: &[u8]) -> Vec { let header_size = LoaderV4State::program_data_offset(); let mut data = Vec::with_capacity(header_size + program_data.len()); - // SAFETY: LoaderV4State is POD with no uninitialized padding - let header: &[u8] = unsafe { - std::slice::from_raw_parts( - (state as *const LoaderV4State) as *const u8, - header_size, - ) - }; - data.extend_from_slice(header); + data.extend_from_slice(loader_v4_state_to_bytes(state)); data.extend_from_slice(program_data); data } diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs index 146bce0a6..b784a2bce 100644 --- a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -15,7 +15,10 @@ use solana_sdk_ids::bpf_loader_upgradeable; use solana_sysvar::rent::Rent; use solana_transaction_context::TransactionContext; -use super::{adjust_authority_lamports, validate_authority}; +use super::{ + adjust_authority_lamports, close_buffer_account, get_deploy_slot, + validate_authority, +}; /// Finalizes a V1 program from a buffer account, converting to V3 (upgradeable loader) format. /// @@ -52,7 +55,7 @@ pub(crate) fn process_finalize_v1_program_from_buffer( signers: &HashSet, invoke_context: &InvokeContext, transaction_context: &TransactionContext, - slot: u64, + remote_slot: u64, authority: Pubkey, ) -> Result<(), InstructionError> { validate_authority(signers, invoke_context)?; @@ -71,28 +74,23 @@ pub(crate) fn process_finalize_v1_program_from_buffer( ctx.get_index_of_instruction_account_in_transaction(3)?, )?; - let prog_key = *transaction_context.get_key_of_account_at_index( - ctx.get_index_of_instruction_account_in_transaction(1)?, - )?; let data_key = *transaction_context.get_key_of_account_at_index( ctx.get_index_of_instruction_account_in_transaction(2)?, )?; - let buf_key = *transaction_context.get_key_of_account_at_index( - ctx.get_index_of_instruction_account_in_transaction(3)?, - )?; let elf_data = buf_acc.borrow().data().to_vec(); + + let deploy_slot = get_deploy_slot(invoke_context); + ic_msg!( invoke_context, - "FinalizeV1: prog={} data={} buf={} len={}", - prog_key, - data_key, - buf_key, - elf_data.len() + "FinalizeV1: elf_len={} remote_slot={} deploy_slot={}", + elf_data.len(), + remote_slot, + deploy_slot ); // Build V3 program_data account: ProgramData header + ELF - let deploy_slot = slot.saturating_sub(5); // Bypass cooldown let program_data_content = { let state = UpgradeableLoaderState::ProgramData { slot: deploy_slot, @@ -132,7 +130,7 @@ pub(crate) fn process_finalize_v1_program_from_buffer( acc.set_owner(bpf_loader_upgradeable::id()); acc.set_executable(false); acc.set_data_from_slice(&program_data_content); - acc.set_remote_slot(slot); + acc.set_remote_slot(remote_slot); acc.set_undelegating(false); } @@ -143,27 +141,13 @@ pub(crate) fn process_finalize_v1_program_from_buffer( acc.set_owner(bpf_loader_upgradeable::id()); acc.set_executable(true); acc.set_data_from_slice(&program_content); - acc.set_remote_slot(slot); + acc.set_remote_slot(remote_slot); acc.set_undelegating(false); } // Close buffer account - { - let mut buf = buf_acc.borrow_mut(); - buf.set_lamports(0); - buf.resize(0, 0); - // this hack allows us to close the account and remove it from accountsdb - buf.set_ephemeral(true); - buf.set_delegated(false); - } + close_buffer_account(buf_acc); adjust_authority_lamports(auth_acc, lamports_delta)?; - ic_msg!( - invoke_context, - "FinalizeV1: created {} and {}, closed {}", - prog_key, - data_key, - buf_key - ); Ok(()) } diff --git a/programs/magicblock/src/clone_account/process_set_authority.rs b/programs/magicblock/src/clone_account/process_set_authority.rs index 56d3badcb..522d84f22 100644 --- a/programs/magicblock/src/clone_account/process_set_authority.rs +++ b/programs/magicblock/src/clone_account/process_set_authority.rs @@ -11,7 +11,7 @@ use solana_pubkey::Pubkey; use solana_sdk_ids::loader_v4; use solana_transaction_context::TransactionContext; -use super::validate_authority; +use super::{loader_v4_state_to_bytes, validate_authority}; /// Updates the authority field in a LoaderV4 program's header. /// @@ -68,13 +68,8 @@ pub(crate) fn process_set_program_authority( status: current.status, }; - let header: &[u8] = unsafe { - std::slice::from_raw_parts( - (&new_state as *const LoaderV4State) as *const u8, - header_size, - ) - }; - acc.data_as_mut_slice()[..header_size].copy_from_slice(header); + acc.data_as_mut_slice()[..header_size] + .copy_from_slice(loader_v4_state_to_bytes(&new_state)); ic_msg!( invoke_context, diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index 4077eab78..1b5106288 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -170,12 +170,12 @@ declare_process_instruction!( transaction_context, pubkey, ), - FinalizeProgramFromBuffer { slot } => { + FinalizeProgramFromBuffer { remote_slot } => { process_finalize_program_from_buffer( &signers, invoke_context, transaction_context, - slot, + remote_slot, ) } SetProgramAuthority { authority } => process_set_program_authority( @@ -184,15 +184,16 @@ declare_process_instruction!( transaction_context, authority, ), - FinalizeV1ProgramFromBuffer { slot, authority } => { - process_finalize_v1_program_from_buffer( - &signers, - invoke_context, - transaction_context, - slot, - authority, - ) - } + FinalizeV1ProgramFromBuffer { + remote_slot, + authority, + } => process_finalize_v1_program_from_buffer( + &signers, + invoke_context, + transaction_context, + remote_slot, + authority, + ), } } ); From edfdf3dae8999604bb27c44c38e4c95d7d42e7f3 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 19 Feb 2026 14:08:59 +0400 Subject: [PATCH 06/16] fix: fmt and broken test --- .../src/mutate_accounts/process_mutate_accounts.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index a04029667..364379016 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -797,7 +797,7 @@ mod tests { } #[test] - fn test_mod_remote_slot_allows_equal_update() { + fn test_mod_remote_slot_rejects_equal_update() { init_logger!(); let mod_key = Pubkey::new_unique(); @@ -833,12 +833,11 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Ok(()), + Err(MagicBlockProgramError::OutOfOrderUpdate.into()), ); - let _account_authority = accounts.drain(0..1).next().unwrap(); - let modified_account = accounts.drain(0..1).next().unwrap(); - assert_eq!(modified_account.lamports(), 200); - assert_eq!(modified_account.remote_slot(), 100); + let account = accounts.remove(1); // [authority, account] + assert_eq!(account.lamports(), 100); + assert_eq!(account.remote_slot(), 100); } } From 1a0744990e760facedd3d42c783461097ef30fc5 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 19 Feb 2026 15:57:41 +0400 Subject: [PATCH 07/16] fix: restore post deploy wait for programs --- magicblock-account-cloner/src/lib.rs | 8 ++++++++ programs/magicblock/src/clone_account/common.rs | 2 +- .../src/mutate_accounts/process_mutate_accounts.rs | 9 +++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index 107695ba0..f722619e5 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -677,6 +677,14 @@ impl Cloner for ChainlinkCloner { } } + // After cloning a program we need to wait at least one slot for it to become + // usable, so we do that here + let current_slot = self.block.load().slot; + let mut block_updated = self.block.subscribe(); + while self.block.load().slot == current_slot { + let _ = block_updated.recv().await; + } + Ok(last_sig) } } diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index d429b4c69..af68fe3b8 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -126,7 +126,7 @@ pub fn validate_remote_slot( return Ok(()); }; let current = account.borrow().remote_slot(); - if incoming <= current { + if incoming < current { ic_msg!( invoke_context, "Account {} incoming remote_slot {} is older than current {}; rejected", diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 364379016..b3e077ade 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -797,7 +797,7 @@ mod tests { } #[test] - fn test_mod_remote_slot_rejects_equal_update() { + fn test_mod_remote_slot_allows_equal_update() { init_logger!(); let mod_key = Pubkey::new_unique(); @@ -833,11 +833,12 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Err(MagicBlockProgramError::OutOfOrderUpdate.into()), + Ok(()), ); - let account = accounts.remove(1); // [authority, account] - assert_eq!(account.lamports(), 100); + accounts.remove(0); // authority + let account = accounts.remove(0); + assert_eq!(account.lamports(), 200); assert_eq!(account.remote_slot(), 100); } } From 4adbc667a7db9a76606fbd06918c1ab3b3a12170 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 19 Feb 2026 17:46:07 +0400 Subject: [PATCH 08/16] chore: remove irrelevant integration tests --- .../tests/09_account_clone_logs.rs | 172 ------------------ 1 file changed, 172 deletions(-) delete mode 100644 test-integration/test-cloning/tests/09_account_clone_logs.rs diff --git a/test-integration/test-cloning/tests/09_account_clone_logs.rs b/test-integration/test-cloning/tests/09_account_clone_logs.rs deleted file mode 100644 index 547a33d92..000000000 --- a/test-integration/test-cloning/tests/09_account_clone_logs.rs +++ /dev/null @@ -1,172 +0,0 @@ -use integration_test_tools::IntegrationTestContext; -use solana_sdk::{ - native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, - signer::Signer, -}; -use test_kit::init_logger; -use tracing::*; - -fn random_pubkey() -> Pubkey { - Keypair::new().pubkey() -} - -#[test] -fn test_account_clone_logs_not_delegated() { - init_logger!(); - let ctx = IntegrationTestContext::try_new().unwrap(); - - // 1. Create account not delegated - let pubkey = random_pubkey(); - let airdrop_sig = ctx - .airdrop_chain(&pubkey, 2 * LAMPORTS_PER_SOL) - .expect("failed to airdrop"); - debug!("Airdrop tx: {airdrop_sig}"); - - // 2. Get account from ephemeral to trigger clone - let acc = ctx.fetch_ephem_account(pubkey); - assert!(acc.is_ok()); - - // 3. Find the cloning transaction - let all_sigs = ctx - .get_signaturestats_for_address_ephem(&pubkey) - .expect("failed to get transaction signatures"); - debug!("All transactions for account: {:#?}", all_sigs); - - let clone_sig = ctx - .last_transaction_mentioning_account_ephem(&pubkey) - .expect("failed to find cloning transaction"); - debug!("Cloning transaction (latest): {clone_sig}"); - - // 4. Verify logs contain the delegation message - ctx.assert_ephemeral_logs_contain( - clone_sig, - "MutateAccounts: account is not delegated to any validator", - ); -} - -#[test] -fn test_account_clone_logs_delegated_to_other_validator() { - init_logger!(); - let ctx = IntegrationTestContext::try_new().unwrap(); - - // 1. Create account and delegate to another validator - let payer_chain = Keypair::new(); - let account_kp = Keypair::new(); - let other_validator = random_pubkey(); - - ctx.airdrop_chain(&payer_chain.pubkey(), 5 * LAMPORTS_PER_SOL) - .expect("failed to airdrop to payer"); - ctx.airdrop_chain(&account_kp.pubkey(), 2 * LAMPORTS_PER_SOL) - .expect("failed to airdrop account"); - - let (delegate_sig, confirmed) = ctx - .delegate_account_to_validator( - &payer_chain, - &account_kp, - Some(other_validator), - ) - .expect("failed to delegate account"); - assert!(confirmed, "Failed to confirm delegation"); - debug!("Delegation tx: {delegate_sig}"); - - // 2. Get account from ephemeral to trigger clone - let acc = ctx.fetch_ephem_account(account_kp.pubkey()); - assert!(acc.is_ok()); - - // 3. Find the cloning transaction - let clone_sig = ctx - .last_transaction_mentioning_account_ephem(&account_kp.pubkey()) - .expect("failed to find cloning transaction"); - debug!("Cloning transaction: {clone_sig}"); - - // 4. Verify logs contain the delegation message - ctx.assert_ephemeral_logs_contain( - clone_sig, - &format!( - "MutateAccounts: account is delegated to another validator: {}", - other_validator - ), - ); -} - -#[test] -fn test_account_clone_logs_delegated_to_us() { - init_logger!(); - let ctx = IntegrationTestContext::try_new().unwrap(); - - let payer_chain = Keypair::new(); - let delegated_kp = Keypair::new(); - - ctx.airdrop_chain(&payer_chain.pubkey(), 5 * LAMPORTS_PER_SOL) - .expect("failed to airdrop to payer"); - - let (airdrop_sig, deleg_sig) = ctx - .airdrop_chain_and_delegate( - &payer_chain, - &delegated_kp, - 2 * LAMPORTS_PER_SOL, - ) - .expect("failed to airdrop and delegate"); - debug!("Airdrop + delegation tx: {airdrop_sig}, {deleg_sig}"); - - // 2. Get account from ephemeral to trigger clone - let acc = ctx.fetch_ephem_account(delegated_kp.pubkey()); - assert!(acc.is_ok()); - - // 3. Find the cloning transaction (should be most recent one mentioning account) - let clone_sig = ctx - .last_transaction_mentioning_account_ephem(&delegated_kp.pubkey()) - .expect("failed to find cloning transaction"); - debug!("Cloning transaction: {clone_sig}"); - - // 4. Verify logs contain the delegated true message - // In that case on additional log is printed - ctx.assert_ephemeral_logs_contain( - clone_sig, - "MutateAccounts: setting delegated to true", - ); -} - -#[test] -fn test_account_clone_logs_confined_delegation() { - init_logger!(); - let ctx = IntegrationTestContext::try_new().unwrap(); - - // 1. Create account and delegate to system program (confined) - let payer_chain = Keypair::new(); - let account_kp = Keypair::new(); - - ctx.airdrop_chain(&payer_chain.pubkey(), 5 * LAMPORTS_PER_SOL) - .expect("failed to airdrop to payer"); - ctx.airdrop_chain(&account_kp.pubkey(), 2 * LAMPORTS_PER_SOL) - .expect("failed to airdrop account"); - - let (delegate_sig, confirmed) = ctx - .delegate_account_to_any_validator( - &payer_chain, - &account_kp, - Some(solana_sdk::system_program::id()), - ) - .expect("failed to delegate to system program"); - assert!(confirmed, "Failed to confirm delegation"); - debug!("Delegation tx: {delegate_sig}"); - - // 2. Get account from ephemeral to trigger clone - let acc = ctx.fetch_ephem_account(account_kp.pubkey()); - assert!(acc.is_ok()); - - // 3. Find the cloning transaction - let clone_sig = ctx - .last_transaction_mentioning_account_ephem(&account_kp.pubkey()) - .expect("failed to find cloning transaction"); - debug!("Cloning transaction: {clone_sig}"); - - ctx.assert_ephemeral_logs_contain( - clone_sig, - "MutateAccounts: setting delegated to true", - ); - ctx.assert_ephemeral_logs_contain( - clone_sig, - "MutateAccounts: setting confined to true", - ); -} From 7d238520b8d1f20b285e073a38d52d2ef41625ce Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 20 Feb 2026 14:44:28 +0400 Subject: [PATCH 09/16] fix: use rent sysvar instead of default --- programs/magicblock/src/clone_account/common.rs | 11 +++++++++++ .../src/clone_account/process_finalize_buffer.rs | 5 ++--- .../src/clone_account/process_finalize_v1_buffer.rs | 8 +++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index af68fe3b8..888bcbc06 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -182,6 +182,17 @@ pub fn get_deploy_slot(invoke_context: &InvokeContext) -> u64 { .unwrap_or(0) } +/// Calculates rent-exempt lamports for the given data length using the Rent sysvar. +pub fn minimum_balance( + invoke_context: &InvokeContext, + data_len: usize, +) -> Result { + invoke_context + .get_sysvar_cache() + .get_rent() + .map(|rent| rent.minimum_balance(data_len)) +} + /// Sets account fields from AccountCloneFields and data. pub fn set_account_from_fields( account: &RefCell, diff --git a/programs/magicblock/src/clone_account/process_finalize_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_buffer.rs index e734d22bb..47bcd2eb9 100644 --- a/programs/magicblock/src/clone_account/process_finalize_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -11,12 +11,11 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_sdk_ids::loader_v4; -use solana_sysvar::rent::Rent; use solana_transaction_context::TransactionContext; use super::{ adjust_authority_lamports, close_buffer_account, get_deploy_slot, - loader_v4_state_to_bytes, validate_authority, + loader_v4_state_to_bytes, minimum_balance, validate_authority, }; use crate::validator::validator_authority_id; @@ -83,7 +82,7 @@ pub(crate) fn process_finalize_program_from_buffer( ); // Calculate rent-exempt lamports for full program account - let prog_lamports = Rent::default().minimum_balance(program_data.len()); + let prog_lamports = minimum_balance(invoke_context, program_data.len())?; let lamports_delta = prog_lamports as i64 - prog_current_lamports as i64 - buf_lamports as i64; diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs index b784a2bce..98bffa866 100644 --- a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -12,12 +12,11 @@ use solana_log_collector::ic_msg; use solana_program_runtime::invoke_context::InvokeContext; use solana_pubkey::Pubkey; use solana_sdk_ids::bpf_loader_upgradeable; -use solana_sysvar::rent::Rent; use solana_transaction_context::TransactionContext; use super::{ adjust_authority_lamports, close_buffer_account, get_deploy_slot, - validate_authority, + minimum_balance, validate_authority, }; /// Finalizes a V1 program from a buffer account, converting to V3 (upgradeable loader) format. @@ -112,9 +111,8 @@ pub(crate) fn process_finalize_v1_program_from_buffer( }; // Calculate rent-exempt lamports for both accounts - let rent = Rent::default(); - let data_lamports = rent.minimum_balance(program_data_content.len()); - let prog_lamports = rent.minimum_balance(program_content.len()).max(1); + let data_lamports = minimum_balance(invoke_context, program_data_content.len())?; + let prog_lamports = minimum_balance(invoke_context, program_content.len())?.max(1); let prog_current = prog_acc.borrow().lamports(); let data_current = data_acc.borrow().lamports(); From 2210c9b4739d16ef550a206f7f8f26e2e9d69ffb Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 20 Feb 2026 16:50:58 +0400 Subject: [PATCH 10/16] fix: fmt --- .../src/clone_account/process_finalize_v1_buffer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs index 98bffa866..88acd6f6a 100644 --- a/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -111,8 +111,10 @@ pub(crate) fn process_finalize_v1_program_from_buffer( }; // Calculate rent-exempt lamports for both accounts - let data_lamports = minimum_balance(invoke_context, program_data_content.len())?; - let prog_lamports = minimum_balance(invoke_context, program_content.len())?.max(1); + let data_lamports = + minimum_balance(invoke_context, program_data_content.len())?; + let prog_lamports = + minimum_balance(invoke_context, program_content.len())?.max(1); let prog_current = prog_acc.borrow().lamports(); let data_current = data_acc.borrow().lamports(); From ef0e7096a36b9467fbbf76ff26334a1bbaa4c3ac Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Mon, 2 Mar 2026 14:39:44 +0400 Subject: [PATCH 11/16] fix: add check for invalid pending clone reversals --- .../src/clone_account/process_cleanup.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs index bdf35b53e..fcceee419 100644 --- a/programs/magicblock/src/clone_account/process_cleanup.rs +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -10,9 +10,10 @@ use solana_pubkey::Pubkey; use solana_transaction_context::TransactionContext; use super::{ - adjust_authority_lamports, close_buffer_account, remove_pending_clone, - validate_and_get_index, validate_authority, + adjust_authority_lamports, close_buffer_account, is_pending_clone, + remove_pending_clone, validate_and_get_index, validate_authority, }; +use crate::errors::MagicBlockProgramError; /// Cleans up a failed multi-transaction clone. /// @@ -26,6 +27,19 @@ pub(crate) fn process_cleanup_partial_clone( pubkey: Pubkey, ) -> Result<(), InstructionError> { validate_authority(signers, invoke_context)?; + + // Safety check: only cleanup accounts that are actually in a pending clone state. + // This prevents accidental deletion of valid accounts if cleanup is called + // when no multi-tx clone was in progress (e.g., CloneAccountInit failed early). + if !is_pending_clone(&pubkey) { + ic_msg!( + invoke_context, + "CleanupPartialClone: account {} is not in pending clone state; refusing to delete", + pubkey + ); + return Err(MagicBlockProgramError::NoPendingClone.into()); + } + remove_pending_clone(&pubkey); let ctx = transaction_context.get_current_instruction_context()?; From cb10a4f67b44b2d5e0385ebe2292e042f69c2be0 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 4 Mar 2026 15:40:01 +0400 Subject: [PATCH 12/16] chore: fix Cargo.lock post rebase --- test-integration/Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index e793be94b..4489d9149 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3763,6 +3763,8 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-keypair", + "solana-loader-v3-interface 3.0.0", + "solana-loader-v4-interface", "solana-log-collector", "solana-program-runtime", "solana-pubkey", From dfa02d0f21e938b73332fd6fb23d6b6a3d53759c Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 5 Mar 2026 20:45:07 +0400 Subject: [PATCH 13/16] fix: address review comments --- magicblock-account-cloner/src/lib.rs | 47 ++- .../magicblock/src/clone_account/common.rs | 12 + programs/magicblock/src/clone_account/mod.rs | 2 + .../src/clone_account/process_cleanup.rs | 3 +- .../src/clone_account/process_clone.rs | 2 +- .../src/clone_account/process_clone_init.rs | 2 +- .../magicblock/src/clone_account/tests.rs | 384 ++++++++++++++++++ .../magicblock/src/utils/instruction_utils.rs | 79 ++++ 8 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 programs/magicblock/src/clone_account/tests.rs diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index f722619e5..d02a5cc05 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -106,7 +106,11 @@ impl ChainlinkCloner { Ok(sig) } - fn sign_tx(&self, ixs: &[Instruction], blockhash: Hash) -> Transaction { + fn create_signed_tx( + &self, + ixs: &[Instruction], + blockhash: Hash, + ) -> Transaction { let kp = validator_authority(); Transaction::new_signed_with_payer( ixs, @@ -247,7 +251,7 @@ impl ChainlinkCloner { // let ixs = self.maybe_add_crank_commits_ix(request, clone_ix); let ixs = vec![clone_ix]; - self.sign_tx(&ixs, blockhash) + self.create_signed_tx(&ixs, blockhash) } /// Builds crank commits instruction for periodic account commits. @@ -292,11 +296,14 @@ impl ChainlinkCloner { let first_chunk = data[..MAX_INLINE_DATA_SIZE.min(data.len())].to_vec(); let init_ix = Self::clone_init_ix( request.pubkey, + // we assume the cloned accounts do not have data field + // not exceeding the max solana limit, which is always true + // since the source of cloning is always base chain data.len() as u32, first_chunk, fields, ); - txs.push(self.sign_tx(&[init_ix], blockhash)); + txs.push(self.create_signed_tx(&[init_ix], blockhash)); // Continue txs for remaining chunks let mut offset = MAX_INLINE_DATA_SIZE; @@ -311,7 +318,7 @@ impl ChainlinkCloner { chunk, is_last, ); - txs.push(self.sign_tx(&[continue_ix], blockhash)); + txs.push(self.create_signed_tx(&[continue_ix], blockhash)); offset = end; } @@ -320,7 +327,7 @@ impl ChainlinkCloner { async fn send_cleanup(&self, pubkey: Pubkey) { let blockhash = self.block.load().blockhash; - let tx = self.sign_tx(&[Self::cleanup_ix(pubkey)], blockhash); + let tx = self.create_signed_tx(&[Self::cleanup_ix(pubkey)], blockhash); if let Err(e) = self.send_tx(tx).await { error!(pubkey = %pubkey, error = ?e, "Failed to cleanup partial clone"); } @@ -371,7 +378,7 @@ impl ChainlinkCloner { buffer_fields, )]; ixs.extend(finalize_ixs); - vec![self.sign_tx(&ixs, blockhash)] + vec![self.create_signed_tx(&ixs, blockhash)] } else { // Large: multi-transaction flow self.build_large_program_txs( @@ -386,6 +393,12 @@ impl ChainlinkCloner { /// V1 programs are converted to V3 (upgradeable loader) format. /// Supports programs of any size via multi-transaction cloning. + /// + /// NOTE: we don't support modifying this kind of program once it was + /// deployed into our validator once. + /// By nature of being immutable on chain this should never happen. + /// Thus we avoid having to run the upgrade instruction and get + /// away with just directly modifying the program and program data accounts. fn build_v1_program_txs( &self, program: LoadedProgram, @@ -532,7 +545,7 @@ impl ChainlinkCloner { first_chunk.to_vec(), fields, ); - let mut txs = vec![self.sign_tx(&[init_ix], blockhash)]; + let mut txs = vec![self.create_signed_tx(&[init_ix], blockhash)]; // Middle chunks (all except last) let last_offset = (num_chunks - 1) * MAX_INLINE_DATA_SIZE; @@ -546,7 +559,7 @@ impl ChainlinkCloner { chunk.to_vec(), false, ); - txs.push(self.sign_tx(&[continue_ix], blockhash)); + txs.push(self.create_signed_tx(&[continue_ix], blockhash)); } // Last chunk with finalize instructions @@ -558,7 +571,7 @@ impl ChainlinkCloner { true, )]; ixs.extend(finalize_ixs); - txs.push(self.sign_tx(&ixs, blockhash)); + txs.push(self.create_signed_tx(&ixs, blockhash)); txs } @@ -624,10 +637,12 @@ impl Cloner for ChainlinkCloner { // Large account: multi-tx with cleanup on failure let txs = self.build_large_account_txs(&request, blockhash); - let mut last_sig = Signature::default(); + let mut last_sig = None; for tx in txs { match self.send_tx(tx).await { - Ok(sig) => last_sig = sig, + Ok(sig) => { + last_sig.replace(sig); + } Err(e) => { self.send_cleanup(request.pubkey).await; return Err(ClonerError::FailedToCloneRegularAccount( @@ -638,7 +653,7 @@ impl Cloner for ChainlinkCloner { } } - Ok(last_sig) + Ok(last_sig.unwrap_or_default()) } async fn clone_program( @@ -663,10 +678,12 @@ impl Cloner for ChainlinkCloner { // Both V1 and V4 use buffer_pubkey for multi-tx cloning let buffer_pubkey = derive_buffer_pubkey(&program_id).0; - let mut last_sig = Signature::default(); + let mut last_sig = None; for tx in txs { match self.send_tx(tx).await { - Ok(sig) => last_sig = sig, + Ok(sig) => { + last_sig.replace(sig); + } Err(e) => { self.send_cleanup(buffer_pubkey).await; return Err(ClonerError::FailedToCloneProgram( @@ -685,6 +702,6 @@ impl Cloner for ChainlinkCloner { let _ = block_updated.recv().await; } - Ok(last_sig) + Ok(last_sig.unwrap_or_default()) } } diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index 888bcbc06..003fb57b7 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -195,10 +195,22 @@ pub fn minimum_balance( /// Sets account fields from AccountCloneFields and data. pub fn set_account_from_fields( + invoke_context: &InvokeContext, account: &RefCell, data: &[u8], fields: &AccountCloneFields, ) { + ic_msg!( + invoke_context, + "account state: lamports={}, owner={}, executable={}, delegated={}, confined={}, remote_slot={}, data_len={}", + fields.lamports, + fields.owner, + fields.executable, + fields.delegated, + fields.confined, + fields.remote_slot, + data.len() + ); let mut acc = account.borrow_mut(); acc.set_lamports(fields.lamports); acc.set_owner(fields.owner); diff --git a/programs/magicblock/src/clone_account/mod.rs b/programs/magicblock/src/clone_account/mod.rs index 063d0fc50..19eb2f087 100644 --- a/programs/magicblock/src/clone_account/mod.rs +++ b/programs/magicblock/src/clone_account/mod.rs @@ -32,6 +32,8 @@ mod process_clone_init; mod process_finalize_buffer; mod process_finalize_v1_buffer; mod process_set_authority; +#[cfg(test)] +mod tests; use std::collections::HashSet; diff --git a/programs/magicblock/src/clone_account/process_cleanup.rs b/programs/magicblock/src/clone_account/process_cleanup.rs index fcceee419..a2a3d94bc 100644 --- a/programs/magicblock/src/clone_account/process_cleanup.rs +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -40,8 +40,6 @@ pub(crate) fn process_cleanup_partial_clone( return Err(MagicBlockProgramError::NoPendingClone.into()); } - remove_pending_clone(&pubkey); - let ctx = transaction_context.get_current_instruction_context()?; let auth_acc = transaction_context.get_account_at_index( ctx.get_index_of_instruction_account_in_transaction(0)?, @@ -68,5 +66,6 @@ pub(crate) fn process_cleanup_partial_clone( close_buffer_account(account); adjust_authority_lamports(auth_acc, lamports_delta)?; + remove_pending_clone(&pubkey); Ok(()) } diff --git a/programs/magicblock/src/clone_account/process_clone.rs b/programs/magicblock/src/clone_account/process_clone.rs index 63236b5b7..4f8fed31d 100644 --- a/programs/magicblock/src/clone_account/process_clone.rs +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -63,7 +63,7 @@ pub(crate) fn process_clone_account( let current_lamports = account.borrow().lamports(); let lamports_delta = fields.lamports as i64 - current_lamports as i64; - set_account_from_fields(account, &data, &fields); + set_account_from_fields(invoke_context, account, &data, &fields); adjust_authority_lamports(auth_acc, lamports_delta)?; Ok(()) diff --git a/programs/magicblock/src/clone_account/process_clone_init.rs b/programs/magicblock/src/clone_account/process_clone_init.rs index 8037ee531..f8d289609 100644 --- a/programs/magicblock/src/clone_account/process_clone_init.rs +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -96,7 +96,7 @@ pub(crate) fn process_clone_account_init( let mut data = vec![0u8; total_data_len as usize]; data[..initial_data.len()].copy_from_slice(&initial_data); - set_account_from_fields(account, &data, &fields); + set_account_from_fields(invoke_context, account, &data, &fields); adjust_authority_lamports(auth_acc, lamports_delta)?; add_pending_clone(pubkey); diff --git a/programs/magicblock/src/clone_account/tests.rs b/programs/magicblock/src/clone_account/tests.rs new file mode 100644 index 000000000..caf7a4372 --- /dev/null +++ b/programs/magicblock/src/clone_account/tests.rs @@ -0,0 +1,384 @@ +//! Tests for clone account instructions. + +use std::collections::HashMap; + +use magicblock_magic_program_api::instruction::AccountCloneFields; +use solana_account::{AccountSharedData, ReadableAccount}; +use solana_instruction::{error::InstructionError, AccountMeta}; +use test_kit::init_logger; + +use super::*; +use crate::{ + errors::MagicBlockProgramError, + instruction_utils::InstructionUtils, + test_utils::{ + ensure_started_validator, process_instruction, AUTHORITY_BALANCE, + }, +}; + +fn clone_fields(lamports: u64, remote_slot: u64) -> AccountCloneFields { + AccountCloneFields { + lamports, + remote_slot, + ..Default::default() + } +} + +fn setup_with_account( + pubkey: Pubkey, + lamports: u64, + remote_slot: u64, +) -> HashMap { + let mut account = AccountSharedData::new(lamports, 0, &pubkey); + account.set_remote_slot(remote_slot); + let mut map = HashMap::new(); + map.insert(pubkey, account); + ensure_started_validator(&mut map); + map +} + +// Build transaction_accounts in instruction order (matching ix.accounts) +fn tx_accounts( + mut account_data: HashMap, + ix_accounts: &[AccountMeta], +) -> Vec<(Pubkey, AccountSharedData)> { + ix_accounts + .iter() + .flat_map(|acc| { + account_data + .remove(&acc.pubkey) + .map(|shared_data| (acc.pubkey, shared_data)) + }) + .collect() +} + +// ----------------- +// CloneAccount +// ----------------- + +#[test] +fn test_clone_account_basic() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 100, 0); + + let fields = AccountCloneFields { + lamports: 500, + owner: Pubkey::from([1; 32]), + executable: true, + delegated: true, + confined: true, + remote_slot: 42, + }; + let ix = InstructionUtils::clone_account_instruction( + pubkey, + vec![1, 2, 3], + fields, + ); + + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + result.drain(0..1); // skip authority + let account = result.drain(0..1).next().unwrap(); + + assert_eq!(account.lamports(), 500); + assert_eq!(account.owner(), &Pubkey::from([1; 32])); + assert!(account.executable()); + assert!(account.delegated()); + assert!(account.confined()); + assert_eq!(account.remote_slot(), 42); + assert_eq!(account.data(), &[1, 2, 3]); +} + +#[test] +fn test_clone_account_rejects_stale_slot() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 100, 100); + + let ix = InstructionUtils::clone_account_instruction( + pubkey, + vec![], + clone_fields(200, 50), // slot 50 < current 100 + ); + + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::OutOfOrderUpdate.into()), + ); +} + +#[test] +fn test_clone_account_rejects_delegated_account() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let mut account = AccountSharedData::new(100, 0, &pubkey); + account.set_delegated(true); + let mut accounts = HashMap::new(); + accounts.insert(pubkey, account); + ensure_started_validator(&mut accounts); + + let ix = InstructionUtils::clone_account_instruction( + pubkey, + vec![], + clone_fields(200, 0), + ); + + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::AccountIsDelegated.into()), + ); +} + +#[test] +fn test_clone_account_adjusts_authority_lamports() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 100, 0); + + let ix = InstructionUtils::clone_account_instruction( + pubkey, + vec![], + clone_fields(300, 0), + ); + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + + let authority = result.drain(0..1).next().unwrap(); + assert_eq!(authority.lamports(), AUTHORITY_BALANCE - 200); // 300 - 100 delta +} + +// ----------------- +// CloneAccountInit + Continue +// ----------------- + +#[test] +fn test_clone_init_allocates_buffer() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 0, 0); + + let ix = InstructionUtils::clone_account_init_instruction( + pubkey, + 10, + vec![1, 2, 3, 4], + clone_fields(1000, 50), + ); + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + + result.drain(0..1); // authority + let account = result.drain(0..1).next().unwrap(); + assert_eq!(account.data(), &[1, 2, 3, 4, 0, 0, 0, 0, 0, 0]); + assert_eq!(account.lamports(), 1000); + assert!(is_pending_clone(&pubkey)); +} + +#[test] +fn test_clone_continue_writes_at_offset() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + // Setup account with 10 bytes (simulating post-init state) + let mut account = AccountSharedData::new(1000, 10, &pubkey); + account.set_remote_slot(50); + let mut accounts = HashMap::new(); + accounts.insert(pubkey, account); + ensure_started_validator(&mut accounts); + + add_pending_clone(pubkey); + + let ix = InstructionUtils::clone_account_continue_instruction( + pubkey, + 4, + vec![5, 6, 7], + false, + ); + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + + result.drain(0..1); + let account = result.drain(0..1).next().unwrap(); + assert_eq!(&account.data()[4..7], &[5, 6, 7]); + assert!(is_pending_clone(&pubkey)); // not removed when is_last=false +} + +#[test] +fn test_clone_continue_completes_clone() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let mut account = AccountSharedData::new(1000, 10, &pubkey); + account.set_remote_slot(50); + let mut accounts = HashMap::new(); + accounts.insert(pubkey, account); + ensure_started_validator(&mut accounts); + + add_pending_clone(pubkey); + + let ix = InstructionUtils::clone_account_continue_instruction( + pubkey, + 7, + vec![8, 9, 10], + true, + ); + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + + result.drain(0..1); + let account = result.drain(0..1).next().unwrap(); + assert_eq!(&account.data()[7..10], &[8, 9, 10]); + assert!(!is_pending_clone(&pubkey)); // removed when is_last=true +} + +#[test] +fn test_clone_init_rejects_double_init() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 0, 0); + + add_pending_clone(pubkey); + + let ix = InstructionUtils::clone_account_init_instruction( + pubkey, + 10, + vec![], + clone_fields(100, 0), + ); + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::CloneAlreadyPending.into()), + ); +} + +#[test] +fn test_clone_init_rejects_oversized_initial_data() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 0, 0); + + let ix = InstructionUtils::clone_account_init_instruction( + pubkey, + 5, + vec![1, 2, 3, 4, 5, 6], + clone_fields(100, 0), + ); + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(InstructionError::InvalidArgument), + ); +} + +#[test] +fn test_clone_continue_rejects_without_init() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 0, 0); + + let ix = InstructionUtils::clone_account_continue_instruction( + pubkey, + 0, + vec![1], + true, + ); + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::NoPendingClone.into()), + ); +} + +#[test] +fn test_clone_continue_rejects_offset_overflow() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 0, 0); + + add_pending_clone(pubkey); + // Account has 0 data bytes, but we try to write at offset 5 + let ix = InstructionUtils::clone_account_continue_instruction( + pubkey, + 5, + vec![1], + true, + ); + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(InstructionError::InvalidArgument), + ); +} + +// ----------------- +// CleanupPartialClone +// ----------------- + +#[test] +fn test_cleanup_only_works_for_pending_clones() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 100, 0); + + // Not in pending clones + let ix = InstructionUtils::cleanup_partial_clone_instruction(pubkey); + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::NoPendingClone.into()), + ); +} + +#[test] +fn test_cleanup_resets_account_and_returns_lamports() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let accounts = setup_with_account(pubkey, 500, 0); + + add_pending_clone(pubkey); + + let ix = InstructionUtils::cleanup_partial_clone_instruction(pubkey); + let mut result = process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Ok(()), + ); + + let authority = result.drain(0..1).next().unwrap(); + assert_eq!(authority.lamports(), AUTHORITY_BALANCE + 500); // got lamports back + + let account = result.drain(0..1).next().unwrap(); + assert_eq!(account.lamports(), 0); + assert!(account.ephemeral()); // marked for removal + + assert!(!is_pending_clone(&pubkey)); +} diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index d86375636..831cea34e 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -289,6 +289,85 @@ impl InstructionUtils { ) } + // ----------------- + // CloneAccount + // ----------------- + #[cfg(test)] + pub fn clone_account_instruction( + pubkey: Pubkey, + data: Vec, + fields: magicblock_magic_program_api::instruction::AccountCloneFields, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::CloneAccount { + pubkey, + data, + fields, + }, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(pubkey, false), + ], + ) + } + + #[cfg(test)] + pub fn clone_account_init_instruction( + pubkey: Pubkey, + total_data_len: u32, + initial_data: Vec, + fields: magicblock_magic_program_api::instruction::AccountCloneFields, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::CloneAccountInit { + pubkey, + total_data_len, + initial_data, + fields, + }, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(pubkey, false), + ], + ) + } + + #[cfg(test)] + pub fn clone_account_continue_instruction( + pubkey: Pubkey, + offset: u32, + data: Vec, + is_last: bool, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::CloneAccountContinue { + pubkey, + offset, + data, + is_last, + }, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(pubkey, false), + ], + ) + } + + #[cfg(test)] + pub fn cleanup_partial_clone_instruction(pubkey: Pubkey) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::CleanupPartialClone { pubkey }, + vec![ + AccountMeta::new(validator_authority_id(), true), + AccountMeta::new(pubkey, false), + ], + ) + } + // ----------------- // Utils // ----------------- From 7f2636813c0932cbe606cff659756de5c271edbf Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 6 Mar 2026 12:11:44 +0400 Subject: [PATCH 14/16] test: add more comprehensive security tests --- magicblock-account-cloner/src/lib.rs | 96 ++++--------------- .../magicblock/src/clone_account/tests.rs | 60 +++++++++++- .../magicblock/src/utils/instruction_utils.rs | 59 +++++++++++- 3 files changed, 130 insertions(+), 85 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index d02a5cc05..3ec250f33 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -121,7 +121,7 @@ impl ChainlinkCloner { } // ----------------- - // Instruction Builders + // Instruction Builders (delegates to InstructionUtils) // ----------------- fn clone_ix( @@ -129,15 +129,7 @@ impl ChainlinkCloner { data: Vec, fields: AccountCloneFields, ) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::CloneAccount { - pubkey, - data, - fields, - }, - clone_account_metas(pubkey), - ) + InstructionUtils::clone_account_instruction(pubkey, data, fields) } fn clone_init_ix( @@ -146,15 +138,11 @@ impl ChainlinkCloner { initial_data: Vec, fields: AccountCloneFields, ) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::CloneAccountInit { - pubkey, - total_data_len: total_len, - initial_data, - fields, - }, - clone_account_metas(pubkey), + InstructionUtils::clone_account_init_instruction( + pubkey, + total_len, + initial_data, + fields, ) } @@ -164,24 +152,13 @@ impl ChainlinkCloner { data: Vec, is_last: bool, ) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::CloneAccountContinue { - pubkey, - offset, - data, - is_last, - }, - clone_account_metas(pubkey), + InstructionUtils::clone_account_continue_instruction( + pubkey, offset, data, is_last, ) } fn cleanup_ix(pubkey: Pubkey) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::CleanupPartialClone { pubkey }, - clone_account_metas(pubkey), - ) + InstructionUtils::cleanup_partial_clone_instruction(pubkey) } fn finalize_program_ix( @@ -189,26 +166,15 @@ impl ChainlinkCloner { buffer: Pubkey, remote_slot: u64, ) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::FinalizeProgramFromBuffer { remote_slot }, - vec![ - AccountMeta::new_readonly(validator_authority_id(), true), - AccountMeta::new(program, false), - AccountMeta::new(buffer, false), - ], + InstructionUtils::finalize_program_from_buffer_instruction( + program, + buffer, + remote_slot, ) } fn set_authority_ix(program: Pubkey, authority: Pubkey) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::SetProgramAuthority { authority }, - vec![ - AccountMeta::new_readonly(validator_authority_id(), true), - AccountMeta::new(program, false), - ], - ) + InstructionUtils::set_program_authority_instruction(program, authority) } // ----------------- @@ -423,7 +389,7 @@ impl ChainlinkCloner { InstructionUtils::disable_executable_check_instruction( &validator_authority_id(), ), - Self::finalize_v1_program_ix( + InstructionUtils::finalize_v1_program_from_buffer_instruction( program_id, program_data_addr, buffer_pubkey, @@ -445,29 +411,6 @@ impl ChainlinkCloner { Ok(Some(txs)) } - /// Builds finalize instruction for V1 programs (creates V3 accounts from buffer). - fn finalize_v1_program_ix( - program: Pubkey, - program_data: Pubkey, - buffer: Pubkey, - remote_slot: u64, - authority: Pubkey, - ) -> Instruction { - Instruction::new_with_bincode( - magicblock_program::ID, - &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { - remote_slot, - authority, - }, - vec![ - AccountMeta::new_readonly(validator_authority_id(), true), - AccountMeta::new(program, false), - AccountMeta::new(program_data, false), - AccountMeta::new(buffer, false), - ], - ) - } - /// V2/V3/V4 programs use LoaderV4 with proper deploy flow. /// Supports programs of any size via multi-transaction cloning. fn build_v4_program_txs( @@ -600,13 +543,6 @@ impl ChainlinkCloner { } /// Shared account metas for clone instructions. -fn clone_account_metas(pubkey: Pubkey) -> Vec { - vec![ - AccountMeta::new(validator_authority_id(), true), - AccountMeta::new(pubkey, false), - ] -} - #[async_trait] impl Cloner for ChainlinkCloner { async fn clone_account( diff --git a/programs/magicblock/src/clone_account/tests.rs b/programs/magicblock/src/clone_account/tests.rs index caf7a4372..0c3ede1c7 100644 --- a/programs/magicblock/src/clone_account/tests.rs +++ b/programs/magicblock/src/clone_account/tests.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; -use magicblock_magic_program_api::instruction::AccountCloneFields; +use magicblock_magic_program_api::instruction::{ + AccountCloneFields, MagicBlockInstruction, +}; use solana_account::{AccountSharedData, ReadableAccount}; use solana_instruction::{error::InstructionError, AccountMeta}; use test_kit::init_logger; @@ -56,6 +58,38 @@ fn tx_accounts( // CloneAccount // ----------------- +#[test] +fn test_rejects_wrong_signer_pubkey() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let wrong_signer = Pubkey::new_unique(); + let mut accounts = setup_with_account(pubkey, 100, 0); + // Add wrong signer account + accounts + .insert(wrong_signer, AccountSharedData::new(1000, 0, &wrong_signer)); + + // Build instruction with WRONG pubkey as signer (not validator authority) + let ix = solana_instruction::Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::CloneAccount { + pubkey, + data: vec![], + fields: clone_fields(200, 0), + }, + vec![ + AccountMeta::new(wrong_signer, true), // wrong signer! + AccountMeta::new(pubkey, false), + ], + ); + + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(InstructionError::MissingRequiredSignature), + ); +} + #[test] fn test_clone_account_basic() { init_logger!(); @@ -138,6 +172,30 @@ fn test_clone_account_rejects_delegated_account() { ); } +#[test] +fn test_clone_account_rejects_ephemeral_account() { + init_logger!(); + let pubkey = Pubkey::new_unique(); + let mut account = AccountSharedData::new(100, 0, &pubkey); + account.set_ephemeral(true); + let mut accounts = HashMap::new(); + accounts.insert(pubkey, account); + ensure_started_validator(&mut accounts); + + let ix = InstructionUtils::clone_account_instruction( + pubkey, + vec![], + clone_fields(200, 0), + ); + + process_instruction( + &ix.data, + tx_accounts(accounts, &ix.accounts), + ix.accounts, + Err(MagicBlockProgramError::AccountIsEphemeral.into()), + ); +} + #[test] fn test_clone_account_adjusts_authority_lamports() { init_logger!(); diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index 831cea34e..0ad279144 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -292,7 +292,6 @@ impl InstructionUtils { // ----------------- // CloneAccount // ----------------- - #[cfg(test)] pub fn clone_account_instruction( pubkey: Pubkey, data: Vec, @@ -312,7 +311,6 @@ impl InstructionUtils { ) } - #[cfg(test)] pub fn clone_account_init_instruction( pubkey: Pubkey, total_data_len: u32, @@ -334,7 +332,6 @@ impl InstructionUtils { ) } - #[cfg(test)] pub fn clone_account_continue_instruction( pubkey: Pubkey, offset: u32, @@ -356,7 +353,6 @@ impl InstructionUtils { ) } - #[cfg(test)] pub fn cleanup_partial_clone_instruction(pubkey: Pubkey) -> Instruction { Instruction::new_with_bincode( crate::id(), @@ -368,6 +364,61 @@ impl InstructionUtils { ) } + // ----------------- + // Program Cloning + // ----------------- + pub fn finalize_program_from_buffer_instruction( + program: Pubkey, + buffer: Pubkey, + remote_slot: u64, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::FinalizeProgramFromBuffer { remote_slot }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + AccountMeta::new(buffer, false), + ], + ) + } + + pub fn finalize_v1_program_from_buffer_instruction( + program: Pubkey, + program_data: Pubkey, + buffer: Pubkey, + remote_slot: u64, + authority: Pubkey, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::FinalizeV1ProgramFromBuffer { + remote_slot, + authority, + }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + AccountMeta::new(program_data, false), + AccountMeta::new(buffer, false), + ], + ) + } + + pub fn set_program_authority_instruction( + program: Pubkey, + authority: Pubkey, + ) -> Instruction { + Instruction::new_with_bincode( + crate::id(), + &MagicBlockInstruction::SetProgramAuthority { authority }, + vec![ + AccountMeta::new_readonly(validator_authority_id(), true), + AccountMeta::new(program, false), + ], + ) + } + // ----------------- // Utils // ----------------- From 7b8cf7c12b45add3d5ef3fb3cdccf5e99de431ec Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 6 Mar 2026 12:18:40 +0400 Subject: [PATCH 15/16] chore: add doc comment explaining buffer account closure --- programs/magicblock/src/clone_account/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/magicblock/src/clone_account/common.rs b/programs/magicblock/src/clone_account/common.rs index 003fb57b7..80c5b64da 100644 --- a/programs/magicblock/src/clone_account/common.rs +++ b/programs/magicblock/src/clone_account/common.rs @@ -166,7 +166,8 @@ pub fn close_buffer_account(account: &RefCell) { let mut acc = account.borrow_mut(); acc.set_lamports(0); acc.resize(0, 0); - // this hack allows us to close the account and remove it from accountsdb + // Setting ephemeral flag on empty account, forces + // accountsdb to remove it, thus reclaiming space acc.set_ephemeral(true); acc.set_delegated(false); } From 2ca3687fb78e5dfde6575b6d2b57421c363d4140 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Sun, 8 Mar 2026 23:05:07 +0400 Subject: [PATCH 16/16] fix: post rebase test fixes --- programs/magicblock/src/clone_account/tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/programs/magicblock/src/clone_account/tests.rs b/programs/magicblock/src/clone_account/tests.rs index 0c3ede1c7..0ba06133b 100644 --- a/programs/magicblock/src/clone_account/tests.rs +++ b/programs/magicblock/src/clone_account/tests.rs @@ -35,7 +35,7 @@ fn setup_with_account( account.set_remote_slot(remote_slot); let mut map = HashMap::new(); map.insert(pubkey, account); - ensure_started_validator(&mut map); + ensure_started_validator(&mut map, None); map } @@ -156,7 +156,7 @@ fn test_clone_account_rejects_delegated_account() { account.set_delegated(true); let mut accounts = HashMap::new(); accounts.insert(pubkey, account); - ensure_started_validator(&mut accounts); + ensure_started_validator(&mut accounts, None); let ix = InstructionUtils::clone_account_instruction( pubkey, @@ -180,7 +180,7 @@ fn test_clone_account_rejects_ephemeral_account() { account.set_ephemeral(true); let mut accounts = HashMap::new(); accounts.insert(pubkey, account); - ensure_started_validator(&mut accounts); + ensure_started_validator(&mut accounts, None); let ix = InstructionUtils::clone_account_instruction( pubkey, @@ -257,7 +257,7 @@ fn test_clone_continue_writes_at_offset() { account.set_remote_slot(50); let mut accounts = HashMap::new(); accounts.insert(pubkey, account); - ensure_started_validator(&mut accounts); + ensure_started_validator(&mut accounts, None); add_pending_clone(pubkey); @@ -288,7 +288,7 @@ fn test_clone_continue_completes_clone() { account.set_remote_slot(50); let mut accounts = HashMap::new(); accounts.insert(pubkey, account); - ensure_started_validator(&mut accounts); + ensure_started_validator(&mut accounts, None); add_pending_clone(pubkey);