Private payments on Ethereum L1. Each transfer is settled by one STARK, verified in one transaction.
No trusted setup · No pairing curves · No SNARK wrapper · Zero-knowledge proofs · Post-quantum notes
Warning
Testnet, before any external audit. The launch pool runs on Sepolia and holds no real value.
43 private transfers have settled on it, one transaction each, and the live verifier still
accepts all 43. Every launch proof passes the zero-knowledge rank check, 1328 = 1328 in
spec/launch-*/rank.json. The Merkle digests are 24 bytes, the anonymity
set is small, and a batch carries one intent. Nothing is deployed on mainnet. Read
docs/20-security-status.md first.
| Proof system | STARK over Goldilocks, challenges in |
| On chain | Solidity verifier, one transaction per transfer, every constraint evaluated on chain. 7,066,977 to 7,882,382 gas across 43 settlements |
| Proof | 112,916 bytes, one direct proof of the join-split: 44 columns, |
| Soundness | 80 provable bits and 142 conjectured, computed and returned by the verifier on chain |
| Privacy | Notes in a depth-32 Poseidon tree, spent by nullifier. The trace is masked, and a rank check on the masks runs before a proof leaves the device |
| Encryption to the recipient | X-Wing (ML-KEM-768 + X25519) with ChaCha20-Poly1305, 1,186 bytes per note |
| Trust | No setup ceremony, no proxy, no upgrade key over the verifier. Security rests on hash functions and the soundness of the STARK |
The full record of the launch, with every transaction and every figure recomputed from the chain: Private transfers on Ethereum L1.
- The system in one picture
- Privacy: what is hidden, from whom, and what is not
- Keys and notes
- Why Ethereum L1, and what that costs
- A settlement, step by step
- The verifier
- Deployed on Sepolia
- Limits
- Status and what comes next
- Repository, build and documentation
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','fontSize':'14px','lineColor':'#64748b','primaryColor':'#0f172a','primaryTextColor':'#0f172a','primaryBorderColor':'#334155','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1','edgeLabelBackground':'#ffffff','titleColor':'#0f172a'}}}%%
flowchart LR
classDef device fill:#0e7490,stroke:#22d3ee,color:#ecfeff,stroke-width:2px
classDef relay fill:#4c1d95,stroke:#a78bfa,color:#f5f3ff,stroke-width:2px
classDef chain fill:#1e3a8a,stroke:#60a5fa,color:#eff6ff,stroke-width:2px
classDef store fill:#0f172a,stroke:#64748b,color:#e2e8f0,stroke-width:1px
classDef recv fill:#065f46,stroke:#34d399,color:#ecfdf5,stroke-width:2px
subgraph S["Device of the sender"]
direction TB
W["Wallet<br/>holds sk and notes"]:::device
N["Notes for their owners<br/>sealed with X-Wing<br/>1,186 bytes each"]:::device
PR["Zero-knowledge STARK<br/>I own these notes,<br/>they are in the tree,<br/>value is conserved,<br/>the relay fee is this"]:::device
W --> N
W --> PR
end
subgraph R["Relayer, behind Tor"]
RL["checks the proof<br/>with a free call,<br/>then submits it"]:::relay
end
subgraph E["Ethereum L1, one transaction"]
direction TB
POOL["ShieldedPool"]:::chain
AD["ComposedStarkVerifier"]:::chain
VER["RealSplitVerifier<br/>+ LaunchEvaluator"]:::chain
TREE[("Note tree<br/>depth 32")]:::store
NUL[("Nullifier set")]:::store
LOG[("OutputNote log")]:::store
POOL -->|verifyBatch| AD --> VER
POOL -->|append outputs| TREE
POOL -->|mark spent| NUL
POOL -->|emit sealed notes| LOG
end
RC["Recipient<br/>scans, finds, opens"]:::recv
PR -->|proof +<br/>12 public words| RL
N -.->|sealed notes| RL
RL -->|settleBatch| POOL
LOG -->|view tag match,<br/>trial decrypt| RC
A note is a private claim on value. To pay someone, the wallet proves on the device that it owns notes already in the tree of the pool, and that the new notes it creates carry the same value less the relay fee. A relayer submits that proof in one Ethereum transaction. The pool verifies it, marks the nullifiers of the spent notes, appends the new notes to the tree, and emits them sealed to their owners, who find them by scanning the chain.
The relayer sees the proof and the sealed notes, never a key. The fee and its recipient are words of the proven statement, so a relayer can submit a transfer or drop it, and cannot change it.
Privacy: what is hidden, from whom, and what is not
Every note is a leaf in one tree. A leaf is a commitment, a Poseidon compression that hides
the value, asset and owner of the note behind a random blinding. With PoseidonGoldilocks.hash2), the pool computes (ShieldedPool._computeCommitmentWith)
where 0x4E4F5445
is the ASCII of NOTE. The pool never sees spend_pk or the blinding, only ownerCommit.
Spending a note publishes its nullifier, derived from the note, its position and a key only
its owner holds. The pool refuses any nullifier it has recorded (NullifierAlreadySpent) and two
equal nullifiers in one intent (DuplicateNullifier), so a note cannot be spent twice. The public
words and the events name no leaf. They show that some notes under a root were spent, and not which.
The proof is zero-knowledge. The prover appends a mask pair to the trace, columns 42 and 43, filled with fresh randomness, and a rank check on the masks runs before a proof leaves the device. Hiding is a property of the prover, outside this repository. The verifier here checks soundness only.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart LR
classDef priv fill:#0f172a,stroke:#22d3ee,color:#e2e8f0,stroke-width:2px
classDef proof fill:#4c1d95,stroke:#a78bfa,color:#f5f3ff,stroke-width:2px
classDef pub fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px
subgraph OWN["Known only to the owner"]
S1["value and asset"]:::priv
S2["random blinding"]:::priv
S3["spending key sk"]:::priv
S4["which leaves are theirs"]:::priv
S1 ~~~ S2
S3 ~~~ S4
end
subgraph ZK["What the proof establishes, and nothing more"]
P1["the input notes are leaves<br/>under a published root"]:::proof
P2["the prover knows<br/>their spending key"]:::proof
P3["each nullifier comes<br/>from its own note"]:::proof
P4["value in = value out<br/>+ public leg + fee"]:::proof
P1 ~~~ P2
P3 ~~~ P4
end
subgraph PUB["Visible to everyone on chain"]
C1["the root used"]:::pub
C2["two nullifiers"]:::pub
C3["two new commitments"]:::pub
C4["fee and fee recipient"]:::pub
C1 ~~~ C2
C3 ~~~ C4
end
OWN ==>|"witness,<br/>stays on the device"| ZK
ZK ==>|"12 public words"| PUB
The anonymity set of a spend is every note under the root it proves against: at most
GoldilocksIncrementalTree.MAX_LEAVES), and in practice every note inserted
by an independent depositor before that root. The Sepolia pool has few independent depositors,
and its anonymity set is small.
| Action | Public on chain | Hidden |
|---|---|---|
Deposit (absorb) |
depositor address, asset, amount, the new commitment | who will own the note |
| Private transfer | two nullifiers, two new commitments, the root, two sealed notes, the asset, the relay fee and its recipient | sender, recipient, amount, and which notes were spent |
| Private swap | the same, plus the clearing price | who traded, how much, which notes |
| Withdraw | recipient address, amount, fee, asset, two nullifiers, two new commitments | which notes paid for it, and who deposited them |
A private transfer carries no public amount and no recipient, and the pool refuses one that names a
recipient (NoPublicLegFieldsSet). Value enters the pool only through a deposit, and a settlement
whose public amount is negative reverts (ShieldInViaDepositOnly).
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','actorBkg':'#0f172a','actorTextColor':'#e2e8f0','actorBorder':'#334155','noteBkgColor':'#fef3c7','noteBorderColor':'#d97706','signalColor':'#475569','signalTextColor':'#0f172a','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
sequenceDiagram
autonumber
actor Sender
participant Relayer
participant Chain as Ethereum
actor Recipient
actor Observer as Anyone watching
Sender->>Sender: build notes, seal each opening,<br/>prove the spend on the device
Sender->>Relayer: over Tor: proof + 12 public words + sealed notes
Note over Relayer: sees the words, the proof and the<br/>ciphertext, never a key or an IP address
Relayer->>Chain: eth_call first, then settleBatch
Note over Chain,Observer: nullifiers, new commitments, root, fee,<br/>sealed notes, the masked proof
Chain-->>Observer: the same bytes everyone sees
Chain-->>Recipient: OutputNote(leafIndex, sealed note)
Recipient->>Recipient: view tag matches, trial-decrypt,<br/>recompute the commitment and check it
Note over Recipient: learns value, asset and blinding,<br/>and can spend the note later
A spend also names an association root, and the pool accepts it only if it is registered in
AssociationSetRegistry (UnknownAssociationRoot). Publishing is open to anyone and append-only.
The registry refuses a zero or non-canonical digest and records any other, and the proof
establishes that the input notes of the spend lie under the root it names. A root published over a
subset of the notes lets a user show that their funds come from a set they choose, without
revealing which notes in that set are theirs.
Each sealed note starts with a version byte and a one-byte view tag derived from the per-note
shared secret. A wallet fetches every OutputNote and trial-decrypts only those whose tag matches.
That is about one in 256, most of them the notes of other people.
The tag is one byte by design. A wider tag would let an indexer filter by recipient, and the filter would then be the recipient set. Wallets fetch everything, so every query is the same.
A proof verified today stays verified. A ciphertext published today stays on chain forever, and anyone can store it now and open it on the day a quantum computer exists. So the two halves are protected differently:
- Proofs rest on hash functions: Poseidon inside the circuit, Keccak on chain. There is no discrete-log or pairing assumption to break.
- Notes are sealed with X-Wing, a hybrid of ML-KEM-768 and X25519. A note stays secret as long as either primitive holds.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart TB
classDef secret fill:#0f172a,stroke:#f43f5e,color:#ffe4e6,stroke-width:2px
classDef derived fill:#0f172a,stroke:#22d3ee,color:#e2e8f0,stroke-width:2px
classDef shared fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px
SEED["64-byte seed<br/>from the recovery words"]:::secret
SK["spending key sk<br/>never leaves the wallet"]:::secret
XW["X-Wing keypair<br/>ML-KEM-768<br/>+ X25519"]:::secret
SPK["spend_pk = Poseidon(sk, SPEND)"]:::derived
NK["nk = Poseidon(sk, NULL)"]:::secret
ADDR["nox1 address<br/>version ‖ spend_pk ‖ X-Wing public key"]:::shared
OC["ownerCommit<br/>compress(spend_pk, blinding)"]:::derived
CM["commitment<br/>compress(pub, ownerCommit)"]:::shared
NF["nullifier<br/>Poseidon(Poseidon(nk, cm), position)"]:::shared
SEED -->|BLAKE3 spend key| SK
SEED -->|BLAKE3 receive key| XW
SK --> SPK
SK --> NK
SPK --> ADDR
XW --> ADDR
SPK --> OC --> CM
NK --> NF
CM --> NF
Poseidon is one-way, so spend_pk and nk do not give sk, and the circuit asks for sk to
spend. A holder of nk can compute the nullifiers of the notes of an account, and cannot spend them.
A note moves through the pool like this:
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','primaryColor':'#0f172a','primaryTextColor':'#e2e8f0','primaryBorderColor':'#22d3ee','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
stateDiagram-v2
direction LR
[*] --> Pending: absorb (deposit)<br/>or settleBatch (output)
Pending --> Provable: commitRoot publishes<br/>a root that contains it
Provable --> Provable: more roots published<br/>(the last 128 stay valid)
Provable --> Spent: settleBatch reveals<br/>its nullifier
Spent --> [*]
Pending: in the frontier of the tree,<br/>no published root yet
Provable: provable against any<br/>root in the window
Spent: nullifier recorded,<br/>can never spend again
Deposits and outputs update the frontier of the tree without computing the root, which takes 32
Poseidon compressions. Leaf commitRoot folds the frontier once and publishes a root for everything
inserted before it. Anyone may call it, and one call serves every deposit before it. The pool
keeps the last ROOT_WINDOW = 128 roots, so a proof built against a recent root still settles
after new deposits land.
On L1 the verifier contract is the only judge. There is no sequencer to trust, no bridge to secure, no data-availability committee, and no upgrade key: the pool holds its verifier in an immutable, and no Shield contract sits behind a proxy. A transfer is final when its block is. The price is that a whole proof must fit the limits of a single transaction:
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart LR
classDef limit fill:#7f1d1d,stroke:#f87171,color:#fef2f2,stroke-width:2px
classDef choice fill:#1e3a8a,stroke:#60a5fa,color:#eff6ff,stroke-width:2px
classDef result fill:#065f46,stroke:#34d399,color:#ecfdf5,stroke-width:2px
L1["131,072 bytes<br/>per transaction"]:::limit
L2["16,777,216 gas<br/>per transaction"]:::limit
L3["calldata priced per byte<br/>with a floor (EIP-7623)"]:::limit
L4["24,576-byte contracts"]:::limit
C0["the join-split proved<br/>directly: 44 columns, 2^13 rows"]:::choice
C1["19 queries at rate 1/64<br/>+ 4 rounds of 20-bit grinding<br/>+ 8 × 25-bit final grinding"]:::choice
C2["24-byte digests"]:::choice
C3["radix-4 FRI, early stop<br/>at 512 coefficients"]:::choice
C4["constraints compiled ahead of time,<br/>pinned by hash"]:::choice
R1["112,916-byte proof,<br/>116,708 bytes of calldata"]:::result
R2["7.07M to 7.88M gas,<br/>every constraint<br/>checked on chain"]:::result
L1 --> C0 & C1 & C2 & C3
L2 --> C0 & C4
L3 --> C2
L4 --> C4
C0 & C1 & C2 & C3 --> R1
C0 & C3 & C4 --> R2
| gas | at 0.077 gwei* | at 1 gwei* | |
|---|---|---|---|
a private transfer: settleBatch with verification, two nullifiers, two leaves, two sealed notes, 43 settlements |
7,066,977 to 7,882,382 | $1.50 to $1.68 | $19.52 to $21.77 |
of which the verifier, verifyBatch alone |
4,980,509 | $1.06 | $13.76 |
| deposit one note, 80 deposits | 229,490 to 958,623 | $0.05 to $0.20 | $0.63 to $2.65 |
publish a root, commitRoot
|
4,424,015 | $0.94 | $12.22 |
*ETH at $2,762.47 from the Chainlink ETH/USD feed and a base fee of 0.077 gwei, both read at
mainnet block 26,036,876. The gas is what each receipt says, and the dollars move with the market.
The verifier row is the execution gas of verifyBatch on the proof of settlement 0x1efa772d…8fa8:
eth_estimateGas of that call, less its base cost and its calldata.
Every transaction hash is in appendix B of the
launch record.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','pie1':'#4c1d95','pie2':'#0e7490','pie3':'#1e3a8a','pie4':'#94a3b8','pieStrokeColor':'#ffffff','pieOuterStrokeColor':'#cbd5e1','pieTitleTextSize':'16px','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
pie showData title One private transfer, settlement 0x1efa772d…8fa8, 7,337,580 gas
"verifier execution" : 4980509
"calldata, 116,708 bytes" : 1836152
"pool logic and events" : 499919
"transaction base" : 21000
Every intent is 12 public words, the whole interface between the proof and the pool
(ShieldedPool._decodeIntent):
| # | word | meaning | the pool checks |
|---|---|---|---|
| 0 | noteRoot |
the tree root the inputs are proven under | canonical, and one of the last 128 roots |
| 1 | assocRoot |
the association set the inputs belong to | canonical, and registered |
| 2, 3 |
nf0, nf1
|
the nullifiers of the two input notes | canonical, unseen, and different from each other |
| 4, 5 |
outCm0, outCm1
|
the two output commitments | canonical, then appended to the tree |
| 6 | publicAmount |
value leaving the pool, 0 for a private transfer or swap |
Goldilocks.MAX_VALUE) |
| 7 | fee |
the relay fee, or the protocol fee on a public leg | with no public leg, at most maxRelayFee of the asset. With one, at most 0.5% of the amount |
| 8 | assetId |
which token | registered |
| 9 | clearingPrice |
for swaps, scaled by |
below |
| 10 | recipient |
who receives the public leg | an address, set with a public leg and zero without |
| 11 | feeRecipient |
who receives the fee, the relayer | an address, and zero when the fee is zero |
The verifier sees these words as 36 Goldilocks limbs (PublicWords.publicsOf): words 0 to 5 are
four 64-bit limbs each, low limb first, words 6 to 9 one limb each, and the addresses in words 10
and 11 split into publicAmount and fee are also below
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','actorBkg':'#0f172a','actorTextColor':'#e2e8f0','actorBorder':'#334155','noteBkgColor':'#f1f5f9','noteBorderColor':'#94a3b8','signalColor':'#475569','signalTextColor':'#0f172a','activationBkgColor':'#e0f2fe','activationBorderColor':'#0284c7','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
sequenceDiagram
autonumber
participant R as Relayer
participant P as ShieldedPool
participant A as ComposedStarkVerifier
participant V as RealSplitVerifier
participant E as LaunchEvaluator
participant T as Token
R->>+P: settleBatch(proof, words, residual, attestation, clientData)
rect rgb(241, 245, 249)
Note over P: gate every intent
P->>P: fields canonical · root known · association set registered<br/>fee capped · nullifiers unseen, then marked spent
end
rect rgb(224, 242, 254)
Note over P,V: verify in one view call, no state written
P->>+A: verifyBatch(proof, words)
A->>+V: verifyWholeComposed(head, claims, queries, publics, evaluator)
V->>+E: evaluate(frame, claims, coefficients, publics, point)
E-->>-V: comp_z
V-->>-A: true
A-->>-P: true
end
rect rgb(236, 253, 245)
Note over P: effects before any transfer
P->>P: append both outputs to the frontier
P-->>R: OutputNote × 2 (sealed notes), BatchSettled
end
rect rgb(254, 243, 199)
Note over P,T: transfers last
P->>P: credit the relay fee to feeRecipient
P->>T: pay the public leg
end
deactivate P
Every nullifier is marked spent and every output inserted before the first token transfer, and
settleBatch is nonReentrant. A relay fee is credited to feeRecipient, which takes it with
claim. The launch pool has no settler configured, so anyone may settle, and a sender can submit
its own proof with no relayer at all.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart TB
classDef step fill:#1e3a8a,stroke:#60a5fa,color:#eff6ff,stroke-width:2px
classDef gate fill:#0f172a,stroke:#22d3ee,color:#e2e8f0,stroke-width:2px
classDef eval fill:#4c1d95,stroke:#a78bfa,color:#f5f3ff,stroke-width:2px
D["Decode at the deployed shape<br/>every field canonical, no trailing bytes"]:::step
TR["Main transcript<br/>36 publics → trace root → β, γ in F_p² → permutation root<br/>→ α → composition root → z<br/>→ frame and 93 periodic claims → δ → seed"]:::step
CZ["LaunchEvaluator<br/>38 transitions and 62 boundaries at z,<br/>weighted 1, α, α², …<br/>give comp_z"]:::eval
FT["FRI transcript from the seed<br/>4 roots, each with a 20-bit nonce, then its β<br/>512 final coefficients, 8 chained 25-bit nonces,<br/>19 positions"]:::step
FQ["FRI queries: layer-zero leaf opened,<br/>four radix-4 folds checked"]:::step
FIN["Final layer: 512 coefficients,<br/>evaluated at each final point"]:::gate
BQ["Base queries at the same positions:<br/>trace, permutation, composition, periodic row<br/>opened under their roots"]:::step
DEEP["DEEP identity with comp_z,<br/>against the value in the layer-zero leaf"]:::gate
OK(["accept"]):::gate
D --> TR --> CZ --> FT --> FQ --> FIN --> BQ --> DEEP --> OK
This is RealSplitVerifier.verifyWholeComposed on the launch stack. The verifier replays the
transcript once and hands the out-of-domain frame, the periodic claims, the composition
coefficients, LaunchEvaluator. The constraints
of the evaluator are compiled from the circuit ahead of time and pinned by hash
(LaunchImage.t.sol). No caller supplies comp_z.
Field. Goldilocks, RealQueryVerify.GEN), so it is a quadratic non-residue,
Every challenge, the out-of-domain point
Domains. For trace length
The trace generator is
Transcript. A Keccak-256 sponge: the state starts at RealQueryVerify.friChallenges).
Composition, as ProgramFormAir computes it. With
The statement of the spend enters as boundaries: pins that set a cell to a public limb ProgramFormEvaluatorBase). Every other
DEEP, at the query point RealQueryVerify.nDeepCoeffs) and
$$ \mathrm{DEEP}(x) = \sum_{r=0}^{1}\sum_{c=0}^{43} k_{44r+c},\frac{T_c(x) - T_c(g^{r}z)}{x - g^{r}z}
- k_{88},\frac{C(x) - \mathrm{comp}_z}{x - z}
- \sum_{m=0}^{92} k_{89+m},\frac{P_m(x) - P_m(z)}{x - z}. $$
The mask pair is opened as one $\mathbb{F}{p^2}$ value, $M{42} + X,M_{43}$ in slot 42, and slot
43 must be zero (MaskSlotNotZero). The coefficient of column 43 is
FRI at radix 4. Write
applied to
since
Soundness. The verifier computes both figures on chain from its own parameters
(StagedStarkVerifier). With
The query term credits each query with half the rate bits less the Johnson-bound loss
soundnessTermsForSize(1) returns
(80774533, 81933909) in millionths of a bit, and soundnessBits() returns
| Contract | Address |
|---|---|
ShieldedPool |
0x8e377752C8890E23A1E9F40eBbD41183Fc6949e2 |
ComposedStarkVerifier |
0xf64c399696E10C84C73B66350b45bA0fCD860927 |
RealSplitVerifier |
0x59AA962433060D0206C3595afEb1793c621747eA |
LaunchEvaluator |
0x619A5ecdEe779Ec4455bbFa2eC3a5f4f9DEE3FF6 |
PoseidonGoldilocks, the tree hasher |
0x0096416e4385BBd459141140A542f30E05b1A4d7 |
AssociationSetRegistry |
0x4375eE7D015aC8E404A03deb577E90b08de32Df3 |
The pool is deployed at block 11,772,152. Beta mode is off, so deposits are open to anyone.
Every claim above can be read from a public Sepolia RPC with cast from Foundry, with no key and
no transaction:
export RPC=https://ethereum-sepolia-rpc.publicnode.com
export POOL=0x8e377752C8890E23A1E9F40eBbD41183Fc6949e2
export ADAPTER=0xf64c399696E10C84C73B66350b45bA0fCD860927
# soundness the verifier computes: conjectured, provable
cast call $ADAPTER "soundnessBits()(uint256,uint256)" --rpc-url $RPC
# the query and commit terms, in millionths of a bit
cast call $ADAPTER "soundnessTermsForSize(uint256)(uint256,uint256)" 1 --rpc-url $RPC
# the verifier the pool is bound to, the words per intent, and beta mode
cast call $POOL "verifier()(address)" --rpc-url $RPC
cast call $POOL "wordsPerIntent()(uint256)" --rpc-url $RPC
cast call $POOL "betaMode()(bool)" --rpc-url $RPC
# no proxy: the EIP-1967 implementation slot of the pool is empty
cast storage $POOL 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url $RPCAny settlement can be checked again against the live verifier with a free eth_call of
verifyBatch on its proof and words.
| Limit | Why |
|---|---|
| No external audit | the verifier, the evaluator and the pool are tested and partly proved in Lean, and no outside party has reviewed them |
| Lean coverage | the Lean FRI bound is evaluated at other parameters, and the pool model has no relay fee. formal/lean/README.md lists every gap |
| 24-byte digests | collision resistance |
| Deposits and withdrawals are public | an address, an amount and a time enter or leave the pool in the clear |
| Amount and timing correlation | depositing 3.14 and withdrawing 3.14 an hour later links them, whatever the proof hides |
| Anonymity set size | privacy grows with the number of notes under a root, and the Sepolia pool has few |
| The asset is public | an ETH transfer can only come from an ETH note |
| Computational zero knowledge | the masks come from a keyed hash of device randomness, so hiding rests on that hash and on the randomness of the device |
| Network metadata | an RPC provider sees the IP address of a wallet that does not route through Tor |
| One intent per batch | every settlement pays for a whole verification |
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart LR
classDef done fill:#065f46,stroke:#34d399,color:#ecfdf5,stroke-width:2px
classDef next fill:#f8fafc,stroke:#94a3b8,color:#334155,stroke-width:2px,stroke-dasharray:5 4
A["one-transaction<br/>verification"]:::done
E["every constraint<br/>evaluated on chain"]:::done
Z["zero-knowledge<br/>proofs"]:::done
I["43 private transfers<br/>on Sepolia"]:::done
P["a transfer received<br/>and opened on a phone"]:::done
R["automatic relayer<br/>behind Tor"]:::done
O["beta ended,<br/>deposits open"]:::done
H["32-byte digests"]:::next
M["batches of more<br/>than one intent"]:::next
J["external audit"]:::next
K["mainnet"]:::next
A --> E --> Z --> I
I --> P
I --> R
I --> O
O --> H
O --> M
H --> J
M --> J
J --> K
contracts/shield/ pool, note tree, Poseidon hasher, fees, staking, association sets
contracts/shield/verifier/ verifier, adapter, constraint evaluator, transcript, Merkle, FRI, field arithmetic
contracts/faucet/ testnet faucet
test/shield/ unit, property, invariant, symbolic and real-proof tests
spec/ the launch program, real proofs, refused proofs and reference vectors
script/shield/ deploy, settle and verify, from Solidity
script/tools/ the program generators CI holds to the launch program
formal/lean/ Lean proofs, and where they stop short of the launch stack
docs/ the design, one topic per file
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, -apple-system, Segoe UI, Helvetica, Arial, sans-serif','lineColor':'#64748b','edgeLabelBackground':'#ffffff','titleColor':'#0f172a','clusterBkg':'#f8fafc','clusterBorder':'#cbd5e1'}}}%%
flowchart TB
classDef pool fill:#1e3a8a,stroke:#60a5fa,color:#eff6ff,stroke-width:2px
classDef ver fill:#4c1d95,stroke:#a78bfa,color:#f5f3ff,stroke-width:2px
classDef lib fill:#0f172a,stroke:#64748b,color:#e2e8f0,stroke-width:1px
Pool["ShieldedPool"]:::pool
Tree["GoldilocksIncrementalTree"]:::lib
Hasher["PoseidonGoldilocks"]:::lib
Reg["AssociationSetRegistry"]:::pool
Router["ShieldFeeRouter"]:::pool
Stake["NoxShieldStaking"]:::pool
Adapter["ComposedStarkVerifier<br/>extends StagedStarkVerifier"]:::ver
Verifier["RealSplitVerifier"]:::ver
Evaluator["LaunchEvaluator<br/>image pinned by hash"]:::ver
Query["RealQueryVerify<br/>RealQueryWalk"]:::lib
Tx["StarkTranscript"]:::lib
Mk["StarkMerkle"]:::lib
Air["ProgramFormAir"]:::lib
Pool --> Adapter --> Verifier --> Query
Verifier --> Evaluator --> Air
Query --> Tx & Mk
Pool --> Tree --> Hasher
Pool --> Reg
Pool --> Router --> Stake
git clone --recurse-submodules https://github.com/NON-OS/l1-shield
cd l1-shield
forge build
forge test # 617 tests, real launch proofs among them
cd formal/lean && lake exe cache get && lake buildOne test forks mainnet and returns early when MAINNET_RPC_URL is unset. DeployedSurface.t.sol
uses ffi to run test/tools/import_closure.py, and fails if a contract outside the declared list
reaches the deployed path. The symbolic checks run under Halmos, as docs/14
shows.
| 01 Architecture | the contracts and how they fit |
| 02 Threat model | who is trusted with what |
| 03 Verifier · 04 Codec · 05 Transcript · 06 Merkle and FRI · 07 Constraints | the verifier, byte by byte |
| 08 Pool · 09 Tree · 10 Fees and governance · 11 Faucet | the pool and its economics |
| 12 Gas · 13 Deployment | what each operation costs, and how a stack goes on chain |
| 14 Testing · 15 Glossary | how it is tested, and the vocabulary |
| 16 Wallet integration · 17 Client data | building a wallet |
| 18 Gas research · 19 Deployments and receipts | every number, with its receipt |
| 20 Security status | what is checked today, and what is not |
Report a vulnerability privately to team@nonos.systems, as SECURITY.md describes, and never in a public issue.
MIT, see LICENSE. Files whose SPDX header reads AGPL-3.0-or-later are under that
license instead.
The banner uses "Etna Volcano Paroxysmal Eruption July 30 2011" by gnuckx, licensed under CC BY 2.0, recoloured by NØNOS.
