# 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.*

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

@solana, @web3, @rust, @ethereum, @anchor

---

**CHAKRA SERIES - 9**

**Where Math Meets Money**  
Inside submit\_[proof.rs](http://proof.rs): The On Chain Moment secp256k1\_recover Decides Everything

Welcome to Day 9.

Day 7 was the start. initialize\_[intent.rs](http://intent.rs) locks the funds and fires the signal. Day 8 was the escape hatch. cancel\_[intent.rs](http://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](http://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](http://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](http://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

[

solana\_secp256k1\_recover - Rust
---------------------------------

Public key recovery from secp256k1 ECDSA signatures.

https://docs.rs



](https://docs.rs/solana-secp256k1-recover/latest/solana_secp256k1_recover/)

[

Data structures and encoding | ethereum.org
-------------------------------------------

An overview of the fundamental Ethereum data structures.

https://ethereum.org

![Data structures and encoding | ethereum.org](https://storage.googleapis.com/papyrus_images/a2f4796307f924bab5bfc9c7cc5d2e56c6d4cd00a64075af07b94771f8b35745.png)

](https://ethereum.org/developers/docs/data-structures-and-encoding/)

[

Account Constraints
-------------------

Anchor Account Constraints Examples

https://www.anchor-lang.com



](https://www.anchor-lang.com/docs/references/account-constraints)

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=_vl-okF3cs8)

* * *

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

---

*Originally published on [Chakra](https://paragraph.com/@chakra-papers/where-math-meets-money)*
