# Who Holds The Keys

*The Control Panel Behind CHAKRA*

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

@solana, @web3, @anchor, @rust

---

**CHAKRA SERIES - 10**

**Who Holds The Keys** Inside [admin.rs](http://admin.rs): The Control Panel Behind CHAKRA, And The Centralization I Am Not Hiding

Welcome to Day 10. Again continuing the docs

Day 7, 8 and 9 covered the three instructions a normal user or a Sentinel Node would ever call. initialize\_intent starts an intent. cancel\_intent rescues it if nothing happens. submit\_proof finishes it if everything happened correctly.

There is one file left in the instructions folder. [admin.rs](http://admin.rs). And I saved it for last on purpose, because while it is the file regular users will basically never touch, it is also if I am being completely honest the most powerful file in the entire program.

* * *

**Part 1: Why This File Is Different**

Every file we have looked at so far does something because a user did something. Locked funds because a user called initialize\_intent. Refunded because a user called cancel\_intent.

[admin.rs](http://admin.rs) is the opposite. It is the protocol talking to itself. It is how the three config accounts from Day 4, TssConfig, GlobalConfig, and SentinelAccount, actually get created and updated in the first place. Without this file running first, none of the other instructions can even start. The escrow has no treasury address. submit\_proof has no tss\_pubkey to check against. The sentinel registry is empty so every proof gets rejected before the body even runs.

Four instructions live here. initialize\_tss\_config, update\_tss\_config, initialize\_config, and the pair add\_sentinel and remove\_sentinel through a shared ManageSentinel struct. Let me go through each one.

* * *

**Part 2: initialize\_tss\_config, The Most Important One Time Call In The Program**

rust

    pub fn handle_initialize_tss_config(
        ctx: Context<InitializeTssConfig>,
        tss_pubkey: [u8; 64],
        threshold: u8,
        total_nodes: u8,
    ) -> Result<()> {
        let tss_config = &mut ctx.accounts.tss_config;
        tss_config.tss_pubkey = tss_pubkey;
        tss_config.threshold = threshold;
        tss_config.total_nodes = total_nodes;
        tss_config.admin = ctx.accounts.admin.key();
        tss_config.bump = ctx.bumps.tss_config;
        Ok(())
    }
    

Remember Day 4 where I said the 64 byte tss\_pubkey field is the single most important field in the entire protocol, the thing every submit\_proof call gets checked against on Day 9. This is the function that writes it for the very first time.

Whoever calls this becomes tss\_config.admin permanently for that account. And this tss\_pubkey has to exactly match the public key whose shards are sitting across the three Sentinel Nodes from Day 2. If this value is wrong by even one byte at this moment, every single submit\_proof from then on fails with InvalidProof forever, until someone with admin access fixes it with the next function.

* * *

**Part 3: update\_tss\_config, The Key Rotation Door**

rust

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

One constraint. tss\_config.admin == admin.key(). That is the entire gate. If you are the admin pubkey stored back when initialize\_tss\_config ran, you can call this and overwrite tss\_pubkey, threshold, and total\_nodes with new values.

Why does this exist. Now chakra uses Shamir Secret Sharing with the coordinator briefly reconstructing the key in memory. Soon upgrades to true FROST where the key never gets reconstructed anywhere ever. When that upgrade happens the way the master public key is derived changes completely. This function is the door that lets CHAKRA walk through that upgrade without redeploying the entire Anchor program. Same program, same escrow logic, same error codes, just a new tss\_pubkey registered through this one call.

* * *

**Part 4: initialize\_config, And The is\_initialized Guard**

rust

    pub fn handle_initialize_config(
        ctx: Context<InitializeConfig>,
        treasury: Pubkey,
    ) -> Result<()> {
        let config = &mut ctx.accounts.config;
        config.admin = ctx.accounts.admin.key();
        config.treasury = treasury;
        config.is_initialized = true;
        config.bump = ctx.bumps.config;
        Ok(())
    }
    

This sets config.treasury, which is exactly where the escrowed lamports go on a successful submit\_proof, the thing I was honest about on Day 9 being still a work in progress.

is\_initialized = true here is small but it matters. Because config lives at a fixed PDA seeded only by b"config", there is exactly one GlobalConfig account that can ever exist. The init constraint already stops a second initialize\_config call from succeeding on the same address, but is\_initialized as an explicit flag gives any future instruction an easy cheap way to check whether setup actually happened without relying purely on account existence. Belt and suspenders, same philosophy as the booleans in EscrowState from Day 4.

* * *

**Part 5: ManageSentinel, Growing The Network One Node At A Time**

rust

    pub fn handle_add_sentinel(
        ctx: Context<ManageSentinel>,
        sentinel_pubkey: Pubkey,
    ) -> Result<()> {
        let sentinel = &mut ctx.accounts.sentinel_account;
        sentinel.sentinel_pubkey = sentinel_pubkey;
        sentinel.is_active = true;
        sentinel.bump = ctx.bumps.sentinel_account;
        Ok(())
    }
    
    pub fn handle_remove_sentinel(
        ctx: Context<ManageSentinel>,
        _sentinel_pubkey: Pubkey,
    ) -> Result<()> {
        let sentinel = &mut ctx.accounts.sentinel_account;
        sentinel.is_active = false;
        Ok(())
    }
    

init\_if\_needed in the accounts struct is doing something specific here. Normally init means create this account and it fails if the account already exists. But CHAKRA does not know in advance how many sentinel nodes will ever exist across its lifetime. init\_if\_needed says create this PDA if this is the first time we are seeing this sentinel\_pubkey, otherwise just use the existing one. That is how add\_sentinel works identically for the very first node and the fiftieth node.

And notice remove\_sentinel does not delete anything. It just sets is\_active = false. The account stays on chain forever. This is the soft delete pattern, same is\_active check from submit\_proof on Day 9. If a node ever gets compromised, flipping this one boolean instantly and permanently locks that node out of every future submit\_proof, while still leaving a permanent public record that it happened.

* * *

**Part 6: The Part I Actually Want To Talk About**

Here is the honest thing about every constraint in this entire file. config.admin == admin.key(). tss\_config.admin == admin.key(). Right now on devnet all of those admin keys are the same keypair. My keypair. Sitting in one file on my laptop.

That means right now one private key has the power to register a completely different tss\_pubkey through update\_tss\_config, add or deactivate any sentinel through ManageSentinel, and effectively rewrite the entire trust model from Day 9 from the outside.

I want to say this loudly instead of quietly because I think it is more important than almost anything else in this series. If this exact setup were ever deployed to mainnet as is and that one keypair got compromised, an attacker would not need to break secp256k1 or compromise two Sentinel Nodes at all. They could just call update\_tss\_config with their own key, then submit fake proofs that pass against their own key, and drain every escrow. Without touching any cryptography at all.

For protocol on devnet run by one person this is normal and honestly the only way I could move this fast alone. But it cannot be the end state. Before any real funds are involved this admin authority has to move off a single keypair. Things I am actively thinking about, a multisig so updates need multiple signers, or a time lock so any update\_tss\_config call has a mandatory delay before it takes effect giving everyone time to notice if something looks wrong.

That is [admin.rs](http://admin.rs). That is the complete on chain side of CHAKRA.

* * *

Tomorrow we cross over to the other side entirely. The Sentinel Network. The actual Rust processes that heard the ControlIntent event from Day 6 and now have to do something about it.

See y'all tomorrow for Day 11.

* * *

Here are some resources

init\_if\_needed in Anchor How conditional account initialization works and when it is safe to use.  
[https://www.anchor-lang.com/docs/references/account-constraints#init\_if\_needed](https://www.anchor-lang.com/docs/references/account-constraints#init_if_needed)

Multisig on Solana How multisig programs work, relevant to the centralization point above.  
[https://docs.squads.so](https://docs.squads.so)

  

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

Written by Maha.. thankyou for reading

@solana @web3 @anchor @rust

---

*Originally published on [Chakra](https://paragraph.com/@chakra-papers/who-holds-the-keys)*
