Escape-hatch circuit
The proof that lets one note leave the pool without any attester, the eleven note values it discloses, what it proves and does not, and what the escape_hatch instruction computes itself.
The escape-hatch proof takes one note out of the pool to a public destination with no attester involved: no guardian node, screener, fee enclave, data endorser, zone authority or relayer. The price is that the note's contents become public.
The sequence below shows the whole operation. Only your device and the shielded pool program take part.
sequenceDiagram participant U as Your device participant P as Shielded pool participant D as Destination U->>U: prove the note and disclose it U->>P: escape_hatch with the proof P->>P: check roots, slot, proof P->>P: stage the nullifier P->>P: hold amount less the fee in escrow Note over P,D: 48 hours later, anyone releases it P->>D: amount less the fee
What it proves
The circuit is EscapeHatch(10, 8, 1, 4, 11, 8, 1, 8), with a note tree of 10 levels of 8 children and one level of 4 (2³² leaves), and a nullifier tree of 11 levels of 8 children and one level of 8 (2³⁶ leaves). It has 17,337 non-linear constraints. For the one note being spent it proves the following.
The note is real. Its kind is identity-owned or address-owned, and its amount is not zero. A dummy note cannot be escaped.
The prover owns it. For an identity-owned note, the owner is the identity commitment (sometimes called the user commitment) rebuilt from the prover's spending key, nullifier key and outgoing viewing key with their blinders and the other five slot leaves. For an address-owned note, the owner is the program owner commitment of the spender authority address in the public values, which the handler takes from an account that must sign. That address must be zero for an identity-owned note.
The token matches. The token address's two halves hash to the note's token identifier, so the program pays out the token the note actually holds.
The note exists. Its leaf, the note commitment wrapped with its insertion slot and position, is in the note tree at the given root.
The note is within its validity window at the execution slot, exactly as in a value transaction.
The nullifier is correct for the note, its owner's nullifier key and the domain of its owner kind, chosen exactly as a value transaction chooses it. A note therefore has the same nullifier whether it is spent in a value transaction or escaped, so it cannot be spent once each way.
The nullifier is unspent. It falls strictly between a leaf of the nullifier tree and its successor at the given nullifier root. This check is inside the escape-hatch proof; there is no separate freshness proof, because there is no attester signature for one to sit outside of.
The note's blinding factors are non-zero.
Values are in range. The slot is 48-bit, and every address half is 128-bit, including the token's.
For an identity-owned note the circuit also opens the platform wallet slot of the owner's identity and publishes the platform wallet address.
The 24 public values
The handler rebuilds the fold of 24 values from its own accounts and the payload. They fall into these groups.
Roots and the clock (slots 0 to 2). The note-tree root comes from the spend tree's root history, by sequence number. The nullifier-tree root comes from the nullifier staging buffer's active roots, also by sequence number. The execution slot comes from the payload and is checked against the clock and the process policy's proof-age window.
Context digest (slots 3 and 4). Keccak-256 of the payload before the proof and of every account address the instruction names, the escaper's first, in two halves.
Nullifier (slot 5). From the payload.
Token address (slots 6 and 7). From the mint account. The handler refuses a mint whose token program is not supported, and Token-2022's native mint.
Destination (slots 8 and 9). The address of the escape's one destination account, the account the release pays: a wallet for SOL, and for any other token a token account, which the handler first checks is an initialized token account of the note's mint. Because the account's own address is folded, a proof made for one account fails for any other, even another token account with the same owner.
Withdrawal rate (slot 10). The process policy's default withdraw rate. The circuit does not use it; it is folded so that the proof commits to the rate the handler will charge.
The disclosed note (slots 11 to 21). Eleven values from the payload: the note's kind, zone, amount, valid-after slot, valid-before slot, owner commitment, memo hash, insertion slot, insertion position, and the platform wallet address in two halves (zero for an address-owned note). The note's version is not among them. It stays a private input to the note commitment, so a wrong version still fails the membership check, but it is not published.
Spender authority (slots 22 and 23). The address of a signing account for an address-owned note, or zero.
What an escape reveals
Every one of those values is public. Together with the token and the destination, anyone watching the chain learns which note left, how much it held, which identity or address owned it, and which platform wallet that identity registered. The view below shows what a chain observer can read.
- Program
- Shielded pool, escape_hatch
- Escaper
- The signing account that pays
- Token
- The mint
- Destination
- The receiving address
- Amount
- The whole note
- Owner
- Identity commitment or address commitment
- Platform wallet
- For an identity-owned note
- Note position
- Insertion slot and position
- Zone, memo hash, validity window
- As stored in the note
- Keys
- PrivateNever leave your device
What it does not prove
The escape hatch skips everything that depends on someone other than the note's owner.
The owner is in the identity tree. Nothing checks it. Ownership rests on the note instead: the rebuilt identity commitment must be the owner of a note in the note tree, and only a registered identity could have received one.
The destination is acceptable. The circuit does not check it. The destination is any address the prover chooses, bound into the fold so no one else can change it. The handler checks only that it is writable and, for a token, that it is a token account of the note's mint.
The fee. The circuit does not compute it. The handler computes it from the disclosed amount and the withdraw rate.
Screening, zone approval and the token allowlist. None of them apply to an escape. A note in a zone can be escaped without its zone authority's signature.
What the handler does
escape_hatch checks and settles the escape in this order.
Check the token and the pool token account
The token must use a supported token program and must not be the refused native mint. The pool token account must be the canonical one for that token, and for SOL it must be the pool's lamport account.
Resolve the roots and the slot
The note-tree root and the nullifier root must both still be active, and the execution slot must be within the proof-age window.
Compute the fee and the payout
The fee is the disclosed amount times the withdraw rate over 10,000, rounded up, and the payout is the amount less the fee. The pool token account must hold at least the payout, and for SOL the pool's lamport account must hold at least the payout above its reserve.
Check the destination
The destination must be writable in the escape's transaction, which refuses the System Program, the sysvars and the other built-in accounts the runtime keeps read-only, since no release could pay one. For a token other than SOL, it must also be an initialized token account of the note's mint under its token program. For SOL it is any other address.
Verify the proof
The handler folds the 24 values and verifies the proof under the escape-hatch key compiled into the program.
Stage the nullifier
The nullifier is staged in the nullifier staging buffer (the code calls it the turnstile), which refuses one it already holds.
Hold the payout and credit the fee
The payout moves from the pool token account into the escape's escrow, which records the destination and pays it there 864,000 slots later, and the fee is credited to the token's escape-hatch fee accrual. SOL is held in lamports, moved from the pool's lamport account.
The escaper signs and pays the transaction fee. The context digest covers every account address, the escaper's included, so only the escaper the proof was made for can submit it, and no account can be swapped after proving.
Reference
| Entry | circuits/zk/escape_hatch.circom |
|---|---|
| Instantiation | EscapeHatch(10, 8, 1, 4, 11, 8, 1, 8) |
| AGGREGATE_SLOT_COUNT | 24 |
| AMOUNT_BITS | 64 |
| BPS_BITS | 14 |
| BPS_SCALE | 10000 |
| GROUP_CONTEXT | 3 |
| GROUP_DISCLOSURE | 11 |
| GROUP_EXIT_LANE | 6 |
| GROUP_FEE | 10 |
| GROUP_NOTE_EFFECT | 5 |
| GROUP_ROOTS_AND_CLOCK | 0 |
| GROUP_SPENDER_OWNER | 22 |
| LIMB_BITS | 128 |
| MAXIMUM_BPS | 10000 |
| SLOT_BITS | 48 |
circuits/zk/circom-manifest.json.| Tag | Instruction | Version | Payload bytes | Accounts |
|---|---|---|---|---|
| 25 | escape_hatch | v0 | 329 | 15 |
| 27 | claim_escape_hatch_fee | v0 | 3 | 8 |
| 36 | release_escape_escrow | v0 | 0 | 6 |
contracts/solana/tools/umbra-manifest/manifest/shielded-pool.json.Related
Last updated
Batch append circuit
The forester's proof that twelve staged values were inserted into an indexed tree correctly, what the program supplies to it, and what happens to the pool while no forester runs.
Fee collection circuit
The circuit that lets the treasury identity collect up to twenty protocol fee notes at a time, what its proof shows, and what the claim_fee_20 handler checks and pays.