diff --git a/test/poseidon2-opt/.gitignore b/test/poseidon2-opt/.gitignore new file mode 100644 index 00000000..67076e62 --- /dev/null +++ b/test/poseidon2-opt/.gitignore @@ -0,0 +1,18 @@ +# Foundry build artifacts +cache/ +out/ + +# Vendored third-party libraries (populate locally via `make setup`) +# lib/forge-std https://github.com/foundry-rs/forge-std +# lib/poseidon2-evm https://github.com/zemse/poseidon2-evm +# lib/poseidon2-solidity https://github.com/V-k-h/poseidon2-solidity +lib/ + +# Circom benchmark artefacts (populated by `make cross-check` / `bench-circom`; +# also ignored by bench/circom/.gitignore — duplicated here so the root +# .gitignore is self-documenting) +bench/circom/pot12.ptau +bench/circom/build_* + +# Safety catch-all for accidental secrets +.env diff --git a/test/poseidon2-opt/Makefile b/test/poseidon2-opt/Makefile new file mode 100644 index 00000000..db22f7c7 --- /dev/null +++ b/test/poseidon2-opt/Makefile @@ -0,0 +1,43 @@ +.PHONY: help setup setup-light build test bench cross-check cross-fuzz bench-circom clean + +help: + @echo "poseidon2-opt — Makefile targets" + @echo "" + @echo " make setup populate lib/ AND download pot12.ptau (idempotent)" + @echo " make build forge build (lib/ only — skips ptau)" + @echo " make test forge test — correctness + fuzz suite (lib/ only)" + @echo " make bench forge test — gas benchmark (lib/ only)" + @echo " make cross-check Solidity <-> Circom equality on fixed inputs" + @echo " make cross-fuzz Solidity <-> Circom equality on random inputs" + @echo " (override count via CROSS_CHECK_FUZZ=N, default 4)" + @echo " make bench-circom Circom R1CS + Groth16 proving benchmark" + @echo " make clean remove Foundry and Circom build artifacts" + +setup: + @bash scripts/setup-libs.sh + +# Solidity-only setup — lib/ clones without the ~5 MB pot12.ptau download. +setup-light: + @SKIP_PTAU=1 bash scripts/setup-libs.sh + +build: setup-light + @forge build + +test: setup-light + @forge test + +bench: setup-light + @FOUNDRY_PROFILE=bench forge test -vv + +cross-check: setup + @bash test/cross_check.sh + +cross-fuzz: setup + @CROSS_CHECK_FUZZ=$${CROSS_CHECK_FUZZ:-4} bash test/cross_check.sh + +bench-circom: setup + @bash bench/circom/scripts/bench_full.sh + +clean: + @forge clean + @rm -rf bench/circom/build_* diff --git a/test/poseidon2-opt/README.md b/test/poseidon2-opt/README.md new file mode 100644 index 00000000..0b43ac7c --- /dev/null +++ b/test/poseidon2-opt/README.md @@ -0,0 +1,161 @@ +# Poseidon2 Optimized Implementations + +Gas-optimized Poseidon2 hash function implementations in **Solidity** and **Circom**, targeting **BN254 scalar field** with **x^5 S-box**. Compatible with Noir/Barretenberg. + +## Quick Start + +On a fresh checkout, no manual setup is required — `make test`/`build`/`bench` auto-populate `lib/`, and `make cross-check`/`cross-fuzz`/`bench-circom` additionally fetch `pot12.ptau` (~5 MB) on first use. + +```shell +make test # correctness + fuzz suite (forge test, ~0.2 s, 256 fuzz runs/test) +make bench # Solidity gas benchmark +make cross-check # Solidity <-> Circom equality on 9 fixed inputs +make cross-fuzz # Solidity <-> Circom equality on boundary + N random inputs (default N=4) +make bench-circom # Circom R1CS + Groth16 proving benchmark +make help # list all targets +``` + +To populate dependencies manually (e.g. before running `forge` directly): + +```shell +make setup # full bootstrap: lib/ + pot12.ptau + # or `SKIP_PTAU=1 bash scripts/setup-libs.sh` for lib/ only +``` + +This clones `forge-std`, `zemse/poseidon2-evm` and `V-k-h/poseidon2-solidity` into `lib/` at pinned refs and (unless `SKIP_PTAU=1`) downloads the Powers-of-Tau file into `bench/circom/`. Both `lib/` and `pot12.ptau` are **gitignored** — they live locally only. + +## Implementations + +| Contract / Circuit | t | Interface | Key Optimization | +|--------------------|---|-----------|------------------| +| **Poseidon2T2** | 2 | `hash1(uint256)` | D=[1,2], zero mulmod in internal matrix | +| **Poseidon2T2FF** | 2 | `compress(uint256, uint256)` | Feed-forward, optimal for Merkle trees | +| **Poseidon2T3** | 3 | `hash2(uint256, uint256)` | D=[1,1,2], zero mulmod | +| **Poseidon2T4** | 4 | `hash3(uint256, uint256, uint256)` | M4 matrix manually unrolled | +| **Poseidon2T4Sponge** | 4 | `hash1` - `hash9` | Sponge with dirty-value tracking, Noir-compatible IV | +| **Poseidon2T8** | 8 | `hash7`, `hash4_padded` - `hash6_padded` | M4 Kronecker product, packed RC storage | + +All Solidity implementations are `library` contracts with `internal pure` functions. Circom circuits mirror the same algorithms with `var` optimization to minimize R1CS constraints. + +### Optimization Techniques + +- **Dirty-value tracking**: Annotate value bounds (0/3, 1/3, 2/3) to safely use `add` instead of `addmod`, saving gas in hot loops +- **Packed RC storage**: Partial rounds store only 1 RC per round (for state[0]) instead of t, reducing bytecode by 60-77% +- **S-box function extraction** (T4S, T8): Deduplicate inline x^5 S-box blocks into a reusable Yul function +- **Circom var optimization**: Use `var` for matrix intermediates instead of `signal`, reducing R1CS constraints (e.g. T4: 612 vs NethermindEth 648) + +## Project Structure + +``` +src/ +├── solidity/ # Solidity implementations (Poseidon2T2..T8) +└── circom/ # Circom implementations (poseidon2_t2..t8) + +bench/ # Benchmark suite (Poseidon1 vs Poseidon2 vs third-party) +├── solidity/ # Gas benchmarks (wrappers, vendored, FullBenchmark.t.sol) +└── circom/ # Constraint benchmarks (circuits, vendored, scripts) + +test/ +├── Correctness.t.sol # Fixed test vectors + cross-impl agreement (zemse / V-k-h) +├── Fuzz.t.sol # Property-based fuzz: input mod invariance + output in-range +└── cross_check.sh # Solidity <-> Circom equality (fixed inputs + optional fuzz mode) + +scripts/ +├── setup-libs.sh # Idempotent bootstrap: clones lib/* + (optionally) fetches pot12.ptau +└── lib.sh # Shared shell helpers: preflight checks, slugify, circom autodetect + +Makefile # Wraps forge/cross-check/bench workflows with auto-setup +``` + +## Usage + +The recommended entry points are the Makefile targets in [Quick Start](#quick-start). Each auto-runs `setup-libs.sh`, so a fresh clone works out of the box. + +Under the hood they map to: + +| Make target | Underlying command | Approx wall-clock | +| ------------------- | ------------------------------------------------------------------------ | ---------------------- | +| `make build` | `forge build` | ~5 s | +| `make test` | `forge test` (correctness vectors + 6 fuzz tests × 256 runs each) | <1 s | +| `make bench` | `FOUNDRY_PROFILE=bench forge test -vv` | <1 s | +| `make cross-check` | `bash test/cross_check.sh` (9 fixed Solidity↔Circom comparisons) | ~2 min | +| `make cross-fuzz` | `CROSS_CHECK_FUZZ=$${CROSS_CHECK_FUZZ:-4} bash test/cross_check.sh` (9 fixed + 36 boundary + 6N random) | ~12 min at N=4 | +| `make bench-circom` | `bash bench/circom/scripts/bench_full.sh` | ~10 min | +| `make clean` | `forge clean && rm -rf bench/circom/build_*` | <1 s | + +External prerequisites (must be installed on the host). The `cross-check`, +`cross-fuzz`, and `bench-circom` targets pre-flight all of these and abort +with an actionable error if any is missing. + +| Tool | Required for | Install command | +|------|--------------|-----------------| +| `forge` (from [Foundry](https://book.getfoundry.sh/), 2024-08+ recommended for solc 0.8.30 support) | every target | `curl -L https://foundry.paradigm.xyz \| bash && foundryup` | +| [`circom`](https://docs.circom.io/) | cross-check, cross-fuzz, bench-circom | `cargo install --git https://github.com/iden3/circom` | +| [`snarkjs`](https://github.com/iden3/snarkjs) | cross-check, cross-fuzz, bench-circom | `npm install -g snarkjs` | +| `node` (≥ 18) | cross-check, cross-fuzz, bench-circom | https://nodejs.org/ | +| `python3` | cross-fuzz only (random uint256 generation) | (preinstalled on macOS / most Linux) | +| `bc`, `/usr/bin/time` | bench-circom only (averaging + timing) | `apt install bc time` / preinstalled on macOS | +| `curl` or `wget` | first run only (downloads `pot12.ptau`) | (preinstalled on macOS / Linux) | + +## Adding a New Implementation for Comparison + +To add a new Poseidon-family implementation to the benchmark matrix: + +### Solidity (gas benchmark) + +1. **Import or vendor the source** into `bench/solidity/vendored/.sol`, or add it as a git dependency in `scripts/setup-libs.sh`. +2. **Write a thin wrapper** at `bench/solidity/wrappers/Wrapper.sol` with one `external view` method per arity the impl supports. For `internal pure` libraries the wrapper inlines them; for standalone contracts (non-standard ABI like zemse-yul) staticcall them directly from `FullBenchmark` helpers instead. +3. **Wire into `bench/solidity/FullBenchmark.t.sol`**: add the wrapper instance in `setUp()` and a `g = gasleft(); .hash_N(...); console.log(...)` line in the matching `test_gas_N_inputs()` function. +4. Run `make bench` to see the number next to the others. + +### Circom (constraint benchmark) + +1. **Vendor the `.circom` source(s)** under `bench/circom/vendored/` (re-namespaced if needed to avoid `include` collisions). +2. **Add a per-arity wrapper circuit** at `bench/circom/circuits/bench_