Read a CPI failure like a sentence
Break three working CPIs on purpose, then learn to read the logs well enough that the next failure gives you a direction instead of a dead end.
Read a CPI failure like a sentence
Programs fail in specific, repeatable ways. Once you have seen the same error message three times, the fourth time stops feeling like a wall and starts feeling like a clue. Today you go looking for those clues on purpose, by breaking the working program-to-program cross-program invocation from earlier this week in three deliberate ways and reading what the runtime says back.
The scenario
For three days you have been the one calling other programs and watching them obey. The System Program moved lamports when you asked. The Token-2022 program minted tokens when you asked. Yesterday your own program called your own program and the runtime did not blink. Every one of those calls worked because every account, every signer, and every seed lined up exactly with what the callee expected.
Production is the place where they will not line up. A user passes the wrong token account. A PDA derivation drifts by one byte because of a refactor. An account that should be writable is not. When that happens, the only thing standing between you and a fix is your ability to read the program logs and know what they mean. So today you will produce those failures yourself, three of them, each one telling a different story.
The challenge
What you’ll need
- The two-program workspace you built on Day 74 (a caller program that performs a CPI into a callee program you also wrote), and the vault program from Day 73 for the signer-seeds failure
- Your editor of choice
- A terminal running Anchor and the Solana CLI
- The LiteSVM test setup you have been using since Arc 9 to inspect transaction logs without leaving the local process
Steps
- Open the caller program from Day 74. Confirm the happy path still works by running your existing test once. You want a green baseline before you start breaking things, because a failure you cannot compare to success teaches you nothing.
-
Failure one: wrong signer seeds. Open the vault program from Day 73 (the PDA-signed withdraw is the only place in this arc that uses signer seeds). Find the line where you build the
signer_seedsslice for theCpiContext. Change one byte of one seed, or pass the wrong bump. Rebuild and rerun that program’s withdraw test. Copy the fullProgram log:output into a note. You are looking for the line that mentions a privilege escalation or a missing signature on the PDA. -
Failure two: missing or wrong account. Restore the seeds. Now go to the
Accountsstruct on the callee’s instruction. Add a new account constraint that the caller does not yet pass, for example a second PDA derived from a different seed, or change thehas_oneorseedsconstraint on an existing one. Rebuild and run. Capture the log. The runtime will name the failing constraint and the account it tried to validate. Notice how specific that message is once you slow down to read it. -
Failure three: wrong program ID. Restore the constraint. In the caller, change the
programfield you pass intoCpiContext::newso it points at the wrong program. The cleanest way is to addpub system_program: Program<'info, System>to yourBumpaccounts struct, then swapctx.accounts.counter_program.key()forctx.accounts.system_program.key()in the handler. The System Program at11111111111111111111111111111111is a useful wrong answer because it exists and will respond, just not in a way that helps. Rebuild and run. Read the log. The shape of this failure is different from the first two, because the call reaches a real program that has no idea what to do with your instruction data. - For each of the three failures, write one sentence in your notes that finishes the prompt: “When I see this line in the logs, the cause is…”. That mapping is the deliverable. The code goes back to working when you are done, but the mapping stays with you.
Run it
anchor build
anchor test
If you would rather watch a live deploy fail against your local validator instead of using the in-process test runner, deploy the broken caller and stream the logs in a second terminal while you send the failing instruction.
solana logs --url localhost
What just happened
You forced the runtime to enforce three of its rules and you watched it do so in three different voices. Wrong signer seeds produce a privilege error, because the PDA the runtime derived from your seeds is not the PDA the callee expected to sign for. A missing or mismatched account produces a constraint error from Anchor, which names the constraint and the account by the same name you used in your Accounts struct. A wrong program ID produces a foreign error, because some other program received your instruction and rejected it on its own terms.
Those three categories cover a surprising share of real CPI failures you will encounter from here on. The pattern that will save you the most time is not memorising error codes but learning to read the Program log: lines in order, top to bottom, treating each one as a step in a story. Solana’s runtime is verbose on purpose, and the call stack in the logs tells you exactly which program was running when the failure happened. That is usually most of the answer.
One more thing worth sitting with. None of these failures came from logic bugs inside your business code. They came from the boundary between two programs, which is exactly where complexity in Solana tends to concentrate. The reflex to develop is to treat every CPI as a tiny contract with two clauses: the accounts the caller passes, and the constraints the callee enforces. When a CPI breaks, one of those two sides moved.
Resources
- Cross-Program Invocations on the official Solana docs, especially the section on signer privileges
- Anchor error reference for decoding constraint failures by name
-
solana confirmandsolana logsfor streaming runtime output - LiteSVM for inspecting transaction metadata and logs inside your test process
Submission
Submit a screenshot of one of your three captured log outputs, with a short caption naming which failure it is.