# The Real Errors of Chakra

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

By [Chakra](https://paragraph.com/@chakra-papers) · 2026-06-11

@solana@web3@anchor@rust

---

CHAKRA SERIES - 5
-----------------

Welcome to Day 5.

_Yesterday we looked at_ [_state.rs_](http://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_](http://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_](http://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_](http://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_

[

error\_code in anchor\_lang - Rust
----------------------------------

Generates \`Error\` and \`type Result = Result \` types to be used as return types from Anchor instruction handlers. Importantly, the attribute implements \`From\` on the \`ErrorCode\` to support converting from the user defined error enum into the generated \`Error\`.

https://docs.rs



](https://docs.rs/anchor-lang/latest/anchor_lang/attr.error_code.html)

[

Custom Errors
-------------

Learn how to implement custom error handling in Anchor programs.

https://www.anchor-lang.com



](https://www.anchor-lang.com/docs/features/errors)

[

u64 - Rust
----------

The 64-bit unsigned integer type.

https://doc.rust-lang.org



](https://doc.rust-lang.org/std/primitive.u64.html)

![](https://storage.googleapis.com/papyrus_images/b34c4310ed4a8429f54429ccfb1ffb8f03bc3657fa9f8f332bb4e2ae58b0ab84.avif)

_Written by Maha thankyou for reading_

---

*Originally published on [Chakra](https://paragraph.com/@chakra-papers/the-real-errors-of-chakra)*
