-
Notifications
You must be signed in to change notification settings - Fork 22
feat: dynamic build scripts #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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", | ||||||
|
|
@@ -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`. | ||||||
|
|
@@ -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 | ||||||
|
|
||||||
| 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))`. | ||||||
|
|
@@ -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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🧰 Tools🪛 LanguageTool[grammar] ~115-~115: Ensure spelling is correct (QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1) 🤖 Prompt for AI Agents |
||||||
|
|
||||||
|
|
||||||
| #### `email_auth_with_body_parsing_with_qp_encoding.circom` | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.18.1)117-117: Heading levels should only increment by one level at a time (MD001, heading-increment) 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| 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. | ||||||
|
|
||||||
| 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( | ||
zkfriendly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
84-84: Bare URL used
(MD034, no-bare-urls)
🤖 Prompt for AI Agents