From c0b97fabb942a6c2443dcc3ec9af46f9a119c88e Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 16 Apr 2026 15:02:55 +0700 Subject: [PATCH 1/4] fix: undelegation of account via CommitFinalize in intent bundle entrypoint --- .../process_schedule_intent_bundle.rs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs b/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs index 5eaad005c..fc878ce74 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs @@ -111,15 +111,21 @@ pub(crate) fn process_schedule_intent_bundle( secure, ); - let undelegated_accounts_ref = - if let Some(ref value) = args.commit_and_undelegate { - Some(CommitType::extract_commit_accounts( - value.committed_accounts_indices(), - construction_context.transaction_context, - )?) - } else { - None - }; + /// Collect all undelegated account refs + let undelegated_accounts_ref = [ + args.commit_and_undelegate.as_ref(), + args.commit_finalize_and_undelegate.as_ref(), + ] + .into_iter() + .flatten() + .map(|el| el.committed_accounts_indices()) + .try_fold(vec![], |mut acc, indices| { + acc.extend(CommitType::extract_commit_accounts( + indices, + construction_context.transaction_context, + )?); + Ok::<_, InstructionError>(acc) + })?; let scheduled_intent = ScheduledIntentBundle::try_new( args, @@ -130,13 +136,11 @@ pub(crate) fn process_schedule_intent_bundle( )?; let mut undelegated_pubkeys = vec![]; - if let Some(undelegated_accounts_ref) = undelegated_accounts_ref.as_ref() { - // Change owner to dlp and set undelegating flag - // Once account is undelegated we need to make it immutable in our validator. - for (pubkey, account_ref) in undelegated_accounts_ref.iter() { - undelegated_pubkeys.push(pubkey.to_string()); - mark_account_as_undelegated(account_ref); - } + // Change owner to dlp and set undelegating flag + // Once account is undelegated we need to make it immutable in our validator. + for (pubkey, account_ref) in undelegated_accounts_ref.iter() { + undelegated_pubkeys.push(pubkey.to_string()); + mark_account_as_undelegated(account_ref); } if !undelegated_pubkeys.is_empty() { ic_msg!( From 901562e20e8dee495a235a2907fe609f5ff32780 Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 16 Apr 2026 15:03:21 +0700 Subject: [PATCH 2/4] refactor: doc -> comment --- .../src/schedule_transactions/process_schedule_intent_bundle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs b/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs index fc878ce74..23e3262b4 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_intent_bundle.rs @@ -111,7 +111,7 @@ pub(crate) fn process_schedule_intent_bundle( secure, ); - /// Collect all undelegated account refs + // Collect all undelegated account refs let undelegated_accounts_ref = [ args.commit_and_undelegate.as_ref(), args.commit_finalize_and_undelegate.as_ref(), From baa08bd8a2ebc8799bc57239b4a505db7160f079 Mon Sep 17 00:00:00 2001 From: taco-paco Date: Thu, 16 Apr 2026 19:00:11 +0700 Subject: [PATCH 3/4] wip --- .../src/magic_scheduled_base_intent.rs | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 7b61b8502..516d88136 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -325,6 +325,13 @@ impl MagicIntentBundle { &self, context: &ConstructionContext<'_, '_>, ) -> Result<(), InstructionError> { + const CPI_LIMIT: usize = 64; + + const COMMIT_CPIS: usize = 3; + const FINALIZE_CPIS: usize = 1; + const UNDELEGATE_CPIS: usize = 5; + const COMMIT_FINALIZE_CPIS: usize = 1; + if self.is_empty() { ic_msg!( context.invoke_context, @@ -349,19 +356,43 @@ impl MagicIntentBundle { Ok(()) }; + // always 2 budget + let mut commit_stage_cpis = 2; + // always 2 budget + let mut finalize_stage_cpis = 2; if let Some(commit) = &self.commit { - check(commit.get_committed_accounts())?; + let committed = commit.get_committed_accounts(); + let num_committed = committed.len(); + commit_stage_cpis += COMMIT_CPIS * num_committed; + finalize_stage_cpis += FINALIZE_CPIS * num_committed; + + check(committed)?; } if let Some(cau) = &self.commit_and_undelegate { - check(cau.get_committed_accounts())?; + let committed = cau.get_committed_accounts(); + let num_committed = committed.len(); + commit_stage_cpis += COMMIT_CPIS * num_committed; + finalize_stage_cpis += + (FINALIZE_CPIS + UNDELEGATE_CPIS) * num_committed; + + check(committed)?; } if let Some(commit_finalize) = &self.commit_finalize { - check(commit_finalize.get_committed_accounts())?; + let committed = commit_finalize.get_committed_accounts(); + // Only in stage + commit_stage_cpis += COMMIT_FINALIZE_CPIS * committed.len(); + check(committed)?; } if let Some(commit_finalize_and_undelegate) = &self.commit_finalize_and_undelegate { - check(commit_finalize_and_undelegate.get_committed_accounts())?; + let committed = + commit_finalize_and_undelegate.get_committed_accounts(); + let num_committed = committed.len(); + commit_stage_cpis += COMMIT_FINALIZE_CPIS * num_committed; + finalize_stage_cpis += UNDELEGATE_CPIS * num_committed; + + check(committed)?; } Ok(()) From 76f4b0b2655cd9a5ad07419bab46a4400774f09d Mon Sep 17 00:00:00 2001 From: taco-paco Date: Fri, 17 Apr 2026 19:18:24 +0700 Subject: [PATCH 4/4] feat: add tests for cpi restriction --- .../src/magic_scheduled_base_intent.rs | 116 ++++++---- .../process_schedule_commit.rs | 5 +- .../client/src/schedule_commit_context.rs | 204 +++++++++--------- .../test-scenarios/tests/01_commits.rs | 79 ++++++- .../tests/02_commit_and_undelegate.rs | 80 ++++++- .../test-scenarios/tests/utils/mod.rs | 16 +- 6 files changed, 343 insertions(+), 157 deletions(-) diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 516d88136..976c7a27f 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -40,6 +40,11 @@ pub const COMMIT_FEE_LAMPORTS: u64 = 100_000; /// denominated in micro-lamports per CU (mirrors Solana's priority fee model). pub const COMPUTE_UNIT_PRICE_MICRO_LAMPORTS: u64 = 50_000; +/// Number of CPIS limited to 64 on Solana +pub const CPI_LIMIT: usize = 64; +/// Fixed base CPIs per tx stage (SetComputeUnitLimit + SetComputeUnitPrice). +const BASE_STAGE_CPIS: usize = 2; + /// Context necessary for construction of Schedule Action pub struct ConstructionContext<'a, 'ic> { parent_program_id: Option, @@ -277,7 +282,7 @@ impl MagicIntentBundle { commit_finalize_and_undelegate, standalone_actions: actions, }; - this.post_validation(context)?; + this.post_validation(&context.invoke_context)?; Ok(this) } @@ -286,7 +291,7 @@ impl MagicIntentBundle { /// 1. Set of committed accounts shall not overlap with /// set of undelegated accounts /// 2. None for now :) - fn validate( + pub fn validate( args: &MagicIntentBundleArgs, context: &ConstructionContext<'_, '_>, ) -> Result<(), InstructionError> { @@ -321,20 +326,13 @@ impl MagicIntentBundle { /// Post cross intent validation: /// 1. Validates that all committed accounts across the entire intent bundle /// are globally unique by pubkey. - fn post_validation( + pub fn post_validation( &self, - context: &ConstructionContext<'_, '_>, + invoke_context: &&mut InvokeContext<'_>, ) -> Result<(), InstructionError> { - const CPI_LIMIT: usize = 64; - - const COMMIT_CPIS: usize = 3; - const FINALIZE_CPIS: usize = 1; - const UNDELEGATE_CPIS: usize = 5; - const COMMIT_FINALIZE_CPIS: usize = 1; - if self.is_empty() { ic_msg!( - context.invoke_context, + invoke_context, "ScheduleCommit ERR: intent bundle must not be empty.", ); return Err(InstructionError::InvalidInstructionData); @@ -346,7 +344,7 @@ impl MagicIntentBundle { for el in accounts { if !seen.insert(el.pubkey) { ic_msg!( - context.invoke_context, + invoke_context, "ScheduleCommit ERR: duplicate committed account pubkey across bundle: {}", el.pubkey ); @@ -356,46 +354,40 @@ impl MagicIntentBundle { Ok(()) }; - // always 2 budget - let mut commit_stage_cpis = 2; - // always 2 budget - let mut finalize_stage_cpis = 2; if let Some(commit) = &self.commit { - let committed = commit.get_committed_accounts(); - let num_committed = committed.len(); - commit_stage_cpis += COMMIT_CPIS * num_committed; - finalize_stage_cpis += FINALIZE_CPIS * num_committed; - - check(committed)?; + check(commit.get_committed_accounts())?; } if let Some(cau) = &self.commit_and_undelegate { - let committed = cau.get_committed_accounts(); - let num_committed = committed.len(); - commit_stage_cpis += COMMIT_CPIS * num_committed; - finalize_stage_cpis += - (FINALIZE_CPIS + UNDELEGATE_CPIS) * num_committed; - - check(committed)?; + check(cau.get_committed_accounts())?; } if let Some(commit_finalize) = &self.commit_finalize { - let committed = commit_finalize.get_committed_accounts(); - // Only in stage - commit_stage_cpis += COMMIT_FINALIZE_CPIS * committed.len(); - check(committed)?; + check(commit_finalize.get_committed_accounts())?; } if let Some(commit_finalize_and_undelegate) = &self.commit_finalize_and_undelegate { - let committed = - commit_finalize_and_undelegate.get_committed_accounts(); - let num_committed = committed.len(); - commit_stage_cpis += COMMIT_FINALIZE_CPIS * num_committed; - finalize_stage_cpis += UNDELEGATE_CPIS * num_committed; - - check(committed)?; + check(commit_finalize_and_undelegate.get_committed_accounts())?; } - Ok(()) + self.validate_cpi_budget(invoke_context) + } + + pub fn validate_cpi_budget( + &self, + invoke_context: &&mut InvokeContext<'_>, + ) -> Result<(), InstructionError> { + let (commit_stage_cpis, finalize_stage_cpis) = + (self.commit_stage_cpis(), self.finalize_stage_cpis()); + if commit_stage_cpis >= CPI_LIMIT || finalize_stage_cpis >= CPI_LIMIT { + ic_msg!( + invoke_context, + "ScheduleCommit ERR: too many committed accounts.", + ); + + Err(InstructionError::MaxAccountsExceeded) + } else { + Ok(()) + } } pub fn calculate_fee( @@ -615,6 +607,46 @@ impl MagicIntentBundle { self.standalone_actions.get_mut(index.checked_sub(offset)?) } + + fn commit_stage_cpis(&self) -> usize { + const COMMIT_CPIS: usize = 3; + const COMMIT_FINALIZE_CPIS: usize = 1; + + let mut cpis = BASE_STAGE_CPIS; + if let Some(c) = &self.commit { + cpis += COMMIT_CPIS * c.get_committed_accounts().len(); + } + if let Some(cau) = &self.commit_and_undelegate { + cpis += COMMIT_CPIS * cau.get_committed_accounts().len(); + } + if let Some(cf) = &self.commit_finalize { + cpis += COMMIT_FINALIZE_CPIS * cf.get_committed_accounts().len(); + } + if let Some(cfau) = &self.commit_finalize_and_undelegate { + cpis += COMMIT_FINALIZE_CPIS * cfau.get_committed_accounts().len(); + } + + cpis + } + + fn finalize_stage_cpis(&self) -> usize { + const FINALIZE_CPIS: usize = 1; + const UNDELEGATE_CPIS: usize = 5; + + let mut cpis = BASE_STAGE_CPIS; + if let Some(c) = &self.commit { + cpis += FINALIZE_CPIS * c.get_committed_accounts().len(); + } + if let Some(cau) = &self.commit_and_undelegate { + cpis += (FINALIZE_CPIS + UNDELEGATE_CPIS) + * cau.get_committed_accounts().len(); + } + if let Some(cfau) = &self.commit_finalize_and_undelegate { + cpis += UNDELEGATE_CPIS * cfau.get_committed_accounts().len(); + } + + cpis + } } impl MagicBaseIntent { diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index c7e7763b8..e505098fe 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -11,7 +11,7 @@ use solana_pubkey::Pubkey; use crate::{ magic_scheduled_base_intent::{ calculate_commit_fee, validate_commit_schedule_permissions, - CommitAndUndelegate, CommitType, MagicBaseIntent, + CommitAndUndelegate, CommitType, MagicBaseIntent, MagicIntentBundle, ScheduledIntentBundle, UndelegateType, }, magic_sys::fetch_current_commit_nonces, @@ -297,7 +297,7 @@ pub(crate) fn process_schedule_commit( InstructionUtils::scheduled_commit_sent(intent_id, blockhash); let commit_sent_sig = action_sent_transaction.signatures[0]; - let base_intent = if opts.request_undelegation { + let base_intent: MagicIntentBundle = if opts.request_undelegation { MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { commit_action: CommitType::Standalone(committed_accounts), undelegate_action: UndelegateType::Standalone, @@ -306,6 +306,7 @@ pub(crate) fn process_schedule_commit( MagicBaseIntent::Commit(CommitType::Standalone(committed_accounts)) } .into(); + base_intent.validate_cpi_budget(&invoke_context)?; let scheduled_base_intent = ScheduledIntentBundle { id: intent_id, diff --git a/test-integration/schedulecommit/client/src/schedule_commit_context.rs b/test-integration/schedulecommit/client/src/schedule_commit_context.rs index ca69a7d55..6cd34220c 100644 --- a/test-integration/schedulecommit/client/src/schedule_commit_context.rs +++ b/test-integration/schedulecommit/client/src/schedule_commit_context.rs @@ -14,6 +14,7 @@ use solana_sdk::{ commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, hash::Hash, + instruction::Instruction, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::{Keypair, Signature}, @@ -162,82 +163,74 @@ impl ScheduleCommitTestContext { // ----------------- // Schedule Commit specific Transactions // ----------------- - pub fn init_committees(&self) -> Result { + fn init_committee_ixs( + &self, + chunk: &[(Keypair, Pubkey)], + ) -> Vec { let mut ixs = vec![ ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), ComputeBudgetInstruction::set_compute_unit_price(10_000), ]; match self.user_seed { UserSeeds::MagicScheduleCommit => { - ixs.extend(self.committees.iter().map( - |(player, committee)| { - init_account_instruction( - self.payer_chain.pubkey(), - player.pubkey(), - *committee, - ) - }, - )); + ixs.extend(chunk.iter().map(|(player, committee)| { + init_account_instruction( + self.payer_chain.pubkey(), + player.pubkey(), + *committee, + ) + })); } UserSeeds::OrderBook => { - ixs.extend(self.committees.iter().map( - |(book_manager, committee)| { - init_order_book_instruction( - self.payer_chain.pubkey(), - book_manager.pubkey(), - *committee, - ) - }, - )); - - //// TODO (snawaz): currently the size of delegatable-account cannot be - //// more than 10K, else delegation will fail. So Let's revisit this when - //// we relax the limit on the account size, then we can use larger - //// account, say even 10 MB, and execute CommitDiff. - // - // ixs.extend(self.committees.iter().flat_map( - // |(payer, committee)| { - // [grow_order_book_instruction( - // payer.pubkey(), - // *committee, - // 10 * 1024 - // )] - // }, - // )); + ixs.extend(chunk.iter().map(|(book_manager, committee)| { + init_order_book_instruction( + self.payer_chain.pubkey(), + book_manager.pubkey(), + *committee, + ) + })); } }; + ixs + } - let mut signers = self - .committees - .iter() - .map(|(payer, _)| payer) - .collect::>(); - signers.push(&self.payer_chain); - - let tx = Transaction::new_signed_with_payer( - &ixs, - Some(&self.payer_chain.pubkey()), - &signers, - self.try_chain_blockhash()?, - ); - let sig = self.try_chain_client()? - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - self.commitment, - RpcSendTransactionConfig { - skip_preflight: true, - ..Default::default() - }, - ) - .with_context(|| { - format!( - "Failed to initialize committees. Transaction signature: {}", - tx.get_signature() - ) - })?; - - debug!("Initialized committees: {sig}"); - Ok(sig) + pub fn init_committees(&self) -> Result> { + const CHUNK_SIZE: usize = 5; + let chain_client = self.try_chain_client()?; + + self.committees + .chunks(CHUNK_SIZE) + .map(|chunk| { + let ixs = self.init_committee_ixs(chunk); + let mut signers = + chunk.iter().map(|(p, _)| p).collect::>(); + signers.push(&self.payer_chain); + (ixs, signers) + }) + .map(|(ixs, signers)| -> Result { + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&self.payer_chain.pubkey()), + &signers, + self.try_chain_blockhash()?, + ); + chain_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + self.commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .with_context(|| { + format!( + "Failed to initialize committees. Transaction signature: {}", + tx.get_signature() + ) + }) + }) + .collect::>>() } pub fn escrow_lamports_for_payer(&self) -> Result { @@ -262,44 +255,49 @@ impl ScheduleCommitTestContext { .with_context(|| "Failed to escrow fund for payer") } - pub fn delegate_committees(&self) -> Result { - let mut ixs = vec![]; - for (player, _) in &self.committees { - let ix = delegate_account_cpi_instruction( - self.payer_chain.pubkey(), - self.ephem_validator_identity, - player.pubkey(), - self.user_seed, - ); - ixs.push(ix); - } - - let chain_blockhash = self.try_chain_blockhash()?; - - let tx = Transaction::new_signed_with_payer( - &ixs, - Some(&self.payer_chain.pubkey()), - &[&self.payer_chain], - chain_blockhash, - ); - let sig = self - .try_chain_client()? - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - self.commitment, - RpcSendTransactionConfig { - skip_preflight: true, - ..Default::default() - }, - ) - .with_context(|| { - format!( - "Failed to delegate committees on chain '{:?}'", - tx.signatures[0] - ) - })?; - debug!("Delegated committees: {sig}"); - Ok(sig) + pub fn delegate_committees(&self) -> Result> { + const CHUNK_SIZE: usize = 4; + let chain_client = self.try_chain_client()?; + + self.committees + .chunks(CHUNK_SIZE) + .map(|chunk| { + chunk + .iter() + .map(|(player, _)| { + delegate_account_cpi_instruction( + self.payer_chain.pubkey(), + self.ephem_validator_identity, + player.pubkey(), + self.user_seed, + ) + }) + .collect::>() + }) + .map(|ixs| -> Result { + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&self.payer_chain.pubkey()), + &[&self.payer_chain], + self.try_chain_blockhash()?, + ); + chain_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + self.commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .with_context(|| { + format!( + "Failed to delegate committees on chain '{:?}'", + tx.signatures[0] + ) + }) + }) + .collect::>>() } // ----------------- diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index 27b34bd07..0506f47a7 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -12,6 +12,7 @@ use schedulecommit_client::{verify, ScheduleCommitTestContextFields}; use solana_rpc_client::rpc_client::SerializableTransaction; use solana_rpc_client_api::config::RpcSendTransactionConfig; use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, instruction::InstructionError, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, @@ -29,7 +30,7 @@ use utils::{ get_context_with_delegated_committees, }; -use crate::utils::extract_transaction_error; +use crate::utils::{assert_is_instruction_error, extract_transaction_error}; mod utils; @@ -298,6 +299,82 @@ fn schedule_commit_tx( res } +// ----------------- +// CPI budget tests +// ----------------- + +// Commit: commit_stage = 2 + 3*n; overflows CPI_LIMIT(64) at n=21 +// CommitFinalize: commit_stage = 2 + 1*n; overflows CPI_LIMIT(64) at n=62 + +fn assert_schedule_commit_cpi_budget_exceeded( + committees: usize, + commit_type: ScheduleCommitType, +) { + let ctx = get_context_with_delegated_committees( + committees, + UserSeeds::MagicScheduleCommit, + ); + let ScheduleCommitTestContextFields { + payer_chain: payer, + committees, + commitment, + ephem_client, + .. + } = ctx.fields(); + + let mut ixs = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_000_000), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000), + ]; + let ix = schedule_commit_cpi_instruction( + payer.pubkey(), + magicblock_magic_program_api::id(), + magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, + None, + &committees + .iter() + .map(|(p, _)| p.pubkey()) + .collect::>(), + &committees.iter().map(|(_, pda)| *pda).collect::>(), + commit_type, + ); + ixs.push(ix); + + let blockhash = ephem_client.get_latest_blockhash().unwrap(); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = ephem_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + *commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ); + + let (tx_result_err, tx_err) = extract_transaction_error(res); + assert_is_instruction_error( + tx_err.unwrap(), + &tx_result_err, + InstructionError::MaxAccountsExceeded, + ); +} + +#[test] +fn test_commit_exceeds_cpi_budget() { + run_test!({ + assert_schedule_commit_cpi_budget_exceeded( + 21, + ScheduleCommitType::Commit, + ); + }); +} + fn schedule_commit_cpi_illegal_owner( payer: Pubkey, magic_program_id: Pubkey, diff --git a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs index b8431ef5d..a5831ce49 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs @@ -9,6 +9,7 @@ use program_schedulecommit::{ schedule_commit_and_undelegate_cpi_instruction, schedule_commit_and_undelegate_cpi_twice, schedule_commit_and_undelegate_cpi_with_mod_after_instruction, + schedule_commit_cpi_instruction, schedule_commit_instruction_for_order_book, set_count_instruction, update_order_book_instruction, UserSeeds, }, @@ -24,6 +25,7 @@ use solana_rpc_client_api::{ }; use solana_sdk::{ commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, instruction::InstructionError, pubkey::Pubkey, signature::{Keypair, Signature}, @@ -43,7 +45,7 @@ use utils::{ }; use crate::utils::{ - assert_is_one_of_instruction_errors, + assert_is_instruction_error, assert_is_one_of_instruction_errors, assert_one_committee_account_was_not_undelegated_on_chain, }; @@ -884,3 +886,79 @@ fn test_committing_after_failed_undelegation() { set_counter(2222, true); }); } + +// ----------------- +// CPI budget tests +// ----------------- + +// CommitAndUndelegate: finalize_stage = 2 + 6*n; overflows CPI_LIMIT(64) at n=11 +// CommitFinalizeAndUndelegate: finalize_stage = 2 + 5*n; overflows CPI_LIMIT(64) at n=13 + +fn assert_schedule_commit_cpi_budget_exceeded( + n: usize, + commit_type: ScheduleCommitType, +) { + let ctx = get_context_with_delegated_committees( + n, + UserSeeds::MagicScheduleCommit, + ); + let ScheduleCommitTestContextFields { + payer_chain: payer, + committees, + commitment, + ephem_client, + .. + } = ctx.fields(); + + let mut ixs = vec![ + ComputeBudgetInstruction::set_compute_unit_limit(1_000_000), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000), + ]; + let ix = schedule_commit_cpi_instruction( + payer.pubkey(), + magicblock_magic_program_api::id(), + magicblock_magic_program_api::MAGIC_CONTEXT_PUBKEY, + None, + &committees + .iter() + .map(|(p, _)| p.pubkey()) + .collect::>(), + &committees.iter().map(|(_, pda)| *pda).collect::>(), + commit_type, + ); + ixs.push(ix); + + let blockhash = ephem_client.get_latest_blockhash().unwrap(); + let tx = Transaction::new_signed_with_payer( + &ixs, + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + let res = ephem_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + *commitment, + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ); + + let (tx_result_err, tx_err) = extract_transaction_error(res); + assert_is_instruction_error( + tx_err.unwrap(), + &tx_result_err, + InstructionError::MaxAccountsExceeded, + ); +} + +#[test] +fn test_commit_and_undelegate_exceeds_cpi_budget() { + run_test!({ + assert_schedule_commit_cpi_budget_exceeded( + 11, + ScheduleCommitType::CommitAndUndelegate, + ); + }); +} diff --git a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs index 28e688ade..6c44f842b 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs @@ -25,15 +25,15 @@ pub fn get_context_with_delegated_committees( .unwrap(); println!("get_context_with_delegated_committees inside"); - let txhash = ctx.init_committees().unwrap(); - println!("txhash (init_committees): {}", txhash); - - ctx.dump_chain_logs(txhash); - - let txhash = ctx.delegate_committees().unwrap(); - println!("txhash (delegate_committees): {}", txhash); + for sig in ctx.init_committees().unwrap() { + println!("txhash (init_committees): {}", sig); + ctx.dump_chain_logs(sig); + } - ctx.dump_chain_logs(txhash); + for sig in ctx.delegate_committees().unwrap() { + println!("txhash (delegate_committees): {}", sig); + ctx.dump_chain_logs(sig); + } ctx }