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 / zk-circuit

ZK Circuit

WRIT uses a small Groth16 circuit over BN254 to prove knowledge of a secret trapdoor and bind it to a Poseidon nullifier without revealing which secret produced it. The full circuit has ~4,200 R1CS constraints and compiles to a 32 kB proving key.

Proving system
Groth16
Curve
BN254 (alt_bn128 in Ethereum / Solana)
Hash
Poseidon, t=3, R_F=8, R_P=57
Constraints
~4,200 R1CS
Proof size
192 bytes (G1 + G2 + G1 uncompressed)
Verify cost on Solana
~165,000 CU

What the circuit proves

Given a public epoch root R and a public nullifier N, the prover knows a secret s such that:

The first clause makes the nullifier a deterministic function of the secret, so the same human cannot register twice. The second clause lets us rotate the valid secret set without invalidating old registrations — each registration is bound to the epoch root that was live at proof time.

Circuit source

circuits/writ.circomjavascript
pragma circom 2.1.4;

include "circomlib/poseidon.circom";
include "circomlib/mux1.circom";

template Writ(depth) {
    // --- private inputs
    signal input secret;
    signal input path_elements[depth];
    signal input path_indices[depth];

    // --- public inputs
    signal output nullifier;
    signal input  root;

    // leaf = Poseidon(secret)
    component leaf_hash = Poseidon(1);
    leaf_hash.inputs[0] <== secret;

    // Merkle path
    component hashers[depth];
    component muxes[depth];
    signal cur;
    cur <== leaf_hash.out;
    for (var i = 0; i < depth; i++) {
        muxes[i] = Mux1();
        muxes[i].c[0] <== cur;
        muxes[i].c[1] <== path_elements[i];
        muxes[i].s    <== path_indices[i];
        hashers[i]    = Poseidon(2);
        hashers[i].inputs[0] <== muxes[i].c[0];
        hashers[i].inputs[1] <== muxes[i].c[1];
        cur <== hashers[i].out;
    }
    cur === root;

    // nullifier = Poseidon(secret, DOMAIN_WRIT)
    component null_hash = Poseidon(2);
    null_hash.inputs[0] <== secret;
    null_hash.inputs[1] <== 1_936_812_657;  // ASCII "writ" + 0x00
    nullifier <== null_hash.out;
}

component main { public [root] } = Writ(20);

Tree depth 20 gives 220 ≈ 1.05M leaves per epoch — enough to onboard every human on Solana at current adoption without exhausting a single epoch. When epochs fill, a new root is rotated in.

Proof generation

The browser flow uses snarkjs WASM to generate proofs in 1.8–3.2 seconds on a modern laptop. The witness builder runs in a web worker; the prover runs in the main thread but is non-blocking due to chunked polynomial evaluation.

Public inputs: nullifier, root. Private inputs: secret, merkle path.

Client timing

StageMedianP95
Argon2id420 ms680 ms
witness gen110 ms180 ms
prove1.4 s2.9 s
total1.9 s3.7 s
NoteAll timings measured on an M2 MacBook Air, Chrome 128, with WASM and the proving key pre-warmed. Cold first-visit times are ~800 ms higher due to the 480 kB WASM + 32 kB key fetch; subsequent proofs are cached.

On-chain verifier

The verifier is a small Rust module that wraps Solana's alt_bn128_pairing syscall. It consumes the proof bytes plus the two public inputs and either succeeds or errors with ProofInvalid.

programs/writ_registry/src/zk.rsrust
use solana_program::alt_bn128::prelude::*;

pub fn verify_groth16(
    vk: &VerifyingKey,
    proof_a: &[u8; 64],
    proof_b: &[u8; 128],
    proof_c: &[u8; 64],
    inputs: &[[u8; 32]],
) -> Result<()> {
    let prepared = prepare_inputs(vk, inputs)?;
    let lhs  = Pairing::pair(proof_a, proof_b)?;
    let rhs1 = Pairing::pair(&vk.alpha_g1, &vk.beta_g2)?;
    let rhs2 = Pairing::pair(&prepared,    &vk.gamma_g2)?;
    let rhs3 = Pairing::pair(proof_c,      &vk.delta_g2)?;
    let rhs  = rhs1 * rhs2 * rhs3;
    if lhs != rhs { return err!(ErrorCode::ProofInvalid); }
    Ok(())
}

Trusted setup

Groth16 requires a per-circuit trusted setup. WRIT runs a Powers-of-Tau ceremony followed by a circuit-specific Phase 2 MPC. Any contributor can participate; compromise requires every contributor to collude. Transcript and contributor list are published at github.com/WritNetwork/writ-v1/ceremony.

Epoch rotationThe verifying key on-chain includes an epoch root. Rotating the root invalidates proofs made against old roots for new registrations, but existing writs remain valid forever — their nullifier is already committed. There is no migration required for users between epochs.

Non-goals