Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .agents/skills/redacted-circuit-maintenance/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,15 @@ Each section below is a rule plus the incident that produced it (2026-09-27, PR
- After merging, confirm the deploy: the Render service is `srv-d4tfnou3jp1c73eegmeg` (ZK Email
workspace key `render-api-key-zkemail-team`, `GET /v1/services/<id>/deploys`). Or check that a string
from the change appears in the live `/assets/*.js` bundle.

## XXL tier (2^24) build and measurement
- XXL = 4,096-byte header / 200,704-byte body (2× XL), 15.70M gates (1024-bit) and 15.71M (2048-bit),
93.6% of 2^24. About 76 gates per body byte above XL, so a bigger body needs a 2^25 tier.
- Compile on a Linux box with at least 32 GB (cortex): `NARGO_BIN=… BB_BIN=node_modules/@aztec/bb.js/build/amd64-linux/bb
npx tsx scripts/compile-circuits.ts --only=email_mask_1024_xxl` takes 13–16 min and peaks at ~20 GB
(proving-key computation inside write_vk). Don't build it on the Mac during the day.
- Proving takes 124–132 s on 12 cores and peaks at 22.4–22.5 GB RSS of `bb` on Linux (six real emails,
2026-10-01). Linux RSS is reliable when swap doesn't grow; sample `/proc/<bb pid>/status` VmRSS, not
`/usr/bin/time` on node (bb.js's native bb isn't a reaped child, so time reports ~2 GB).
- The artifacts are 37 MB each. The pin script regenerates the CLI's whole circuit table from
`src/circuit-configs.json`, so a new tier only needs configs, artifacts, a pin run and docs.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ $R verify "https://redacted.zk.email/verify?id=…" # proof check + DKIM k
```

- It uses the same circuits as the site: pinned by commit, and each artifact and verification key is checked against its sha256 before use.
- It proves emails with bodies up to **100,352 bytes**. The "large" and "xl" tiers are CLI-only. Bodies over 8,448 bytes need about 7 GB of RAM and about a minute on a recent laptop, and bodies over 48,000 bytes need about 14 GB (a 16 GB+ machine) and a couple of minutes.
- It proves emails with bodies up to **200,704 bytes**. The "large", "xl" and "xxl" tiers are CLI-only. Bodies over 8,448 bytes need about 7 GB of RAM and about a minute on a recent laptop, bodies over 48,000 bytes need about 14 GB (a 16 GB+ machine) and a couple of minutes, and bodies over 100,352 bytes need about 23 GB (a 32 GB+ machine) and 2–3 minutes on 12+ cores.
- `publish` sends only `{publicInputs, proof}`. The raw email and the mask arrays are never sent.
- `verify` also checks that the proof's DKIM public-key hash matches the key published in DNS (or in [archive.zk.email](https://archive.zk.email)) for the revealed `d=`/`s=`. The web verify page doesn't do that check yet.
- `verify` also checks that the proof's DKIM public-key hash matches the key published in DNS (or in [archive.zk.email](https://archive.zk.email)) for the revealed `d=`/`s=`. The web verify page does the same check (#26).
- Agents that fetch the site find the skill through `/llms.txt`, a `<link rel="alternate">` to `SKILL.md`, and a static block in `index.html`. Humans see it in the "For AI agents" section on the home page.
- Install as a Claude Code / agent skill: `npx skills add zkemail/Redacted`, or copy `skills/redacted-email-proof` into your agent's skills directory.

Expand Down Expand Up @@ -95,6 +95,7 @@ selects the smallest circuit that fits your email:
| `email_mask_{1024,2048}_mid` | 1024 / 2048-bit | 2 KB | 8.4 KB | browser or CLI |
| `email_mask_{1024,2048}_large` | 1024 / 2048-bit | 4 KB | 48,000 B | CLI only (~7 GB RAM, ~1 min) |
| `email_mask_{1024,2048}_xl` | 1024 / 2048-bit | 4 KB | 100,352 B | CLI only (~14 GB RAM (16 GB+ machine), ~2–4 min) |
| `email_mask_{1024,2048}_xxl` | 1024 / 2048-bit | 4 KB | 200,704 B | CLI only (~23 GB RAM (32 GB+ machine), ~2–3 min on 12+ cores) |

Verification uses precomputed verification keys (`src/circuit/target/vk/`), so it takes seconds in
the browser for every tier.
Expand Down
2 changes: 1 addition & 1 deletion public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Flow:
1. Get the raw email. Gmail MCP: call `get_message` with `messageFormat: "RAW"`. Gmail API: `format=raw`. Browser: Gmail "Show original" → "Download original".
2. `node ~/.redacted-prover/redacted.mjs inspect email.eml` checks DKIM and shows the signed, canonical content.
3. `... prove email.eml --dry-run [--only-headers from,subject,date] [--hide-body --reveal "text"] [--hide "secret"]` previews exactly what becomes public. Confirm it with the user.
4. `... prove email.eml <same flags> --publish` proves locally and uploads only the proof and its public outputs. It prints `https://redacted.zk.email/verify?id=<uuid>`. Size limits: canonical body up to 100,352 bytes and signed header up to 4,096 bytes. Bodies over 8,448 bytes use the CLI-only "large" tier (~1 min, ~7 GB RAM), and bodies over 48,000 bytes the CLI-only "xl" tier (~2–4 min, ~14 GB RAM, needs a 16 GB+ machine).
4. `... prove email.eml <same flags> --publish` proves locally and uploads only the proof and its public outputs. It prints `https://redacted.zk.email/verify?id=<uuid>`. Size limits: canonical body up to 200,704 bytes and signed header up to 4,096 bytes. Bodies over 8,448 bytes use the CLI-only "large" tier (~1 min, ~7 GB RAM), and bodies over 48,000 bytes the CLI-only "xl" tier (~2–4 min, ~14 GB RAM, needs a 16 GB+ machine), and bodies over 100,352 bytes the CLI-only "xxl" tier (~2–3 min, ~23 GB RAM, needs a 32 GB+ machine).
5. `... verify <verify-url>` checks a proof locally and checks that its DKIM key hashes (modulus and redc) match the sender's DNS key (or archive.zk.email). Exit code 4 flags a legacy v1 proof, made before the 2026 soundness fixes. Don't rely on it.

Privacy: the .eml is never sent anywhere. The published proof contains only the bytes you left unmasked. By default the values of to/cc/bcc/reply-to/list-post/list-unsubscribe are hidden, along with DKIM bh=.
Expand Down
12 changes: 7 additions & 5 deletions skills/redacted-email-proof/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ the smallest tier that fits (RSA 1024- or 2048-bit keys):
| mid | 2,048 B | 8,448 B | seconds, ~3 GB RAM |
| large | 4,096 B | 48,000 B | ~1 min on 18 cores (longer on fewer), **~7 GB RAM** |
| xl | 4,096 B | 100,352 B | ~2–4 min on 18 cores (longer on fewer), **~14 GB RAM** (16 GB+ machine) |
| xxl | 4,096 B | 200,704 B | ~2–2.5 min on 12 cores (longer on fewer), **~23 GB RAM** (32 GB+ machine) |

The large-tier figures were measured on a 34 KB PayPal receipt: 58 s, 6.6 GB peak for the native
`bb`. XL figures were measured on 87.6 KB and 94.9 KB receipts (loaded machine; faster idle). Peak
memory is the kernel's lifetime physical footprint of the `bb` process (`/usr/bin/time -l`): large
7.4 GB, XL 13.7 GB (18-core M-series Mac, 2026-09-29).
7.4 GB, XL 13.7 GB (18-core M-series Mac, 2026-09-29). XXL was measured on six real 102–165 KB
emails on a 12-core, 31 GB Linux box: 124–132 s each, 22.4–22.5 GB peak RSS of `bb` (2026-10-01).

**Quoted-printable emails** (most HTML email: `=3D`, `=E2=82=AC`, soft line breaks) are shown
**decoded** by `inspect`, the preview, `verify` and the website. `--hide` and `--reveal` match
Expand All @@ -72,8 +74,8 @@ DKIM signatures from a domain other than `From:` (ESPs like ccsend.com, subdomai
email.airbnb.com) and `c=simple` header canonicalization are both supported. `verify` prints the
signing `d=` and warns when it isn't the `From:` domain.

The website can prove small and mid in a browser. **Large and XL are CLI-only**, but their proofs
verify on the website like any other (XL in about 7 s).
The website can prove small and mid in a browser. **Large, XL and XXL are CLI-only**, but their
proofs verify on the website like any other (XL in about 7 s, XXL in about 6 s).

## 3. Decide with the user what to reveal, then preview it

Expand Down Expand Up @@ -178,9 +180,9 @@ it to the sender's domain.

- `DKIM verification failed`: the file isn't the original raw message (see step 1), or the key was
rotated and isn't in the archive. Try a newer email.
- `canonical body is N bytes`: the email is longer than the XL tier (100,352-byte body,
- `canonical body is N bytes`: the email is longer than the XXL tier (200,704-byte body,
4,096-byte header). Pick a shorter or plain-text email.
- Out of memory: the large tier needs about 7 GB free and XL about 14 GB (in practice a 16 GB+ machine). Close other programs or
- Out of memory: the large tier needs about 7 GB free and XL about 14 GB (in practice a 16 GB+ machine), and XXL about 23 GB (a 32 GB+ machine). Close other programs or
set `REDACTED_THREADS=4`.
- `bb did not start in time`: bb.js waits only 5 s for the native prover to start. The CLI retries
automatically; if it keeps failing, the machine is overloaded, so try again when it's idle.
Expand Down
13 changes: 11 additions & 2 deletions skills/redacted-email-proof/scripts/redacted.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const API = (process.env.REDACTED_API || `${SITE}/api`).replace(/\/$/, "");
// v1 = the original beta.5 / bb 0.84 circuits. Kept ONLY to verify links created before the upgrade,
// and every v1 proof is reported as legacy: v1 published bytes past the signed length (a prover
// could append unsigned text), and its 2048-bit key hash doesn't bind redc (zkemail.nr PR #62).
const CIRCUIT_REF = "530980ca2869121605bffb13d0d3cf381dadd9b9";
const CIRCUIT_REF = "c0c2700217ab4c7fca912f5ebe1fc910ea2e988d";
const GH_RAW = (ref) => `https://raw.githubusercontent.com/zkemail/Redacted/${ref}/src/circuit`;
const V2 = {
version: 2,
Expand All @@ -53,6 +53,9 @@ const V2 = {
{ name: "email_mask_1024_xl", keyBits: 1024, maxHeaderLength: 4096, maxBodyLength: 100352, file: "email_mask_1024_xl.json",
sha256: "4cf398ca2b9b3fc01c17738a1450dc8246419d57e7680c2670823a175b96ca4a",
vkSha256: "e43d369fd72f7b2d651ed1f8e6c87103a8547de2c3ab20d7e599ec91f0c2d4a8" },
{ name: "email_mask_1024_xxl", keyBits: 1024, maxHeaderLength: 4096, maxBodyLength: 200704, file: "email_mask_1024_xxl.json",
sha256: "44f2aa0dc353659cf296923f328704207ebf36fef0b9f215e24f228e16be00fd",
vkSha256: "feb725929559e1007e7683d1e3f3cdc1837c250f4496e9abacbce5b2ce0a905e" },
{ name: "email_mask_2048_small", keyBits: 2048, maxHeaderLength: 2048, maxBodyLength: 4096, file: "email_mask_2048_small.json",
sha256: "cd6d5ad5131ba0139ae7ffabf394c369c03e2f64ef621206453b8dd4eda60694",
vkSha256: "088a51ad9d8560fe0aa6e42f68ec4c16a70d83c4165806b87e5816784658b649" },
Expand All @@ -65,6 +68,9 @@ const V2 = {
{ name: "email_mask_2048_xl", keyBits: 2048, maxHeaderLength: 4096, maxBodyLength: 100352, file: "email_mask_2048_xl.json",
sha256: "31b7e7db5008745f3c4eea4abe1b4724319079cf4d69ce2b5cd4d5f64919ff90",
vkSha256: "d4f20b3ec18cc317e9ff8e2fad5c1984e49ef92431f39428c4f11c9dc8ce746b" },
{ name: "email_mask_2048_xxl", keyBits: 2048, maxHeaderLength: 4096, maxBodyLength: 200704, file: "email_mask_2048_xxl.json",
sha256: "a04866e3a6b5e04c4bacc6cdf38deefb353d14fa0c16b908d68939a765390726",
vkSha256: "a9f0c0aa1c2b619b8514816b7241f18fc890801a7a48446ad6b347502b4db5b3" },
],
};
const V1 = {
Expand Down Expand Up @@ -1108,12 +1114,15 @@ async function cmdProve(file, opts) {
const threads = Number(process.env.REDACTED_THREADS) || Math.max(1, os.cpus().length);
const large = c.maxBodyLength > 8448;
const xl = c.maxBodyLength > 48000;
const xxl = c.maxBodyLength > 100352;
log(`Generating witness…`);
const t0 = Date.now();
const { witness } = await new noir.Noir(circuit).execute(inputs);
log(
`Proving with UltraHonk (${threads} threads; ` +
(xl
(xxl
? `XXL tier: ~2-3 min on 12+ cores, longer on fewer, ~23 GB RAM, needs a 32 GB+ machine)…`
: xl
? `XL tier: ~2-4 min on 16+ cores, longer on fewer, ~14 GB RAM, needs a 16 GB+ machine)…`
: large
? `large tier: ~1 min on 16+ cores, longer on fewer, ~7 GB RAM)…`
Expand Down
16 changes: 16 additions & 0 deletions src/circuit-configs.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
"keyBits": 1024,
"browser": false
},
{
"name": "email_mask_1024_xxl",
"outputFile": "email_mask_1024_xxl.json",
"maxHeaderLength": 4096,
"maxBodyLength": 200704,
"keyBits": 1024,
"browser": false
},
{
"name": "email_mask_2048_small",
"outputFile": "email_mask_2048_small.json",
Expand Down Expand Up @@ -63,6 +71,14 @@
"maxBodyLength": 100352,
"keyBits": 2048,
"browser": false
},
{
"name": "email_mask_2048_xxl",
"outputFile": "email_mask_2048_xxl.json",
"maxHeaderLength": 4096,
"maxBodyLength": 200704,
"keyBits": 2048,
"browser": false
}
],
"legacy": [
Expand Down
1 change: 1 addition & 0 deletions src/circuit/target/email_mask_1024_xxl.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/circuit/target/email_mask_2048_xxl.json

Large diffs are not rendered by default.

Binary file added src/circuit/target/vk/email_mask_1024_xxl.vk
Binary file not shown.
Binary file added src/circuit/target/vk/email_mask_2048_xxl.vk
Binary file not shown.
3 changes: 2 additions & 1 deletion src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const publicInputCount = (c: CircuitConfig) =>
prefixLength(c.version) + c.maxHeaderLength + c.maxBodyLength;

// Browser-provable v2 circuits. Vite code-splits each JSON, so only the selected one downloads.
// NOTE: the large (2^22) and xl (2^23) tiers are intentionally absent. Proving them needs ~7.4 GB (large) and ~13.7 GB (xl),
// NOTE: the large (2^22), xl (2^23) and xxl (2^24) tiers are intentionally absent. Proving them needs ~7.4 GB (large),
// ~13.7 GB (xl) and ~22.5 GB (xxl),
// beyond a browser's 4 GB WebAssembly memory. Those emails are proved with the CLI
// (skills/redacted-email-proof) and verified here from the VK alone.
const V2_LOADERS: Record<string, () => Promise<unknown>> = {
Expand Down
Loading