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
66 changes: 56 additions & 10 deletions packages/circuits/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Circuit Architecture

## Circom Circuits

### Main Circuits

We provide one main circuit as follows.

#### `email_auth.circom`

A circuit to verify that a message in the subject is authorized by a user of an account salt, derived from an email address in the From field and a random field value called account code.

The circuit has the following blueprint ID and download URLs for the proving key and witness generator:

```json
{
"blueprintId": "7f3c3bc2-7c5d-4682-8d7f-f3d2f9046722",
Expand All @@ -17,8 +21,7 @@ The circuit has the following blueprint ID and download URLs for the proving key
```

It takes as input the following data:
1. a padded email header `padded_header`.
It takes as input the following data:

1. a padded email header `padded_header`.
2. the bytes of the padded email header `padded_header_len`.
3. an RSA public key `public_key`.
Expand All @@ -27,36 +30,78 @@ It takes as input the following data:
6. a starting position of the From field in the email header `from_addr_idx`.
7. a starting position of the Subject field in the email header `subject_idx`.
8. a starting position of the email domain in the email address of the From field `domain_idx`.
10. a starting position of the timestamp in the email header `timestamp_idx`.
11. a starting position of the invitation code in the email header `code_idx`.
9. a starting position of the timestamp in the email header `timestamp_idx`.
10. a starting position of the invitation code in the email header `code_idx`.

Its instances are as follows:

1. an email domain `domain_name`.
2. a Poseidon hash of the RSA public key `public_key_hash`.
3. a nullifier of the email `email_nullifier`.
4. a timestamp in the email header `timestamp`.
5. a masked subject where characters either in the email address or in the invitation code are replaced with zero `masked_subject_str`.
5. a masked subject where characters either in the email address or in the invitation code are replaced with zero `masked_subject_str`.
6. an account salt `account_salt`.
7. a flag whether the email header contains the invitation code `is_code_exist`.

## How to Use

### Build circuits
`yarn && yarn build`

Run any of the following from `packages/circuits`:

````bash
# Build all top-level circuits in src/*.circom (excluding *\_template.circom)
yarn build:all

# Build specific circuit(s) by base name (without .circom)
yarn build --circuit email_auth_with_sender
yarn build --circuit email_auth,email_auth_with_recipient

# Or build specific circuits ad hoc
NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/build-all.ts --circuit email_auth,email_auth_with_recipient

# Use --sequential to reduce peak memory usage
NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/build-all.ts --sequential

# List available circuits (base names)
yarn build:list

### Generate proving keys and verifiers for selected circuits

```bash
# Generate for specific circuit(s)
NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts --output ./build --circuit email_auth_with_sender

# Optional: provide your own randomness and contributor name
NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts \
--output ./build \
--circuit email_auth_with_sender \
--name "Alice" \
--entropy deadbeefcafebabe \
--beacon 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

# Legacy mode still supported via --legacy (applies legacy ptau)
NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts --output ./build --legacy --circuit email_auth_legacy
````

### Run tests

At `packages/circuits`, make a `build` directory, download the zip file from the following link, and place its unzipped directory under `build`.
https://drive.google.com/file/d/1TChinAnHr9eV8H_OV9SVReF8Rvu6h1XH/view?usp=sharing
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix bare URL in documentation.

Line 84 contains a bare URL that should be formatted as a proper Markdown link for better readability and to satisfy linting rules.

Apply this diff:

-At `packages/circuits`, make a `build` directory, download the zip file from the following link, and place its unzipped directory under `build`.
-https://drive.google.com/file/d/1TChinAnHr9eV8H_OV9SVReF8Rvu6h1XH/view?usp=sharing
+At `packages/circuits`, make a `build` directory, download the zip file from [this Google Drive link](https://drive.google.com/file/d/1TChinAnHr9eV8H_OV9SVReF8Rvu6h1XH/view?usp=sharing), and place its unzipped directory under `build`.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

84-84: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In packages/circuits/README.md around line 84, replace the bare URL with a
Markdown-formatted link (e.g., [Design
diagram](https://drive.google.com/file/d/1TChinAnHr9eV8H_OV9SVReF8Rvu6h1XH/view?usp=sharing))
so the link has descriptive anchor text and passes linting; update the line to
use the chosen bracketed text followed by the URL in parentheses.


Then, move `email_auth.zkey` in the unzipped directory `params` to `build`.
Then, move `email_auth.zkey` in the unzipped directory `params` to `build`.

Then run the following command.
`yarn test`

### Generate proving keys and verifier contracts for main circuits

`yarn dev-setup`

## Specification

The `email_auth.circom` makes constraints and computes the public output as follows.

1. Assert that `signature` is valid for `padded_header` and `public_key`.
2. Let `public_key_hash` be `PoseidonHash(public_key)`.
3. Let `email_nullifier` be `PoseidonHash(PoseidonHash(signature))`.
Expand All @@ -68,24 +113,25 @@ The `email_auth.circom` makes constraints and computes the public output as foll
9. If `is_time_exist` is 1, let `timestamp` be an integer parsing `timestamp_str` as a digit string. Otherwise, let `timestamp` be zero.
10. Let `is_code_exist` be 1 if `padded_header` satisfies the regex of the invitation code.
11. Let `code_str` be `padded_header[code_idx:code_idx+64]`.
12. Let `embedded_code` be an integer parsing `code_str` as a hex string.
12. Let `embedded_code` be an integer parsing `code_str` as a hex string.
13. If `is_code_exist` is 1, assert that `embedded_code` is equal to `account_code`.
14. Let `account_salt` be `PoseidonHash(from_addr|0..0, account_code, 0)`.
15. Let `masked_subject` be a string that removes the invitation code with the prefix `code_str` and one email address from `subject`, if they appear in `subject`.

Note that the email address in the subject is assumbed not to overlap with the invitation code.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix spelling error.

Line 115 contains a typo: "assumbed" should be "assumed".

Apply this diff:

-Note that the email address in the subject is assumbed not to overlap with the invitation code.
+Note that the email address in the subject is assumed not to overlap with the invitation code.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Note that the email address in the subject is assumbed not to overlap with the invitation code.
Note that the email address in the subject is assumed not to overlap with the invitation code.
🧰 Tools
🪛 LanguageTool

[grammar] ~115-~115: Ensure spelling is correct
Context: ...hat the email address in the subject is assumbed not to overlap with the invitation code...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
In packages/circuits/README.md around line 115, fix the spelling typo "assumbed"
to "assumed" in the sentence "Note that the email address in the subject is
assumbed not to overlap with the invitation code." — update the word to
"assumed" so the line reads "Note that the email address in the subject is
assumed not to overlap with the invitation code."



#### `email_auth_with_body_parsing_with_qp_encoding.circom`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix heading level increment.

Line 117 jumps from h2 (##) to h4 (####), which violates heading hierarchy. It should be h3 (###).

Apply this diff:

-#### `email_auth_with_body_parsing_with_qp_encoding.circom`
+### `email_auth_with_body_parsing_with_qp_encoding.circom`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### `email_auth_with_body_parsing_with_qp_encoding.circom`
### `email_auth_with_body_parsing_with_qp_encoding.circom`
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

117-117: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

🤖 Prompt for AI Agents
In packages/circuits/README.md around line 117, the heading level jumps from h2
(##) to h4 (####); change the heading to h3 (###) so the document's heading
hierarchy is consistent (replace "####
`email_auth_with_body_parsing_with_qp_encoding.circom`" with "###
`email_auth_with_body_parsing_with_qp_encoding.circom`").


A circuit to verify that a message in the email body, called command, is authorized by a user of an account salt, derived from an email address in the From field and a random field value called account code.
This is basically the same as the `email_auth.circom` described above except for the following features:

- Instead of `subject_idx`, it additionally takes as a private input a padded email body `padded_cleaned_body` and an index of the command in the email body `command_idx`.
- It extracts a substring `command` between a prefix `(<div id=3D\"[^\"]*zkemail[^\"]*\"[^>]*>)"` and a suffix `</div>` from `padded_cleaned_body`.
- It outputs `masked_command` instead of `masked_subject`, which removes the invitation code with the prefix and one email address from `command`.

## How to Build the Container Image for Unit Tests in GitHub Actions

To speed up unit testing in GitHub Actions, we run the tests on our Kubernetes cluster.
To speed up unit testing in GitHub Actions, we run the tests on our Kubernetes cluster.
We create a container image and execute jobs on the cluster from GitHub Actions using the job configuration file `kubernetes/circuit-test-job.yml`.

Follow these steps to build and push the container image for unit tests.
Expand Down
5 changes: 2 additions & 3 deletions packages/circuits/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
"license": "MIT",
"version": "1.0.0",
"scripts": {
"build": "mkdir -p build && circom src/email_auth.circom --r1cs --wasm --sym --c -l ../../node_modules -o ./build",
"build-legacy": "mkdir -p build && circom src/email_auth_legacy.circom --r1cs --wasm --sym --c -l ../../node_modules -o ./build",
"build-recipient": "mkdir -p build && circom src/email_auth_with_recipient.circom --r1cs --wasm --sym --c -l ../../node_modules -o ./build",
"build:all": "NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/build-all.ts --sequential",
"build:list": "npx ts-node scripts/build-all.ts --list",
"dev-setup": "NODE_OPTIONS=--max_old_space_size=16384 npx ts-node scripts/dev-setup.ts --output ./build",
"gen-input": "NODE_OPTIONS=--max_old_space_size=8192 npx ts-node scripts/gen_input.ts",
"test": "NODE_OPTIONS=--max_old_space_size=8192 jest",
Expand Down
150 changes: 150 additions & 0 deletions packages/circuits/scripts/build-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Command } from "commander";
import { mkdir } from "fs/promises";
import { readdir } from "fs/promises";
import path from "path";
import { spawn } from "child_process";

type BuildOptions = {
only?: string[];
outDir: string;
libDir: string;
sequential: boolean;
};

function run(command: string, args: string[], cwd: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, stdio: "inherit", shell: false });
child.on("close", (code) => {
if (code === 0) return resolve();
reject(
new Error(`${command} ${args.join(" ")} exited with code ${code}`),
);
});
child.on("error", reject);
});
}

async function listTopLevelCircuits(srcDir: string): Promise<string[]> {
const entries = await readdir(srcDir, { withFileTypes: true });
return (
entries
.filter((d) => d.isFile() && d.name.endsWith(".circom"))
.map((d) => d.name)
// exclude templates by convention and anything not intended to be built directly
.filter((name) => !name.endsWith("_template.circom"))
);
}

async function buildCircuits(
projectRoot: string,
opts: BuildOptions,
): Promise<void> {
const srcDir = path.join(projectRoot, "src");
const outDir = path.join(projectRoot, opts.outDir);

await mkdir(outDir, { recursive: true });

const allCircuits = await listTopLevelCircuits(srcDir);
const selected =
opts.only && opts.only.length > 0
? allCircuits.filter((n) =>
opts.only!.includes(n.replace(/\.circom$/, "")),
)
: allCircuits;

if (selected.length === 0) {
const hint =
allCircuits.length > 0
? `Available: ${allCircuits.map((n) => n.replace(/\.circom$/, "")).join(", ")}`
: "No circuits found";
throw new Error(`No circuits selected to build. ${hint}`);
}

const buildOne = (fileName: string) => {
const srcPath = path.join("src", fileName);
const args = [
srcPath,
"--r1cs",
"--wasm",
"--sym",
"--c",
"-l",
opts.libDir,
"-o",
opts.outDir,
];
return run("circom", args, projectRoot);
};

if (opts.sequential) {
for (const f of selected) {
console.log(`Building ${f}...`);
await buildOne(f);
}
return;
}

await Promise.all(
selected.map(async (f) => {
console.log(`Building ${f}...`);
await buildOne(f);
})
);
}

async function main() {
const program = new Command();
program
.option("-o, --out-dir <dir>", "output directory", "./build")
.option(
"-l, --lib-dir <dir>",
"circom -l include directory",
"../../node_modules",
)
.option(
"-c, --circuit <names>",
"comma-separated circuit base names (without .circom)",
)
.option("--sequential", "build sequentially to reduce peak memory", false)
.option("--list", "list available circuit base names and exit", false);

program.parse(process.argv);
const flags = program.opts();

const opts: BuildOptions = {
outDir: flags.outDir,
libDir: flags.libDir,
sequential: Boolean(flags.sequential),
only:
(flags.circuit ?? flags.only)
? String(flags.circuit ?? flags.only)
.split(",")
.map((s: string) => s.trim())
.filter(Boolean)
: undefined,
};

const projectRoot = path.resolve(__dirname, "..");

try {
if (flags.list) {
const names = await listTopLevelCircuits(path.join(projectRoot, "src"));
if (names.length === 0) {
console.log("No circuits found.");
return;
}
console.log("Available circuits:");
for (const n of names) {
console.log("-", n.replace(/\.circom$/, ""));
}
return;
}
await buildCircuits(projectRoot, opts);
console.log("All selected circuits built successfully.");
} catch (err) {
console.error(err);
process.exit(1);
}
}

main();
Loading
Loading