Skip to main content

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 background
Challenge

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

  1. 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.
  2. 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_seeds slice for the CpiContext. Change one byte of one seed, or pass the wrong bump. Rebuild and rerun that program’s withdraw test. Copy the full Program log: output into a note. You are looking for the line that mentions a privilege escalation or a missing signature on the PDA.
  3. Failure two: missing or wrong account. Restore the seeds. Now go to the Accounts struct 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 the has_one or seeds constraint 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.
  4. Failure three: wrong program ID. Restore the constraint. In the caller, change the program field you pass into CpiContext::new so it points at the wrong program. The cleanest way is to add pub system_program: Program<'info, System> to your Bump accounts struct, then swap ctx.accounts.counter_program.key() for ctx.accounts.system_program.key() in the handler. The System Program at 11111111111111111111111111111111 is 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.
  5. 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

Submission

Submit a screenshot of one of your three captured log outputs, with a short caption naming which failure it is.

Submit your project