diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df31b3e..c862a0f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,13 @@
# Every test the repository has, on every push and pull request:
#
-# noir nargo fmt, nargo test, the circuits compiled and checked against their frozen pins
-# contracts forge fmt, forge test (the precompiles mocked)
# rust cargo fmt, clippy, cargo test (offline and quick)
# e2e cargo test -- --ignored: the holder binding proved, and the devnet test (a node spawned,
# the pool deployed, Alice and Bob registering and transacting, every balance checked)
# commitlint conventional commits (pull requests)
#
+# The circuits and the contract are emit-protocol's, checked in its own CI; emit-devnet takes its
+# crates from crates.io.
+#
# No job measures time or memory: the demo's timings are for the README, not for CI.
name: CI
@@ -25,8 +26,6 @@ permissions:
env:
CARGO_TERM_COLOR: always
- NOIR_VERSION: 1.0.0-rc.3
- NOIR_ZK_VERSION: 0.3.3
jobs:
commitlint:
@@ -42,80 +41,20 @@ jobs:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v6
- noir:
- name: Noir (fmt, test, freeze --check)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
- - uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- cache: false
- # The noir-zk CLI links bb's static library against libc++.
- - name: Install libc++
- run: sudo apt-get update -q && sudo apt-get install -yq libc++-dev libc++abi-dev
- - name: Cache the toolchains
- id: toolchains
- uses: actions/cache@v4
- with:
- path: ~/.toolchains
- key: toolchains-noir-${{ env.NOIR_VERSION }}-noir-zk-${{ env.NOIR_ZK_VERSION }}
- - name: Install nargo and the noir-zk CLI (per-version directories)
- if: steps.toolchains.outputs.cache-hit != 'true'
- run: |
- nargo_home="$HOME/.toolchains/noir-$NOIR_VERSION"
- mkdir -p "$nargo_home/bin"
- curl -fsSL https://raw.githubusercontent.com/noir-lang/noirup/main/noirup -o "$nargo_home/bin/noirup"
- chmod +x "$nargo_home/bin/noirup"
- NARGO_HOME="$nargo_home" "$nargo_home/bin/noirup" --version "$NOIR_VERSION"
- cargo install --locked noir-zk-cli --version "=$NOIR_ZK_VERSION" --root "$HOME/.toolchains/noir-zk-$NOIR_ZK_VERSION"
- - name: Toolchain paths
- run: |
- echo "$HOME/.toolchains/noir-$NOIR_VERSION/bin" >> "$GITHUB_PATH"
- echo "$HOME/.toolchains/noir-zk-$NOIR_ZK_VERSION/bin" >> "$GITHUB_PATH"
- - name: Cache the bb CRS
- uses: actions/cache@v4
- with:
- path: ~/.bb-crs
- key: noir-zk-srs-0f238856e557-72b8cdad9da8
- - name: Provision the SRS noir-zk pins
- run: scripts/srs.sh
- - name: nargo fmt --check
- working-directory: crates/circuits/noir
- run: nargo fmt --check
- - name: nargo test
- working-directory: crates/circuits/noir
- run: nargo test --workspace
- - name: nargo compile
- working-directory: crates/circuits/noir
- run: nargo compile --workspace
- - name: The compiled circuits match their frozen pins
- working-directory: crates/circuits
- run: noir-zk freeze --target noir/target --out . --assets assets --library emit-devnet@0.2.0 --check
-
- contracts:
- name: Contracts (forge fmt, forge test)
+ rust:
+ name: Rust (fmt, clippy, test)
runs-on: ubuntu-latest
defaults:
run:
- working-directory: contracts
+ working-directory: emit-devnet
steps:
- uses: actions/checkout@v5
with:
- submodules: recursive
- - uses: foundry-rs/foundry-toolchain@v1
- - name: forge fmt --check
- run: forge fmt --check
- - name: forge test
- run: forge test
-
- rust:
- name: Rust (fmt, clippy, test)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
+ path: emit-devnet
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt, clippy
+ rust-src-dir: emit-devnet
# noir-zk-backend links bb's static library against libc++.
- name: Install libc++
run: sudo apt-get update -q && sudo apt-get install -yq libc++-dev libc++abi-dev
@@ -129,11 +68,16 @@ jobs:
e2e:
name: Proving and the devnet (cargo test -- --ignored)
runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: emit-devnet
steps:
- uses: actions/checkout@v5
with:
- submodules: recursive
+ path: emit-devnet
- uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ rust-src-dir: emit-devnet
- uses: foundry-rs/foundry-toolchain@v1
- name: Install libc++
run: sudo apt-get update -q && sudo apt-get install -yq libc++-dev libc++abi-dev
@@ -144,9 +88,6 @@ jobs:
key: noir-zk-srs-0f238856e557-72b8cdad9da8
- name: Provision the SRS noir-zk pins
run: scripts/srs.sh
- - name: Build the contracts
- working-directory: contracts
- run: forge build
- name: The holder binding, proved
run: cargo test -p zkpool -- --ignored holder
- name: The devnet end to end
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index c65a596..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "contracts/lib/forge-std"]
- path = contracts/lib/forge-std
- url = https://github.com/foundry-rs/forge-std
diff --git a/Cargo.lock b/Cargo.lock
index e541944..d46ad9b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3251,8 +3251,9 @@ dependencies = [
[[package]]
name = "eid-circuits"
-version = "0.8.2"
-source = "git+https://github.com/zk-experiments/eid-circuits?tag=v0.8.2#a4a5fe4acd365da4d5947120eac3ee7b1e1bf305"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4d01a2a6dd6e68cb7f7c84e6248acc14e52643f458535be7ee672dada3d6bc"
dependencies = [
"hex",
"noir-zk-backend",
@@ -3265,8 +3266,8 @@ dependencies = [
[[package]]
name = "eid-prover"
-version = "0.8.2"
-source = "git+https://github.com/zk-experiments/eid-circuits?tag=v0.8.2#a4a5fe4acd365da4d5947120eac3ee7b1e1bf305"
+version = "0.8.3"
+source = "git+https://github.com/zk-experiments/eid-circuits?tag=v0.8.3#852d63624e422ba1f3fab1c009deec69135e4fb4"
dependencies = [
"anyhow",
"csca-registry",
@@ -3326,8 +3327,10 @@ dependencies = [
]
[[package]]
-name = "emit-devnet-circuits"
+name = "emit-circuits"
version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528c0038a26fc30aa0d78ea2f8a7f9795d52c2b4c6a4600fb3c11af49f32d5ba"
dependencies = [
"eid-circuits",
"hex",
@@ -3351,7 +3354,7 @@ dependencies = [
"alloy-sol-types",
"ark-ff 0.6.0",
"clap",
- "emit-devnet-circuits",
+ "emit-circuits",
"eyre",
"hex",
"noir-zk-backend",
@@ -3362,6 +3365,30 @@ dependencies = [
"zk-encryption-circuits",
]
+[[package]]
+name = "emit-protocol"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "243f7f52eeaecdf5f575087334a326efad20e24d26ead9cb4c21c45639ccaec3"
+dependencies = [
+ "ark-ff 0.6.0",
+ "hex",
+ "sha2 0.10.9",
+ "thiserror 2.0.21",
+ "zk-encryption",
+]
+
+[[package]]
+name = "emit-protocol-abi"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef76904e35c4cc9d9e3f0acc0abaaef8f1945d50db50e1adfba075380822e8e1"
+dependencies = [
+ "alloy",
+ "emit-protocol",
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "enr"
version = "0.13.0"
@@ -5163,9 +5190,9 @@ dependencies = [
[[package]]
name = "lazy_static"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
dependencies = [
"spin",
]
@@ -6412,9 +6439,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113"
[[package]]
name = "pkcs1"
@@ -12816,8 +12843,9 @@ dependencies = [
[[package]]
name = "zk-encryption"
-version = "0.1.0"
-source = "git+https://github.com/zk-experiments/zk-encryption?tag=v0.1.3#89d8d4bc45238c74366a9f2266e7017fbf4eb661"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2f4222b11ca2a9ae6fe7e800ace8954c0acd7d774c77a9f26b225a3091c2e2d"
dependencies = [
"ark-bn254",
"ark-ec",
@@ -12834,8 +12862,9 @@ dependencies = [
[[package]]
name = "zk-encryption-circuits"
-version = "0.1.0"
-source = "git+https://github.com/zk-experiments/zk-encryption?tag=v0.1.3#89d8d4bc45238c74366a9f2266e7017fbf4eb661"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e05e7977a49df04895683050e0a2db31cc1596dadfce1f75178ee6ddbbc72e"
dependencies = [
"ark-ff 0.6.0",
"hex",
@@ -12858,7 +12887,9 @@ dependencies = [
"clap",
"csca-registry",
"eid-prover",
- "emit-devnet-circuits",
+ "emit-circuits",
+ "emit-protocol",
+ "emit-protocol-abi",
"eyre",
"futures-util",
"hex",
diff --git a/Cargo.toml b/Cargo.toml
index 2355ea7..8fb25ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-members = ["crates/circuits", "crates/node", "crates/cli"]
+members = ["crates/node", "crates/cli"]
resolver = "2"
[workspace.package]
@@ -10,27 +10,27 @@ license = "MIT"
publish = false
[workspace.dependencies]
-# noir-zk 0.3.3 (crates.io): layered registries, the generic pipeline kernels (the key hash a
-# witness bb constrains), Chonk via bb's FFI. Every layer below is bound with the same release.
+# noir-zk 0.3.3 (crates.io): layered registries, the generic pipeline kernels, Chonk via bb's FFI.
+# Every layer below is bound with the same release.
noir-zk-core = "=0.3.3"
noir-zk-backend = { version = "=0.3.3", features = ["packs", "http"] }
-noir-zk-codegen = "=0.3.3"
-# The identity layer: eid-circuits v0.8.2 (noir-zk 0.3.3; families eid/dsc, eid/sod, eid/document;
-# RSA steps with a 2-bit windowed exponentiation since v0.8.1).
-eid-circuits = { git = "https://github.com/zk-experiments/eid-circuits", tag = "v0.8.2", features = ["packs", "http"] }
-eid-prover = { git = "https://github.com/zk-experiments/eid-circuits", tag = "v0.8.2" }
+# eid-prover renders the eid steps' inputs for a registration (the tag of the eid-circuits release
+# emit-circuits wraps; eid-prover itself isn't on crates.io).
+eid-prover = { git = "https://github.com/zk-experiments/eid-circuits", tag = "v0.8.3" }
csca-registry = { git = "https://github.com/zk-experiments/csca-registry", tag = "v0.3.1" }
-# The channel layer: zk-encryption v0.1.3 (noir-zk 0.3.3; frozen as zk-encryption@0.1.3, the session
-# 6% smaller since v0.1.2; bytecode bundled).
-zk-encryption-circuits = { git = "https://github.com/zk-experiments/zk-encryption", tag = "v0.1.3", features = ["serde"] }
-emit-devnet-circuits = { path = "crates/circuits" }
+# The channel layer: zk-encryption-circuits 0.1.5 (crates.io; bytecode bundled), the one
+# emit-circuits wraps.
+zk-encryption-circuits = { version = "=0.1.5", features = ["serde"] }
+# The protocol (github.com/zk-experiments/emit-protocol): its values, the pool's ABI and the
+# circuits' registry.
+emit-protocol = "=0.1.0"
+emit-protocol-abi = "=0.1.0"
+emit-circuits = "=0.1.0"
ark-ff = "0.6"
hex = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-sha2 = "0.10"
toml = "0.9"
-ureq = { version = "3", default-features = false, features = ["rustls"] }
# Witness solving and proving are unusably slow unoptimised (noir-zk's note).
[profile.dev]
diff --git a/README.md b/README.md
index c99e9dc..98a5fd5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,16 @@
# emit-devnet
-The Emit V2 private transfer, end to end on a local chain: a reth dev node whose EVM verifies folded Chonk proofs (`ZK_VERIFY`) and hashes with the circuits' Poseidon2 (`POSEIDON2`), a private note pool contract, and a console wallet (`zkpool`) with which Alice and Bob, each bound to a synthetic passport, deposit, pay each other on the post-quantum channel, split, merge and withdraw. Each holder first registers in the pool's identity cache, proving the passport once (`identity_register`: eid's steps, then the registration); every transaction after that is one proof folded with noir-zk's generic kernels, of `member_transfer` (membership in the identity tree, the channel session, DG1 sealed, the 2-in / 2-out JoinSplit bound to the registered key, and its note opening sealed; see [The identity cache](#the-identity-cache)). Every note is 0 or at least 1/3 ETH.
+The Emit V2 private transfer, end to end on a local chain: a reth dev node whose EVM verifies folded Chonk proofs (`ZK_VERIFY`) and hashes with the circuits' Poseidon2 (`POSEIDON2`), a private note pool contract, and a console wallet (`zkpool`) with which Alice and Bob, each bound to a synthetic passport, deposit, pay each other on the post-quantum channel, split, merge and withdraw. Each holder first registers in the pool's identity cache, proving the passport once (`identity_register`: eid's steps, then the registration); every transaction after that is one proof folded with noir-zk's generic kernels, of `member_transfer` (membership in the identity tree, the channel session, DG1 sealed, the 2-in / 2-out JoinSplit bound to the registered key, and its note opening sealed; see emit-protocol's [circuits](https://github.com/zk-experiments/emit-protocol#the-circuits)). Every note is 0 or at least 1/3 ETH.
+
+The recipient's note is escrowed: the pool holds output 0 until its owner resolves it, a second proof of `member_resolve` (membership, then the resolve) that accepts it (the note, less a fee, enters the tree) or rejects it (the sender's refund note does); after the escrow window anyone may refund it to the sender. The sealed DG1 and the lattice ciphertext travel off-chain, keyed by the transfer's chain commitment `C_t` (on the devnet a directory the wallets share, the *mailbox*; outside it the envelope-inbox service): the chain carries only `env_commit`, a commitment to them, so no personal data (not even encrypted) is written to it, and the receiver can screen the sender's MRZ before taking the funds. See emit-protocol's [escrow](https://github.com/zk-experiments/emit-protocol#the-escrow).
+
+**What lives where.** The protocol (its circuits, the pool contract, and the crates
+`emit-protocol`, `emit-protocol-abi`, `emit-circuits`) is
+[emit-protocol](https://github.com/zk-experiments/emit-protocol): **what** a transfer carries and
+the rules the chain **enforces**. This repository runs it: the node with its precompiles, the
+console wallet, the devnet scripts and demo, and the mailbox, its stand-in for the **delivery**
+layer (envelope-inbox outside the devnet). The crates (`emit-protocol`, `emit-protocol-abi`,
+`emit-circuits` 0.1.0) come from crates.io.
Design: `emit-v2-transfer-mechanism.md` and `emit-private-transfer-design.md` (the Outbe notes); the circuits and wallet logic come from the `postquantum-zk-encryption-experiment` repository, the channel from [zk-encryption](https://github.com/zk-experiments/zk-encryption), the identity layer from [eid-circuits](https://github.com/zk-experiments/eid-circuits), the pipelines from [noir-zk](https://github.com/zk-experiments/noir-zk).
@@ -11,11 +21,11 @@ flowchart TB
pins["pins.toml: versions, catalogs + SHA-256, registry root, deployment root"]
cdn[("CDNs: circuits.zk-eid.dev, circuits.zk-experiments.dev")]
- subgraph circuits["crates/circuits: the combining registry"]
+ subgraph circuits["emit-circuits (emit-protocol): the combining registry"]
direction LR
- eid["eid-circuits 0.8.2 eid/dsc, eid/sod, eid/document"]
- chan["zk-encryption 0.1.3 session, envelope, note_envelope"]
- own["emit-devnet@0.2.0 (own) transfer_holder, register, identity_member"]
+ eid["eid-circuits 0.8.3 eid/dsc, eid/sod, eid/document"]
+ chan["zk-encryption 0.1.5 session, envelope, note_envelope"]
+ own["emit-protocol@0.1.0 transfer_holder, dg1_envelope, escrow_resolve, register, identity_member"]
pl["pipelines fold, verify, DEPLOYMENT_ROOT"]
eid --> pl
chan --> pl
@@ -25,10 +35,13 @@ flowchart TB
subgraph cli["crates/cli: zkpool wallet (Alice, Bob)"]
direction LR
prove["prove: register / transfer one 40,192-byte proof"]
- sync["sync / listen update trees, scan received notes"]
+ sync["sync / listen update trees, scan escrows"]
+ mailbox[("mailbox off-chain envelopes by C_t")]
wallet[("wallet JSON keys, notes, trees")]
prove --> wallet
sync --> wallet
+ prove --> mailbox
+ mailbox --> sync
end
subgraph node["crates/node: emit-node (reth v2.6.0)"]
@@ -38,9 +51,9 @@ flowchart TB
rpc --> evm
end
- subgraph contracts["contracts/"]
+ subgraph contracts["EmitV2Pool (emit-protocol, deployed by zkpool from emit-protocol-abi)"]
direction LR
- pool["EmitV2Pool verify proof, check public fields, pay fee and withdrawals"]
+ pool["EmitV2Pool verify proof, check public fields, escrow output 0, resolve / refund, pay fee and withdrawals"]
imt["IMT: note tree"]
idt["IdentityTree"]
pool --> imt
@@ -52,24 +65,28 @@ flowchart TB
pins -. "checked at startup" .-> node
circuits --> prove
circuits -- "verify(pipeline_root, proof)" --> evm
- prove -- "register / transact" --> rpc
+ prove -- "register / transact / resolve" --> rpc
evm --> pool
pool -- "events" --> sync
```
- `ZK_VERIFY(pipeline_root ‖ proof)` returns the proof's `uint256[35]` public fields or reverts. `POSEIDON2(x₁..xₙ)` is Noir's sponge.
-- `EmitV2Pool` calls `ZK_VERIFY`, then checks every public field against the calldata: ctx, `H(pqCiphertext)`, roots, nullifiers, the identity root (the registry root, date and scope for a registration), the date and `msg.value`. It pays the fee to `block.coinbase` and `vPubOut` to the payout address.
+- `EmitV2Pool` calls `ZK_VERIFY`, then checks every public field against the calldata: ctx, roots, nullifiers, the identity root (the registry root, date and scope for a registration), the date and `msg.value`. It appends output 1, escrows output 0 with its refund note until the owner resolves it (a `member_resolve` proof) or the window passes, emits `env_commit` for the off-chain envelope, pays the fee to `block.coinbase` and `vPubOut` to the payout address.
- Both trees are `IMT`s of depth 32 that keep every root they had: a proof against an older root stays valid.
-- The wallet follows `NewCommitment`, `NewNullifier`, `Envelope` and `IdentityRegistered`. `Receiver::scan` recovers the notes sent to it and the sender's MRZ.
-- The wallet JSON holds `sk`, the Receiver and Senders, the notes, the registration, and each tree's frontier with the paths of the wallet's own leaves.
+- The wallet follows `NewCommitment`, `NewNullifier`, `Envelope`, `Escrowed`, `EscrowClosed` and `IdentityRegistered`. With the off-chain envelope from the mailbox, `Receiver::scan` recovers the escrowed notes sent to it and the sender's MRZ.
+- The wallet JSON holds `sk`, the Receiver and Senders, the notes, the escrows (received and sent), the registration, and each tree's frontier with the paths of the wallet's own leaves.
The pipelines, each folded into one proof by noir-zk's generic kernels:
```mermaid
flowchart LR
- subgraph mem["member_transfer (5 apps, 31 slots): every transaction"]
+ subgraph mem["member_transfer (5 apps, 27 slots): every transaction"]
+ direction LR
+ m1["identity_member"] --> m2["session"] --> m3["dg1_envelope"] --> m4["transfer_holder"] --> m5["note envelope"]
+ end
+ subgraph res["member_resolve (2 apps, 8 slots): an escrow's owner accepts or rejects"]
direction LR
- m1["identity_member"] --> m2["session"] --> m3["DG1 envelope"] --> m4["transfer_holder"] --> m5["note envelope"]
+ s1["identity_member"] --> s2["escrow_resolve"]
end
subgraph reg["identity_register (4 apps, 6 slots): once per passport"]
direction LR
@@ -77,54 +94,47 @@ flowchart LR
end
```
-Setup: the eid DSC/SOD/document bytecode comes from the packs on circuits.zk-eid.dev (catalog SHA-256 pinned, pack SHA-256 from the catalog, every file checked against eid's registry pins); the channel layer is release v0.1.3, frozen as `zk-encryption@0.1.3`.
+Setup: the eid DSC/SOD/document bytecode comes from the packs on circuits.zk-eid.dev (catalog SHA-256 pinned, pack SHA-256 from the catalog, every file checked against eid's registry pins); the channel layer is zk-encryption-circuits 0.1.5, whose frozen library identity is `zk-encryption@0.1.3`; eid-circuits 0.8.3's is still `eid-circuits@0.8.2`.
```
-crates/circuits/ build.rs (codegen), circuits/manifest.toml (own and wrapped families, pipelines),
- noir/ (apps transfer_holder, register, identity_member; lib/emit,
- lib/identity_cache), resources/ + assets/ (frozen keys, ABIs, bytecode),
- src/{lib.rs, pins.rs, setup.rs}
crates/node/ src/main.rs (reth CLI + EVM factory), src/precompiles.rs, tests/devnet.rs
-crates/cli/ src/{main.rs, wallet.rs, chain.rs, emit.rs, identity.rs, tree.rs, document.rs},
+crates/cli/ src/{main.rs, wallet.rs, chain.rs, emit.rs, identity.rs, tree.rs, document.rs,
+ mailbox.rs},
fixtures/documents.json (six synthetic passports), tests/cli.rs
-contracts/ src/{IMT.sol, EmitV2Pool.sol}, test/EmitV2Pool.t.sol, script/Deploy.s.sol
-scripts/ devnet.sh (node + deploy), demo.sh (the whole flow, checked), srs.sh (bb's CRS)
+scripts/ devnet.sh (node + `zkpool deploy`), demo.sh (the whole flow, checked), srs.sh (bb's CRS)
.github/workflows/ ci.yml (every test, the proving and devnet ones included)
genesis.json chain 3607, Prague from genesis, 300M gas limit, three funded dev accounts
-pins.toml
```
## Pins and where each artefact comes from
-`pins.toml` is compiled into the node and the wallet and checked at startup (`crates/circuits/src/pins.rs`): the node refuses to start and the wallet refuses to prove if a check fails (`emit-node node --pins-offline` checks only the local ones).
+emit-circuits' `pins.toml` is compiled into the node and the wallet and checked at startup (`emit_circuits::pins`): the node refuses to start and the wallet refuses to prove if a check fails (`emit-node node --pins-offline` checks only the local ones).
| what | version / pin | from | checked |
|---|---|---|---|
| noir-zk (core, backend, codegen, kernels) | `=0.3.3` | crates.io | kernels family root `0x297133fa…ae43` and version |
-| this repository's layers (emit: transfer_holder; identity_cache: register, member) | `emit-devnet@0.2.0`, family roots `transfer_holder` `0x2a50870b…e27a`, `register` `0x127dc46e…6c41`, `member` `0x03f6f187…64be` | this repository (`crates/circuits/noir`, frozen with `noir-zk freeze`, bytecode bundled) | library and every family root; bytecode against its pin when loaded |
-| channel layer | release `v0.1.3` (library `zk-encryption@0.1.3`, on noir-zk 0.3.3 from crates.io) | bytecode bundled in `zk-encryption-circuits`; catalog `https://circuits.zk-experiments.dev/zk-encryption/0.1.3/catalog.json` | catalog SHA-256 `5bb7d1b5…b2c5`; its families' roots equal the compiled-in ones |
-| identity layer | `eid-circuits@0.8.2`, git tag `v0.8.2` (`eid-circuits`, `eid-prover`) | DSC, SOD and document steps: packs on `https://circuits.zk-eid.dev`, catalog `catalog@0.8.2.json` | catalog SHA-256 `4de0e4f0…53eb`, its version and its DSC/SOD/document labels in eid's registry; each pack's SHA-256 against the catalog; every unpacked `.b64` / `.vk` against eid's registry pins |
+| the protocol's layers (emit: transfer_holder, dg1_envelope, escrow_resolve; identity_cache: register, member) | `emit-protocol@0.1.0`, family roots `transfer_holder` `0x18ee3ff5…efc7`, `dg1_envelope` `0x0b7d4c28…3119`, `escrow_resolve` `0x04c880b8…2757`, `register` `0x1d0042a0…c1eb`, `member` `0x00f3d510…0a75` | emit-protocol (`noir/`, frozen into `emit-circuits` with `noir-zk freeze`, bytecode bundled) | library and every family root; bytecode against its pin when loaded |
+| channel layer | `zk-encryption-circuits` 0.1.5 from crates.io (library `zk-encryption@0.1.3`, on noir-zk 0.3.3) | bytecode bundled in `zk-encryption-circuits`; catalog `https://circuits.zk-experiments.dev/zk-encryption/0.1.5/catalog.json` | catalog SHA-256 `b797c5ed…bb81`; its families' roots equal the compiled-in ones |
+| identity layer | `eid-circuits` 0.8.3 from crates.io (frozen library `eid-circuits@0.8.2`), `eid-prover` by git tag `v0.8.3` | DSC, SOD and document steps: packs on `https://circuits.zk-eid.dev`, catalog `catalog@0.8.2.json` (0.8.3 didn't refreeze: the same pinned bytecode) | catalog SHA-256 `4de0e4f0…53eb`, its version and its DSC/SOD/document labels in eid's registry; each pack's SHA-256 against the catalog; every unpacked `.b64` / `.vk` against eid's registry pins |
| CSCA registry | tag `registry-20260928-1039`, root `0x27bef40a…02a2` | `https://registry.zk-eid.dev//registry.json` | file SHA-256 `17a21c0f…4444` and `commitment.root` |
-| deployment | root `0x26c8ef80…9bf5`; `identity_register` `0x25bcf960…5730` (4); `member_transfer` `0x0bb7cc32…0d54` (5) | computed by the codegen | equal to the generated constants |
-| toolchain | nargo 1.0.0-rc.3, bb 7.0.0-nightly.20260927 (via `barretenberg-rs`), reth v2.6.0, solc 0.8.30 | noirup / `cargo install`, crates.io, git tag | nargo 1.0.0-rc.3 and noir-zk-cli 0.3.3 only to refreeze this repo's layers: `nargo compile --workspace` in `crates/circuits/noir`, then `noir-zk freeze --target noir/target --out . --assets assets --library emit-devnet@0.2.0` in `crates/circuits` (CI runs it with `--check`) |
+| deployment | root `0x02d515b4…a88a`; `identity_register` `0x0a2bcc6a…55cf` (4); `member_transfer` `0x0185c63e…ee6c` (5); `member_resolve` `0x23173a42…6853` (2) | computed by emit-circuits' codegen | equal to the generated constants |
+| pool contract | `EmitV2Pool` from emit-protocol (solc 0.8.30) | `emit-protocol-abi`'s artifact (ABI and bytecode); `zkpool deploy` deploys it | emit-protocol's CI checks the artifact against `forge build` |
+| toolchain | bb 7.0.0-nightly.20260927 (via `barretenberg-rs`), reth v2.6.0 | crates.io, git tag | the Noir and Solidity toolchains are emit-protocol's |
-Caches: `~/.cache/emit-devnet` (`$EMIT_DEVNET_CACHE`): the pinned files, eid's unpacked packs (the demo needs common, rsa4096, rsa2048, bp384, bp256: about 300 MB). The prover reads bb's CRS from `~/.bb-crs` (`$BB_CRS_PATH`), checked against noir-zk's pinned hashes; `scripts/srs.sh` provisions it.
+Caches: `~/.cache/emit-protocol` (`$EMIT_PROTOCOL_CACHE`): the pinned files, eid's unpacked packs (the demo needs common, rsa4096, rsa2048, bp384, bp256: about 300 MB). The prover reads bb's CRS from `~/.bb-crs` (`$BB_CRS_PATH`), checked against noir-zk's pinned hashes; `scripts/srs.sh` provisions it.
## Running it
-Prerequisites: Rust 1.94+, Foundry (forge, cast), bb's CRS in `~/.bb-crs` (`scripts/srs.sh`), network access the first time. On Linux, libc++ (`libc++-dev libc++abi-dev`: bb's static library links against it).
+Prerequisites: Rust 1.94+, emit-protocol checked out next to this repository (`../emit-protocol`), Foundry's `cast` (the devnet test reads balances with it), bb's CRS in `~/.bb-crs` (`scripts/srs.sh`), network access the first time. On Linux, libc++ (`libc++-dev libc++abi-dev`: bb's static library links against it).
```sh
-git submodule update --init # forge-std
scripts/demo.sh # builds, starts the node, deploys, runs Alice and Bob, checks balances
cargo test -- --ignored devnet # the same as a test, against a spawned node (ports 28545-28551, 30545)
cargo test -- --ignored holder # proves member_transfer with another key than the registered one: refused
cargo test # unit tests: pins, pipelines, precompiles, the trees, the CLI offline
-(cd contracts && forge test) # the pool's logic with a mocked verifier and hash
-(cd crates/circuits/noir && nargo test --workspace) # the circuits' tests (nargo 1.0.0-rc.3)
```
-CI (`.github/workflows/ci.yml`) runs all of these on every push and pull request, plus `nargo fmt`, `cargo fmt`, clippy, `forge fmt` and `noir-zk freeze --check` (the compiled circuits against their frozen keys); the demo script itself is the devnet test's flow, run by hand.
+The circuits' and the contract's own tests (`nargo test`, `forge test`, `freeze --check`) are emit-protocol's. CI (`.github/workflows/ci.yml`) runs the above on every push and pull request, plus `cargo fmt` and clippy, with emit-protocol checked out alongside; the demo script itself is the devnet test's flow, run by hand.
By hand: `source scripts/devnet.sh` starts the node (`.devnet/`) and deploys the pool, exporting `ZKPOOL_POOL`, `ZKPOOL_HOME`, `ZKPOOL_RPC`, `ZKPOOL_WS`; then:
@@ -136,8 +146,10 @@ zkpool -w alice identity register # the passport proved once: requi
zkpool -w bob identity register
zkpool -w bob listen & # Ctrl-C to stop, or --until N / --timeout S
zkpool -w alice deposit --amount 100
-zkpool -w alice transfer --to bob --amount 60 # the handshake
+zkpool -w alice transfer --to bob --amount 60 # the handshake, escrowed until Bob resolves it
zkpool -w alice transfer --to bob --amount 5 # the ratchet
+zkpool -w bob escrows # the escrows addressed to Bob, with the sender's MRZ
+zkpool -w bob resolve --all # accept them (--reject hands one back)
zkpool -w bob notes
zkpool -w bob split --amounts 10,49.99
zkpool -w bob merge
@@ -145,7 +157,7 @@ zkpool -w bob withdraw --amount 30 --to 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293
zkpool -w bob balance
```
-The node alone: `emit-node node --dev --chain genesis.json --http --ws` (all of reth's flags apply). The pool: `scripts/devnet.sh` runs `forge script script/Deploy.s.sol` with `DEPLOYMENT_ROOT`, `REGISTER_PIPELINE`, `MEMBER_PIPELINE` and `REGISTRY_ROOTS` from `zkpool info`; the deployer's first contract is `0x5FbDB2315678afecb367f032d93F642f64180aa3`.
+The node alone: `emit-node node --dev --chain genesis.json --http --ws` (all of reth's flags apply). The pool: `scripts/devnet.sh` runs `zkpool deploy`, which deploys `EmitV2Pool` from emit-protocol-abi's bytecode with the pinned deployment and pipeline roots (`--escrow-window`, default one day) and accepts the fixtures' CSCA registry root and the published one; the deployer's first contract is `0x5FbDB2315678afecb367f032d93F642f64180aa3`.
Dev accounts: accounts 0–2 of the public Anvil/Hardhat test mnemonic (`test test … junk`), whose private keys are published in Foundry's and Hardhat's documentation; funded with 10⁹ ETH in `genesis.json`. They are public knowledge: never use them, or send funds to them, on any real network.
@@ -159,7 +171,7 @@ Dev accounts: accounts 0–2 of the public Anvil/Hardhat test mnemonic (`test te
`scripts/demo.sh` runs the whole flow below and checks it; this section walks through it step by step, with the output of one run (Apple M5 Max; tx hashes, keys and commitments shown as `0x…`, timings as ≈, gas as measured). Every `zkpool` command reads `ZKPOOL_POOL`, `ZKPOOL_HOME`, `ZKPOOL_RPC` and `ZKPOOL_WS` from the environment `scripts/devnet.sh` exports. Alice holds the US fixture (RSA-4096 CSCA → RSA-2048 DSC), Bob the German one (brainpoolP384 → brainpoolP256); both are synthetic passports of the same specimen holder, so their MRZs differ only in the country.
-What any observer can read from a `transact`: its calldata (the pipeline root, the note tree root, the two nullifiers and commitments, `vPubIn`, `vPubOut`, `fee`, `payout`, the 1,536-byte ML-KEM ciphertext and the 40,192-byte proof) and, by running `ZK_VERIFY` on that proof, every public slot; the events; the sending EOA (devnet only, see [What is devnet-only](#what-is-devnet-only)). Every transaction proves the same pipeline, so they all have one shape. None of it says who pays whom or how much moves inside the pool: nullifiers aren't linkable to the commitments they spend, commitments hide their owner and value, and the envelopes are ciphertexts.
+What any observer can read from a `transact`: its calldata (the pipeline root, the note tree root, the two nullifiers and commitments, `vPubIn`, `vPubOut`, `fee`, `payout` and the 40,192-byte proof) and, by running `ZK_VERIFY` on that proof, every public slot; the events; the sending EOA (devnet only, see [What is devnet-only](#what-is-devnet-only)). Every transaction proves the same pipeline, so they all have one shape. None of it says who pays whom or how much moves inside the pool: nullifiers aren't linkable to the commitments they spend, commitments hide their owner and value, the note envelope is a ciphertext, and the sealed DG1 and the lattice ciphertext aren't on-chain at all (only `env_commit`, a commitment to them). A `resolve` shows which escrow closed and whether it was accepted (a new leaf and a fee) or rejected (the refund leaf), not by whom.
### 1. Start the node and deploy the pool
@@ -167,19 +179,20 @@ What any observer can read from a `transact`: its calldata (the pipeline root, t
source scripts/devnet.sh
```
-Starts `emit-node node --dev` on `genesis.json` (datadir `.devnet/chain`, http 8545, ws 8546), checks the pins, then deploys with `forge script script/Deploy.s.sol` from the deployer account.
+Starts `emit-node node --dev` on `genesis.json` (datadir `.devnet/chain`, http 8545, ws 8546), checks the pins, then deploys with `zkpool deploy` from the deployer account.
```
-pins: ok emit-devnet layers emit-devnet@0.2.0 family roots (transfer_holder, register, member)
-pins: ok deployment root 0x26c8ef8078714da209ce35c2012a5f91a87aa8de58546937c0ceb68bd8c49bf5
-pins: ok pipeline identity_register root 0x25bcf9604364971c2f6d153e68114723a12f0a5bfe72d0f930bfd13769d45730 length 4
-pins: ok pipeline member_transfer root 0x0bb7cc32fbc09ac1b3235bb7a38af8b4c5d1f68864a0fecb16d070a8b8200d54 length 5
+pins: ok emit-protocol layers emit-protocol@0.1.0 family roots (transfer_holder, dg1_envelope, escrow_resolve, register, member)
+pins: ok deployment root 0x02d515b4c11b8b715af4d6fa14644a3e7e8dcc52dd9923f3424f1304561da88a
+pins: ok pipeline identity_register root 0x0a2bcc6a85b08c9ee3046ec9387b53a583e188447c51ce7a45cc989702e855cf length 4
+pins: ok pipeline member_transfer root 0x0185c63e50408ddc230b73c27e117be74fe278a964be0e45def5cdc527a8ee6c length 5
+pins: ok pipeline member_resolve root 0x23173a425869da40ad85bb78639280b69ccc656121c183cfba1e6e575bcd6853 length 2
…
precompiles: ZK_VERIFY at 0x0000000000000000000000000000000000000100 (1200000 gas + 3/word), POSEIDON2 at 0x0000000000000000000000000000000000000101 (60 + 360/permutation)
node pid …, rpc http://127.0.0.1:8545, EmitV2Pool 0x5FbDB2315678afecb367f032d93F642f64180aa3
```
-On-chain: the `EmitV2Pool` constructor stores the deployment root and the two accepted pipeline roots (`identity_register`, `member_transfer`) and creates its `IdentityTree`; two `addRegistryRoot` calls accept the fixtures' CSCA registry root and the published one (`RegistryRoot` ×2).
+On-chain: the `EmitV2Pool` constructor stores the deployment root, the three accepted pipeline roots (`identity_register`, `member_transfer`, `member_resolve`) and the escrow window (a day), and creates its `IdentityTree`; two `addRegistryRoot` calls accept the fixtures' CSCA registry root and the published one (`RegistryRoot` ×2).
### 2. Identities
@@ -227,8 +240,8 @@ zkpool -w bob identity register
Proves `identity_register` (eid's DSC, SOD and document steps for the passport, with the document in this epoch's registration scope read from the pool, then `register`), with a fresh blinding `r`, verifies it locally and sends `register(proof)`. The expiry is the epoch's last second or the passport's expiry, whichever is first. In the epoch's last day (`RENEWAL_WINDOW`) the wallet registers for the next epoch instead, valid at once and until that epoch ends, so a registration never lasts less than a day. A wallet with no live registration can't transact: `deposit`, `transfer` and the rest refuse with a pointer to `identity register`.
```
-alice: registered (identity leaf 0, valid until …): identity_register proved in ≈1.7 s (40192 B proof, verified locally in 17 ms), gas 2574707 (40260 B calldata), block 2, tx 0x…
-bob: registered (identity leaf 1, valid until …): identity_register proved in ≈3.9 s (40192 B proof, verified locally in 16 ms), gas 2007304 (40260 B calldata), block 3, tx 0x…
+alice: registered (identity leaf 0, valid until …): identity_register proved in ≈1.7 s (40192 B proof, verified locally in 18 ms), gas 2574895 (40260 B calldata), block 2, tx 0x…
+bob: registered (identity leaf 1, valid until …): identity_register proved in ≈4.0 s (40192 B proof, verified locally in 18 ms), gas 2007864 (40260 B calldata), block 3, tx 0x…
```
On-chain: `register(bytes proof)`, calldata the proof alone. `ZK_VERIFY` returns 6 slots: `registry_root, date, scope, nullifier, leaf, expiry`. The pool checks the registry root is accepted, the date is within a day of the block time, `scope = registrationScope(date / 7 days)` (or, in the epoch's last day, the next epoch's), the document's nullifier is unused and `date ≤ expiry` < that epoch's end; then writes `documentRegistered[nullifier]`, appends the leaf to the identity tree (its new root joins the known roots) and emits `IdentityRegistered(leaf, index, expiry)`. Alice's is the first insert and writes the tree's filled subtrees, hence 2.58M gas against Bob's 2.01M. An observer sees the leaf, its index and expiry, the scoped nullifier, the date and the sending EOA, but the leaf is blinded: Bob, who knows Alice's address and will read her MRZ, can't tell which registration is hers. The wallet stores the leaf, index, DG1 salt, expiry and `r`.
@@ -239,7 +252,7 @@ On-chain: `register(bytes proof)`, calldata the proof alone. `ZK_VERIFY` returns
zkpool -w bob listen --until 2 --timeout 900 &
```
-Subscribes to the pool's logs over WebSocket and syncs on each new block's events; stops after 2 received notes.
+Subscribes to the pool's logs over WebSocket and syncs on each new block's events; stops after 2 notes received into escrow.
### 6. Alice deposits 100
@@ -247,13 +260,13 @@ Subscribes to the pool's logs over WebSocket and syncs on each new block's event
zkpool -w alice deposit --amount 100
```
-Every transaction proves `member_transfer`: `identity_member` (her leaf under the current identity root, today's date, a fresh DG1 commitment, the holder tag), the channel session, the DG1 envelope, `transfer_holder` and the note envelope. The inputs are two dummy notes of her own key (fresh nullifiers), the outputs a 100 ETH note and a zero note to herself (every output is 0 or at least 1/3 ETH, which `transfer_holder` checks); the channel is a throwaway (nobody can open the envelopes); fee 0.
+Every transaction proves `member_transfer`: `identity_member` (her leaf under the current identity root, today's date, a fresh DG1 commitment, the holder tag), the channel session, the DG1 envelope, `transfer_holder` and the note envelope. The inputs are two dummy notes of her own key (fresh nullifiers), the outputs a zero note (output 0, escrowed and never resolved) and the 100 ETH note to herself (output 1, appended at once; every output is 0 or at least 1/3 ETH, which `transfer_holder` checks); the channel is a throwaway (nobody can open the envelopes, so the wallet puts nothing in the mailbox); fee 0.
```
-alice: deposit 100 ETH: member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 17 ms), gas 3307982 (42180 B calldata), block 4, tx 0x…
+alice: deposit 100 ETH: member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 18 ms), gas 2690673 (40580 B calldata), block 4, tx 0x…
```
-On-chain: `transact(pipeline = member_transfer root, root, nullifiers[2], commitments[2], vPubIn = 100 ETH, vPubOut = 0, fee = 0, payout = 0, pqCiphertext (1,536 B), proof (40,192 B))` with `msg.value = 100 ETH`. `ZK_VERIFY` returns 31 slots: `identity_root, date, holder_tag`; the session's `ctx, C_t, E.x, E.y, tag, ct_commitment`; the DG1 envelope's `c_id0..5`; the transfer's `cid, root, N₀, N₁, C₀, C₁, v_in, v_out, fee, payout`; the note envelope's `c_note0..5`. The pool checks the identity root is one the identity tree had, the date, every transfer field against the calldata, `ctx = H("emit-v2/ctx", cid, N₀, N₁, C₀, C₁)`, `ct_commitment = H(pqCiphertext)`, the note root one the note tree had, the nullifiers unspent and distinct, and `msg.value = vPubIn`. It marks both nullifiers spent, appends both commitments (note tree leaves 0 and 1, each new root known from then on) and emits `NewNullifier` ×2, `NewCommitment(commitment, leafIndex)` ×2 and `Envelope(cT, e, tag, ct, pqCiphertext, cNote, cId)`. The first deposit pays for the note tree's first writes (3.31M gas; later transactions ≈2.77-2.78M). An observer sees 100 ETH enter from Alice's EOA (`vPubIn` is public); who owns the result and how it's split between the two commitments are hidden.
+On-chain: `transact(pipeline = member_transfer root, root, nullifiers[2], commitments[2], vPubIn = 100 ETH, vPubOut = 0, fee = 0, payout = 0, proof (40,192 B))` with `msg.value = 100 ETH`. `ZK_VERIFY` returns 27 slots: `identity_root, date, holder_tag`; the session's `ctx, C_t, E.x, E.y, tag, ct_commitment`; the DG1 envelope's `cid_commit`; the transfer's `cid, root, N₀, N₁, C₀, C₁, C_r, v_in, v_out, fee, payout`; the note envelope's `c_note0..5`. The pool checks the identity root is one the identity tree had, the date, every transfer field against the calldata, `ctx = H("emit-v2/ctx", cid, N₀, N₁, C₀, C₁)`, the note root one the note tree had, the nullifiers unspent and distinct, and `msg.value = vPubIn`. It marks both nullifiers spent, appends C₁ (note tree leaf 0), escrows C₀ with C_r and emits `NewNullifier` ×2, `NewCommitment(C₁, 0)`, `Escrowed(1, C₀, C_t, env_commit, deadline)` and `Envelope(cT, e, tag, ct, cNote)`. The first deposit pays for the note tree's first writes (2.69M gas; later transfers ≈2.13-2.14M). An observer sees 100 ETH enter from Alice's EOA (`vPubIn` is public); who owns the result, and that output 0 is empty, are hidden.
### 7. Alice pays Bob 60 (the handshake)
@@ -261,13 +274,13 @@ On-chain: `transact(pipeline = member_transfer root, root, nullifiers[2], commit
zkpool -w alice transfer --to bob --amount 60
```
-The first transfer to a contact is the handshake: the session encapsulates to Bob's Grumpkin and ML-KEM keys, the DG1 envelope seals Alice's MRZ and the note envelope output 0's opening (value and randomness) under the new chain key. Inputs: her 100 note and a dummy; outputs: 60 to Bob's `pk`, 39.99 change to herself; fee 0.01 (the default).
+The first transfer to a contact is the handshake: the session encapsulates to Bob's Grumpkin and ML-KEM keys, the DG1 envelope seals Alice's MRZ and the note envelope output 0's opening (value and randomness) under the new chain key. Inputs: her 100 note and a dummy; outputs: 60 to Bob's `pk` (escrowed), 39.99 change to herself; fee 0.01 (the default). Before sending, the wallet writes the off-chain envelope (`u ‖ v`, then `c_id`: 1,728 bytes) to the mailbox under `C_t`, and keeps the refund note C_r among its pending notes.
```
-alice: transfer 60 ETH to bob (handshake): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 16 ms), gas 2772309 (42180 B calldata), block 5, tx 0x…
+alice: transfer 60 ETH to bob (handshake), in escrow until bob resolves it: member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 21 ms), gas 2137955 (40580 B calldata), block 5, tx 0x…
```
-On-chain: `transact` as in step 6 with `vPubIn = vPubOut = 0`, `fee = 0.01 ETH`, paid from the shielded value to `block.coinbase`. Note tree leaves 2 and 3; `NewNullifier` ×2, `NewCommitment` ×2, `Envelope`. An observer sees a member transfer with a 0.01 ETH fee from Alice's EOA and nothing of its receiver or amount. Bob's `Receiver::scan` first looks each Envelope's `C_t` up in the chain-key windows he holds, then tests its tag against his bundle's keys; this one matches the tag, so he derives the channel's first chain key from `E` and the lattice ciphertext (his ML-KEM key), opens `c_note` (checking it opens `C₀` for his `pk`) and `c_id` (Alice's MRZ), and keeps the note.
+On-chain: `transact` as in step 6 with `vPubIn = vPubOut = 0`, `fee = 0.01 ETH`, paid from the shielded value to `block.coinbase`. Note tree leaf 1 (the change); escrow 2 (`Escrowed(2, …)`). An observer sees a member transfer with a 0.01 ETH fee from Alice's EOA and nothing of its receiver, its amount or the sender's identity. Bob's wallet reads the envelope for `C_t` from the mailbox, checks it opens `env_commit`, and runs `Receiver::scan`: it first looks `C_t` up in the chain-key windows he holds, then tests the tag against his bundle's keys; this one matches the tag, so he derives the channel's first chain key from `E` and the lattice ciphertext (his ML-KEM key), opens `c_note` (checking it opens `C₀` for his `pk`) and `c_id` (Alice's MRZ), and keeps the escrow to resolve.
### 8. Alice pays Bob 5 (the ratchet)
@@ -275,42 +288,62 @@ On-chain: `transact` as in step 6 with `vPubIn = vPubOut = 0`, `fee = 0.01 ETH`,
zkpool -w alice transfer --to bob --amount 5
```
-The second transfer to Bob advances the channel's ratchet (the session encapsulates to a throwaway key: every transfer has one shape). Inputs: the 39.99 change; outputs: 5 to Bob, 34.98 to herself.
+The second transfer to Bob advances the channel's ratchet (the session encapsulates to a throwaway key: every transfer has one shape). Inputs: the 39.99 change; outputs: 5 to Bob (escrowed), 34.98 to herself.
```
-alice: transfer 5 ETH to bob (ratchet index 1): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 16 ms), gas 2772325 (42180 B calldata), block 6, tx 0x…
+alice: transfer 5 ETH to bob (ratchet index 1), in escrow until bob resolves it: member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 25 ms), gas 2137824 (40580 B calldata), block 6, tx 0x…
```
-On-chain: `transact` as in step 7. Note tree leaves 4 and 5; the same events. An observer can't tell a handshake from a ratchet. Bob finds it by `C_t` in the window of chain keys he holds for the channel.
+On-chain: `transact` as in step 7. Note tree leaf 2; escrow 3. An observer can't tell a handshake from a ratchet. Bob finds it by `C_t` in the window of chain keys he holds for the channel.
Bob's listener, meanwhile:
```
bob: listening to 0x5FbDB2315678afecb367f032d93F642f64180aa3 (block 3)
-bob: received 60 ETH (handshake, block 5) from P 2 (10 + 49.99 ETH): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 17 ms), gas 2769539 (42180 B calldata), block 7, tx 0x…
-bob: merge 2 -> 1 (14.99 ETH): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 17 ms), gas 2772065 (42180 B calldata), block 8, tx 0x…
-bob: withdraw 30 ETH to 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC: member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 17 ms), gas 2776772 (42180 B calldata), block 9, tx 0x…
- 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC: 999999999.99657… -> 1000000029.99572… ETH
+bob: split 1 -> 2 (10 + 49.99 ETH): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 20 ms), gas 2131981 (40580 B calldata), block 11, tx 0x…
+bob: accept own escrow: member_resolve proved in ≈0.6 s (40192 B proof, verified locally in 19 ms), gas 1990418 (40260 B calldata), block 12, tx 0x…
+bob: merge 2 -> 1 (14.99 ETH): member_transfer proved in ≈1.3 s (40192 B proof, verified locally in 19 ms), gas 2135013 (40580 B calldata), block 13, tx 0x…
+bob: withdraw 30 ETH to 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC: member_transfer proved in ≈1.4 s (40192 B proof, verified locally in 18 ms), gas 2142483 (40580 B calldata), block 14, tx 0x…
+ 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC: 999999999.99533… -> 1000000029.99499… ETH
```
-On-chain: `transact` each, as in step 7 (note tree leaves 6-11, `NewNullifier` ×2, `NewCommitment` ×2, `Envelope` each). The withdrawal pays 30 ETH from the pool to `payout`, which an observer sees, with the fee; a split, a merge and a payment to someone else look the same on-chain.
+On-chain: `transact` each, as in step 7 (note tree leaves 7-10), and a `resolve`. The withdrawal pays 30 ETH from the pool to `payout`, which an observer sees, with the fee; a split, a merge and a payment to someone else look the same on-chain.
-### 11. A note below the minimum is refused
+### 12. A note below the minimum is refused
```sh
zkpool -w bob split --amounts 0.1
```
-Every output note is 0 or at least 1/3 ETH (`MIN_NOTE_VALUE`, 333,333,333,333,333,333 wei). `transfer_holder` asserts it, so no valid proof creates a smaller note; the wallet checks it before proving, to fail early and clearly.
+Every output note is 0 or at least 1/3 ETH (`MIN_NOTE_VALUE`, 333,333,333,333,333,333 wei). `transfer_holder` and `escrow_resolve` assert it, so no valid proof creates a smaller note; the wallet checks it before proving, to fail early and clearly.
```
Error: a 0.1 ETH note is below the minimum of 0.333333333333333333 ETH (a note is 0 or at least 1/3 ETH)
@@ -343,7 +377,7 @@ Error: a 0.1 ETH note is below the minimum of 0.333333333333333333 ETH (a note i
On-chain: nothing. A change of less than 1/3 ETH can't be kept either: pay it out with the rest, or leave a larger change.
-### 12. Balances
+### 13. Balances
```sh
zkpool -w alice balance
@@ -353,46 +387,42 @@ cast balance $ZKPOOL_POOL --rpc-url $ZKPOOL_RPC
```
```
-alice: shielded 34.98 ETH in 1 note(s); EOA 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 999999899.99337… ETH
-bob: shielded 34.97 ETH in 2 note(s); EOA 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 1000000029.99572… ETH
+alice: shielded 34.97 ETH in 2 note(s); EOA 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 999999899.99370… ETH
+bob: shielded 34.97 ETH in 2 note(s); EOA 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 1000000029.99499… ETH
bob: 2 note(s), 34.97 ETH
- leaf 8 14.99 ETH 0x…
+ leaf 9 14.99 ETH 0x…
leaf 10 19.98 ETH 0x…
-pool contract holds 69.95 ETH (= 34.98 + 34.97 shielded)
+pool contract holds 69.94 ETH (= 34.97 + 34.97 shielded)
```
-Alice: 100 − 60 − 5 − 2 × 0.01. Bob: 65 − 30 − 3 × 0.01. The pool holds exactly the shielded total; the five fees went to the block producers.
+Alice: 100 − 60 − 5 − 3 × 0.01 (the 2 came back). Bob: 65 − 30 − 3 × 0.01. The pool holds exactly the shielded total; the six fees went to the block producers.
### Summary
| step | call | pipeline | prove | gas | calldata | events | state written |
|---|---|---|---:|---:|---:|---|---|
-| Alice registers | `register` | `identity_register` | ≈1.7 s | 2,574,707 | 40,260 B | `IdentityRegistered` | `documentRegistered`, identity leaf 0 (first insert: filled subtrees), its root known |
-| Bob registers | `register` | `identity_register` | ≈3.9 s | 2,007,304 | 40,260 B | `IdentityRegistered` | `documentRegistered`, identity leaf 1, its root known |
-| Alice deposits 100 | `transact` | `member_transfer` | ≈1.3 s | 3,307,982 | 42,180 B | `NewNullifier` ×2, `NewCommitment` ×2, `Envelope` | 2 nullifiers, note leaves 0-1 (first insert: filled subtrees), 2 roots known; +100 ETH to the pool |
-| Alice → Bob 60 (handshake) | `transact` | `member_transfer` | ≈1.3 s | 2,772,309 | 42,180 B | the same | 2 nullifiers, note leaves 2-3, 2 roots known; fee to coinbase |
-| Alice → Bob 5 (ratchet) | `transact` | `member_transfer` | ≈1.3 s | 2,772,325 | 42,180 B | the same | 2 nullifiers, note leaves 4-5, 2 roots known; fee to coinbase |
-| Bob splits | `transact` | `member_transfer` | ≈1.3 s | 2,769,539 | 42,180 B | the same | 2 nullifiers, note leaves 6-7, 2 roots known; fee to coinbase |
-| Bob merges | `transact` | `member_transfer` | ≈1.3 s | 2,772,065 | 42,180 B | the same | 2 nullifiers, note leaves 8-9, 2 roots known; fee to coinbase |
-| Bob withdraws 30 | `transact` | `member_transfer` | ≈1.3 s | 2,776,772 | 42,180 B | the same | 2 nullifiers, note leaves 10-11, 2 roots known; fee to coinbase, 30 ETH to payout |
+| Alice registers | `register` | `identity_register` | ≈1.7 s | 2,574,895 | 40,260 B | `IdentityRegistered` | `documentRegistered`, identity leaf 0 (first insert: filled subtrees), its root known |
+| Bob registers | `register` | `identity_register` | ≈4.0 s | 2,007,864 | 40,260 B | `IdentityRegistered` | `documentRegistered`, identity leaf 1, its root known |
+| Alice deposits 100 | `transact` | `member_transfer` | ≈1.3 s | 2,690,673 | 40,580 B | `NewNullifier` ×2, `NewCommitment`, `Escrowed`, `Envelope` | 2 nullifiers, note leaf 0 (first insert: filled subtrees), its root known, escrow 1 (empty); +100 ETH to the pool |
+| Alice → Bob 60 (handshake) | `transact` | `member_transfer` | ≈1.5 s | 2,137,955 | 40,580 B | the same | 2 nullifiers, note leaf 1, escrow 2; fee to coinbase; the envelope in the mailbox |
+| Alice → Bob 5 (ratchet) | `transact` | `member_transfer` | ≈1.5 s | 2,137,824 | 40,580 B | the same | 2 nullifiers, note leaf 2, escrow 3; fee to coinbase |
+| Bob accepts 60 | `resolve` | `member_resolve` | ≈1.0 s | 1,987,491 | 40,260 B | `EscrowClosed`, `NewCommitment` | escrow 2 closed, note leaf 3 |
+| Bob accepts 5 | `resolve` | `member_resolve` | ≈0.6 s | 1,990,320 | 40,260 B | the same | escrow 3 closed, note leaf 4 |
+| Alice → Bob 2 | `transact` | `member_transfer` | ≈1.3 s | 2,134,867 | 40,580 B | as Alice's other transfers | 2 nullifiers, note leaf 5, escrow 6; fee to coinbase |
+| Bob rejects 2 | `resolve` | `member_resolve` | ≈0.8 s | 1,986,925 | 40,260 B | `EscrowClosed`, `NewCommitment` | escrow 6 closed, note leaf 6 (Alice's refund) |
+| Bob splits | `transact`, `resolve` | `member_transfer`, `member_resolve` | ≈1.3 s + 0.8 s | 2,131,981 + 1,990,418 | 40,580 + 40,260 B | the same | note leaves 7-8, escrow 7 opened and closed; fee to coinbase |
+| Bob merges | `transact` | `member_transfer` | ≈1.3 s | 2,135,013 | 40,580 B | the same | 2 nullifiers, note leaf 9, escrow 8 (empty); fee to coinbase |
+| Bob withdraws 30 | `transact` | `member_transfer` | ≈1.4 s | 2,142,483 | 40,580 B | the same | 2 nullifiers, note leaf 10, escrow 9 (empty); fee to coinbase, 30 ETH to payout |
## The precompiles
-`ZK_VERIFY` at `0x0000000000000000000000000000000000000100`: input `pipeline_root (32 bytes) ‖ proof` (a whole number of 32-byte fields). The pipeline must be one of the pinned deployment's (`identity_register`, `member_transfer`); the proof is verified under noir-zk's hiding key with the deployment root, the pipeline root and length checked, and the output is every public field ABI-encoded as `uint256[]`: `deployment_root, pipeline_root, length`, then the 32 slots, the pipeline's in order and zeros after (always 35 fields). A proof that doesn't verify reverts with the reason as bytes, after charging the gas. Gas: `1,200,000 + 3 per 32-byte word`: verification takes 17-20 ms on an Apple M5 Max (the wallet times it before each send), priced at ecrecover's rate (3,000 gas for about 50 µs, 60 Mgas/s); with the 40,224-byte input that is 1,203,771. The outcome is cached by the input's keccak256 (1,024 entries): reth runs every transaction twice, building the block and validating it, and the second run is a lookup; the gas is the same either way.
+`ZK_VERIFY` at `0x0000000000000000000000000000000000000100`: input `pipeline_root (32 bytes) ‖ proof` (a whole number of 32-byte fields). The pipeline must be one of the pinned deployment's (`identity_register`, `member_transfer`, `member_resolve`); the proof is verified under noir-zk's hiding key with the deployment root, the pipeline root and length checked, and the output is every public field ABI-encoded as `uint256[]`: `deployment_root, pipeline_root, length`, then the 32 slots, the pipeline's in order and zeros after (always 35 fields). A proof that doesn't verify reverts with the reason as bytes, after charging the gas. Gas: `1,200,000 + 3 per 32-byte word`: verification takes 17-20 ms on an Apple M5 Max (the wallet times it before each send), priced at ecrecover's rate (3,000 gas for about 50 µs, 60 Mgas/s); with the 40,224-byte input that is 1,203,771. The outcome is cached by the input's keccak256 (1,024 entries): reth runs every transaction twice, building the block and validating it, and the second run is a lookup; the gas is the same either way.
-`POSEIDON2` at `0x0000000000000000000000000000000000000101`: input `n ≥ 1` 32-byte big-endian field elements, each below the BN254 scalar modulus (otherwise it halts); returns Noir's `Poseidon2::hash(inputs, n)` (t = 4, rate 3, the same `pso-poseidon` code the circuits' Rust mirror uses). Gas: `60 + 360 per permutation`, one permutation per three inputs (a tree node: 420; the ciphertext commitment, 53 inputs: 6,540).
+`POSEIDON2` at `0x0000000000000000000000000000000000000101`: input `n ≥ 1` 32-byte big-endian field elements, each below the BN254 scalar modulus (otherwise it halts); returns Noir's `Poseidon2::hash(inputs, n)` (t = 4, rate 3, the same `pso-poseidon` code the circuits' Rust mirror uses). Gas: `60 + 360 per permutation`, one permutation per three inputs (a tree node: 420; `env_commit`, 3 inputs: 420).
-## The contracts
+## The contract
-`EmitV2Pool` (inherits `IMT`), constructor `(bytes32 deploymentRoot, bytes32 registerPipeline, bytes32 memberPipeline)` (the roots of `identity_register`, `member_transfer`); it deploys its `IdentityTree` (an `IMT` only the pool appends to).
-
-- `register(bytes proof)`: calls `ZK_VERIFY` with `registerPipeline`; the registry root is accepted; `|date − block.timestamp| ≤ 1 day`; `scope = registrationScope(epoch)` with `epoch = date / IDENTITY_EPOCH`, or, when `date` is in the epoch's last `RENEWAL_WINDOW` (1 day), `registrationScope(epoch + 1)` (then `epoch` is the next one); the document's nullifier (in that scope) is unused, then marked used; `date ≤ expiry < (epoch + 1) · IDENTITY_EPOCH`. Appends the leaf to the identity tree and emits `IdentityRegistered(bytes32 leaf, uint256 index, uint256 expiry)`.
-- `transact(bytes32 pipeline, bytes32 root, bytes32[2] nullifiers, bytes32[2] commitments, uint256 vPubIn, uint256 vPubOut, uint256 fee, address payout, bytes pqCiphertext, bytes proof) payable`: `pipeline` must be `memberPipeline` (anything else reverts `UnknownPipeline`; the argument leaves room for another pipeline); calls `ZK_VERIFY`; the identity root is one the identity tree had; compares the public fields with the calldata (the transfer's fields start at slot 3) (`cid = block.chainid`, root, N₀, N₁, C₀, C₁, vPubIn, vPubOut, fee, payout) and the deployment and pipeline roots; recomputes `ctx = H("emit-v2/ctx", cid, N₀, N₁, C₀, C₁)`; checks `ct = H("eid-envelope/latticect/v1", u ‖ v)` over the 1,536-byte ML-KEM ciphertext exactly as zk-encryption's `Ciphertext::commitment` packs it (ByteDecode₁₂, every coefficient < q, 20 coefficients per field, the first lowest); the root is one the note tree had; the nullifiers are unspent and distinct; `|date − block.timestamp| ≤ 1 day`; `msg.value = vPubIn`. Then marks both nullifiers spent, inserts both commitments, emits `NewNullifier(bytes32)` ×2, `NewCommitment(bytes32 commitment, uint256 leafIndex)` ×2 and `Envelope(bytes32 cT, bytes32[2] e, bytes32 tag, bytes32 ct, bytes pqCiphertext, bytes32[6] cNote, bytes32[6] cId)`, pays `fee` to `block.coinbase` and `vPubOut` to `payout`.
-- Owner: `addRegistryRoot(uint256)` (a ring of 8), `setDateTolerance(uint256)`, `transferOwnership(address)`.
-- Views: `currentRoot()`, `isKnownRoot(uint256)`, `nextIndex()`, `zeros(uint256)`, `EMPTY_ROOT`, `nullifierSpent(uint256)`, `isKnownRegistryRoot(uint256)`, `registryRoots(uint256)`, `ctCommitment(bytes)`, `deploymentRoot()`, `registerPipeline()`, `memberPipeline()`, `identities()` (the `IdentityTree`: `currentRoot()`, `isKnownRoot(uint256)`, `nextIndex()`, …), `registrationScope(uint256 epoch)`, `IDENTITY_EPOCH` (7 days), `RENEWAL_WINDOW` (1 day), `documentRegistered(uint256)`, `owner()`.
-- Errors: `InvalidProof(bytes)`, `OutputMismatch(string field)`, `UnknownPipeline`, `BadCiphertext`, `UnknownRoot`, `UnknownIdentityRoot`, `NullifierSpent`, `UnknownRegistryRoot`, `DateOutOfRange`, `WrongScope`, `AlreadyRegistered`, `ExpiryOutOfRange`, `ValueMismatch`, `PaymentFailed`, `NotOwner`, `TreeFull` (`IdentityTree`: `NotPool`).
-
-`IMT`: an append-only depth-32 tree over `POSEIDON2` (node `H(left, right)`, empty leaf 0, the empty subtrees' roots as constants), filled-subtree inserts, and every root it ever had kept as known (a mapping). A leaf is never removed, so a proof against an older root is as sound as one against the latest (the nullifiers stop double spends), and a proof can't go stale while other transactions land; the lookup is one storage read instead of a scan of a ring. The wallet's tree (`crates/cli/src/tree.rs`) computes the same roots, and a test checks the empty root.
+`EmitV2Pool` and its `IMT` trees are emit-protocol's: see its [contract](https://github.com/zk-experiments/emit-protocol#the-contract) section for every entry point, event, view and error. This repository deploys it (`zkpool deploy`) from `emit-protocol-abi`'s bytecode and talks to it through that crate's bindings. The wallet's tree (`crates/cli/src/tree.rs`) computes the same roots as `IMT`, and a test checks the empty root.
## The wallet
@@ -405,78 +435,46 @@ Alice: 100 − 60 − 5 − 2 × 0.01. Bob: 65 − 30 − 3 × 0.01. The pool ho
| `identity show` | the bundle again |
| `identity register [--force]` | proves `identity_register` (the document in this epoch's registration scope, read from the pool, or in the epoch's last day the next epoch's) and sends `register`; refuses if that wouldn't outlast the live registration, unless `--force`; keeps the leaf, its index, the DG1 salt, the expiry (that epoch's last second or the passport's expiry, whichever is first) and the leaf's blinding `r` (fresh from the OS CSPRNG) in the wallet; a stored registration without `r` (made before the leaf was blinded) refuses to prove membership until registered again |
| `contact add --name N --bundle B` | accepts a bundle (format, chain id, validity window, Grumpkin key on the curve, ML-KEM key and its commitment): the handshake's prerequisite |
-| `deposit --amount A [--fee F]` | vPubIn = A from the EOA, dummy inputs, a note of A − F to itself, on a throwaway channel |
-| `transfer --to N --amount A [--fee 0.01]` | spends one or two notes: A to the contact's `pk`, the change to itself; the first transfer to a contact is the handshake, later ones ratchet; the channel state is written before broadcasting |
-| `withdraw --amount A --to ADDR [--fee 0.01]` | vPubOut = A to ADDR, the change to itself |
-| `split --amounts a[,b] [--fee 0.01]` | one note into two: `a,b` needs a note worth exactly a + b + fee, `a` alone keeps the rest in the second |
-| `merge [--fee 0.01]` | the two smallest notes into one |
+| `deposit --amount A [--fee F]` | vPubIn = A from the EOA, dummy inputs, a note of A − F to itself (output 1, appended at once; output 0, escrowed, is empty), on a throwaway channel |
+| `transfer --to N --amount A [--fee 0.01]` | spends one or two notes: A to the contact's `pk` (output 0, escrowed until the contact resolves it; its refund note waits in the wallet), the change to itself; the first transfer to a contact is the handshake, later ones ratchet; the channel state is written before broadcasting, the off-chain envelope (u, v, c_id) into the mailbox before sending |
+| `withdraw --amount A --to ADDR [--fee 0.01]` | vPubOut = A to ADDR, the change to itself (output 1) |
+| `split --amounts a[,b] [--fee 0.01]` | one note into two: `a,b` needs a note worth exactly a + b + fee, `a` alone keeps the rest in the second; `a` is output 0, escrowed, so the wallet accepts its own escrow at once (a `member_resolve` proof) |
+| `merge [--fee 0.01]` | the two smallest notes into one (output 1) |
+| `escrows` | the escrows addressed here (C₀, value, handshake or ratchet index, deadline, the sender's MRZ) and those this wallet sent |
+| `resolve (--escrow C0 \| --all) [--reject] [--fee 0]` | proves `member_resolve` and sends `resolve`: accept (the note less the fee becomes this wallet's) or reject (it returns to the sender); an escrow is named by a prefix of its C₀ |
+| `refund --escrow C0` | after the window, hands an escrow this wallet sent back (its refund note returns here); anyone may, it needs no proof |
| `notes`, `balance` | the notes (leaf, value, commitment, sender's MRZ), the shielded and EOA balances |
-| `sync` | catches up from the pool's logs: the note tree, spent notes, the pending outputs, the identity tree (`IdentityRegistered`, which also gives the wallet's own leaf its index), and every Envelope through `Receiver::scan` |
+| `sync` | catches up from the pool's logs: the note tree, spent notes, the pending outputs (change, accepted escrows, refunds), the identity tree (`IdentityRegistered`, which also gives the wallet's own leaf its index), closed escrows, and every escrowed transfer whose envelope is in the mailbox (and opens `env_commit`) through `Receiver::scan`: a note addressed here becomes an escrow to resolve |
The wallet doesn't keep the trees' leaves. For each tree it keeps the frontier (the last left node at each height, as the contract's filled subtrees) and the path of each of its own leaves, and updates those paths as leaves are appended: 32 hashes per appended leaf plus a comparison per own leaf and height, and 32 fields (about 1 KB of JSON) per own leaf, whatever the tree's size. A spend reads its path; nothing is rebuilt. It still reads every leaf from the events, so it learns nothing from a server and tells none which notes are its. A spent note's path is dropped.
-| `listen [--until N] [--timeout S]` | subscribes to the pool's logs over WebSocket and syncs on each, printing received notes with the sender's MRZ |
+| `listen [--until N] [--timeout S]` | subscribes to the pool's logs over WebSocket and syncs on each, printing notes received into escrow with the sender's MRZ |
-Each transaction is proven in-process: identity_member proves the wallet's leaf under the identity tree's current root at today's date and commits DG1 afresh, the channel envelope seals it under the transfer's chain key, and `member_transfer::fold` folds the five apps. The wallet then verifies the proof locally (as `ZK_VERIFY` will) and sends `transact` from its EOA. The fee leaves the shielded value to the block producer. Dummy inputs (deposits, splits) are held by the wallet's own key with fresh nullifiers, as `transfer_holder` requires.
+Each transaction is proven in-process: identity_member proves the wallet's leaf under the identity tree's current root at today's date and commits DG1 afresh, the DG1 envelope seals it under the transfer's chain key (publishing only a commitment to the ciphertext), and `member_transfer::fold` folds the five apps. The wallet then verifies the proof locally (as `ZK_VERIFY` will) and sends `transact` from its EOA. The fee leaves the shielded value to the block producer. Dummy inputs (deposits, splits) are held by the wallet's own key with fresh nullifiers, as `transfer_holder` requires.
## Measured (Apple M5 Max, 18 cores; `scripts/demo.sh`, one run)
| transaction | passport | pipeline | prove | gas |
|---|---|---|---:|---:|
-| Alice registers | US: RSA-4096 → RSA-2048 | `identity_register` | 1.68 s | 2,574,707 |
-| Bob registers | DE: brainpoolP384 → brainpoolP256 | `identity_register` | 3.86 s | 2,007,304 |
-| Alice deposits 100 | US | `member_transfer` | 1.26 s | 3,307,982 |
-| Alice → Bob 60 (handshake) | US | `member_transfer` | 1.27 s | 2,772,309 |
-| Alice → Bob 5 (ratchet) | US | `member_transfer` | 1.27 s | 2,772,325 |
-| Bob splits 60 → 10 + 49.99 | DE | `member_transfer` | 1.27 s | 2,769,539 |
-| Bob merges 5 + 10 → 14.99 | DE | `member_transfer` | 1.27 s | 2,772,065 |
-| Bob withdraws 30 | DE | `member_transfer` | 1.27 s | 2,776,772 |
-
-A member transfer proves in about 1.27 s (1.26-1.27 s) whatever the passport. Its cost is the fold: the ML-KEM session app (50,127 gates) and the kernels (one step kernel per app, each verifying two folded proofs: `kernel_init` 12,369, `kernel_step` 25,878, `kernel_hiding` 37,233 gates). A registration costs a proof verification (1.2M), its calldata (40,260 B) and one identity-tree insert (the first one writes the tree's filled subtrees: 2.57M, later ones about 2.0M). The proving process (the wallet) peaks at about 1.0 GB for a transfer or the US registration and 1.39 GB for the DE one (`/usr/bin/time -l`).
-
-Every proof is 40,192 bytes and verifies in 16-18 ms. A transfer's 2.77M gas: 564k calldata (EIP-7623's floor, 1.38M, isn't binding), 1.20M `ZK_VERIFY`, 34k for 66 `POSEIDON2` calls (64 tree nodes, ctx, the ciphertext commitment), about 545k decoding and packing the ML-KEM ciphertext in Solidity, and the rest storage (nullifiers, tree, known roots), the Envelope event and the payments. The first deposit pays about 540k more for the tree's first writes. The known-roots mapping costs a new slot per insert (20k more than overwriting a ring slot) and saves the ring's scan (up to 32 cold reads, 67k) on every root check: transfers cost 10-30k less than with the ring. The whole demo takes about 30 s with the packs cached (first run: about 300 MB of packs).
-
-## The identity cache
-
-The passport is proved once per registration (eid's DSC, SOD and document steps: about 1 s for an RSA passport, 3.3 s for the German brainpool one). Every transaction proves membership in the on-chain identity tree instead, bound to the registered key; the receiver gets the sender's MRZ through the DG1 envelope. The pool accepts no transfer without a registration.
-
-**Circuits** (this repository, `emit-devnet@0.2.0`; `crates/circuits/noir/lib/identity_cache`, `lib/emit`):
-
-| app | family (layout) | record | gates |
-|---|---|---|---:|
-| `register` | `identity_cache/register`: link in 0 (`PayloadCommitment`), public `leaf`, `expiry` | `[payload_commitment, leaf, expiry]` | 4,202 |
-| `identity_member` | `identity_cache/member`: link out 0 (`PayloadCommitment`), public `identity_root`, `date`, `holder_tag` | `[payload_commitment, identity_root, date, holder_tag]` | 4,729 |
-| `transfer_holder` | `emit/transfer_holder`: binds `ctx` (0) and `holder_tag` (1), link out 12, the rest public | `[ctx, holder_tag, cid, root, N₀, N₁, C₀, C₁, v_in, v_out, fee, payout, note_commitment]` | 6,870 |
-
-- `register(payload_salt, dg1, sk, expiry, r)` parses the DG1 bytes with eid's own `parse_dg1` (eid-circuits v0.8.2's `eid_steps`), rebuilds the payload with eid's `plaintext` and recomputes `commit(payload_salt, payload)`, which the kernel checks equals the document step's link; asserts `expiry ≤` the passport's date of expiry (the MRZ's, last second, UTC); publishes the leaf `L = H(IDENTITY, H(PK, sk), H(payload), expiry, r)` (`IDENTITY = "emit-v2/identity/v2"`; the holder's shielded address, the six-field DG1 payload hashed, the expiry, and `r`, a uniform random blinding the wallet draws and keeps) and `expiry`. The blinding is what keeps the registration private: the holder hands the shielded address to anyone who pays them, and a payee reads the MRZ from their envelopes, so without `r` either could recompute `L` and find the registration. It is the fifth input, which costs one gate: Poseidon2 absorbs three per permutation, so four inputs and five both take two.
-- `identity_member(identity_root, date, sk, payload, payload_salt, expiry, r, index, path, ctx)` recomputes the leaf (a wrong `r` gives another leaf, not in the tree), checks its depth-32 path to `identity_root` and `date ≤ expiry`, and returns a fresh `commit(payload_salt, payload)` as its link (the DG1 envelope continues it unchanged) and the holder tag `H(HOLDER, sk, ctx)` (`HOLDER = "emit-v2/holder"`).
-- `transfer_holder` is the JoinSplit (`emit::transfer`) with `ins[0].sk = ins[1].sk` (dummies included), every output value 0 or at least `MIN_NOTE_VALUE` (1/3 ETH in wei, 104 gates), and `holder_tag = H(HOLDER, ins[0].sk, ctx)` in its record; the kernel binds it to the member's published tag, and `ctx` to the session's.
-
-**Pipelines** (the deployment has two):
-
-| pipeline | positions | apps / folded circuits | slots |
-|---|---|---|---:|
-| `identity_register` | eid/dsc, eid/sod, eid/document, identity_cache/register | 4 / 9 | 6: registry_root, date, scope, nullifier, leaf, expiry |
-| `member_transfer` | identity_cache/member, channel/session, channel/envelope, emit/transfer_holder, channel/note_envelope | 5 / 11 | 31: identity_root, date, holder_tag, then the session's, the envelope's, the transfer's and the note envelope's |
-
-(Folded circuits: the apps, a kernel per app, and the hiding kernel.)
+| Alice registers | US: RSA-4096 → RSA-2048 | `identity_register` | 1.74 s | 2,574,895 |
+| Bob registers | DE: brainpoolP384 → brainpoolP256 | `identity_register` | 4.00 s | 2,007,864 |
+| Alice deposits 100 | US | `member_transfer` | 1.30 s | 2,690,673 |
+| Alice → Bob 60 (handshake) | US | `member_transfer` | 1.31 s | 2,137,955 |
+| Alice → Bob 5 (ratchet) | US | `member_transfer` | 1.30 s | 2,137,824 |
+| Bob accepts 60 | DE | `member_resolve` | 0.80 s | 1,987,491 |
+| Bob accepts 5 | DE | `member_resolve` | 0.62 s | 1,990,320 |
+| Alice → Bob 2 | US | `member_transfer` | 1.31 s | 2,134,867 |
+| Bob rejects 2 | DE | `member_resolve` | 0.81 s | 1,986,925 |
+| Bob splits 60 → 10 + 49.99 (+ accepts his 10) | DE | `member_transfer`, `member_resolve` | 1.31 s + 0.63 s | 2,131,981 + 1,990,418 |
+| Bob merges 5 + 10 → 14.99 | DE | `member_transfer` | 1.32 s | 2,135,013 |
+| Bob withdraws 30 | DE | `member_transfer` | 1.35 s | 2,142,483 |
-**Design decisions:**
+A member transfer proves in about 1.3 s whatever the passport (the DG1 envelope app is now smaller, 339 gates against the channel envelope's). Its cost is the fold: the ML-KEM session app (53,381 gates) and the kernels (one step kernel per app, each verifying two folded proofs). A resolve folds two apps (membership and the 3,025-gate `escrow_resolve`) and proves in under a second. A registration costs a proof verification (1.2M), its calldata (40,260 B) and one identity-tree insert (the first one writes the tree's filled subtrees: 2.58M, later ones about 2.0M).
-- *Transactions require a registration.* The deployment has two pipelines, `identity_register` and `member_transfer`, and `transact` accepts only `member_transfer`. The registration is what binds a shielded key to a passport; a transfer proof carries membership of that key, so the notes' owner is the registered holder.
-- *Expiry from the MRZ.* `register` takes the 95-byte DG1 buffer as its private input, rebuilds the payload from it (the kernel checks its commitment against the document step's link; `pack_be` is injective, so the bytes are pinned) and reads the date of expiry as the document step does. `expiry` is at most that date; the chain caps it at the end of the registration's epoch. The register app is 4,202 gates.
-- *Holder binding.* `identity_member` takes `ctx` as a private input and publishes `holder_tag = H(HOLDER, sk, ctx)`; the kernel binds `transfer_holder`'s tag to it and `transfer_holder`'s `ctx` to the session's. Equal tags mean equal `(sk, ctx)`, so the member's key is the spender's and its `ctx` the transfer's. Membership is the pipeline's first app. The wallet computes `ctx` from the nullifiers and commitments before proving. `ctx` is unique per transfer, so tags don't link transfers.
-- *One key per transfer.* `transfer_holder` requires both inputs held by one key and tags that key. The wallet makes dummy inputs with its own key and a fresh `rho` (a fresh nullifier). A transfer can't spend another key's note alongside the holder's.
-- *Minimum note value.* Every output of `transfer_holder` is 0 or at least `MIN_NOTE_VALUE` (1/3 of the native coin, in wei). Note values are private, so the circuit enforces it; the contract can't.
-- *Registration scope.* The document step's scope is `registrationScope(epoch)`, `keccak256("emit-v2/register", chainid, pool, epoch) mod p`, `epoch = date / 7 days`, and the contract records the document's nullifier in that scope: one registration per passport per epoch, and `expiry < (epoch + 1) · 7 days`. In an epoch's last day (`RENEWAL_WINDOW`) the next epoch's scope is also accepted, with expiry up to that epoch's end. A passport has at most one live registration per pool, two during that day.
-- *Contracts.* The identity tree is a separate contract (`IdentityTree is IMT`, created by the pool); `IMT` keeps its state in contract storage. `transact` takes the pipeline root as its first argument. The chain checks the proof's date against the block time; the circuit checks the registration's expiry against that date.
+Every proof is 40,192 bytes and verifies in 16-18 ms. A transfer's 2.13-2.14M gas: calldata of 40,580 B (the proof and the transfer's fields; the 1,536-byte ciphertext is gone), 1.20M `ZK_VERIFY`, 34 `POSEIDON2` calls (32 tree nodes, ctx, `env_commit`), and the rest storage (nullifiers, a tree insert, a known root, the escrow), the events and the payments: about 640k less than when the pool decoded and packed the ML-KEM ciphertext in Solidity and appended both outputs. A resolve costs about 1.99M (a proof verification, a tree insert, the escrow closed). The first deposit pays about 550k more for the tree's first writes. The whole demo takes about 45 s with the packs cached (first run: about 300 MB of packs).
-**Soundness notes:**
+## The circuits and the escrow
-- *Revocation.* A registration is checked against the CSCA registry once, when it is made; revoking the DSC or CSCA afterwards takes effect only when the registration expires, at the end of its epoch (at most 7 days, plus the 1-day date tolerance, as a member proof's date may lag the block by a day). Likewise the passport's own expiry: a registration made on its last day ends with it.
-- *Privacy.* A registration is public: its leaf, its expiry, the document's nullifier in the epoch's scope (in the `identity_register` proof's public fields), and, in this devnet, the EOA that sent it and when. The leaf is blinded by `r`: knowing the holder's shielded address (given to everyone who pays them) and MRZ (read by every payee from the DG1 envelope) doesn't let anyone recompute the leaf and find the registration; only the wallet holding `r` and `sk` can. The nullifier is eid's document nullifier in the epoch's scope, `H("eid-nullifier/v1", scope, SOD messageDigest)`: unlinkable across epochs and pools. The MRZ alone doesn't give it, but whoever holds the passport's SOD (anyone who has read its chip; with the passport in hand, the MRZ opens the chip) can compute it for the public scope and find the registration; the blinding doesn't cover it. Later transfers don't reveal which leaf they prove: a member proof publishes only the identity root, the date and a holder tag that changes with every ctx. The anonymity set is the tree's leaves at that root; which root a proof uses dates it roughly (the wallet uses the latest). Losing `r` makes the registration unusable, and the scoped nullifier blocks registering the passport again before the next epoch (or its last day).
-- *A leaked `sk`* lets its holder spend the notes and prove membership as the registered identity (the MRZ goes to receivers under that name) until the registration expires: the same as losing the notes.
-- *One passport, many holder keys:* prevented within an epoch by the scoped nullifier, except for the renewal's overlap: in an epoch's last day a passport can hold its current registration under one key and the next epoch's under another. Two users sharing a passport (the devnet's fixtures allow it) can't both register in the same epoch.
-- *Front-running.* A register proof commits to its leaf; anyone may submit it, with the same effect.
+The apps, pipelines, design decisions and soundness notes (the identity cache, the holder binding, the minimum note, the registration scope) and the escrow (the off-chain envelope, screening then resolving, the refund, the event sequence) are emit-protocol's: see its [circuits](https://github.com/zk-experiments/emit-protocol#the-circuits) and [escrow](https://github.com/zk-experiments/emit-protocol#the-escrow) sections. What is this repository's: the wallet's conventions on top of them (self-transfers keep their note in output 1, a split accepts its own escrow at once) and the mailbox below.
## What is devnet-only
@@ -484,12 +482,15 @@ The passport is proved once per registration (eid's DSC, SOD and document steps:
- **Registry roots set by the owner.** The pool accepts the csca-registry roots its owner adds (the deploy adds the fixtures' root and the pinned published one).
- **Synthetic passports.** The six fixtures (US, DE, FR, IT, NL, ES) are mock documents signed by mock CSCAs; the pool accepts their registry's root. Two users may even share a passport.
- **Dev keys and chain.** Well-known keys, chain id 3607, instant blocks, one node; the datadir is thrown away by `scripts/devnet.sh`.
+- **The mailbox.** The off-chain envelopes live in a directory the wallets share (`/mailbox`), written by the sender and deleted when the escrow closes; outside the devnet the envelope-inbox service carries them (a deposit keyed by `C_t`, fetched with a token only the two parties know, deleted by its indexer when the escrow closes).
+- **Resolves from the EOA.** A resolve is sent from the owner's EOA like every transaction here; its fee argument (paid from the note to `block.coinbase`) is what a relayer would be paid with.
## Gaps
- A registration can't be revoked early (no per-leaf revocation list); the epoch bounds it (7 days, plus a day of date tolerance).
- eid's scoped nullifier can be recomputed by anyone holding the passport's SOD, who can then find its registration (see the privacy note above).
-- The ciphertext commitment is packed in Solidity (about 545k gas).
+- Output 0 of a deposit, merge or withdrawal is an empty escrow nobody resolves (it is refundable, to a zero-value note): to an observer, an escrow that is never resolved hints at a self-transfer. A wallet that resolves (rejects) its own empty escrows too would hide it, at a proof each.
+- The receiver must resolve within the window, or the sender (or anyone) can refund it.
- `ZK_VERIFY`'s gas constant is from one machine.
- A failed ratchet transaction leaves its chain index consumed: its calldata is public even when it reverts, so reusing the key on other plaintext would be key reuse; the receiver's window of 8 absorbs the gap. A failed handshake restarts the contact's channel.
- The Foundry tests mock both precompiles (Foundry's EVM can't load them); the real hash and verifier are exercised by the devnet test and the demo.
diff --git a/contracts/foundry.toml b/contracts/foundry.toml
deleted file mode 100644
index 4d80284..0000000
--- a/contracts/foundry.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[profile.default]
-src = "src"
-out = "out"
-libs = ["lib"]
-solc = "0.8.30"
-evm_version = "prague"
-via_ir = true
-optimizer = true
-optimizer_runs = 200
diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std
deleted file mode 160000
index 8bbcf6e..0000000
--- a/contracts/lib/forge-std
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824
diff --git a/contracts/script/Deploy.s.sol b/contracts/script/Deploy.s.sol
deleted file mode 100644
index 6912086..0000000
--- a/contracts/script/Deploy.s.sol
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.28;
-
-import {Script, console} from "forge-std/Script.sol";
-import {EmitV2Pool} from "../src/EmitV2Pool.sol";
-
-/// Deploys the pool for the pinned deployment and accepts the given csca-registry roots.
-/// Env: DEPLOYMENT_ROOT, REGISTER_PIPELINE (identity_register), MEMBER_PIPELINE (member_transfer)
-/// (pins.toml; `zkpool info` prints them), REGISTRY_ROOTS
-/// (comma-separated: the synthetic fixtures' root and the published registry's).
-contract Deploy is Script {
- function run() external returns (EmitV2Pool pool) {
- uint256[] memory registries = vm.envUint("REGISTRY_ROOTS", ",");
- vm.startBroadcast();
- pool = new EmitV2Pool(
- vm.envBytes32("DEPLOYMENT_ROOT"), vm.envBytes32("REGISTER_PIPELINE"), vm.envBytes32("MEMBER_PIPELINE")
- );
- for (uint256 i = 0; i < registries.length; i++) {
- pool.addRegistryRoot(registries[i]);
- }
- vm.stopBroadcast();
- console.log("EmitV2Pool", address(pool));
- }
-}
diff --git a/contracts/src/EmitV2Pool.sol b/contracts/src/EmitV2Pool.sol
deleted file mode 100644
index 743b049..0000000
--- a/contracts/src/EmitV2Pool.sol
+++ /dev/null
@@ -1,330 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.28;
-
-import {IMT, Poseidon2} from "./IMT.sol";
-
-/// @notice The identity cache's tree: an IMT of registration leaves the pool appends to.
-contract IdentityTree is IMT {
- address public immutable pool;
-
- error NotPool();
-
- constructor() {
- pool = msg.sender;
- }
-
- function insert(uint256 leaf) external returns (uint256) {
- if (msg.sender != pool) revert NotPool();
- return _insert(leaf);
- }
-}
-
-/// @notice The Emit V2 private note pool: `register` takes a proof of the `identity_register` pipeline (the
-/// passport's eid steps, then the registration) and appends its leaf to the identity tree; one entry point,
-/// `transact`, for deposits, private transfers and withdrawals, each a folded proof of `member_transfer`
-/// (membership in the identity tree, the channel session, DG1 sealed, the 2-in / 2-out JoinSplit bound to the
-/// registered key, its note opening sealed), verified by the ZK_VERIFY precompile. Only registered holders
-/// transact; every note is 0 or at least 1/3 of the native coin (the transfer circuit checks it). The checks are the design's "What the chain checks"
-/// (emit-v2-transfer-mechanism.md §5) and the identity cache's (README).
-contract EmitV2Pool is IMT {
- address internal constant ZK_VERIFY = address(0x0100);
-
- /// Poseidon2 domains (the ASCII tag as a big-endian integer).
- uint256 internal constant D_CTX = 0x656d69742d76322f637478; // "emit-v2/ctx"
- uint256 internal constant D_LATTICE_CT = 0x6569642d656e76656c6f70652f6c61747469636563742f7631; // "eid-envelope/latticect/v1"
- /// The BN254 scalar field's modulus.
- uint256 internal constant P = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
-
- /// The ML-KEM-768 ciphertext (u, v) as ByteEncode_12: 1024 coefficients mod q.
- uint256 public constant PQ_CIPHERTEXT_BYTES = 1536;
- uint256 internal constant Q = 3329;
-
- /// The public fields of a proof: deployment root, pipeline root, length, then the 32 slots, the pipeline's
- /// first and zeros after (circuits/manifest.toml; `Outputs::from_fields` in the generated code).
- uint256 internal constant FIELDS = 35;
- uint256 internal constant F_DEPLOYMENT = 0;
- uint256 internal constant F_PIPELINE = 1;
- /// member_transfer: identity root, date, holder tag, then the transfer's fields from 6 (31 slots).
- uint256 internal constant F_IDENTITY_ROOT = 3;
- uint256 internal constant F_DATE = 4;
- uint256 internal constant MEMBER_AT = 6;
- /// The transfer's fields, from its first (session, DG1 envelope, transfer, note envelope).
- uint256 internal constant T_CTX = 0;
- uint256 internal constant T_C_T = 1;
- uint256 internal constant T_E_X = 2;
- uint256 internal constant T_E_Y = 3;
- uint256 internal constant T_TAG = 4;
- uint256 internal constant T_CT = 5;
- uint256 internal constant T_C_ID = 6; // 6..11
- uint256 internal constant T_CID = 12;
- uint256 internal constant T_ROOT = 13;
- uint256 internal constant T_N0 = 14;
- uint256 internal constant T_C0 = 16;
- uint256 internal constant T_V_IN = 18;
- uint256 internal constant T_V_OUT = 19;
- uint256 internal constant T_FEE = 20;
- uint256 internal constant T_PAYOUT = 21;
- uint256 internal constant T_C_NOTE = 22; // 22..27
- /// identity_register: registry root, date, scope, nullifier, leaf, expiry.
- uint256 internal constant F_REGISTRY_ROOT = 3;
- uint256 internal constant F_SCOPE = 5;
- uint256 internal constant F_NULLIFIER = 6;
- uint256 internal constant F_LEAF = 7;
- uint256 internal constant F_EXPIRY = 8;
-
- uint256 public constant REGISTRY_RING = 8;
-
- bytes32 public immutable deploymentRoot;
- /// The pipelines accepted: identity_register, member_transfer.
- bytes32 public immutable registerPipeline;
- bytes32 public immutable memberPipeline;
- /// The identity cache's tree.
- IdentityTree public immutable identities;
- /// Registrations last until the end of their epoch at most (unix time / IDENTITY_EPOCH): the longest a
- /// revoked passport keeps transacting, so it should match the registry's revocation latency.
- uint256 public constant IDENTITY_EPOCH = 7 days;
- /// In an epoch's last RENEWAL_WINDOW, a holder may register for the next epoch ahead, so a registration
- /// made late in an epoch isn't cut short.
- uint256 public constant RENEWAL_WINDOW = 1 days;
- address public owner;
- uint256 public dateTolerance = 1 days;
- uint256[REGISTRY_RING] public registryRoots;
- uint256 public registryIndex;
- mapping(uint256 => bool) public nullifierSpent;
- /// Document nullifiers (each in its epoch's registrationScope) already registered.
- mapping(uint256 => bool) public documentRegistered;
-
- event NewNullifier(bytes32 nullifier);
- event NewCommitment(bytes32 commitment, uint256 leafIndex);
- event Envelope(
- bytes32 cT, bytes32[2] e, bytes32 tag, bytes32 ct, bytes pqCiphertext, bytes32[6] cNote, bytes32[6] cId
- );
- event IdentityRegistered(bytes32 leaf, uint256 index, uint256 expiry);
- event RegistryRoot(uint256 root);
-
- error NotOwner();
- error InvalidProof(bytes reason);
- error OutputMismatch(string field);
- error UnknownPipeline();
- error BadCiphertext();
- error UnknownRoot();
- error UnknownIdentityRoot();
- error NullifierSpent();
- error UnknownRegistryRoot();
- error DateOutOfRange();
- error WrongScope();
- error AlreadyRegistered();
- error ExpiryOutOfRange();
- error ValueMismatch();
- error PaymentFailed();
-
- constructor(bytes32 deploymentRoot_, bytes32 registerPipeline_, bytes32 memberPipeline_) {
- deploymentRoot = deploymentRoot_;
- registerPipeline = registerPipeline_;
- memberPipeline = memberPipeline_;
- identities = new IdentityTree();
- owner = msg.sender;
- }
-
- modifier onlyOwner() {
- if (msg.sender != owner) revert NotOwner();
- _;
- }
-
- /// @notice Accepts a csca-registry root (a ring of the last REGISTRY_RING). Devnet: the owner sets them;
- /// on a real chain an eid module holds them.
- function addRegistryRoot(uint256 root) external onlyOwner {
- registryIndex = (registryIndex + 1) % REGISTRY_RING;
- registryRoots[registryIndex] = root;
- emit RegistryRoot(root);
- }
-
- function setDateTolerance(uint256 seconds_) external onlyOwner {
- dateTolerance = seconds_;
- }
-
- function transferOwnership(address to) external onlyOwner {
- owner = to;
- }
-
- function isKnownRegistryRoot(uint256 root) public view returns (bool) {
- if (root == 0) return false;
- for (uint256 i = 0; i < REGISTRY_RING; i++) {
- if (registryRoots[i] == root) return true;
- }
- return false;
- }
-
- /// @notice The nullifier scope of a registration dated in `epoch`: one per pool and epoch.
- function registrationScope(uint256 epoch) public view returns (uint256) {
- return uint256(keccak256(abi.encode("emit-v2/register", block.chainid, address(this), epoch))) % P;
- }
-
- /// @notice Registers a passport's holder in the identity cache: a proof of identity_register (the document
- /// in the scope of its date's epoch, then the leaf and its expiry). The document's nullifier in that scope is
- /// used once and the registration ends with the epoch (and, as the circuit checks, at most at the passport's
- /// expiry). In the epoch's last RENEWAL_WINDOW the document may instead be in the next epoch's scope, the
- /// registration then lasting to that epoch's end. So a passport has at most one live registration per pool,
- /// two while a renewal overlaps the current one's last day.
- function register(bytes calldata proof) external {
- uint256[] memory f = _verify(registerPipeline, proof);
- if (!isKnownRegistryRoot(f[F_REGISTRY_ROOT])) revert UnknownRegistryRoot();
- uint256 date = f[F_DATE];
- _checkDate(date);
- uint256 epoch = date / IDENTITY_EPOCH;
- if (f[F_SCOPE] != registrationScope(epoch)) {
- bool renewing = date + RENEWAL_WINDOW >= (epoch + 1) * IDENTITY_EPOCH;
- if (!renewing || f[F_SCOPE] != registrationScope(epoch + 1)) revert WrongScope();
- epoch += 1;
- }
- uint256 nullifier = f[F_NULLIFIER];
- if (documentRegistered[nullifier]) revert AlreadyRegistered();
- uint256 expiry = f[F_EXPIRY];
- if (expiry < date || expiry >= (epoch + 1) * IDENTITY_EPOCH) revert ExpiryOutOfRange();
- documentRegistered[nullifier] = true;
- uint256 leaf = f[F_LEAF];
- emit IdentityRegistered(bytes32(leaf), identities.insert(leaf), expiry);
- }
-
- /// @notice Spends two notes (nullifiers) and creates two (commitments). A deposit has vPubIn = msg.value
- /// and dummy inputs; a withdrawal pays vPubOut to `payout`; the fee goes to the block producer. `pipeline`
- /// is member_transfer's root (the only one accepted; the argument leaves room for another).
- function transact(
- bytes32 pipeline,
- bytes32 root,
- bytes32[2] calldata nullifiers,
- bytes32[2] calldata commitments,
- uint256 vPubIn,
- uint256 vPubOut,
- uint256 fee,
- address payout,
- bytes calldata pqCiphertext,
- bytes calldata proof
- ) external payable {
- if (pipeline != memberPipeline) revert UnknownPipeline();
- uint256 t = MEMBER_AT;
- uint256[] memory f = _verify(pipeline, proof);
- // The registration's expiry is checked in the circuit against this date.
- if (!identities.isKnownRoot(f[F_IDENTITY_ROOT])) revert UnknownIdentityRoot();
- _checkDate(f[F_DATE]);
-
- // The proof's public fields are the calldata's.
- _eq(f[t + T_CID], block.chainid, "cid");
- _eq(f[t + T_ROOT], uint256(root), "root");
- _eq(f[t + T_N0], uint256(nullifiers[0]), "n0");
- _eq(f[t + T_N0 + 1], uint256(nullifiers[1]), "n1");
- _eq(f[t + T_C0], uint256(commitments[0]), "c0");
- _eq(f[t + T_C0 + 1], uint256(commitments[1]), "c1");
- _eq(f[t + T_V_IN], vPubIn, "vPubIn");
- _eq(f[t + T_V_OUT], vPubOut, "vPubOut");
- _eq(f[t + T_FEE], fee, "fee");
- _eq(f[t + T_PAYOUT], uint256(uint160(payout)), "payout");
- // ctx isn't calldata: recomputed from the transfer's own fields.
- _eq(f[t + T_CTX], _ctx(nullifiers, commitments), "ctx");
- // ct = H(pqCiphertext), as the session app commits it.
- _eq(f[t + T_CT], _ctCommitment(pqCiphertext), "ct");
-
- // The pool's state.
- if (!isKnownRoot(uint256(root))) revert UnknownRoot();
- if (
- nullifiers[0] == nullifiers[1] || nullifierSpent[uint256(nullifiers[0])]
- || nullifierSpent[uint256(nullifiers[1])]
- ) revert NullifierSpent();
- if (msg.value != vPubIn) revert ValueMismatch();
-
- // Effects.
- for (uint256 i = 0; i < 2; i++) {
- nullifierSpent[uint256(nullifiers[i])] = true;
- emit NewNullifier(nullifiers[i]);
- }
- for (uint256 i = 0; i < 2; i++) {
- emit NewCommitment(commitments[i], _insert(uint256(commitments[i])));
- }
- _envelope(f, t, pqCiphertext);
-
- // Value leaving the pool: the fee to the block producer, vPubOut to the payout account.
- _pay(block.coinbase, fee);
- _pay(payout, vPubOut);
- }
-
- function _checkDate(uint256 date) internal view {
- if (date + dateTolerance < block.timestamp || date > block.timestamp + dateTolerance) {
- revert DateOutOfRange();
- }
- }
-
- function _verify(bytes32 pipeline, bytes calldata proof) internal view returns (uint256[] memory f) {
- (bool ok, bytes memory ret) = ZK_VERIFY.staticcall(abi.encodePacked(pipeline, proof));
- if (!ok) revert InvalidProof(ret);
- f = abi.decode(ret, (uint256[]));
- if (f.length != FIELDS) revert InvalidProof("fields");
- _eq(f[F_DEPLOYMENT], uint256(deploymentRoot), "deployment root");
- _eq(f[F_PIPELINE], uint256(pipeline), "pipeline root");
- }
-
- function _envelope(uint256[] memory f, uint256 t, bytes calldata pqCiphertext) internal {
- bytes32[6] memory cNote;
- bytes32[6] memory cId;
- for (uint256 i = 0; i < 6; i++) {
- cNote[i] = bytes32(f[t + T_C_NOTE + i]);
- cId[i] = bytes32(f[t + T_C_ID + i]);
- }
- emit Envelope(
- bytes32(f[t + T_C_T]),
- [bytes32(f[t + T_E_X]), bytes32(f[t + T_E_Y])],
- bytes32(f[t + T_TAG]),
- bytes32(f[t + T_CT]),
- pqCiphertext,
- cNote,
- cId
- );
- }
-
- function _pay(address to, uint256 amount) internal {
- if (amount == 0) return;
- (bool ok,) = payable(to).call{value: amount}("");
- if (!ok) revert PaymentFailed();
- }
-
- function _eq(uint256 got, uint256 want, string memory field) internal pure {
- if (got != want) revert OutputMismatch(field);
- }
-
- /// H("emit-v2/ctx", cid, N0, N1, C0, C1).
- function _ctx(bytes32[2] calldata n, bytes32[2] calldata c) internal view returns (uint256) {
- uint256[] memory xs = new uint256[](6);
- xs[0] = D_CTX;
- xs[1] = block.chainid;
- xs[2] = uint256(n[0]);
- xs[3] = uint256(n[1]);
- xs[4] = uint256(c[0]);
- xs[5] = uint256(c[1]);
- return Poseidon2.hash(xs);
- }
-
- /// Poseidon2("eid-envelope/latticect/v1", u ‖ v as canonical 12-bit coefficients, 20 per field, the
- /// first coefficient lowest): zk-encryption's `Ciphertext::commitment`. The bytes are ByteDecode_12'd and
- /// every coefficient must be < q.
- function ctCommitment(bytes calldata ct) external view returns (uint256) {
- return _ctCommitment(ct);
- }
-
- function _ctCommitment(bytes calldata ct) internal view returns (uint256) {
- if (ct.length != PQ_CIPHERTEXT_BYTES) revert BadCiphertext();
- uint256[] memory xs = new uint256[](53);
- xs[0] = D_LATTICE_CT;
- // 1024 coefficients, two per three bytes; field k holds coefficients 20k .. 20k + 19.
- for (uint256 j = 0; j < 512; j++) {
- uint256 b0 = uint8(ct[3 * j]);
- uint256 b1 = uint8(ct[3 * j + 1]);
- uint256 b2 = uint8(ct[3 * j + 2]);
- uint256 c0 = b0 | ((b1 & 0x0f) << 8);
- uint256 c1 = (b1 >> 4) | (b2 << 4);
- if (c0 >= Q || c1 >= Q) revert BadCiphertext();
- uint256 i = 2 * j;
- xs[1 + i / 20] |= c0 << (12 * (i % 20));
- xs[1 + (i + 1) / 20] |= c1 << (12 * ((i + 1) % 20));
- }
- return Poseidon2.hash(xs);
- }
-}
diff --git a/contracts/src/IMT.sol b/contracts/src/IMT.sol
deleted file mode 100644
index dce52cb..0000000
--- a/contracts/src/IMT.sol
+++ /dev/null
@@ -1,107 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.28;
-
-/// @notice Calls to the devnet's POSEIDON2 precompile: Noir's `Poseidon2::hash(inputs, n)` over BN254
-/// (the t = 4 sponge the circuits use).
-library Poseidon2 {
- function hash(uint256[] memory xs) internal view returns (uint256 h) {
- assembly ("memory-safe") {
- let ok := staticcall(gas(), 0x0101, add(xs, 0x20), mul(mload(xs), 0x20), 0x00, 0x20)
- if iszero(and(ok, eq(returndatasize(), 0x20))) { revert(0, 0) }
- h := mload(0x00)
- }
- }
-
- function hash2(uint256 a, uint256 b) internal view returns (uint256 h) {
- assembly ("memory-safe") {
- mstore(0x00, a)
- mstore(0x20, b)
- let ok := staticcall(gas(), 0x0101, 0x00, 0x40, 0x00, 0x20)
- if iszero(and(ok, eq(returndatasize(), 0x20))) { revert(0, 0) }
- h := mload(0x00)
- }
- }
-}
-
-/// @notice An append-only Merkle tree of depth 32 over Poseidon2 (node = H(left, right), empty leaf 0). Every
-/// root it ever had stays known: a leaf is never removed, so a proof against an older root is as sound as one
-/// against the latest (spends are stopped by their nullifiers), and a proof can't go stale while others land.
-/// The wallet's tree (crates/cli/src/tree.rs) is the same.
-abstract contract IMT {
- uint256 public constant DEPTH = 32;
-
- /// The root of the empty tree: zeros(32).
- uint256 public constant EMPTY_ROOT = 0x0b59baa35b9dc267744f0ccb4e3b0255c1fc512460d91130c6bc19fb2668568d;
- /// filled[i]: the last left node at height i.
- uint256[DEPTH] internal filled;
- uint256 public currentRoot = EMPTY_ROOT;
- mapping(uint256 => bool) internal knownRoot;
- uint256 public nextIndex;
-
- error TreeFull();
-
- constructor() {
- knownRoot[EMPTY_ROOT] = true;
- }
-
- /// The root of an empty subtree of height i: zeros(0) = 0, zeros(i + 1) = H(zeros(i), zeros(i)).
- /// Constants (computed with the POSEIDON2 precompile; the wallet's tree test checks the root).
- function zeros(uint256 i) public pure returns (uint256) {
- if (i == 0) return 0;
- if (i == 1) return 0x0b63a53787021a4a962a452c2921b3663aff1ffd8d5510540f8e659e782956f1;
- if (i == 2) return 0x0e34ac2c09f45a503d2908bcb12f1cbae5fa4065759c88d501c097506a8b2290;
- if (i == 3) return 0x21f9172d72fdcdafc312eee05cf5092980dda821da5b760a9fb8dbdf607c8a20;
- if (i == 4) return 0x2373ea368857ec7af97e7b470d705848e2bf93ed7bef142a490f2119bcf82d8e;
- if (i == 5) return 0x120157cfaaa49ce3da30f8b47879114977c24b266d58b0ac18b325d878aafddf;
- if (i == 6) return 0x01c28fe1059ae0237b72334700697bdf465e03df03986fe05200cadeda66bd76;
- if (i == 7) return 0x2d78ed82f93b61ba718b17c2dfe5b52375b4d37cbbed6f1fc98b47614b0cf21b;
- if (i == 8) return 0x067243231eddf4222f3911defbba7705aff06ed45960b27f6f91319196ef97e1;
- if (i == 9) return 0x1849b85f3c693693e732dfc4577217acc18295193bede09ce8b97ad910310972;
- if (i == 10) return 0x2a775ea761d20435b31fa2c33ff07663e24542ffb9e7b293dfce3042eb104686;
- if (i == 11) return 0x0f320b0703439a8114f81593de99cd0b8f3b9bf854601abb5b2ea0e8a3dda4a7;
- if (i == 12) return 0x0d07f6e7a8a0e9199d6d92801fff867002ff5b4808962f9da2ba5ce1bdd26a73;
- if (i == 13) return 0x1c4954081e324939350febc2b918a293ebcdaead01be95ec02fcbe8d2c1635d1;
- if (i == 14) return 0x0197f2171ef99c2d053ee1fb5ff5ab288d56b9b41b4716c9214a4d97facc4c4a;
- if (i == 15) return 0x2b9cdd484c5ba1e4d6efcc3f18734b5ac4c4a0b9102e2aeb48521a661d3feee9;
- if (i == 16) return 0x14f44d672eb357739e42463497f9fdac46623af863eea4d947ca00a497dcdeb3;
- if (i == 17) return 0x071d7627ae3b2eabda8a810227bf04206370ac78dbf6c372380182dbd3711fe3;
- if (i == 18) return 0x2fdc08d9fe075ac58cb8c00f98697861a13b3ab6f9d41a4e768f75e477475bf5;
- if (i == 19) return 0x20165fe405652104dceaeeca92950aa5adc571b8cafe192878cba58ff1be49c5;
- if (i == 20) return 0x1c8c3ca0b3a3d75850fcd4dc7bf1e3445cd0cfff3ca510630fd90b47e8a24755;
- if (i == 21) return 0x1f0c1a8fb16b0d2ac9a146d7ae20d8d179695a92a79ed66fc45d9da4532459b3;
- if (i == 22) return 0x038146ec5a2573e1c30d2fb32c66c8440f426fbd108082df41c7bebd1d521c30;
- if (i == 23) return 0x17d3d12b17fe762de4b835b2180b012e808816a7f2ff69ecb9d65188235d8fd4;
- if (i == 24) return 0x0e1a6b7d63a6e5a9e54e8f391dd4e9d49cdfedcbc87f02cd34d4641d2eb30491;
- if (i == 25) return 0x09244eec34977ff795fc41036996ce974136377f521ac8eb9e04642d204783d2;
- if (i == 26) return 0x1646d6f544ec36df9dc41f778a7ef1690a53c730b501471b6acd202194a7e8e9;
- if (i == 27) return 0x064769603ba3f6c41f664d266ecb9a3a0f6567cd3e48b40f34d4894ee4c361b3;
- if (i == 28) return 0x1595bb3cd19f84619dc2e368175a88d8627a7439eda9397202cdb1167531fd3f;
- if (i == 29) return 0x2a529be462b81ca30265b558763b1498289c9d88277ab14f0838cb1fce4b472c;
- if (i == 30) return 0x0c08da612363088ad0bbc78abd233e8ace4c05a56fdabdd5e5e9b05e428bdaee;
- if (i == 31) return 0x14748d0241710ef47f54b931ac5a58082b1d56b0f0c30d55fb71a6e8c9a6be14;
- revert TreeFull();
- }
-
- function _insert(uint256 leaf) internal returns (uint256 index) {
- index = nextIndex;
- if (index >= 1 << DEPTH) revert TreeFull();
- uint256 node = leaf;
- uint256 i = index;
- for (uint256 h = 0; h < DEPTH; h++) {
- if (i & 1 == 0) {
- filled[h] = node;
- node = Poseidon2.hash2(node, zeros(h));
- } else {
- node = Poseidon2.hash2(filled[h], node);
- }
- i >>= 1;
- }
- nextIndex = index + 1;
- currentRoot = node;
- knownRoot[node] = true;
- }
-
- function isKnownRoot(uint256 root) public view returns (bool) {
- return knownRoot[root];
- }
-}
diff --git a/contracts/test/EmitV2Pool.t.sol b/contracts/test/EmitV2Pool.t.sol
deleted file mode 100644
index 24ce378..0000000
--- a/contracts/test/EmitV2Pool.t.sol
+++ /dev/null
@@ -1,455 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.28;
-
-import {Test} from "forge-std/Test.sol";
-import {EmitV2Pool, IdentityTree} from "../src/EmitV2Pool.sol";
-
-/// Stands in for ZK_VERIFY: returns the fields it was given (or reverts), whatever the proof.
-contract MockVerifier {
- uint256[] public fields;
- bool public fail;
-
- function set(uint256[] calldata f, bool fail_) external {
- fields = f;
- fail = fail_;
- }
-
- fallback(bytes calldata) external returns (bytes memory) {
- require(!fail, "proof does not verify");
- return abi.encode(fields);
- }
-}
-
-/// Stands in for POSEIDON2: keccak256 of the input, reduced mod p (the contract logic doesn't depend on
-/// which hash it is; the devnet end to end checks the real one against the wallet).
-contract MockPoseidon {
- fallback(bytes calldata input) external returns (bytes memory) {
- require(input.length > 0 && input.length % 32 == 0, "words");
- return abi.encode(uint256(keccak256(input)) % 21888242871839275222246405745257275088548364400416422507208617);
- }
-}
-
-contract EmitV2PoolTest is Test {
- address constant VERIFY = address(0x0100);
- address constant POSEIDON = address(0x0101);
- bytes32 constant DEPLOYMENT = bytes32(uint256(0xd0));
- bytes32 constant REGISTER = bytes32(uint256(0xa2));
- bytes32 constant MEMBER = bytes32(uint256(0xa3));
- uint256 constant REGISTRY = 0x1234;
-
- EmitV2Pool pool;
- bytes ct;
- address payout = address(0xB0B);
- address producer = address(0xC0FFEE);
-
- struct Call {
- bytes32 pipeline;
- bytes32 root;
- bytes32[2] n;
- bytes32[2] c;
- uint256 vIn;
- uint256 vOut;
- uint256 fee;
- address payout;
- }
-
- function setUp() public {
- vm.etch(VERIFY, address(new MockVerifier()).code);
- vm.etch(POSEIDON, address(new MockPoseidon()).code);
- pool = new EmitV2Pool(DEPLOYMENT, REGISTER, MEMBER);
- pool.addRegistryRoot(REGISTRY);
- ct = new bytes(1536);
- for (uint256 i = 0; i < 1536; i++) {
- // canonical coefficients: every third byte's high nibble and the others small
- ct[i] = bytes1(uint8(i % 3 == 1 ? 0x21 : i % 7));
- }
- vm.warp(1_790_467_200);
- vm.coinbase(producer);
- vm.deal(address(this), 1000 ether);
- }
-
- function call(uint256 vIn, uint256 vOut, uint256 fee, uint256 salt) internal view returns (Call memory c) {
- c.pipeline = MEMBER;
- c.root = bytes32(pool.currentRoot());
- c.n = [keccak256(abi.encode("n0", salt)), keccak256(abi.encode("n1", salt))];
- c.c = [keccak256(abi.encode("c0", salt)), keccak256(abi.encode("c1", salt))];
- c.vIn = vIn;
- c.vOut = vOut;
- c.fee = fee;
- c.payout = vOut > 0 ? payout : address(0);
- }
-
- /// The fields a valid member_transfer proof of `c` would publish: identity root, date, holder tag, then
- /// the transfer from 6, a zero slot last.
- function fieldsOf(Call memory c) internal view returns (uint256[] memory f) {
- uint256 t = 6;
- f = new uint256[](35);
- f[0] = uint256(DEPLOYMENT);
- f[1] = uint256(c.pipeline);
- f[2] = 5;
- f[3] = pool.identities().currentRoot();
- f[4] = block.timestamp;
- f[5] = 0x7a6;
- (, bytes memory ctx) = POSEIDON.staticcall(
- abi.encode(
- uint256(0x656d69742d76322f637478),
- block.chainid,
- uint256(c.n[0]),
- uint256(c.n[1]),
- uint256(c.c[0]),
- uint256(c.c[1])
- )
- );
- f[t] = abi.decode(ctx, (uint256));
- f[t + 1] = 0xc7;
- f[t + 5] = pool.ctCommitment(ct);
- f[t + 12] = block.chainid;
- f[t + 13] = uint256(c.root);
- f[t + 14] = uint256(c.n[0]);
- f[t + 15] = uint256(c.n[1]);
- f[t + 16] = uint256(c.c[0]);
- f[t + 17] = uint256(c.c[1]);
- f[t + 18] = c.vIn;
- f[t + 19] = c.vOut;
- f[t + 20] = c.fee;
- f[t + 21] = uint256(uint160(c.payout));
- for (uint256 i = 0; i < 6; i++) {
- f[t + 22 + i] = 0x100 + i;
- f[t + 6 + i] = 0x200 + i;
- }
- }
-
- function send(Call memory c, uint256[] memory f, uint256 value) internal {
- MockVerifier(VERIFY).set(f, false);
- pool.transact{value: value}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function deposit(uint256 v, uint256 salt) internal returns (Call memory c) {
- c = call(v, 0, 0, salt);
- send(c, fieldsOf(c), v);
- }
-
- function test_deposit_inserts_both_commitments_and_emits() public {
- Call memory c = call(100 ether, 0, 0, 1);
- uint256[] memory f = fieldsOf(c);
- MockVerifier(VERIFY).set(f, false);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.NewNullifier(c.n[0]);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.NewNullifier(c.n[1]);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.NewCommitment(c.c[0], 0);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.NewCommitment(c.c[1], 1);
- pool.transact{value: 100 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- assertEq(pool.nextIndex(), 2);
- assertEq(address(pool).balance, 100 ether);
- assertTrue(pool.nullifierSpent(uint256(c.n[0])));
- assertTrue(pool.isKnownRoot(uint256(c.root)), "the previous root stays known");
- assertTrue(pool.currentRoot() != uint256(c.root));
- }
-
- function test_withdraw_pays_payout_and_fee_to_the_producer() public {
- deposit(100 ether, 1);
- Call memory c = call(0, 30 ether, 1 ether, 2);
- send(c, fieldsOf(c), 0);
- assertEq(payout.balance, 30 ether);
- assertEq(producer.balance, 1 ether);
- assertEq(address(pool).balance, 69 ether);
- }
-
- function test_withdraw_beyond_the_pool_reverts() public {
- deposit(10 ether, 1);
- Call memory c = call(0, 30 ether, 0, 2);
- uint256[] memory f = fieldsOf(c);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.PaymentFailed.selector);
- pool.transact(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_replay_is_refused() public {
- Call memory c = deposit(1 ether, 1);
- c.root = bytes32(pool.currentRoot());
- uint256[] memory f = fieldsOf(c);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.NullifierSpent.selector);
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_equal_nullifiers_are_refused() public {
- Call memory c = call(1 ether, 0, 0, 1);
- c.n[1] = c.n[0];
- uint256[] memory f = fieldsOf(c);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.NullifierSpent.selector);
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_every_root_stays_known() public {
- bytes32 first = bytes32(pool.currentRoot());
- for (uint256 i = 0; i < 40; i++) {
- deposit(1, 10 + i); // 80 inserts: more than Emit V1's ring of 32 kept
- }
- Call memory c = call(1, 0, 0, 99);
- c.root = first;
- send(c, fieldsOf(c), 1);
-
- c = call(1, 0, 0, 100);
- c.root = bytes32(uint256(0x404));
- uint256[] memory f = fieldsOf(c);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.UnknownRoot.selector);
- pool.transact{value: 1}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_value_must_equal_vpubin() public {
- Call memory c = call(1 ether, 0, 0, 1);
- MockVerifier(VERIFY).set(fieldsOf(c), false);
- vm.expectRevert(EmitV2Pool.ValueMismatch.selector);
- pool.transact{value: 2 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_a_proof_that_does_not_verify_reverts() public {
- Call memory c = call(1 ether, 0, 0, 1);
- MockVerifier(VERIFY).set(fieldsOf(c), true);
- vm.expectRevert();
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_non_canonical_ciphertext_is_refused() public {
- bytes memory bad = new bytes(1536);
- bad[0] = 0xff;
- bad[1] = 0x0f; // c0 = 0xfff >= q
- vm.expectRevert(EmitV2Pool.BadCiphertext.selector);
- pool.ctCommitment(bad);
- vm.expectRevert(EmitV2Pool.BadCiphertext.selector);
- pool.ctCommitment(new bytes(1535));
- }
-
- function test_only_the_owner_sets_registry_roots() public {
- vm.prank(address(0xBAD));
- vm.expectRevert(EmitV2Pool.NotOwner.selector);
- pool.addRegistryRoot(1);
- pool.addRegistryRoot(0x5678);
- assertTrue(pool.isKnownRegistryRoot(0x5678));
- assertTrue(pool.isKnownRegistryRoot(REGISTRY));
- }
-
- /// The tree: filled-subtree inserts give the root of a complete tree with zero leaves.
- function test_tree_root_is_the_complete_tree() public {
- deposit(1, 1);
- (, bytes memory h) = POSEIDON.staticcall(
- abi.encode(
- uint256(keccak256(abi.encode("c0", uint256(1)))), uint256(keccak256(abi.encode("c1", uint256(1))))
- )
- );
- uint256 node = abi.decode(h, (uint256));
- for (uint256 i = 1; i < 32; i++) {
- (, h) = POSEIDON.staticcall(abi.encode(node, pool.zeros(i)));
- node = abi.decode(h, (uint256));
- }
- assertEq(pool.currentRoot(), node);
- }
-
- // ------------------------------------------------------------ the identity cache
-
- /// The fields of an identity_register proof: registry root, date, scope, nullifier, leaf, expiry.
- function registration(uint256 leaf, uint256 nullifier, uint256 expiry) internal view returns (uint256[] memory f) {
- f = new uint256[](35);
- f[0] = uint256(DEPLOYMENT);
- f[1] = uint256(REGISTER);
- f[2] = 4;
- f[3] = REGISTRY;
- // vm.getBlockTimestamp: under via_ir, block.timestamp may be read before a vm.warp.
- uint256 now_ = vm.getBlockTimestamp();
- f[4] = now_;
- f[5] = pool.registrationScope(now_ / 7 days);
- f[6] = nullifier;
- f[7] = leaf;
- f[8] = expiry;
- }
-
- /// The last second of the current epoch.
- function epochEnd() internal view returns (uint256) {
- return (vm.getBlockTimestamp() / 7 days + 1) * 7 days - 1;
- }
-
- function registerOk(uint256 leaf, uint256 nullifier) internal {
- MockVerifier(VERIFY).set(registration(leaf, nullifier, epochEnd()), false);
- pool.register(hex"00");
- }
-
- function test_register_appends_the_leaf_and_emits() public {
- uint256 expiry = epochEnd();
- MockVerifier(VERIFY).set(registration(0x1eaf, 0xd0c, expiry), false);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.IdentityRegistered(bytes32(uint256(0x1eaf)), 0, expiry);
- pool.register(hex"00");
- assertEq(pool.identities().nextIndex(), 1);
- assertTrue(pool.documentRegistered(0xd0c));
- assertEq(pool.nextIndex(), 0, "the note tree is untouched");
- assertTrue(pool.identities().currentRoot() != pool.identities().EMPTY_ROOT());
- }
-
- function test_register_checks() public {
- uint256 ok = epochEnd();
- uint256[] memory f;
-
- f = registration(1, 2, ok);
- f[5] = 0;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.WrongScope.selector);
- pool.register(hex"00");
-
- f = registration(1, 2, ok);
- f[3] = 0x9999;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.UnknownRegistryRoot.selector);
- pool.register(hex"00");
-
- f = registration(1, 2, ok);
- f[4] = block.timestamp - 2 days;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.DateOutOfRange.selector);
- pool.register(hex"00");
-
- MockVerifier(VERIFY).set(registration(1, 2, ok + 1), false);
- vm.expectRevert(EmitV2Pool.ExpiryOutOfRange.selector);
- pool.register(hex"00");
-
- MockVerifier(VERIFY).set(registration(1, 2, block.timestamp - 1), false);
- vm.expectRevert(EmitV2Pool.ExpiryOutOfRange.selector);
- pool.register(hex"00");
-
- registerOk(1, 2);
- MockVerifier(VERIFY).set(registration(3, 2, ok), false);
- vm.expectRevert(EmitV2Pool.AlreadyRegistered.selector);
- pool.register(hex"00");
-
- // The next epoch has its own scope (the same document gives another nullifier there).
- vm.warp(ok + 1);
- f = registration(3, 2, ok);
- f[5] = pool.registrationScope(block.timestamp / 7 days - 1);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.WrongScope.selector);
- pool.register(hex"00");
- registerOk(3, 4);
- }
-
- /// In the epoch's last day a holder registers for the next epoch, until its end.
- function test_register_ahead_in_the_last_day() public {
- uint256 end = epochEnd();
- uint256 nextEnd = end + 7 days;
- uint256[] memory f;
-
- // Before the last day, the next epoch's scope is refused.
- f = registration(1, 2, nextEnd);
- f[5] = pool.registrationScope(vm.getBlockTimestamp() / 7 days + 1);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.WrongScope.selector);
- pool.register(hex"00");
-
- vm.warp(end + 1 - 1 days);
- f = registration(1, 2, nextEnd + 1);
- f[5] = pool.registrationScope(vm.getBlockTimestamp() / 7 days + 1);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.ExpiryOutOfRange.selector);
- pool.register(hex"00");
-
- f[8] = nextEnd;
- MockVerifier(VERIFY).set(f, false);
- vm.expectEmit(address(pool));
- emit EmitV2Pool.IdentityRegistered(bytes32(uint256(1)), 0, nextEnd);
- pool.register(hex"00");
-
- // The current epoch's scope still works (another nullifier); the next one's is used up.
- registerOk(3, 4);
- vm.warp(end + 1);
- MockVerifier(VERIFY).set(registration(5, 2, nextEnd), false);
- vm.expectRevert(EmitV2Pool.AlreadyRegistered.selector);
- pool.register(hex"00");
- }
-
- function test_register_takes_only_the_register_pipeline() public {
- // A transfer proof (another pipeline's fields) is refused: the verifier is asked for REGISTER and the
- // proof's pipeline root must be REGISTER.
- uint256[] memory f = registration(1, 2, epochEnd());
- f[1] = uint256(MEMBER);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(abi.encodeWithSelector(EmitV2Pool.OutputMismatch.selector, "pipeline root"));
- pool.register(hex"00");
-
- f = new uint256[](34);
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(abi.encodeWithSelector(EmitV2Pool.InvalidProof.selector, bytes("fields")));
- pool.register(hex"00");
- }
-
- function test_member_transfer_checks() public {
- registerOk(0x1eaf, 0xd0c);
- Call memory c = call(1 ether, 0, 0, 1);
-
- uint256[] memory f = fieldsOf(c);
- f[3] = 0x404;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.UnknownIdentityRoot.selector);
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
-
- // Expired: a date the chain doesn't accept (the circuit checks the registration's expiry against it).
- f = fieldsOf(c);
- f[4] = block.timestamp + 2 days;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(EmitV2Pool.DateOutOfRange.selector);
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
-
- f = fieldsOf(c);
- f[0] ^= 1;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(abi.encodeWithSelector(EmitV2Pool.OutputMismatch.selector, "deployment root"));
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
-
- // Every transfer field is read at member_transfer's offsets.
- string[12] memory names =
- ["cid", "root", "n0", "n1", "c0", "c1", "vPubIn", "vPubOut", "fee", "payout", "ctx", "ct"];
- uint256[12] memory index = [uint256(18), 19, 20, 21, 22, 23, 24, 25, 26, 27, 6, 11];
- for (uint256 k = 0; k < 12; k++) {
- f = fieldsOf(c);
- f[index[k]] ^= 1;
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(abi.encodeWithSelector(EmitV2Pool.OutputMismatch.selector, names[k]));
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
- }
-
- function test_an_old_identity_root_stays_valid() public {
- registerOk(1, 1);
- uint256 first = pool.identities().currentRoot();
- registerOk(2, 2);
- Call memory c = call(1 ether, 0, 0, 1);
- uint256[] memory f = fieldsOf(c);
- f[3] = first;
- send(c, f, 1 ether);
- }
-
- function test_transact_takes_only_member_transfer() public {
- Call memory c = call(1 ether, 0, 0, 1);
- c.pipeline = REGISTER;
- MockVerifier(VERIFY).set(fieldsOf(c), false);
- vm.expectRevert(EmitV2Pool.UnknownPipeline.selector);
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
-
- // Another pipeline's proof sent as a member transfer: the pipeline root differs.
- c = call(1 ether, 0, 0, 1);
- uint256[] memory f = fieldsOf(c);
- f[1] = uint256(bytes32(uint256(0xa1)));
- MockVerifier(VERIFY).set(f, false);
- vm.expectRevert(abi.encodeWithSelector(EmitV2Pool.OutputMismatch.selector, "pipeline root"));
- pool.transact{value: 1 ether}(c.pipeline, c.root, c.n, c.c, c.vIn, c.vOut, c.fee, c.payout, ct, hex"00");
- }
-
- function test_only_the_pool_inserts_identities() public {
- IdentityTree t = pool.identities();
- vm.expectRevert(IdentityTree.NotPool.selector);
- t.insert(1);
- }
-}
diff --git a/crates/circuits/Cargo.toml b/crates/circuits/Cargo.toml
deleted file mode 100644
index 86be987..0000000
--- a/crates/circuits/Cargo.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-[package]
-name = "emit-devnet-circuits"
-description = "The combining registry: the emit layer's transfer app (frozen here as emit-devnet@0.1.0), eid-circuits' identity layer and zk-encryption's channel layer wrapped, the pipelines, and the pinned loaders"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-license.workspace = true
-publish.workspace = true
-
-[dependencies]
-noir-zk-core.workspace = true
-noir-zk-backend.workspace = true
-eid-circuits.workspace = true
-zk-encryption-circuits.workspace = true
-hex.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-sha2.workspace = true
-toml.workspace = true
-ureq.workspace = true
-
-[build-dependencies]
-noir-zk-codegen.workspace = true
-eid-circuits.workspace = true
-zk-encryption-circuits.workspace = true
diff --git a/crates/circuits/assets/identity_member@1.0.0.b64 b/crates/circuits/assets/identity_member@1.0.0.b64
deleted file mode 100644
index 865f707..0000000
--- a/crates/circuits/assets/identity_member@1.0.0.b64
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/crates/circuits/assets/register@1.0.0.b64 b/crates/circuits/assets/register@1.0.0.b64
deleted file mode 100644
index c8a26cc..0000000
--- a/crates/circuits/assets/register@1.0.0.b64
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/crates/circuits/assets/transfer_holder@1.0.0.b64 b/crates/circuits/assets/transfer_holder@1.0.0.b64
deleted file mode 100644
index 93a7f50..0000000
--- a/crates/circuits/assets/transfer_holder@1.0.0.b64
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/crates/circuits/build.rs b/crates/circuits/build.rs
deleted file mode 100644
index 61381f8..0000000
--- a/crates/circuits/build.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-//! Generates the combining registry from `circuits/manifest.toml`: this crate's own emit layer
-//! (frozen by `noir-zk freeze`, bytecode bundled), the identity and channel layers wrapped from
-//! their crates' generated registries, the pipelines and every root.
-
-use std::path::PathBuf;
-
-fn main() {
- let dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
- let out = PathBuf::from(std::env::var("OUT_DIR").unwrap_or_default());
- println!("cargo:rerun-if-changed=circuits");
- println!("cargo:rerun-if-changed=resources");
- println!("cargo:rerun-if-changed=assets");
- let options = noir_zk_codegen::Options {
- bundle: vec!["*".into()],
- sources: vec![
- (
- "eid-circuits".into(),
- noir_zk_codegen::wrapped(
- eid_circuits::circuits::LIBRARY,
- eid_circuits::circuits::REGISTRY,
- eid_circuits::circuits::FAMILIES,
- ),
- ),
- (
- "zk-encryption".into(),
- noir_zk_codegen::wrapped(
- zk_encryption_circuits::circuits::LIBRARY,
- zk_encryption_circuits::circuits::REGISTRY,
- zk_encryption_circuits::circuits::FAMILIES,
- ),
- ),
- ],
- };
- std::fs::write(
- out.join("circuits.rs"),
- noir_zk_codegen::generate_registry_with(&dir, &options),
- )
- .unwrap_or_else(|e| panic!("circuits.rs: {e}"));
-}
diff --git a/crates/circuits/circuits/manifest.toml b/crates/circuits/circuits/manifest.toml
deleted file mode 100644
index 9d8dfbd..0000000
--- a/crates/circuits/circuits/manifest.toml
+++ /dev/null
@@ -1,123 +0,0 @@
-# Written by noir-zk freeze; do not edit.
-
-noir = "1.0.0-rc.3"
-bb = "7.0.0-nightly.20260927"
-
-[library]
-name = "emit-devnet"
-version = "0.2.0"
-
-# ------------------------------------------------------------ families (hand-written; freeze keeps them)
-
-[[circuit]]
-label = "identity_member"
-version = "1.0.0"
-system = "chonk"
-role = "app"
-status = "active"
-bytecode_sha256 = "4cdcaaf8cee2fe4fee5022c4173dc12edbe51dd1d465cd6f2bb324a4836b562c"
-vk_sha256 = "950a5e69351edf074c4971b476a362ae33484b19d2d8f88968b7c52cc48d85b6"
-vk_hash = "0x1f3042d556da07f411b58b2e6403f836cfb0646f4753d98e070a4d4fe34d42ee"
-
-[[circuit]]
-label = "register"
-version = "1.0.0"
-system = "chonk"
-role = "app"
-status = "active"
-bytecode_sha256 = "f8c43422ba2630d59f020582de52ace3737ee976738fc6c3659ed9086bf8c071"
-vk_sha256 = "53cba21a90c059d8f4a7b0ca5ba5ce18cc0d1445d058acef04e53a6d2e0c93d7"
-vk_hash = "0x03fdab595abcf59e0e8c1667d45c664f53e0ba476300da2b865d0fd2d701904e"
-
-[[circuit]]
-label = "transfer_holder"
-version = "1.0.0"
-system = "chonk"
-role = "app"
-status = "active"
-bytecode_sha256 = "68a83386d6a50edb7b13f15bfe02189a447f6024cbd47ac2c251405127e07190"
-vk_sha256 = "e6c67e2ef9322b037b43637b9c69754a49745f45918c85e1350926c351541452"
-vk_hash = "0x2a6216853fccd0c00f6059f6aa3448403f7b82f19bb6186889b77799f38081bc"
-
-# The transfer of member_transfer: [ctx, holder_tag, cid, root, N0, N1, C0, C1, v_in, v_out, fee, payout_recipient,
-# note_commitment]; both inputs held by one key, every output 0 or at least 1/3 of the native coin, bound to the
-# session's ctx and to the holder tag identity_member published (the same key registered), the rest public, the
-# note payload's commitment its link out.
-[[family]]
-layer = "emit"
-name = "transfer_holder"
-members = ["transfer_holder"]
-link_out = { index = 12, link = "PayloadCommitment" }
-binds = [{ slot = "ctx", index = 0 }, { slot = "holder_tag", index = 1 }]
-public_from = 2
-slots = ["cid", "root", "n0", "n1", "c0", "c1", "v_in", "v_out", "fee", "payout"]
-
-# The identity cache: prove the passport once (register, after eid's document step), then membership.
-# register: [payload_commitment, leaf, expiry]; continues the document's DG1 commitment, leaf and expiry public.
-[[family]]
-layer = "identity_cache"
-name = "register"
-members = ["register"]
-link_in = { index = 0, link = "PayloadCommitment" }
-public_from = 1
-slots = ["leaf", "expiry"]
-
-# identity_member: [payload_commitment, identity_root, date, holder_tag]; a fresh commitment to the registered DG1
-# for its envelope, the rest public.
-[[family]]
-layer = "identity_cache"
-name = "member"
-members = ["identity_member"]
-link_out = { index = 0, link = "PayloadCommitment" }
-public_from = 1
-slots = ["identity_root", "date", "holder_tag"]
-
-# ------------------------------------------------------------ wrapped layers (build.rs: noir_zk_codegen::wrapped)
-
-# The identity layer: eid-circuits v0.8.2 (DSC, SOD and document steps).
-[[family]]
-layer = "eid"
-name = "dsc"
-source = "eid-circuits"
-
-[[family]]
-layer = "eid"
-name = "sod"
-source = "eid-circuits"
-
-[[family]]
-layer = "eid"
-name = "document"
-source = "eid-circuits"
-
-# The channel layer: zk-encryption v0.1.3 (frozen as zk-encryption@0.1.3).
-[[family]]
-layer = "channel"
-name = "session"
-source = "zk-encryption"
-
-[[family]]
-layer = "channel"
-name = "envelope"
-source = "zk-encryption"
-
-[[family]]
-layer = "channel"
-name = "note_envelope"
-source = "zk-encryption"
-
-# ------------------------------------------------------------ pipelines
-# The session publishes ctx and C_t; the DG1 envelope, the transfer and the
-# note envelope are bound to them. The document's DG1 commitment is consumed
-# by its envelope before the transfer leaves the note's.
-
-# The passport proved once: the document, then its registration in the identity cache: 6 slots.
-[[pipeline]]
-name = "identity_register"
-positions = ["eid-circuits/eid/dsc", "eid-circuits/eid/sod", "eid-circuits/eid/document", "emit-devnet/identity_cache/register"]
-
-# A transfer by a registered holder: membership (a fresh DG1 commitment, the holder tag), the session, DG1
-# sealed, the transfer bound to the tag, its note opening sealed: 31 slots.
-[[pipeline]]
-name = "member_transfer"
-positions = ["emit-devnet/identity_cache/member", "zk-encryption/channel/session", "zk-encryption/channel/envelope", "emit-devnet/emit/transfer_holder", "zk-encryption/channel/note_envelope"]
diff --git a/crates/circuits/noir/Nargo.toml b/crates/circuits/noir/Nargo.toml
deleted file mode 100644
index 537291f..0000000
--- a/crates/circuits/noir/Nargo.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[workspace]
-members = [
- "lib/emit",
- "lib/identity_cache",
- "circuits/transfer_holder",
- "circuits/register",
- "circuits/identity_member",
-]
diff --git a/crates/circuits/noir/circuits/identity_member/Nargo.toml b/crates/circuits/noir/circuits/identity_member/Nargo.toml
deleted file mode 100644
index 030cd17..0000000
--- a/crates/circuits/noir/circuits/identity_member/Nargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "identity_member"
-type = "bin"
-authors = ["zk-experiments"]
-compiler_version = ">=1.0.0"
-
-[dependencies]
-identity_cache = { path = "../../lib/identity_cache" }
-emit = { path = "../../lib/emit" }
diff --git a/crates/circuits/noir/circuits/identity_member/src/main.nr b/crates/circuits/noir/circuits/identity_member/src/main.nr
deleted file mode 100644
index ec40090..0000000
--- a/crates/circuits/noir/circuits/identity_member/src/main.nr
+++ /dev/null
@@ -1,31 +0,0 @@
-//! Membership in the identity cache as a Chonk app: returns `[payload_commitment, identity_root,
-//! date, holder_tag]` (`identity_cache::member`); the payload commitment is the link the DG1
-//! envelope continues.
-use emit::TREE_DEPTH;
-use identity_cache::{member, MEMBER_FIELDS};
-
-fn main(
- identity_root: Field,
- date: u64,
- sk: Field,
- payload: [Field; 6],
- payload_salt: Field,
- expiry: u64,
- blinding: Field,
- index: Field,
- path: [Field; TREE_DEPTH],
- ctx: Field,
-) -> return_data [Field; MEMBER_FIELDS] {
- member(
- identity_root,
- date,
- sk,
- payload,
- payload_salt,
- expiry,
- blinding,
- index,
- path,
- ctx,
- )
-}
diff --git a/crates/circuits/noir/circuits/register/Nargo.toml b/crates/circuits/noir/circuits/register/Nargo.toml
deleted file mode 100644
index d66653b..0000000
--- a/crates/circuits/noir/circuits/register/Nargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "register"
-type = "bin"
-authors = ["zk-experiments"]
-compiler_version = ">=1.0.0"
-
-[dependencies]
-identity_cache = { path = "../../lib/identity_cache" }
-eid_steps = { git = "https://github.com/zk-experiments/eid-circuits", tag = "v0.8.2", directory = "noir/lib/steps" }
diff --git a/crates/circuits/noir/circuits/register/src/main.nr b/crates/circuits/noir/circuits/register/src/main.nr
deleted file mode 100644
index dd1e68b..0000000
--- a/crates/circuits/noir/circuits/register/src/main.nr
+++ /dev/null
@@ -1,15 +0,0 @@
-//! The identity cache's registration as a Chonk app, after eid's document step: continues the
-//! step's DG1 PayloadCommitment and returns `[payload_commitment, leaf, expiry]`
-//! (`identity_cache::register`).
-use eid_steps::document::DG1_MAX;
-use identity_cache::{register, REGISTER_FIELDS};
-
-fn main(
- payload_salt: Field,
- dg1: [u8; DG1_MAX],
- sk: Field,
- expiry: u64,
- blinding: Field,
-) -> return_data [Field; REGISTER_FIELDS] {
- register(payload_salt, dg1, sk, expiry, blinding)
-}
diff --git a/crates/circuits/noir/circuits/transfer_holder/Nargo.toml b/crates/circuits/noir/circuits/transfer_holder/Nargo.toml
deleted file mode 100644
index 99fbb62..0000000
--- a/crates/circuits/noir/circuits/transfer_holder/Nargo.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[package]
-name = "transfer_holder"
-type = "bin"
-authors = ["zk-experiments"]
-compiler_version = ">=1.0.0"
-
-[dependencies]
-emit = { path = "../../lib/emit" }
diff --git a/crates/circuits/noir/circuits/transfer_holder/src/main.nr b/crates/circuits/noir/circuits/transfer_holder/src/main.nr
deleted file mode 100644
index c058e0e..0000000
--- a/crates/circuits/noir/circuits/transfer_holder/src/main.nr
+++ /dev/null
@@ -1,29 +0,0 @@
-//! The Emit V2 transfer of the `member_transfer` pipeline as a Chonk app: the transfer with both
-//! inputs held by one key and that key's holder tag in the transfer's ctx
-//! (`emit::transfer_holder`): `[ctx, holder_tag, cid, root, N0, N1, C0, C1, v_in, v_out, fee,
-//! payout_recipient, note_commitment]`.
-use emit::{InNote, OutNote, transfer_holder, TRANSFER_HOLDER_FIELDS};
-
-fn main(
- cid: Field,
- root: Field,
- v_in: Field,
- v_out: Field,
- fee: Field,
- payout_recipient: Field,
- ins: [InNote; 2],
- outs: [OutNote; 2],
- note_salt: Field,
-) -> return_data [Field; TRANSFER_HOLDER_FIELDS] {
- transfer_holder(
- cid,
- root,
- v_in,
- v_out,
- fee,
- payout_recipient,
- ins,
- outs,
- note_salt,
- )
-}
diff --git a/crates/circuits/noir/lib/emit/Nargo.toml b/crates/circuits/noir/lib/emit/Nargo.toml
deleted file mode 100644
index 5d0365e..0000000
--- a/crates/circuits/noir/lib/emit/Nargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "emit"
-type = "lib"
-authors = ["zk-experiments"]
-compiler_version = ">=1.0.0"
-
-[dependencies]
-channel = { git = "https://github.com/zk-experiments/zk-encryption", tag = "v0.1.3", directory = "noir/lib/channel" }
-poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" }
diff --git a/crates/circuits/noir/lib/emit/src/lib.nr b/crates/circuits/noir/lib/emit/src/lib.nr
deleted file mode 100644
index 2c843de..0000000
--- a/crates/circuits/noir/lib/emit/src/lib.nr
+++ /dev/null
@@ -1,233 +0,0 @@
-//! Emit V2 private transfer (2-in / 2-out JoinSplit, shielded keys), with
-//! the note opening for the recipient committed to as a channel payload for
-//! the note envelope app to seal. The channel itself (the KEM, C_t) is the
-//! session app's (channel::session). Reference: reference/src/emit.rs.
-//! Specification: README.md.
-
-mod tests;
-mod vectors;
-
-use channel::payload::commit;
-use poseidon::poseidon2::Poseidon2;
-
-/// Domain tags ("emit-v2/