Cover photo

The 60 Lines That Define Everything

The Cost of a Byte: Why Cross-Chain Security Begins with Perfect Rust Data Models.

CHAKRA SERIES - 4

Welcome to Day 4. Today, we leave the high-level concept papers behind and look at the actual code defining CHAKRA. Before we talk about threshold signatures or complex execution logic, we have to talk about the bedrock: how we model our data on-chain.

We are looking directly at the codebase, starting with the foundation that everything else rests on - The Rust state.

Firstly let me tell you something about how protocols actually work.

Most people think the hardest part of building a cross-chain protocol is the cryptography. The threshold signatures. The distributed key generation. The Lagrange interpolation.

And yes that stuff is hard. I spent months on it.

But you know what breaks protocols more often than bad cryptography?

Bad data models.

Decisions made in the first week of writing code that you can never take back. Fields that are in the wrong place. Types that are too small. Booleans that should have been enums. Accounts that were never designed to be closed cleanly.

These decisions live forever in a deployed program. On Solana they live in accounts. And accounts cost rent. And rent means real money. So every byte you waste is real money wasted by real users.

Today I want to show you the file where I made all those decisions for CHAKRA.

It is called state.rs. It is 60 lines of Rust. And it is the foundation that every single other piece of CHAKRA is built on.


Part 1: What Is State in an Anchor Program

If you have never written Solana programs before this concept is important to understand.

On Ethereum when your smart contract saves data it goes into contract storage. The contract owns the storage. You cannot really separate them.

On Solana it works completely differently.

Programs do not store data. Programs are stateless. They are just logic. Pure logic.

Data lives in accounts. Separate accounts. Owned by the program but separate from it.

So when CHAKRA needs to remember something — who initiated an intent, how much they locked, what the timeout is, whether it has been finalized — all of that lives in an account. A specific account structure defined in state.rs.

Think of state.rs as the blueprint. The struct definitions. The shapes of the containers. The program logic uses these containers. The accounts on-chain hold the data. state.rs is what connects the two.


Part 2: The Four Structs

CHAKRA has four account types defined in state.rs.

I am going to walk through each one slowly.

TssConfig

rust

#[account]
pub struct TssConfig {
    pub tss_pubkey: [u8; 64],
    pub threshold: u8,
    pub total_nodes: u8,
    pub admin: Pubkey,
    pub bump: u8,
}

This is the most important account in the entire protocol.

tss_pubkey is a 64-byte array. Not 65 bytes. Not 33 bytes. 64 bytes exactly.

Why 64?

A secp256k1 public key in uncompressed form is 65 bytes. But the first byte is always 0x04 — it is a prefix that just says "this is an uncompressed key." It carries no information. So we strip it and store only the 64 bytes of actual key material.

This 64-byte value is what every TSS proof gets verified against. When the Sentinel Network signs an intent and submits the proof, Solana runs secp256k1_recover on the signature and recovers a public key. That recovered key gets compared byte by byte against this field. If they match the proof is valid. If they do not match the proof is rejected.

Everything depends on this field being correct.

threshold and total_nodes are u8 — unsigned 8-bit integers. Maximum value 255. We only have 3 nodes right now so u8 is more than enough. These fields exist so the program knows the signing rules. Currently 2-of-3.

admin is the Pubkey that is allowed to update the TssConfig. This is important for key rotation. If the Sentinel Network needs to refresh its keys in Milestone 2, the admin can call update_tss_config to register the new public key. Nobody else can.

bump is the canonical bump seed for the PDA derivation. I will explain why we store this in Part 3.

Space calculation: 8 (Anchor discriminator) + 64 + 1 + 1 + 32 + 1 = 107 bytes.


EscrowState

rust

#[account]
pub struct EscrowState {
    pub owner: Pubkey,
    pub target_chain_id: u64,
    pub nonce: u64,
    pub amount: u64,
    pub start_slot: u64,
    pub timeout_slot: u64,
    pub is_finalized: bool,
    pub is_cancelled: bool,
    pub bump: u8,
    pub source_chain: [u8; 32],
    pub target_chain: [u8; 32],
    pub target_address: [u8; 64],
}

This is the escrow account. One of these gets created every time a user initiates a cross-chain intent. It holds all the information about that specific intent.

Let me go field by field because every single one matters.

owner — the Pubkey of the user who created this intent. This is the person who gets refunded if the intent times out and cancel is called. The program enforces this. You cannot call cancel on someone else's escrow.

target_chain_id — a u64 representing the EIP-155 chain ID of the target blockchain. Base Sepolia is 84532. Ethereum mainnet is 1. Polygon is 137. This is a standard. We use it so the Sentinel Nodes and the ChakraReceiver contract can both independently verify they are working on the same chain.

nonce — a u64 that makes each intent unique. If you send 100 intents to Base you need 100 different nonces. The PDA seed includes the nonce so each intent gets its own account address. Without nonces all your intents would try to write to the same account and fail.

amount — how many lamports the user locked. u64 because lamport values can be large and u64 goes up to about 18 quintillion. More than enough.

start_slot — the Solana slot when the intent was created. We record this for analytics and for potential dispute resolution in future versions.

timeout_slot — the absolute slot number after which cancel_intent becomes callable. Not a duration. An absolute slot. This is important. When initialize_intent runs it takes the current slot and adds timeout_slots to get this value. After that the user can always recover their funds. The Sentinel Network has until this slot to complete execution and submit proof. After this slot the protocol gives up and refunds.

is_finalized — boolean. True when a valid TSS proof has been submitted and verified. The escrow account gets closed when this happens.

is_cancelled — boolean. True when the intent timed out and was cancelled. The escrow account gets closed when this happens too.

These two booleans are your safety net. Before any state-changing operation the program checks both. If is_finalized is true you cannot cancel. If is_cancelled is true you cannot finalize. You cannot accidentally double-spend. You cannot accidentally double-refund.

bump — PDA bump. Same reason as TssConfig.

source_chain — 32 bytes. Usually contains a string like "solana" zero-padded to 32 bytes. This tells the Sentinel Network where the intent originated.

target_chain — 32 bytes. Usually contains "base" or "ethereum" zero-padded.

target_address — 64 bytes. This is the destination address on the target chain. 64 bytes because we want to support any chain. An Ethereum address is 20 bytes. A Bitcoin address is much shorter. A Solana address is 32 bytes. By making this field 64 bytes we can support all of them with room to spare. The unused bytes are zero-padded.

Space calculation: 8 + 32 + 8 + 8 + 8 + 8 + 8 + 1 + 1 + 1 + 32 + 32 + 64 = 211 bytes.


GlobalConfig

rust

#[account]
pub struct GlobalConfig {
    pub admin: Pubkey,
    pub treasury: Pubkey,
    pub is_initialized: bool,
    pub bump: u8,
}

The protocol-wide configuration. Created once by the admin when the program is first set up.

admin — who controls the protocol. Can add and remove Sentinel Nodes. Can update the TssConfig. Full protocol authority.

treasury — where the lamports go when an escrow is finalized. When submit_proof runs and verifies successfully, the escrow account closes and the lamports go here. This is how the protocol captures fees in future versions.

is_initialized — a simple guard against calling initialize_config twice. Once it is true, you cannot reinitialize. This prevents a class of attack where someone tries to reset protocol state after it is already set up.

bump — PDA bump.

Space calculation: 8 + 32 + 32 + 1 + 1 = 74 bytes.


SentinelAccount

rust

#[account]
pub struct SentinelAccount {
    pub sentinel_pubkey: Pubkey,
    pub is_active: bool,
    pub bump: u8,
}

The authorization record for a Sentinel Node.

For a Sentinel Node to call submit_proof it needs to be in this registry. The admin creates a SentinelAccount for each node when it is authorized. The admin can deactivate a node by setting is_active to false without deleting the account.

sentinel_pubkey — the Solana keypair address of the Sentinel Node process. When a Sentinel Node signs and submits a transaction, this is the key it signs with.

is_active — the on/off switch. If a Sentinel Node is compromised, the admin sets this to false immediately. Any further proof submissions from that node fail the authorization check. The attack is contained.

bump — PDA bump.

Space calculation: 8 + 32 + 1 + 1 = 42 bytes.


Part 3: Why We Store the Bump

Every single struct has a bump field. If you are new to Solana this might seem weird. Let me explain.

Program Derived Addresses on Solana are calculated from seeds and a bump value. The bump is a number from 0 to 255 that is decremented until the derived address is not on the secp256k1 curve — meaning it has no corresponding private key and only the program can sign for it.

The canonical bump is the highest valid bump. Anchor finds this automatically when you use the init constraint.

Why store it? Because every time you want to verify or use a PDA in a subsequent instruction you need to recalculate it from the seeds and bump. If you store the bump you save the computation of finding it again. More importantly you make the account addresses deterministic — given the same seeds and stored bump, you always get the same address. No ambiguity.

In production Solana programs storing the bump is standard practice. It also saves compute units which costs less in fees.


Part 4: What This File Does Not Contain

state.rs has no logic. Zero.

No function calls. No if statements. No calculations. Nothing.

Just structure definitions.

This is intentional. In Anchor the state and the logic are completely separate. The instructions in the instructions/ folder handle all the logic. state.rs is purely the data model.

This separation makes the code much easier to audit. If you want to understand what can go wrong with an EscrowState account, you read the instructions that use it. The state definition itself cannot have bugs because it cannot do anything.


Part 5: The Cost of a Byte

When I was designing these 4 structs, I spent hours counting bytes ofcourse with alot of errors. On Solana, data storage isn't free. Every byte you store requires "rent exemption," which means locking up real SOL.

If you design a sloppy data model that wastes 100 bytes per transaction, and your protocol processes 10,000 transactions a day, you are burning your users' money for nothing. That’s why TssConfig is exactly 107 bytes, and EscrowState is a tight 211 bytes. We store only what is mathematically required to guarantee safety.

This file is the bedrock. It doesn't execute anything, but it defines the boundaries of what CHAKRA can and cannot do. If the container is broken, the logic inside it won't matter.

I know looking at raw structs can feel dry if you're waiting for the action, but you can't build a mainframe without laying down the foundation first. Now that you know the exact shape of our state and why every single field exists... we are ready to make it move.

Tomorrow for Day 5, we are opening up the next file. We’ll look at the actual Rust logic that takes these structs, locks the funds, and spins the gears into motion.


Here are some resources


post image

Written by Maha.. See y'all tomorrow.