Cover photo

Where Math Meets Money

How CHAKRA taps into Solana's native secp256k1_recover syscall to verify Ethereum-style cross-chain proof in a single runtime leap.

CHAKRA SERIES - 9

Where Math Meets Money
Inside submit_proof.rs: The On Chain Moment secp256k1_recover Decides Everything

Welcome to Day 9.

Day 7 was the start. initialize_intent.rs locks the funds and fires the signal. Day 8 was the escape hatch. cancel_intent.rs is what happens if nothing comes back in time.

Today is the third path. The happy path. The Sentinel Network heard the signal from Day 7, ran the 2 of 3 threshold signing ceremony I described back on Day 2, executed on Base Sepolia, and now comes back to Solana with proof that it actually happened.

This file is submit_proof.rs, and on Day 5 when I introduced InvalidProof I said this is the moment where math meets money. Today you get to see that moment in actual code.


Part 1: The Accounts Struct, Five New Faces

rust

#[derive(Accounts)]
pub struct SubmitProof<'info> {
#[account(mut)]
pub sentinel: Signer<'info>,
#[account(
    seeds = [b"sentinel", sentinel.key().as_ref()],
    bump = sentinel_auth.bump,
    constraint = sentinel_auth.is_active @ ChakraError::UnauthorizedSentinel
)]
pub sentinel_auth: Account<'info, SentinelAccount>,

#[account(
    seeds = [b"config"],
    bump = config.bump,
)]
pub config: Account<'info, GlobalConfig>,

#[account(
    mut,
    seeds = [
        b"escrow",
        escrow_account.owner.as_ref(),
        &escrow_account.target_chain_id.to_le_bytes(),
        &escrow_account.nonce.to_le_bytes()
    ],
    bump = escrow_account.bump,
    close = treasury
)]
pub escrow_account: Box<Account<'info, EscrowState>>,

#[account(
    seeds = [b"tss_config"],
    bump = tss_config.bump,
)]
pub tss_config: Account<'info, TssConfig>,

/// CHECK: Treasury wallet from config
#[account(mut, address = config.treasury)]
pub treasury: AccountInfo<'info>,

pub system_program: Program<'info, System>,
}

sentinel is the Signer here, not the user. This whole instruction is called by a Sentinel Node, not by the person who started the intent.

sentinel_auth checks constraint = sentinel_auth.is_active. This is the SentinelAccount registry from Day 4 and the UnauthorizedSentinel error from Day 5, finally being used. Even if someone has somehow produced a perfect TSS signature, if they are not a registered active sentinel, this constraint stops them right here, before the function body even runs.

escrow_account is wrapped in Box<>. Quick note on that, Box just moves the account data onto the heap instead of the stack. Anchor instructions have a limited stack size, and EscrowState at 211 bytes plus all the other accounts in this struct can get close to that limit. Boxing it is a common pattern to stay safely under the limit.

And look at close = treasury this time, not close = owner like yesterday. I will come back to this in Part 5.


Part 2: The Familiar Gatekeepers

rust

require!(!escrow.is_finalized, ChakraError::AlreadyFinalized);
require!(!escrow.is_cancelled, ChakraError::AlreadyCancelled);
require!(clock.slot <= escrow.timeout_slot, ChakraError::TimeoutReached);

Same family of checks as cancel_intent, but flipped. Cancel needed the timeout to have passed. Submit proof needs the timeout to NOT have passed yet. TimeoutReached is the mirror image of TimeoutNotReached from yesterday. Together, these two checks across two files are what guarantee an intent can only ever land in exactly one final state.


Part 3: Rebuilding The Message, And Why Big Endian

Now here is the part I told you to remember from Day 7.

rust

let mut msg_data = Vec::with_capacity(8 + 8 + 8 + 64);
msg_data.extend_from_slice(&escrow.target_chain_id.to_be_bytes());
msg_data.extend_from_slice(&escrow.nonce.to_be_bytes());
msg_data.extend_from_slice(&escrow.amount.to_be_bytes());
msg_data.extend_from_slice(&escrow.target_address);

let msg_hash = keccak256(&msg_data).to_bytes();

to_be_bytes(). Big endian. Back in initialize_intent.rs, the PDA seeds used to_le_bytes(), little endian, because that is just the normal Rust and Solana convention for deriving addresses, nobody outside Solana ever needs to recompute those seeds.

But this msg_data right here is different. This exact byte sequence, in this exact order, with this exact endianness, has to be reproduced perfectly by ChakraReceiver.sol on Base Sepolia, because the Sentinel Network signed a hash of this same data, and the contract on the other side needs to recompute the identical hash to verify that signature.

Ethereum and EVM chains use big endian as their convention. So this is CHAKRA speaking Ethereum's language on purpose, right here, in the middle of a Solana program. Get this wrong by even the endianness of one field, and the hash on Solana will never match the hash on Base, and every single proof will fail forever.

And keccak256, not sha256. Why. Because Ethereum's ECDSA signatures are produced over keccak256 hashes. If the Sentinel Nodes signed a keccak256 hash back during the signing ceremony from Day 2, then Solana has to also hash with keccak256 here, or the recovered key will be garbage.


Part 4: secp256k1_recover, The Syscall That Makes This Whole Project Possible

rust

let mut sig_bytes = [0u8; 64];
sig_bytes[0..32].copy_from_slice(&signature_r);
sig_bytes[32..64].copy_from_slice(&signature_s);

let recovery_id = signature_v
.checked_sub(27)
.ok_or(ChakraError::InvalidProof)?;

let recovered_pubkey = secp256k1_recover(&msg_hash, recovery_id, &sig_bytes)
.map_err(|_| ChakraError::InvalidProof)?;

require!(
recovered_pubkey.to_bytes() == tss_config.tss_pubkey,
ChakraError::InvalidProof
);

escrow.is_finalized = true;

A quick note on signature_v.checked_sub(27) first. In Ethereum, the recovery id v is encoded as either 27 or 28, an old historical convention. ECDSA recovery actually only needs a recovery id of 0 or 1. So we subtract 27 to convert back. And notice it is checked_sub, not a plain subtraction. If somehow signature_v came in less than 27, a plain subtraction would underflow a u8 and wrap around to some huge number. checked_sub catches that and returns InvalidProof instead. One more quiet little safety check, same family as the ones from Day 5.

Now, secp256k1_recover. This is the single most important function call in the entire program. Given a message hash, a recovery id, and a signature, it mathematically recovers the public key that must have produced that signature. Not "checks against" a public key. Recovers one, from nothing but the hash and the signature.

This is a native syscall on Solana, meaning it runs as actual compiled machine instructions inside the validator, not as some slow library running inside the program's own bytecode. That is a big deal. This is part of why CHAKRA can exist on Solana specifically. Verifying an Ethereum style ECDSA signature, cheaply, on chain, in a single instruction, is something most chains simply cannot do natively.

And then the line that everything in this entire series has been building toward.

recovered_pubkey.to_bytes() == tss_config.tss_pubkey

That's it. That is the whole trust model of CHAKRA, on one line. Remember the 64 byte tss_pubkey from Day 4, the field I said everything depends on being correct. This is the line it was waiting for. If the recovered key matches, the proof is real, is_finalized flips to true, and emit!(IntentFinalized { escrow_pda, tx_hash }) fires, the same IntentFinalized event from Day 6.

If it does not match, even by one byte, InvalidProof fires, the transaction reverts, is_finalized stays false, and the escrow stays exactly where it was, still cancellable later if needed. Nothing in between. No partial trust.


Part 5: Where The Money Actually Goes

Let's look at one final design detail honestly.

When this instruction succeeds, close = treasury routes the escrowed lamports straight to config.treasury instead of returning them to the user. This is the exact opposite of cancel_intent.rs, where funds go back to the original owner.

For our Milestone 1 release, these locked tokens act as the fee that compensates the Sentinel Network for doing the actual cross-chain heavy lifting on the other side. Right now, treasury is a placeholder destination while I map out the long-term mechanics. Once the network expands past me running all three nodes, this pool will dynamically distribute payouts to decentralized node operators based on performance. It’s a Milestone 1 framework, not the final mainnet economic model—but keeping it completely transparent here is better than pretending it's fully set in stone


Part 6: The Whole Series, In One Line

If you asked me to reduce CHAKRA’s entire security apparatus to a single sentence, it would be this: Every moving part before this moment—the Distributed Key Generation, the Shamir Secret Sharing, and the Lagrange interpolation—exists solely to reconstruct a 64-byte public key on this exact line of code that matches tss_config.tss_pubkey.

An attacker who hasn't compromised a 2-of-3 quorum cannot make those bytes match. Not because a rule tells them no, but because the mathematics won't allow it.


What's Next

I am officially pausing the writing train for the next few days. I've shown you the complete architectural vision, the custom errors, the off-chain wire, and the cryptographic proof engine. Now, it's time to put down the essays and code the hell out of CHAKRA.

See you all on the other side. Thank you for reading!


Here are some final resources for y'all

Play Video
post image