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..3ec250f33 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -1,60 +1,83 @@ -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; use async_trait::async_trait; -use magicblock_accounts_db::AccountsDb; use magicblock_chainlink::{ cloner::{ errors::{ClonerError, ClonerResult}, 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>, config: ChainLinkConfig, tx_scheduler: TransactionSchedulerHandle, - accounts_db: Arc, block: LatestBlock, } @@ -63,392 +86,558 @@ 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, } } - 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 create_signed_tx( + &self, + ixs: &[Instruction], + blockhash: Hash, + ) -> Transaction { + let kp = validator_authority(); + Transaction::new_signed_with_payer( + ixs, + Some(&kp.pubkey()), + &[&kp], + blockhash, + ) + } + + // ----------------- + // Instruction Builders (delegates to InstructionUtils) + // ----------------- + + fn clone_ix( + pubkey: Pubkey, + data: Vec, + fields: AccountCloneFields, + ) -> Instruction { + InstructionUtils::clone_account_instruction(pubkey, data, fields) + } + + fn clone_init_ix( + pubkey: Pubkey, + total_len: u32, + initial_data: Vec, + fields: AccountCloneFields, + ) -> Instruction { + InstructionUtils::clone_account_init_instruction( + pubkey, + total_len, + initial_data, + fields, + ) + } + + fn clone_continue_ix( + pubkey: Pubkey, + offset: u32, + data: Vec, + is_last: bool, + ) -> Instruction { + InstructionUtils::clone_account_continue_instruction( + pubkey, offset, data, is_last, + ) + } + + fn cleanup_ix(pubkey: Pubkey) -> Instruction { + InstructionUtils::cleanup_partial_clone_instruction(pubkey) + } + + fn finalize_program_ix( + program: Pubkey, + buffer: Pubkey, + remote_slot: u64, + ) -> Instruction { + InstructionUtils::finalize_program_from_buffer_instruction( + program, + buffer, + remote_slot, + ) + } + + fn set_authority_ix(program: Pubkey, authority: Pubkey) -> Instruction { + InstructionUtils::set_program_authority_instruction(program, authority) + } + + // ----------------- + // 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, + ); - let message = Self::build_clone_message(request); + // 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 modify_ix = InstructionUtils::modify_accounts_instruction( - vec![account_modification], - message, + self.create_signed_tx(&ixs, blockhash) + } + + /// 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, + // 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.create_signed_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.create_signed_tx(&[continue_ix], blockhash)); + offset = end; + } + + txs } - /// 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( + async fn send_cleanup(&self, pubkey: Pubkey) { + let blockhash = self.block.load().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"); + } + } + + // ----------------- + // 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, - ); - - 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" - ); - - // 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, - ); - - Ok(Some(tx)) + RemoteProgramLoader::V1 => { + self.build_v1_program_txs(program, blockhash) } + _ => self.build_v4_program_txs(program, blockhash), } } - #[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, - ) - .await - { - Ok(initiated) => { - trace!( - duration_ms = initiated.elapsed().as_millis() as u64, - "Lookup table reservation completed" - ); - } - Err(err) => { - error!(error = ?err, "Failed to reserve lookup tables"); - } + /// 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.create_signed_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. + /// + /// 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, + blockhash: Hash, + ) -> 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 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(), + ); + + // 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::finalize_v1_program_from_buffer_instruction( + program_id, + program_data_addr, + buffer_pubkey, + remote_slot, + chain_authority, + ), + InstructionUtils::enable_executable_check_instruction( + &validator_authority_id(), + ), + ]; + + let txs = self.build_program_txs_from_finalize( + buffer_pubkey, + elf_data, + finalize_ixs, + blockhash, + ); + + Ok(Some(txs)) + } + + /// 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; + let remote_slot = program.remote_slot; + + // 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 program_data = program.program_data; + let (buffer_pubkey, _) = derive_buffer_pubkey(&program_id); + + 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)?, }; + + // 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, remote_slot), + deploy_ix, + Self::set_authority_ix(program_id, chain_authority), + InstructionUtils::enable_executable_check_instruction( + &validator_authority_id(), + ), + ]; + + let txs = self.build_program_txs_from_finalize( + buffer_pubkey, + program_data, + finalize_ixs, + blockhash, + ); + + Ok(Some(txs)) } - 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, - )); - } + /// 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 total_len = program_data.len() as u32; + let num_chunks = + total_len.div_ceil(MAX_INLINE_DATA_SIZE as u32) as usize; + + // 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, + first_chunk.to_vec(), + fields, + ); + 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; + 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.create_signed_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.create_signed_tx(&ixs, blockhash)); + + txs } - 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 - ))) - } - _ => Err(ClonerError::CommittorServiceError(format!( - "{:?}", - err - ))), - } + // ----------------- + // Lookup Tables + // ----------------- + + fn maybe_prepare_lookup_tables(&self, pubkey: Pubkey, owner: Pubkey) { + 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"); } - } + }); } } +/// Shared account metas for clone instructions. #[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.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 = None; + for tx in txs { + match self.send_tx(tx).await { + Ok(sig) => { + last_sig.replace(sig); + } + Err(e) => { + self.send_cleanup(request.pubkey).await; + return Err(ClonerError::FailedToCloneRegularAccount( + request.pubkey, + Box::new(e), + )); + } + } + } + + Ok(last_sig.unwrap_or_default()) } 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| { + + let Some(txs) = + self.build_program_txs(program, blockhash).map_err(|e| { ClonerError::FailedToCreateCloneProgramTransaction( program_id, - Box::new(err), + Box::new(e), ) })? - { - 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; + 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 = None; + for tx in txs { + match self.send_tx(tx).await { + Ok(sig) => { + last_sig.replace(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()) } + + // 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.unwrap_or_default()) } } 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..7be891bd7 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(), ))); @@ -432,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-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..e790c9daa 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 { remote_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 { remote_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..80c5b64da --- /dev/null +++ b/programs/magicblock/src/clone_account/common.rs @@ -0,0 +1,224 @@ +//! Shared utilities for clone account and mutate account instruction processing. + +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; +use solana_transaction_context::TransactionContext; + +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, + invoke_context: &InvokeContext, +) -> Result<(), InstructionError> { + let auth = validator_authority_id(); + if signers.contains(&auth) { + return Ok(()); + } + ic_msg!(invoke_context, "Validator authority 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) +} + +/// 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, +) -> 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.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); + // Setting ephemeral flag on empty account, forces + // accountsdb to remove it, thus reclaiming space + 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) +} + +/// 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( + 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); + 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/mod.rs b/programs/magicblock/src/clone_account/mod.rs new file mode 100644 index 000000000..19eb2f087 --- /dev/null +++ b/programs/magicblock/src/clone_account/mod.rs @@ -0,0 +1,70 @@ +//! 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; +#[cfg(test)] +mod tests; + +use std::collections::HashSet; + +pub(crate) use common::*; +use lazy_static::lazy_static; +use parking_lot::RwLock; +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; +use solana_pubkey::Pubkey; + +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..a2a3d94bc --- /dev/null +++ b/programs/magicblock/src/clone_account/process_cleanup.rs @@ -0,0 +1,71 @@ +//! Cleanup for failed multi-transaction clones. + +use std::collections::HashSet; + +use solana_account::ReadableAccount; +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, 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. +/// +/// 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)?; + + // 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()); + } + + 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); + + 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 new file mode 100644 index 000000000..4f8fed31d --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone.rs @@ -0,0 +1,70 @@ +//! Single-transaction account cloning for accounts <63KB. + +use std::collections::HashSet; + +use magicblock_magic_program_api::instruction::AccountCloneFields; +use solana_account::ReadableAccount; +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, set_account_from_fields, validate_and_get_index, + validate_authority, validate_mutable, validate_remote_slot, +}; + +/// 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)?; + + // 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(); + let lamports_delta = fields.lamports as i64 - current_lamports as i64; + + 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_continue.rs b/programs/magicblock/src/clone_account/process_clone_continue.rs new file mode 100644 index 000000000..de8b7cf12 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone_continue.rs @@ -0,0 +1,93 @@ +//! 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..f8d289609 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_clone_init.rs @@ -0,0 +1,104 @@ +//! 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; +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, + set_account_from_fields, validate_and_get_index, validate_authority, + validate_mutable, validate_remote_slot, +}; +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)?; + + // 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={}", + 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); + + set_account_from_fields(invoke_context, account, &data, &fields); + + 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..47bcd2eb9 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_finalize_buffer.rs @@ -0,0 +1,115 @@ +//! 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_transaction_context::TransactionContext; + +use super::{ + adjust_authority_lamports, close_buffer_account, get_deploy_slot, + loader_v4_state_to_bytes, minimum_balance, 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 `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. +/// +/// # 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, + remote_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 buf_data = buf_acc.borrow().data().to_vec(); + let buf_lamports = buf_acc.borrow().lamports(); + let prog_current_lamports = prog_acc.borrow().lamports(); + + let deploy_slot = get_deploy_slot(invoke_context); + + // Build LoaderV4 account data: header + ELF + 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); + + 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 = minimum_balance(invoke_context, 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(remote_slot); + prog.set_undelegating(false); + } + + // Close buffer account + close_buffer_account(buf_acc); + + adjust_authority_lamports(auth_acc, lamports_delta)?; + 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()); + 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 new file mode 100644 index 000000000..88acd6f6a --- /dev/null +++ b/programs/magicblock/src/clone_account/process_finalize_v1_buffer.rs @@ -0,0 +1,153 @@ +//! 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_transaction_context::TransactionContext; + +use super::{ + adjust_authority_lamports, close_buffer_account, get_deploy_slot, + minimum_balance, 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, + remote_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 data_key = *transaction_context.get_key_of_account_at_index( + ctx.get_index_of_instruction_account_in_transaction(2)?, + )?; + + let elf_data = buf_acc.borrow().data().to_vec(); + + let deploy_slot = get_deploy_slot(invoke_context); + + ic_msg!( + invoke_context, + "FinalizeV1: elf_len={} remote_slot={} deploy_slot={}", + elf_data.len(), + remote_slot, + deploy_slot + ); + + // Build V3 program_data account: ProgramData header + ELF + let program_data_content = { + let state = UpgradeableLoaderState::ProgramData { + slot: deploy_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 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(); + 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(remote_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(remote_slot); + acc.set_undelegating(false); + } + + // Close buffer account + close_buffer_account(buf_acc); + + adjust_authority_lamports(auth_acc, lamports_delta)?; + 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..522d84f22 --- /dev/null +++ b/programs/magicblock/src/clone_account/process_set_authority.rs @@ -0,0 +1,81 @@ +//! 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::{loader_v4_state_to_bytes, 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, + }; + + acc.data_as_mut_slice()[..header_size] + .copy_from_slice(loader_v4_state_to_bytes(&new_state)); + + ic_msg!( + invoke_context, + "SetProgramAuthority: {} authority -> {}", + key, + new_authority + ); + Ok(()) +} diff --git a/programs/magicblock/src/clone_account/tests.rs b/programs/magicblock/src/clone_account/tests.rs new file mode 100644 index 000000000..0ba06133b --- /dev/null +++ b/programs/magicblock/src/clone_account/tests.rs @@ -0,0 +1,442 @@ +//! Tests for clone account instructions. + +use std::collections::HashMap; + +use magicblock_magic_program_api::instruction::{ + AccountCloneFields, MagicBlockInstruction, +}; +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, None); + 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_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!(); + 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, None); + + 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_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, None); + + 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!(); + 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, None); + + 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, None); + + 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/errors.rs b/programs/magicblock/src/errors.rs index 60e9156cd..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,29 +30,26 @@ 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("Account already has a pending clone in progress")] + CloneAlreadyPending, - #[error("Tried to persist data that could not be resolved.")] - AttemptedToPersistUnresolvedData, + #[error("No pending clone found for account")] + NoPendingClone, - #[error("Tried to persist data that was resolved from storage.")] - AttemptedToPersistDataFromStorage, + #[error("Clone offset mismatch")] + CloneOffsetMismatch, - #[error("Encountered an error when persisting account modification data.")] - FailedToPersistAccountModData, + #[error("The account is delegated and not currently undelegating")] + AccountIsDelegated, - #[error("The account is delegated and not currently undelegating.")] - AccountIsDelegatedAndNotUndelegating, + #[error("The account is ephemeral and cannot be mutated")] + AccountIsEphemeral, - #[error( - "Remote slot updates cannot be older than the current remote slot." - )] - IncomingRemoteSlotIsOlderThanCurrentRemoteSlot, + #[error("Updates cannot be older than the current remote slot")] + OutOfOrderUpdate, + #[error("Buffer account not found for program finalization")] + BufferAccountNotFound, } diff --git a/programs/magicblock/src/lib.rs b/programs/magicblock/src/lib.rs index 725ec69fd..255b9bbb2 100644 --- a/programs/magicblock/src/lib.rs +++ b/programs/magicblock/src/lib.rs @@ -1,3 +1,4 @@ +mod clone_account; mod ephemeral_accounts; pub mod errors; mod magic_context; 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..1b5106288 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,76 @@ 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 { remote_slot } => { + process_finalize_program_from_buffer( + &signers, + invoke_context, + transaction_context, + remote_slot, + ) + } + SetProgramAuthority { authority } => process_set_program_authority( + &signers, + invoke_context, + transaction_context, + authority, + ), + FinalizeV1ProgramFromBuffer { + remote_slot, + authority, + } => process_finalize_v1_program_from_buffer( + &signers, + invoke_context, + transaction_context, + remote_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..b3e077ade 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -10,8 +10,10 @@ use solana_sdk_ids::system_program; use solana_transaction_context::TransactionContext; use crate::{ + clone_account::{ + is_ephemeral, validate_not_delegated, validate_remote_slot, + }, errors::MagicBlockProgramError, - mutate_accounts::account_mod_data::resolve_account_mod_data, validator::validator_authority_id, }; @@ -99,7 +101,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; @@ -107,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); @@ -120,6 +121,7 @@ pub(crate) fn process_mutate_accounts( ); continue; } + let account_key = transaction_context .get_key_of_account_at_index(account_transaction_index)?; @@ -135,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 { @@ -209,46 +186,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!( @@ -297,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 @@ -316,22 +260,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(()) } @@ -569,8 +497,7 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Err(MagicBlockProgramError::AccountIsDelegatedAndNotUndelegating - .into()), + Err(MagicBlockProgramError::AccountIsDelegated.into()), ); } @@ -865,7 +792,7 @@ mod tests { ix.data.as_slice(), transaction_accounts, ix.accounts, - Err(MagicBlockProgramError::IncomingRemoteSlotIsOlderThanCurrentRemoteSlot.into()), + Err(MagicBlockProgramError::OutOfOrderUpdate.into()), ); } @@ -909,9 +836,9 @@ mod tests { Ok(()), ); - 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); + accounts.remove(0); // authority + let account = accounts.remove(0); + assert_eq!(account.lamports(), 200); + assert_eq!(account.remote_slot(), 100); } } 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..0ad279144 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, @@ -294,6 +289,136 @@ impl InstructionUtils { ) } + // ----------------- + // CloneAccount + // ----------------- + 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), + ], + ) + } + + 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), + ], + ) + } + + 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), + ], + ) + } + + 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), + ], + ) + } + + // ----------------- + // 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 // ----------------- 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", 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", - ); -} 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")