Skip to content

feat: fail intent if it will hit MaxInstructionTraceLengthExceeded on Solana#1162

Draft
taco-paco wants to merge 5 commits intomasterfrom
fix/committor/committees-limit
Draft

feat: fail intent if it will hit MaxInstructionTraceLengthExceeded on Solana#1162
taco-paco wants to merge 5 commits intomasterfrom
fix/committor/committees-limit

Conversation

@taco-paco
Copy link
Copy Markdown
Contributor

@taco-paco taco-paco commented Apr 17, 2026

Summary

Right now user can schedule Intent with as many account as IX can contain. This can lead to situation where even Two stage commit flow will exceed Solana's limit of 64 CPIs.

This PR will reject such intents on scheduling level and make error explicit rather than the silent Intent failure.

Compatibility

  • No breaking changes
  • Config change (describe):
  • Migration needed (describe):

Testing

  • tests (or explain)

Checklist

  • docs updated (if needed)
  • closes #

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced CPI budget validation to prevent scheduled transactions from exceeding computational instruction limits. The system enforces a maximum threshold and rejects operations that would exceed it.
  • Tests

    • Added comprehensive integration tests to validate CPI budget enforcement across various transaction commit scenarios.

@github-actions
Copy link
Copy Markdown

Manual Deploy Available

You can trigger a manual deploy of this PR branch to testnet:

Deploy to Testnet 🚀

Alternative: Comment /deploy on this PR to trigger deployment directly.

⚠️ Note: Manual deploy requires authorization. Only authorized users can trigger deployments.

Comment updated automatically when the PR is synchronized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

This change introduces CPI-budget validation to the magic intent bundle processing. A new constant CPI_LIMIT and internal computation methods are added to MagicIntentBundle to estimate and validate per-stage CPI usage. The validate_cpi_budget method is called during commit scheduling to reject instructions exceeding the limit. Additionally, the test framework is refactored to batch committee initialization and delegation operations into chunks, with return types updated to reflect multiple transaction signatures per operation. New integration tests verify the CPI budget validation behavior for different commit scenarios.

Suggested reviewers

  • snawaz
  • GabrielePicco
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/committor/committees-limit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/magicblock/src/schedule_transactions/process_schedule_commit.rs (1)

254-309: 🧹 Nitpick | 🔵 Trivial

Move CPI-budget validation before fee charging.

validate_cpi_budget runs after charge_delegated_payer, fetch_current_commit_nonces, check_commit_limits, and the committee loop (including mark_account_as_undelegated mutations). While all of these are rolled back when the instruction errors, they do unnecessary work for an intent that will always be rejected. Since the CPI-budget check only needs committed_accounts.len() and the intent shape (determined by opts.request_undelegation), you can validate very early — ideally right after the committee loop builds committed_accounts — to fail fast and avoid touching fee vaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/magicblock/src/schedule_transactions/process_schedule_commit.rs`
around lines 254 - 309, The CPI-budget validation
(base_intent.validate_cpi_budget) must run before doing fee operations and
nonce/limit checks so we fail fast; move the call to validate_cpi_budget right
after the committee loop finishes constructing committed_accounts (use
committed_accounts.len() and opts.request_undelegation to build the same
MagicBaseIntent/ MagicIntentBundle shape) and only proceed to
fetch_current_commit_nonces, check_commit_limits, charge_delegated_payer, and
mark_account_as_undelegated if the validation succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@programs/magicblock/src/magic_scheduled_base_intent.rs`:
- Around line 43-46: The constant CPI_LIMIT is named and documented as "Number
of CPIS limited to 64" but Solana's real bound is MAX_INSTRUCTION_TRACE_LENGTH
(total instructions including top-level + CPIs) and the code's comparisons treat
64 as a hard reject threshold; update to avoid off-by-one/semantic mismatch by
either renaming CPI_LIMIT to MAX_INSTRUCTION_TRACE_LENGTH and updating its
doc-comment to state it bounds total instruction trace length (so places using
CPI_LIMIT and BASE_STAGE_CPIS keep the same numeric value but correct meaning),
or if you want to preserve the "max CPIs" intent keep the symbol CPI_LIMIT and
change its value to 63 to maintain a one-instruction safety margin; adjust any
uses of CPI_LIMIT/BASE_STAGE_CPIS accordingly so checks remain consistent with
the chosen interpretation.
- Around line 375-391: The current validate_cpi_budget function logs a generic
"too many committed accounts" message without indicating which stage overflowed
or the CPI counts; update the ic_msg! call inside validate_cpi_budget to include
which stage exceeded CPI_LIMIT (commit or finalize), the offending stage's CPI
value (from commit_stage_cpis() or finalize_stage_cpis()), the other stage's CPI
value, and the CPI_LIMIT constant so operators can see "commit_stage_cpis=X,
finalize_stage_cpis=Y, CPI_LIMIT=Z" and which stage triggered the error; keep
returning InstructionError::MaxAccountsExceeded and use the existing
invoke_context/ic_msg! plumbing.

In `@test-integration/schedulecommit/test-scenarios/tests/01_commits.rs`:
- Around line 302-366: The helper function
assert_schedule_commit_cpi_budget_exceeded is duplicated between 01_commits.rs
and 02_commit_and_undelegate.rs; extract it into a shared test utils module
(e.g., tests/utils/mod.rs) and import it from both test files. Move the function
body (including get_context_with_delegated_committees usage,
ScheduleCommitTestContextFields destructuring, ComputeBudgetInstruction setup,
schedule_commit_cpi_instruction call, transaction creation/sending, and the
final assertions) into a single pub fn
assert_schedule_commit_cpi_budget_exceeded(...) in the utils module, update both
test files to use use
crate::tests::utils::assert_schedule_commit_cpi_budget_exceeded (or the correct
path) and remove the duplicated copies so tests compile and run against the
single shared helper.

---

Outside diff comments:
In `@programs/magicblock/src/schedule_transactions/process_schedule_commit.rs`:
- Around line 254-309: The CPI-budget validation
(base_intent.validate_cpi_budget) must run before doing fee operations and
nonce/limit checks so we fail fast; move the call to validate_cpi_budget right
after the committee loop finishes constructing committed_accounts (use
committed_accounts.len() and opts.request_undelegation to build the same
MagicBaseIntent/ MagicIntentBundle shape) and only proceed to
fetch_current_commit_nonces, check_commit_limits, charge_delegated_payer, and
mark_account_as_undelegated if the validation succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfa4ebef-dbbf-4142-ba78-4862910a736c

📥 Commits

Reviewing files that changed from the base of the PR and between 6d77e7d and 76f4b0b.

📒 Files selected for processing (6)
  • programs/magicblock/src/magic_scheduled_base_intent.rs
  • programs/magicblock/src/schedule_transactions/process_schedule_commit.rs
  • test-integration/schedulecommit/client/src/schedule_commit_context.rs
  • test-integration/schedulecommit/test-scenarios/tests/01_commits.rs
  • test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs
  • test-integration/schedulecommit/test-scenarios/tests/utils/mod.rs

Comment on lines +43 to +46
/// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

CPI_LIMIT doc and threshold semantics.

Solana's actual constant is MAX_INSTRUCTION_TRACE_LENGTH = 64, which bounds total instructions in the trace (top-level + all CPIs), not strictly CPIs. With stepped formulas (2 + 3·n, 2 + 6·n, 2 + 5·n) the estimate never lands on exactly 64, so >= vs > is indistinguishable in practice today — but the naming and doc-comment suggest "max CPIs = 64" while the check semantics are "reject at 64 or more instructions", and a future helper that produces an estimate of exactly 64 would be incorrectly rejected.

Recommend either:

  • Rename to MAX_INSTRUCTION_TRACE_LENGTH and update the comment to match Solana's terminology, or
  • Keep the name but set it to 63 (one less than Solana's hard limit) to preserve a safety margin against the inherent estimate imprecision.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/magicblock/src/magic_scheduled_base_intent.rs` around lines 43 - 46,
The constant CPI_LIMIT is named and documented as "Number of CPIS limited to 64"
but Solana's real bound is MAX_INSTRUCTION_TRACE_LENGTH (total instructions
including top-level + CPIs) and the code's comparisons treat 64 as a hard reject
threshold; update to avoid off-by-one/semantic mismatch by either renaming
CPI_LIMIT to MAX_INSTRUCTION_TRACE_LENGTH and updating its doc-comment to state
it bounds total instruction trace length (so places using CPI_LIMIT and
BASE_STAGE_CPIS keep the same numeric value but correct meaning), or if you want
to preserve the "max CPIs" intent keep the symbol CPI_LIMIT and change its value
to 63 to maintain a one-instruction safety margin; adjust any uses of
CPI_LIMIT/BASE_STAGE_CPIS accordingly so checks remain consistent with the
chosen interpretation.

Comment on lines +375 to 391
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(())
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Diagnostic: include which stage overflowed and by how much.

The log currently only says "too many committed accounts"; it doesn't tell operators whether the commit stage or the finalize stage tripped, nor the estimated CPI count vs CPI_LIMIT. When users hit this they'll need to figure out whether to split their intent or switch commit types — surfacing the numbers is cheap and very helpful.

✏️ Proposed change
     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 {
+        if commit_stage_cpis >= CPI_LIMIT {
             ic_msg!(
                 invoke_context,
-                "ScheduleCommit ERR: too many committed accounts.",
+                "ScheduleCommit ERR: commit-stage CPI estimate {} >= limit {}",
+                commit_stage_cpis, CPI_LIMIT,
             );
-
-            Err(InstructionError::MaxAccountsExceeded)
-        } else {
-            Ok(())
+            return Err(InstructionError::MaxAccountsExceeded);
         }
+        if finalize_stage_cpis >= CPI_LIMIT {
+            ic_msg!(
+                invoke_context,
+                "ScheduleCommit ERR: finalize-stage CPI estimate {} >= limit {}",
+                finalize_stage_cpis, CPI_LIMIT,
+            );
+            return Err(InstructionError::MaxAccountsExceeded);
+        }
+        Ok(())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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 {
ic_msg!(
invoke_context,
"ScheduleCommit ERR: commit-stage CPI estimate {} >= limit {}",
commit_stage_cpis, CPI_LIMIT,
);
return Err(InstructionError::MaxAccountsExceeded);
}
if finalize_stage_cpis >= CPI_LIMIT {
ic_msg!(
invoke_context,
"ScheduleCommit ERR: finalize-stage CPI estimate {} >= limit {}",
finalize_stage_cpis, CPI_LIMIT,
);
return Err(InstructionError::MaxAccountsExceeded);
}
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/magicblock/src/magic_scheduled_base_intent.rs` around lines 375 -
391, The current validate_cpi_budget function logs a generic "too many committed
accounts" message without indicating which stage overflowed or the CPI counts;
update the ic_msg! call inside validate_cpi_budget to include which stage
exceeded CPI_LIMIT (commit or finalize), the offending stage's CPI value (from
commit_stage_cpis() or finalize_stage_cpis()), the other stage's CPI value, and
the CPI_LIMIT constant so operators can see "commit_stage_cpis=X,
finalize_stage_cpis=Y, CPI_LIMIT=Z" and which stage triggered the error; keep
returning InstructionError::MaxAccountsExceeded and use the existing
invoke_context/ic_msg! plumbing.

Comment on lines +197 to 234
pub fn init_committees(&self) -> Result<Vec<Signature>> {
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::<Vec<_>>();
signers.push(&self.payer_chain);
(ixs, signers)
})
.map(|(ixs, signers)| -> Result<Signature> {
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::<Result<Vec<_>>>()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Partial-failure semantics for chunked init/delegate.

init_committees now sends multiple transactions. If chunk N+1 fails, chunks 1..N are already committed on-chain and the returned Err provides no signal about what succeeded, leaving tests in a partially-initialized state. The same applies to delegate_committees (Lines 258-301). If this is acceptable for test harness usage, consider a comment documenting the non-atomic behavior; otherwise return the accumulated signatures alongside the error.

Comment on lines +302 to +366
// -----------------
// 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::<Vec<_>>(),
&committees.iter().map(|(_, pda)| *pda).collect::<Vec<_>>(),
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,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Duplicated helper across test files.

assert_schedule_commit_cpi_budget_exceeded is defined identically here and in test-integration/schedulecommit/test-scenarios/tests/02_commit_and_undelegate.rs (Lines 897-954). Consider extracting it into tests/utils/mod.rs to avoid drift as more commit-type variants are added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test-integration/schedulecommit/test-scenarios/tests/01_commits.rs` around
lines 302 - 366, The helper function assert_schedule_commit_cpi_budget_exceeded
is duplicated between 01_commits.rs and 02_commit_and_undelegate.rs; extract it
into a shared test utils module (e.g., tests/utils/mod.rs) and import it from
both test files. Move the function body (including
get_context_with_delegated_committees usage, ScheduleCommitTestContextFields
destructuring, ComputeBudgetInstruction setup, schedule_commit_cpi_instruction
call, transaction creation/sending, and the final assertions) into a single pub
fn assert_schedule_commit_cpi_budget_exceeded(...) in the utils module, update
both test files to use use
crate::tests::utils::assert_schedule_commit_cpi_budget_exceeded (or the correct
path) and remove the duplicated copies so tests compile and run against the
single shared helper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant