Private payments on Ethereum that no observer can follow and no quantum computer can break.

Three hops, shuffled batches, cover traffic. What leaves cannot be matched to what went in.

Packets of many colours enter a box of three hops and leave identical, in shuffled order.

A deposit becomes a note only you can open. Spending it proves you own a note, never which one.

Every figure was measured on the testnet.

  1. Deposit

    Funds enter the pool as a note only you can spend, signed with a hash-based key.

    one frame transaction, about 7.5M gas

  2. Prove

    Your wallet proves it owns an unspent note without saying which.

    about a second to prove, 450 MB of memory

  3. Send

    The proof leaves as identical 8 KB packets through three post-quantum hops.

    about a minute on a mobile connection

  4. Aggregate

    The exit pairs your spend with another and folds both proofs into one.

    12 to 20 minutes on one machine

  5. Settle

    One transaction verifies the proof and pays both recipients.

    16.3M gas

The gateway knows who sent a packet, not where it goes. The exit knows where it goes, not who sent it. Drag the globe.

Mix, Frankfurt0packets shuffled in batches since you arrived
Gateway, Mumbai16anonymous tokens per member each epoch
Exit, N. Virginia2 → 1spend proofs folded into one for the chain
Reply relay, Singapore8 KBevery packet, every hop, the same size
paymentcover trafficreplysettled

Most privacy systems are held together by elliptic curves. A quantum computer breaks them: it could forge their proofs, sign for their accounts and unwrap recorded traffic.

  • Lattice encryption

    Each layer of every packet is sealed with ML-KEM-768, paired with X25519.

    Replaces X25519 alone

  • Hash-based signatures

    Accounts and the paymaster sign with leanSPHINCS, which rests on hash functions alone.

    Replaces ECDSA

  • Hash-based proofs

    Spends are proven with Spartan-WHIR and checked on chain by a WHIR verifier, with no trusted setup.

    Replaces Groth16 on BN254

Five layers between your wallet and the chain, each one post-quantum.

Five stacked layers: wallet, mixnet, rate limiting, exit and chain

Wallet

Proves it owns an unspent note with Spartan-WHIR, in about a second, without saying which note.

Rests on hash functions

Mixnet

8 KB KEM-Sphinx packets sealed with X-Wing (ML-KEM-768 and X25519) at each of three hops, mixed in batches with cover traffic.

Rests on lattices

Rate limiting

A Binius64 ticket each epoch proves membership anonymously; cheap hash tokens then pay for each packet.

Rests on hash functions

Exit

Checks each spend against the chain and folds two proofs into one with leanVM recursion.

Rests on hash functions

Chain

One EIP-8141 frame transaction verifies the proof with WHIR; a leanSPHINCS signature authorizes the paymaster.

Rests on hash functions

The pool's validation and settlement are modelled in Lean 4 and these properties are proven, with no unfinished proofs and no axioms beyond Lean's standard two. The model is tested against the live chain, transaction by transaction. What it assumes is written down in the soundness statement.

Read the soundness statement

  • Only the settle call ever runs as the pool.
    only_settle_runs_as_pool.lean
    theorem only_settle_runs_as_pool (pool : Nat) (tx : Tx)
        (h : poolPolicy pool tx 0 = .ok ()) :
        ∃ f2, callsAsSender tx = [f2] ∧ f2.target = pool ∧
          f2.selector = SETTLE ∧ f2.value = 0
  • The pool never pays for its own transactions.
    pool_never_pays.lean
    theorem pool_never_pays (pool : Nat) (tx : Tx)
        (h : poolPolicy pool tx 0 = .ok ()) :
        ∃ f0 f1 f2, tx.frames = [f0, f1, f2] ∧ f0.flags = SCOPE_EXECUTION ∧
          f1.flags = SCOPE_PAYMENT ∧ f1.target ≠ pool
  • No note can be spent twice, over any history of transactions.
    run_distinct.lean
    theorem run_distinct {rootOf : List Nat → Nat} :
        ∀ (s : State) (txs : List (Bool × Bool × Spend × Spend)),
          Distinct s.spent → Distinct (run rootOf s txs).spent
  • A settle that fails changes nothing, so its notes stay spendable.
    failed_settle_changes_nothing.lean
    theorem failed_settle_changes_nothing {rootOf : List Nat → Nat}
        {s : State} {p q : Bool} {a b : Spend} {e : Err}
        (h : settle rootOf s p q a b = .error e) : step rootOf s p q a b = s
  • The pool's balance always equals the value of its unspent notes: it cannot pay out more than was deposited.
    settle_preserves_inv.lean
    theorem settle_preserves_inv {rootOf : List Nat → Nat} {s s' : State}
        {p q : Bool} {a b : Spend}
        (inv : Inv s) (sa : Sound s a) (sb : Sound s b) (fr : Fresh s a b)
        (h : settle rootOf s p q a b = .ok s') : Inv s'
  • With the post-quantum paymaster, no signature on the pool's path is ECDSA.
    pq_pool_tx_has_no_ecdsa.lean
    theorem pq_pool_tx_has_no_ecdsa (d : Deployment) (tx : Tx)
        (hpq : d.pqPaymaster = true) (hpp : d.paymaster ≠ d.pool)
        (h : valid d tx = true) :
        ∀ s ∈ tx.sigs, s.scheme = ARBITRARY

A TypeScript SDK, a Foundry library, a node binary and a Lean model. Pick a file.

topi

sdk/spend.tsOpen on GitHub
import { PoolReader, RpcClient, loadPoolDeployment } from "@topi/sdk";

const rpc = new RpcClient("https://rpc1.privacy.ethrex.xyz");
const deployment = loadPoolDeployment("deployments/8141/pool.json");
const pool = new PoolReader(rpc, deployment.pool);

// Rebuild the pool's trees from its public events.
const view = await pool.chainView(deployment);
console.log(view.next_leaf, "notes in the pool");
TypeScript9 lines

One terminal for the whole system: the network on a map, the cloud, the wallet, the prover and the proofs.

▀█▀ topi  leave no tracelocal 4/4 block 641,154
1 wallet2 mixnet map3 chain

leave no trace

■ live

nodes up4/4

packets relayed18,402

batches612

cover packets3,977

Lean14 theorems, all checked

■ a private spend's path

l start local  k stop local  p ping  z zoom maptab switch  o site  g github  j jobs  q quit
$ cargo install --locked --git https://github.com/Naveen-6087/topi topi-tui topi-mixnet$ topi

The console runs from a checkout of the repository: its actions call the scripts and contracts there. Prebuilt binaries come with each tagged release; the node and client binaries are in the same archive.