CHAKRA SERIES - 10
Who Holds The Keys Inside 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. 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 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. 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
Multisig on Solana How multisig programs work, relevant to the centralization point above.
https://docs.squads.so

Written by Maha.. thankyou for reading
@solana @web3 @anchor @rust

