Cover photo

The Real Errors of Chakra

Why Error Codes in a Cross-Chain Protocol Are Not Just Error Messages. They Are Security Guarantees.

CHAKRA SERIES - 5

Welcome to Day 5.

Yesterday we looked at state.rs. The four structs. The data model. The containers that hold every piece of information CHAKRA needs to remember about a cross-chain intent.

If state.rs is the blueprint of the building, today's file is the list of ways the building is not allowed to collapse.

It is called errors.rs. It is ten lines of code. And I want to spend today showing you why those ten lines tell the entire security story of CHAKRA more clearly than any whitepaper could.


Part 1: Why Error Codes Are Not Just Error Messages

When most people think about errors in code they think about something going wrong. A crash. A bug. Something you did not plan for.

In a Solana program errors are the opposite of that.

In a Solana program errors are things you planned for very carefully. They are the explicit list of every situation where the program decides to reject an operation and return funds or stop execution rather than continue.

In a normal application when something goes wrong the worst case is maybe a bad user experience. Some wrong data. A UI that breaks.

In a cross-chain protocol when something goes wrong the worst case is someone losing real money. Permanently. With no way to recover it.

So errors are not afterthoughts. Errors are the contract CHAKRA makes with every user. Here is every situation where we will stop. Here is every situation where we will protect you. Here is every edge case we have thought about and decided how to handle.

Ten error codes. Ten promises.


Part 2: The File

rust

use anchor_lang::prelude::*;

#[error_code]
pub enum ChakraError {
    #[msg("The requested timeout is too short for cross-chain finality.")]
    TimeoutTooShort,
    #[msg("The requested timeout is unreasonably long.")]
    TimeoutTooLong,
    #[msg("The cross-chain intent has not timed out yet.")]
    TimeoutNotReached,
    #[msg("The cross-chain intent has timed out and can no longer be finalized.")]
    TimeoutReached,
    #[msg("This intent has already been finalized by a ZK-Proof.")]
    AlreadyFinalized,
    #[msg("This intent has already been cancelled.")]
    AlreadyCancelled,
    #[msg("User is not authorized to manage this intent.")]
    Unauthorized,
    #[msg("The signer is not an authorized Sentinel node.")]
    UnauthorizedSentinel,
    #[msg("Mathematical overflow or underflow occurred.")]
    MathError,
    #[msg("The provided cryptographic proof is invalid or improperly signed.")]
    InvalidProof,
}

That is the entire file.

Now let me walk through every single error and tell you exactly what situation it is protecting against.


Part 3: The Timeout Errors

There are four errors that deal with time. This is not a coincidence. Time is the hardest thing to get right in a cross-chain system and I got it wrong twice before I got it right.

TimeoutTooShort

rust

#[msg("The requested timeout is too short for cross-chain finality.")]
TimeoutTooShort,

When you call initialize_intent you pass a timeout_slots parameter. This is how long the Sentinel Network has to complete execution before you can cancel and get a refund.

The minimum is 150 slots.

Why 150? Because Solana produces a slot approximately every 400 milliseconds. 150 slots is about 60 seconds. That is the absolute minimum time the Sentinel Network needs to hear the ControlIntent event, coordinate the threshold signing ceremony across three nodes, send the transaction to the target chain, wait for confirmation, and submit the proof back to Solana.

If you try to set a timeout shorter than that, TimeoutTooShort fires. Immediately. Before your funds are locked. Before anything happens.

This error protects you from accidentally setting a timeout so short that the Sentinel Network cannot physically complete the execution before you can cancel it. Without this check a malicious user could set a 1-slot timeout, immediately call cancel, and try to race the system into a broken state.

TimeoutTooLong

rust

#[msg("The requested timeout is unreasonably long.")]
TimeoutTooLong,

The maximum is 216000 slots. That is about 24 hours.

This might seem unnecessary. Why would you care if a timeout is too long?

Because if there is no upper limit, someone could create an intent with a 10-year timeout. Their funds would be locked in the escrow account for 10 years. The protocol would have to maintain that account forever. The rent would need to stay funded. And if the Sentinel Network ever changes — new keys, new nodes, upgraded protocol — that ancient intent with a 10-year timeout would be stuck in limbo with no clean way to handle it.

24 hours is long enough for any reasonable cross-chain execution. If it has not happened in 24 hours something has seriously gone wrong and you should get your money back.

TimeoutNotReached

rust

#[msg("The cross-chain intent has not timed out yet.")]
TimeoutNotReached,

This one fires when you try to call cancel_intent too early.

The Sentinel Network is still working. The execution might still succeed. The timeout has not passed yet. You do not get to cancel yet.

This protects the protocol's integrity. Imagine if you could cancel at any time regardless of timeout. You could initiate an intent, the Sentinel Network starts working, and then you immediately cancel and get your refund. The Sentinel Network continues executing on the target chain anyway — spending gas, completing transactions — and now the proof it tries to submit will fail because you already cancelled. The target chain loses fees for nothing.

TimeoutNotReached prevents that. The Sentinel Network gets the full timeout window to complete its work.

TimeoutReached

rust

#[msg("The cross-chain intent has timed out and can no longer be finalized.")]
TimeoutReached,

This is the mirror. If the Sentinel Network tries to submit a proof after the timeout has already passed, TimeoutReached fires.

The window is closed. The user has already cancelled or can cancel immediately. The protocol will not accept late proofs. A late proof could lead to a situation where the user already got their refund and then the target chain execution also happened — meaning double payment.

TimeoutReached is the hard deadline. It is enforced by the slot clock. No exceptions.


Part 4: The State Errors

AlreadyFinalized

rust

#[msg("This intent has already been finalized by a ZK-Proof.")]
AlreadyFinalized,

Once an intent is finalized it is done. The escrow is closed. The funds went to treasury. There is nothing left to do.

If anything tries to interact with a finalized intent — a second proof submission, a cancel attempt, anything — AlreadyFinalized fires.

Notice the error message says "ZK-Proof" even though the current implementation uses TSS. The message is forward-looking. The verification mechanism will upgrade. The principle stays the same. Once it is finalized, it is immutable.

AlreadyCancelled

rust

#[msg("This intent has already been cancelled.")]
AlreadyCancelled,

Same logic in the other direction. Once cancelled the escrow is closed. The funds went back to the user. Nothing can change that.

AlreadyFinalized and AlreadyCancelled together create what engineers call mutual exclusion. An intent can reach exactly one terminal state. Not both. Not neither. Exactly one. The two booleans in EscrowState enforce this. These two errors enforce it at every interaction point.

This is what prevents double-spend and double-refund. Not by accident. By explicit design.


Part 5: The Authorization Errors

Unauthorized

rust

#[msg("User is not authorized to manage this intent.")]
Unauthorized,

This fires when someone tries to cancel an intent they did not create.

The EscrowState stores the owner field. The cancel_intent instruction checks that the signer matches the owner. If they do not match, Unauthorized fires and nothing happens.

You cannot cancel someone else's escrow. You cannot steal someone else's refund. The program enforces ownership at the instruction level.

UnauthorizedSentinel

rust

#[msg("The signer is not an authorized Sentinel node.")]
UnauthorizedSentinel,

This fires when something tries to call submit_proof but is not a registered Sentinel Node.

Remember from yesterday — SentinelAccount exists in state.rs. The admin registers Sentinel Nodes explicitly. If you are not in that registry, you cannot submit proofs. Period.

This prevents anyone from submitting fake proofs. Even if someone figured out how to forge a valid TSS signature — which is cryptographically infeasible — they would still fail here because they are not in the authorized Sentinel registry.

Defense in depth. The cryptography is one layer. The authorization registry is a second layer.


Part 6: The Safety Errors

MathError

rust

#[msg("Mathematical overflow or underflow occurred.")]
MathError,

On Solana, mathematical overflow in a program causes a panic by default in debug mode. In release mode the behavior depends on your settings.

In CHAKRA we use checked arithmetic everywhere that matters. Instead of a + b we use a.checked_add(b).ok_or(ChakraError::MathError)?. If the addition would overflow a u64, instead of wrapping around to a tiny number or crashing, we return MathError and stop.

Why does this matter for a cross-chain protocol?

Because amounts and slot numbers are u64 values. If someone crafted an intent where adding timeout_slots to the current slot would overflow, the resulting timeout_slot would be a tiny number in the past. The intent would immediately be cancellable. The Sentinel Network would never get a chance to execute.

MathError catches that. The intent never gets created.

InvalidProof

rust

#[msg("The provided cryptographic proof is invalid or improperly signed.")]
InvalidProof,

This is the last line of defense.

When a Sentinel Node calls submit_proof, the program reconstructs the message hash, calls secp256k1_recover with the provided signature components, and compares the recovered public key against the registered TSS public key.

If they do not match — if the signature is wrong, if the message was constructed incorrectly, if any byte is off — InvalidProof fires. The escrow stays locked. The funds stay safe. Nothing changes.

InvalidProof is what makes the cryptographic guarantee real. It is the moment where math meets money. Either the proof is valid and the escrow releases, or it is not and everything stays exactly as it was.


Part 7: What I Actually Got Wrong

I want to be honest about something.

The first version of this file had seven errors. I was missing TimeoutTooLong, MathError, and UnauthorizedSentinel.

TimeoutTooLong I missed because I was thinking about the minimum but not the maximum. One pointed out the long-lock attack during a code review I did informally.

MathError I missed because in my test environment the numbers were always small. It was only when I started thinking about what a malicious actor could pass as inputs that I realized I needed checked arithmetic everywhere.

UnauthorizedSentinel I missed because early on there was only one Sentinel Node running — mine. No need for an authorization registry when there is only one node. As soon as I moved to three nodes the need became obvious.

Three errors added because I thought more carefully about attacks.

That is how you build security. Not by getting it right the first time. By thinking about every person who might want to break the thing you built and making sure you have thought about them too.


The Contract Is Signed Every single one of these errors acts as a hard boundary. They make sure the program logic stays entirely inside its intended lane. Without them, a state-changing transaction isn't just code executing it's a massive vulnerability waiting to be triggered by an edge-case attack vector. By defining exactly how and why our system will refuse to act, we give users absolute predictability. They know their funds can never settle into an unpredictable, stuck state. It is either total cross-chain finality or an immediate, clean refund. Now that we have mapped out the blueprint in state.rs and defined our safety boundaries in errors.rs, we finally have everything we need to build the actual engine.


Some resources for you to refer

post image

Written by Maha thankyou for reading