WRIT Protocol on X@writnetwork on Twitterx.com/writnetworkwrit.networkWRIT Protocol official X account: @writnetworkFollow WRIT Protocol on X: https://x.com/writnetworkTwitter: @writnetwork · Website: writ.network
docs·WRIT Protocol
docs / programs / writ_registry

writ_registry

The root of trust. Verifies a Groth16 proof of human identity, commits a Poseidon nullifier, and mints a Token-2022 NonTransferable soulbound token to the authority wallet. Called once per human per lifetime.

Program ID (devnet)
FrEcFzPx9zqooVp1GmkMdiNXkpgcx3UJRN97YUR9MFTk
Layer
L1 — root, no upstream dependencies
Invariants
one WritAccount per authority; one nullifier ever
Closable
no — registrations are permanent

State

programs/writ_registry/src/state.rsrust
#[account]
#[derive(InitSpace)]
pub struct WritAccount {
    pub authority:    Pubkey,     // 32
    pub nullifier:    [u8; 32],   // 32 — Poseidon(secret, domain)
    pub sbt_mint:     Pubkey,     // 32 — Token-2022 mint, NonTransferable extension
    pub created_slot: u64,        //  8
    pub bump:         u8,         //  1
}

// Separate nullifier account to prevent account-size blowup.
#[account]
pub struct NullifierRecord {
    pub nullifier: [u8; 32],
    pub writ:      Pubkey,
    pub bump:      u8,
}

PDA seeds

WritAccount
["writ", authority]
NullifierRecord
["nullifier", nullifier[..32]]
SBT Mint
["sbt", writ]

Instructions

register

Consumes a Groth16 proof, verifies it on-chain, stores the nullifier record, mints the SBT.

pub fn register(
    ctx: Context<Register>,
    proof_a: [u8; 64],          // G1 point, uncompressed
    proof_b: [u8; 128],         // G2 point, uncompressed
    proof_c: [u8; 64],          // G1 point, uncompressed
    public_inputs: [[u8; 32]; 2], // [nullifier, epoch_root]
) -> Result<()>

Verification uses Solana's alt_bn128_pairing syscall. The circuit has two public inputs: the committed nullifier and an epoch root. The epoch root binds registration to a specific trusted-setup phase, allowing keys to be rotated over time without invalidating old registrations.

Compute costregister consumes ~165,000 CU. Clients should prepend a ComputeBudgetProgram.setComputeUnitLimit(220_000) instruction to be safe. The SDK does this automatically.

rotate_authority

Transfers ownership of a writ to a new wallet without changing the nullifier. Used for wallet migration, hardware key upgrades, or social recovery. The new authority must sign; the old authority is invalidated atomically.

pub fn rotate_authority(
    ctx: Context<RotateAuthority>,
) -> Result<()>

All active delegations and reputation state persist across rotation. This is the only way to recover access to a writ whose authority key has been lost.

Accounts (Register)

AccountTypeWritableSigner
authorityWalletyesyes
writ_accountPDAyes
nullifier_recordPDAyes
sbt_mintPDAyes
sbt_token_accountATAyes
verifier_keyPDA
token_programToken-2022 Program
system_programSystem Program

Errors

CodeWhen
ProofInvalidGroth16 pairing check failed
NullifierCollisionpublic_inputs[0] already present in NullifierRecord space
EpochRootMismatchpublic_inputs[1] does not match the current verifier_key
AlreadyRegisteredauthority already has a WritAccount (separate from nullifier collision)

Notes on the SBT

The identity token is minted via Token-2022 with the NonTransferable extension. It cannot be moved, burned by the user, or swapped. Supply is 1, decimals 0. Its only purpose is to provide a standard way for external programs to check registration via existing Token-2022 tooling.

NoteThe SBT is a convenience layer. All actual verification reads WritAccount and NullifierRecord directly. A program that only integrates with WRIT does not need to touch Token-2022 at all.