From b5902a4e2c38a3669ef63016666b867a15e6d354 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 19:02:26 +0100 Subject: [PATCH 1/4] feat(contracts): vendor the libid-circuits v0.4.0 Honk verifiers bb belongs to libid-circuits, which now ships each circuit's Solidity verifier beside its vk. The pin is committed digests, never a manifest that came down with the download; forge fmt is the one change to what shipped, and CI's generated-tables job regenerates and diffs so the committed bytes stay checkable. The linter skips bb's output, nothing else does. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .github/workflows/ci.yml | 13 +- README.md | 25 + scripts/vendor-circuit-verifiers.sh | 153 + .../circuits/BearerLinkHonkVerifier.sol | 2494 +++++++++++++++++ .../circuits/OidcGoogleHonkVerifier.sol | 2494 +++++++++++++++++ solidity/contracts/circuits/circuits.json | 13 + .../circuits/test/HonkVerifiers.t.sol | 123 + solidity/foundry.toml | 6 + 8 files changed, 5319 insertions(+), 2 deletions(-) create mode 100755 scripts/vendor-circuit-verifiers.sh create mode 100644 solidity/contracts/circuits/BearerLinkHonkVerifier.sol create mode 100644 solidity/contracts/circuits/OidcGoogleHonkVerifier.sol create mode 100644 solidity/contracts/circuits/circuits.json create mode 100644 solidity/contracts/circuits/test/HonkVerifiers.t.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 454cdf8..6b69154 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ name: CI # ts/packages/contracts/src/abis are gitignored), so every job that compiles # the crate or the package regenerates it first — publishing included. # -# Job ids are stable on purpose (solidity, handles-drift, rust, ts, dco): +# Job ids are stable on purpose (solidity, generated-tables, rust, ts, dco): # the release/publish jobs at the bottom of this file hang their `needs:` off # these names. # @@ -93,7 +93,9 @@ jobs: # Generated files match their source. handles.json generates the platform # constants and the handle vector table for every language; a drift between # the source and a committed output leaves every per-language test green - # while the languages disagree with each other. + # while the languages disagree with each other. The vendored Honk verifiers + # are the same shape: circuits.json pins a libid-circuits release by digest, + # and the committed Solidity must be exactly what that release ships. # --------------------------------------------------------------------------- generated-tables: name: Generated tables @@ -118,6 +120,13 @@ jobs: - name: Generated ceremony profiles are current run: python3 scripts/regen-ceremony-profiles.py --check + # Downloads the pinned release and checks it against the committed + # digests before comparing, so this fails on a hand edit, a stale + # vendor after a pin move, and a release asset that changed under its + # tag alike. + - name: Vendored Honk verifiers match the circuits pin + run: scripts/vendor-circuit-verifiers.sh --check + # --------------------------------------------------------------------------- # Rust: vendored artifacts, formatting, clippy, tests. Needs forge twice # over — the vendored artifacts are generated from a fresh `forge build`, diff --git a/README.md b/README.md index 7144008..321ef6a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ solidity/ # Foundry project root contracts/ ceremony/ # NotaryService, CeremonyProofVerifier, Platform Verifiers, # GoogleJwtRoots + circuits/ # the UltraHonk verifiers the Platform Verifiers pin, + # vendored from the pinned libid-circuits release identity/ # IdentityNames, handle normalization factory/ # LibidFactory: deterministic CREATE3 deployment WTIA9.sol # wrapped TIA @@ -23,6 +25,7 @@ rust/contracts/ # libid-contracts crate: alloy bindings + embedded artifact ts/packages/contracts/ # @libid/contracts: viem ABIs, call builders, identity helpers scripts/ vendor-artifacts.sh + vendor-circuit-verifiers.sh regen-identity-handles.py ``` @@ -66,6 +69,28 @@ This generates `solidity/contracts/identity/HandleVectors.sol`, `ts/packages/contracts/src/identity/handleVectors.ts`; CI's handle-tables job fails when any of them drifts from `handles.json`. +## Circuit verifiers + +The ceremony circuits' UltraHonk verifiers are not written here. `bb` derives +each from its circuit's verification key, and +[libid-circuits](https://github.com/libid-org/libid-circuits) runs `bb` and +ships the Solidity in its release tarballs. `solidity/contracts/circuits/` +holds that Solidity, formatted, as a source like any other: `forge build` +compiles it and the crate embeds it, so no consumer runs `bb`. + +`solidity/contracts/circuits/circuits.json` is the pin — the release version +and each tarball's sha256, committed here and checked against every download. +To move it, download the new release's tarballs, take their digests with +`shasum -a 256`, write the version and the digests into `circuits.json`, then: + +```sh +scripts/vendor-circuit-verifiers.sh # rewrite the verifiers from the pin +scripts/vendor-circuit-verifiers.sh --check # verify the committed files match it +``` + +CI's generated-tables job runs the check; a release cannot ship a verifier +that is not what the pinned circuits release shipped. + ## Releasing The Rust crate ([`libid-contracts`](https://crates.io/crates/libid-contracts)) diff --git a/scripts/vendor-circuit-verifiers.sh b/scripts/vendor-circuit-verifiers.sh new file mode 100755 index 0000000..3e5da96 --- /dev/null +++ b/scripts/vendor-circuit-verifiers.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Vendor the ceremony circuits' UltraHonk verifiers from the pinned +# libid-circuits release. +# +# A Honk verifier is not written here: bb derives it from a circuit's +# verification key, and libid-circuits runs bb and ships the result in each +# circuit's release tarball beside the vk. This script downloads those +# tarballs, checks them, formats the Solidity under solidity/foundry.toml and +# writes it to solidity/contracts/circuits/.sol: +# +# bearer-link -> BearerLinkHonkVerifier.sol (the x and github profiles) +# oidc-google -> OidcGoogleHonkVerifier.sol (the google profile) +# +# THE PIN is solidity/contracts/circuits/circuits.json: the release version +# and, per circuit, the contract name and the tarball's sha256. The digests +# are committed literals, so a download is checked against what this +# repository says, never against a manifest that came down with it. The +# release's manifest.json is fetched too, but only to be held to the pin — +# it must name the same version and the same tarball digests — and then to +# check every file inside a tarball the pin has already vouched for. +# +# What ships is bb's output plus exactly two rewrites libid-circuits makes +# (`assembly ("memory-safe")` on every assembly block, for via_ir, and the +# rename off bb's fixed `HonkVerifier`); `forge fmt` is deliberately left to +# the consumer, because libid-circuits carries no Foundry toolchain. So the +# committed file is fmt(shipped) plus the banner below, and `--check` +# regenerates it and fails on any difference — which is what makes the +# committed bytes checkable rather than trusted. +# +# The sources ARE committed, like HandleVectors.sol: forge, the crate and the +# npm package build from the tree with no network, and CI's generated-tables +# job runs `--check` so a hand edit or a stale vendor cannot outlive a PR. +# The compiled artifacts are not; scripts/vendor-artifacts.sh regenerates +# them from `forge build` like every other contract's. +# +# Moving the pin: download the new release's tarballs, read their sha256 +# with `shasum -a 256` (compare against the release page, not against a +# manifest fetched by a script), write the version and the digests into +# circuits.json, run this script, commit both. +# +# Usage: +# scripts/vendor-circuit-verifiers.sh # regenerate from the pin +# scripts/vendor-circuit-verifiers.sh --check # fail if a committed source drifts +# +# Requires curl, jq, tar, forge and shasum or sha256sum. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SOLIDITY="$REPO_ROOT/solidity" +DEST_REL="contracts/circuits" +DEST="$SOLIDITY/$DEST_REL" +PIN="$DEST/circuits.json" +RELEASES="https://github.com/libid-org/libid-circuits/releases/download" + +MODE="write" +while [[ $# -gt 0 ]]; do + case "$1" in + --check) MODE="check"; shift ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +for tool in curl jq tar forge; do + command -v "$tool" >/dev/null || { echo "$tool is required" >&2; exit 1; } +done +sha256() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +[[ -f "$PIN" ]] || { echo "no pin at $PIN" >&2; exit 1; } +VERSION="$(jq -r '.version' "$PIN")" +[[ -n "$VERSION" && "$VERSION" != "null" ]] || { echo "no version in $PIN" >&2; exit 1; } +TAG="v$VERSION" +echo "==> libid-circuits $TAG" + +WORK="$(mktemp -d)" +STAGE="$(mktemp -d)" +trap 'rm -rf "$WORK" "$STAGE"' EXIT + +# The release's own manifest: held to the pin, then used for the per-file +# check inside each tarball. +curl -fsSL -o "$WORK/manifest.json" "$RELEASES/$TAG/manifest.json" +released="$(jq -r '.version' "$WORK/manifest.json")" +[[ "$released" == "$VERSION" ]] || + { echo "the $TAG manifest declares version '$released', the pin says $VERSION" >&2; exit 1; } + +while IFS=$'\t' read -r circuit contract want; do + tarball="libid-circuits-$VERSION-$circuit.tar.gz" + curl -fsSL -o "$WORK/$tarball" "$RELEASES/$TAG/$tarball" + + # The integrity check: the committed digest, nothing downloaded. + got="$(sha256 "$WORK/$tarball")" + [[ "$got" == "$want" ]] || + { echo "$tarball: sha256 $got, the pin says $want" >&2; exit 1; } + declared="$(jq -r --arg t "$tarball" '.tarballs[$t].sha256' "$WORK/manifest.json")" + [[ "$declared" == "$want" ]] || + { echo "$tarball: the $TAG manifest declares sha256 '$declared', the pin says $want" >&2; exit 1; } + + mkdir -p "$WORK/$circuit" + tar xzf "$WORK/$tarball" -C "$WORK/$circuit" + # Every file the manifest lists is in the tarball at the digest it + # names; the tarball is trusted, so this catches a release that was + # assembled wrong, not an attacker. + while IFS=$'\t' read -r name file_want; do + [[ -f "$WORK/$circuit/$name" ]] || + { echo "$tarball: the manifest lists $name, the tarball lacks it" >&2; exit 1; } + file_got="$(sha256 "$WORK/$circuit/$name")" + [[ "$file_got" == "$file_want" ]] || + { echo "$tarball: $name sha256 $file_got, the manifest says $file_want" >&2; exit 1; } + done < <(jq -r --arg t "$tarball" '.tarballs[$t].files | to_entries[] | "\(.key)\t\(.value)"' "$WORK/manifest.json") + + src="$WORK/$circuit/$contract.sol" + [[ -f "$src" ]] || { echo "$tarball: no $contract.sol inside" >&2; exit 1; } + # The interchange format, as libid-circuits' scripts/gen-verifier.sh + # promises it: one concrete contract under the pinned name, every + # assembly block annotated. A file that breaks either would compile to + # something the crate looks up under the wrong name, or not at all. + concrete="$(grep -c '^contract .* is BaseZKHonkVerifier' "$src" || true)" + [[ "$concrete" == 1 ]] && grep -q "^contract $contract is BaseZKHonkVerifier" "$src" || + { echo "$contract.sol: expected exactly one 'contract $contract is BaseZKHonkVerifier', found $concrete" >&2; exit 1; } + if grep -qE 'assembly[[:space:]]*\{' "$src"; then + echo "$contract.sol: an assembly block is not annotated memory-safe" >&2 + exit 1 + fi + + # The banner goes after bb's license header, before the first pragma. + # Then forge fmt under this project's foundry.toml, which is the one + # step libid-circuits leaves to the consumer. + awk -v banner="// Vendored from libid-circuits $TAG ($tarball) by scripts/vendor-circuit-verifiers.sh. Do not edit.\n// The pin is $DEST_REL/circuits.json; \`forge fmt\` is the only change to what shipped." ' + !done && /^pragma / { print banner; done = 1 } + { print } + ' "$src" | (cd "$SOLIDITY" && forge fmt --raw -) > "$STAGE/$contract.sol" + echo "==> $circuit -> $DEST_REL/$contract.sol" +done < <(jq -r '.circuits | to_entries[] | "\(.key)\t\(.value.contract)\t\(.value.sha256)"' "$PIN") + +if [[ "$MODE" == "check" ]]; then + drifted=0 + for staged in "$STAGE"/*.sol; do + name="$(basename "$staged")" + if ! cmp -s "$staged" "$DEST/$name"; then + echo "$DEST_REL/$name is not what the pin vendors; run scripts/vendor-circuit-verifiers.sh" >&2 + drifted=1 + fi + done + [[ "$drifted" == 0 ]] || exit 1 + echo "==> the committed verifiers match the pin" +else + cp "$STAGE"/*.sol "$DEST/" +fi diff --git a/solidity/contracts/circuits/BearerLinkHonkVerifier.sol b/solidity/contracts/circuits/BearerLinkHonkVerifier.sol new file mode 100644 index 0000000..5243f43 --- /dev/null +++ b/solidity/contracts/circuits/BearerLinkHonkVerifier.sol @@ -0,0 +1,2494 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +// Vendored from libid-circuits v0.4.0 (libid-circuits-0.4.0-bearer-link.tar.gz) by scripts/vendor-circuit-verifiers.sh. Do not edit. +// The pin is contracts/circuits/circuits.json; `forge fmt` is the only change to what shipped. +pragma solidity >=0.8.21; + +uint256 constant N = 65536; +uint256 constant LOG_N = 16; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 72; +uint256 constant VK_HASH = 0x1d161afb536683d31a3e426db0feaa30de8be89cc45510579f361266c20e078f; + +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(65536), + logCircuitSize: uint256(16), + publicInputsSize: uint256(72), + ql: Honk.G1Point({ + x: uint256(0x21d8f7a12fca729bc6bfe60cdf2dacfa950154e11529a824e2a84c9c8b7ae5ad), + y: uint256(0x28526fc2bac460041b557dc083b1ba45ed48b9ce11dc54d9693eb115cb835bc7) + }), + qr: Honk.G1Point({ + x: uint256(0x13a030594cd4cc26b066d930f003f59d30f98f6bbdf7f510de7a22f3b4ca91c6), + y: uint256(0x08e14f589148732c712804948a65aac5814ecd254929d1ecefe207128c9d0ee8) + }), + qo: Honk.G1Point({ + x: uint256(0x0207cb7dbdfdc49d7b07079b19e5c24a1f7d200eb8f9f57c38359b850b63d2d9), + y: uint256(0x0979437546d53eb53b21ea07b3b0ff9003d0d8b94a6ba1b74675f002ea21734c) + }), + q4: Honk.G1Point({ + x: uint256(0x17df214e1867d539648dd94ba8e71f81b97c13ec3c457d3b2d497943a4727bce), + y: uint256(0x2fa8b49ccb8c6ec2fa37724a64df40f4770121664d50d0e13aff1d943e9c2027) + }), + qm: Honk.G1Point({ + x: uint256(0x2fcade2ba858ac130be60566c5af5f18ab533cbbd5fe183597eaaa3ece8c4ec8), + y: uint256(0x07217d0d916b000076b0c6ac589c2d45ac5a70912e1dd2917af547e66bcc6b96) + }), + qc: Honk.G1Point({ + x: uint256(0x0550a650096cce86a8ed1b2d93bf65af5868b8cc0a49ca67049bed72fc3202ae), + y: uint256(0x11f6c27fe423a81c335666e00629650287e537fbdafc58c743d15cde0f44caef) + }), + qLookup: Honk.G1Point({ + x: uint256(0x16a5c54dfac02744e9c8edfdd812df41f6692fac5c03dd51eb643339ab9a9685), + y: uint256(0x09e7164978342671c70e53407dca7f0cf80c2d5e1654332763ccef010cd1a7d2) + }), + qArith: Honk.G1Point({ + x: uint256(0x2f9b8f15ab3501c467b1c074e5007a1a40b43828694ecd2845a7019189535765), + y: uint256(0x223c92802d53bf81bf1066e0d7426a040f571b924003d0d5d97d4f80d906bdfe) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x1dd5af2bc33313bae90b5e7be3abfc093d49d2f4b5f1d75e4301ef6eb9db59e8), + y: uint256(0x03c30084892bd7e01fdfabf86ffef536543353ff37900b15ae41808531b2c06c) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qMemory: Honk.G1Point({ + x: uint256(0x1f40d0a610240bdf14f4ff3df8a91eb858a2e8aed1b5774b96e0aee86d5db953), + y: uint256(0x20bfe881117eea3f1b75bba4eb194625fc2eb5951c61d2ea1b57a3dc37bc3f5a) + }), + qNnf: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + s1: Honk.G1Point({ + x: uint256(0x16d07df09b0938e7d0e5710914c491e9a578073dcc215f902f63467bbb873ef4), + y: uint256(0x190deff8947ca1880195384fbea386f313eb72fc156c1427949488b876c59580) + }), + s2: Honk.G1Point({ + x: uint256(0x303e3355a805c782712e3d48c80041d70232a553552bf6fbc9e7b2ecc5fa7b38), + y: uint256(0x2586f8d270c7d1061f4c8627132ea1aeda84747d3d2a8669e0f3930549ffbb5e) + }), + s3: Honk.G1Point({ + x: uint256(0x2328b0f8447c3392021727560158f659a44b0021bf1016d22cbddd6b9c847154), + y: uint256(0x222967c63ccdb06aeb6e9cc6b72d2931ce49286a59f1c5f4638231ce5a656713) + }), + s4: Honk.G1Point({ + x: uint256(0x20eb4577881ab617c1561beb2e099801da537d9f68066698120969821d55df24), + y: uint256(0x12ab1f967fc802acc18e0acc95b26631087258af8a60c0ec21e3a9862b5a44d9) + }), + t1: Honk.G1Point({ + x: uint256(0x2007e7f9cf246e2fff5241dcad09f774e3febf4a5528b4f654a1c53332eee70e), + y: uint256(0x0b93b63b9a2f1b6803aa832ea402038ed701919a50b87f27ea8ab3fecd6e0bc1) + }), + t2: Honk.G1Point({ + x: uint256(0x22b74d79893277da758e319b9254ac723a8bb788a04e9f990e93e24328ef44cc), + y: uint256(0x20fc82a77859ebc3187ff8cce5278805710a6fd48b606045f81b2b652c8c2882) + }), + t3: Honk.G1Point({ + x: uint256(0x10ee38b08d8cb65a4bb9f16fe31932970faa33b310bc0e4a583db8d88f448335), + y: uint256(0x21ff5d9dbda6ecd2b8cb8df09d01108535d127413664ae2b7d480f3a89d7c0da) + }), + t4: Honk.G1Point({ + x: uint256(0x265434c6d481bb44c0003077c2cf237884af36f0a7ca62cf3f8ad4c83b45bd32), + y: uint256(0x0ad46a441ccbfbeb58b2d4b90bc050752f8ba1bbaafca67426e72d99189dbbba) + }), + id1: Honk.G1Point({ + x: uint256(0x07c4cba2626a34ce9919d17f5ad08277a6f1d5d80927e0603789f50b1f008693), + y: uint256(0x2b3fd005d61848b09996ce01e4abad7cf88cffa461227921c622825169c079e6) + }), + id2: Honk.G1Point({ + x: uint256(0x2909d03ff49e1a1830812a909ade2355b29dd92e055e4346de9ef947113028ef), + y: uint256(0x0bdcd9f3e9c8f432d346d3a983e34f896379cb1c312114a010a3b729891ebba2) + }), + id3: Honk.G1Point({ + x: uint256(0x2e019d7c64b6adbda9ce45ee8b0ae4c7bc10773396342554d9b98de7dbf53748), + y: uint256(0x056a601f09e639cc189181bc7809f5af103dd62633d48d2c2a642e5277074eaa) + }), + id4: Honk.G1Point({ + x: uint256(0x199b3a2b71a2cd6d7c59bb9c5201b75b12f32baa0b02d840e468f46f2eff9934), + y: uint256(0x27710345ab4b3a536e76ea99da95b0501778c34b2e14340d706b21a89b0a08d8) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6), + y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x1fde62f333621ba020dd4ad0ec877585e009583273d4a1de45a55638c7cfade9), + y: uint256(0x244ac512eca15ea8b3fa8f9e5e0daefcfd9365414269dddd7f75cb95473fbfc5) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); + +// SmallSubgroupIPA opening-claim layout — mirrors SMALL_IPA_CLAIMS in +// barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa_utils.hpp. +uint256 constant NUM_SMALL_IPA_OPENING_CLAIMS = 5; +uint256 constant SMALL_IPA_BOUNDARY_OPENING_IDX = 3; +uint256 constant NUM_SMALL_IPA_TRANSCRIPT_EVALS = 4; +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly ("memory-safe") { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly ("memory-safe") { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 29; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// Must match UltraFlavor_Generated::EntityId order. +enum WIRE { + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + Q_LOOKUP, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + Q_M, + Q_R, + Q_O, + Q_C, + Q_L, + Q_4, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) external pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[6] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[7] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[0] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[1] = acc; + } + + // Contribution 4: z_perm initialization check (lagrange_first * z_perm = 0) + { + Fr acc = (wire(p, WIRE.LAGRANGE_FIRST) * wire(p, WIRE.Z_PERM)) * domainSep; + evals[2] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[3] = accumulatorNone; + evals[4] = accumulatorOne; + evals[5] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[11] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[12] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[13] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[12] = evals[12] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[13] = evals[13] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[15] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[16] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[17] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[18] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[19] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[14] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[20] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[24] = evals[24] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[28] = evals[28] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[NUM_SMALL_IPA_OPENING_CLAIMS] denominators; + Fr[NUM_SMALL_IPA_OPENING_CLAIMS] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = + ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // (0,0) is the canonical EIP-196 encoding of the identity. It is accepted here + // because polynomial commitments to identically-zero polynomials (e.g. unused + // selector or table polys) are legitimately the identity. On-curve validation + // (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles per EIP-196. + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly ("memory-safe") { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly ("memory-safe") { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, + 5 // pubInputsOffset = NUM_DISABLED_ROWS_IN_SUMCHECK + NUM_ZERO_ROWS = 4 + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + // Row-disabling polynomial: 1 - ∏_{i≥2}(1 - u_i) + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * (Fr.wrap(1) - tp.sumCheckUChallenges[i]); + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.s1; + commitments[3] = vk.s2; + commitments[4] = vk.s3; + commitments[5] = vk.s4; + commitments[6] = vk.id1; + commitments[7] = vk.id2; + commitments[8] = vk.id3; + commitments[9] = vk.id4; + commitments[10] = vk.lagrangeFirst; + commitments[11] = vk.lagrangeLast; + commitments[12] = vk.qLookup; + commitments[13] = vk.t1; + commitments[14] = vk.t2; + commitments[15] = vk.t3; + commitments[16] = vk.t4; + commitments[17] = vk.qm; + commitments[18] = vk.qr; + commitments[19] = vk.qo; + commitments[20] = vk.qc; + commitments[21] = vk.ql; + commitments[22] = vk.q4; + commitments[23] = vk.qArith; + commitments[24] = vk.qDeltaRange; + commitments[25] = vk.qElliptic; + commitments[26] = vk.qMemory; + commitments[27] = vk.qNnf; + commitments[28] = vk.qPoseidon2External; + commitments[29] = vk.qPoseidon2Internal; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Denominators 1/(z - point_i) for the five opening points {r, g*r, r, 1, r}. + mem.denominators[0] = ONE.div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = ONE.div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] = ONE.div(tp.shplonkZ - ONE); + mem.denominators[NUM_SMALL_IPA_OPENING_CLAIMS - 1] = mem.denominators[0]; + + // Iterate the opening claims in three segments — the inner loops can't be merged without an extra induction + // variable, which pushes us into stack-too-deep. + for (uint256 i = 0; i < SMALL_IPA_BOUNDARY_OPENING_IDX; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + + // Boundary slot: claimed value is hardcoded 0, so no constantTermAccumulator contribution. + { + Fr scalingFactor = mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] * mem.batchingChallenge; + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + } + + for (uint256 i = SMALL_IPA_BOUNDARY_OPENING_IDX + 1; i < NUM_SMALL_IPA_OPENING_CLAIMS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i - 1]; + } + + // Group per-claim batching scalars by commitment: [G], [A] (three openings), [Q]. + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = + mem.batchingScalars[1] + mem.batchingScalars[2] + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX]; + scalars[boundary + 2] = mem.batchingScalars[NUM_SMALL_IPA_OPENING_CLAIMS - 1]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Identity bases are accepted: VK selector/table polys may be identically zero, + // and the ecAdd/ecMul precompiles treat (0,0) as the additive identity per EIP-196. + // Soundness against an attacker substituting (0,0) for a non-zero commitment is + // upheld by sumcheck/Shplemini, which would fail on inconsistent evaluations. + + bool success = true; + assembly ("memory-safe") { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract BearerLinkHonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol b/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol new file mode 100644 index 0000000..ecb41dc --- /dev/null +++ b/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol @@ -0,0 +1,2494 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +// Vendored from libid-circuits v0.4.0 (libid-circuits-0.4.0-oidc-google.tar.gz) by scripts/vendor-circuit-verifiers.sh. Do not edit. +// The pin is contracts/circuits/circuits.json; `forge fmt` is the only change to what shipped. +pragma solidity >=0.8.21; + +uint256 constant N = 262144; +uint256 constant LOG_N = 18; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 64; +uint256 constant VK_HASH = 0x1b50bbf6d8ea6efc7ecc2547c25b285511704a10d9247466e84045095d9c3f77; + +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(262144), + logCircuitSize: uint256(18), + publicInputsSize: uint256(64), + ql: Honk.G1Point({ + x: uint256(0x08515254b0b2bbf647e56ed57459e7a64c16f3671ec0b40040e24cae2de769a1), + y: uint256(0x19f82d3334546ad161637f7a51fb2c842c4315275c4024da44f52d59f951dedb) + }), + qr: Honk.G1Point({ + x: uint256(0x047e99ebaa0002607e7ada10258f79a9be3a1cf6690e32cbc8f489afffd4c63d), + y: uint256(0x16a2041afe3ccc04b269627d512fa7136afff3deceae5fae49c274810b12d83d) + }), + qo: Honk.G1Point({ + x: uint256(0x2e316e89831c2ad1377c299e1aae33c18341e31b82b802a14147286cd3eb7ca0), + y: uint256(0x17a7383dffd125bdbabfb45c8ac10e6b8864a7c6a925e20bc8e377d7b39f7d43) + }), + q4: Honk.G1Point({ + x: uint256(0x0a8c8e60875cb0f659516ed955c124adb212f0a695c007a453d26dd721e940c1), + y: uint256(0x16339174b877c03cbc7c85674096efb16b9ea5471b01133e0a12d436893fce9d) + }), + qm: Honk.G1Point({ + x: uint256(0x1285dd2ef272db30649cdfa1f2262fba3c35983ef4deceb0e5f093dc613ab6cf), + y: uint256(0x07bc7a4cc600b01a4a4022cdf42a1d0f41abb867b213cc84070ca6a436b05017) + }), + qc: Honk.G1Point({ + x: uint256(0x2704cb2db8dbb2a6229383fe1f7a0c26e18edbef580dabd94ecc101a8c000566), + y: uint256(0x24eab2285697340ecc7c0485a41d95a5069cbbc721e32bbb6ecbaac8732b185a) + }), + qLookup: Honk.G1Point({ + x: uint256(0x25182937a03a6c2ba9e263545ff28fd79302eb44a72cceb2425965d5420e0b0c), + y: uint256(0x0b55f00da16bf0d0e7eb2e1d212f16adc4139020526e1890051eed208b567f68) + }), + qArith: Honk.G1Point({ + x: uint256(0x259c3a410a4c16e2c0d60e909c5d80c0c45a2e9d534883a342c6df4866d2554c), + y: uint256(0x25fc94235774734903ef66a6100c7cb95f8e7700c83d6ea7df8088c25d6ef1b4) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x2002cc2742d0b1b60e9089886601cb515bd32e1631efffa3015c98d2dcdac063), + y: uint256(0x15fb091ccd4254bb539e726a6568bc91aed65bbe74f606fb3f047c463164aa4a) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qMemory: Honk.G1Point({ + x: uint256(0x23b5b2077446236b5198c270fd8fa54bbb416f214fc52db278f594de8a1e1b7d), + y: uint256(0x1951fc00779e95dad7e2f6413ef993be7bc5edcd75d4b009ff361d0ca5248909) + }), + qNnf: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) + }), + s1: Honk.G1Point({ + x: uint256(0x1757d1f99e0fe4f46de6bb139aa0815888f75288adb232a1bf753022fc77a656), + y: uint256(0x2aecb68071e83250e9b3ffca574a9c99616badaa9fe6266fbcebace96fb82ad5) + }), + s2: Honk.G1Point({ + x: uint256(0x07897ea026c26a2ebfc2aeb63275bd76a72ce2455d899a0f4f8731e8a736a8ad), + y: uint256(0x1d6f2d522f4daffb155914a653aa7440c6ff435c5537b305a9c4d9566d00adad) + }), + s3: Honk.G1Point({ + x: uint256(0x02fe36d7c36b00aaa67ff577f6be31aa9e419f21d556ac171540505b16ecdbb0), + y: uint256(0x11652f3a5f31f4a13badb6b22b42ffe2d1efc5ba169b01040d48d01df499b7c0) + }), + s4: Honk.G1Point({ + x: uint256(0x0d6dd4196a4c3f7dd85026ee6ee6561f4f05c218815ce16b89fb47a042549b94), + y: uint256(0x21c9d576510a4d224c41368265e95747ff7307df314fbe1cafe75f59c5876361) + }), + t1: Honk.G1Point({ + x: uint256(0x022c2e257702c8227a744d35983564a1efbed263b14f025945c36ed229fc1ddd), + y: uint256(0x27f456a22cc2a694bad2998fb70241483b2d5d6d27197a646ee1ba3abc344ea6) + }), + t2: Honk.G1Point({ + x: uint256(0x10c31588d8dfb80f7abac5634e2b9b6e58b36031cbb75ac8e2febebe9417cd45), + y: uint256(0x24b3f9578867af5610a9b7d9a989c6fadb0b87a86aaabd191bf0f63003b5ada4) + }), + t3: Honk.G1Point({ + x: uint256(0x2914a41cda5920827a6e63a413040ac1de2883ec93c1751fa3240eef56bb7c24), + y: uint256(0x024dab849a0daf31508ff240bca405c355fdaf562cc016bf7d19298a98d40575) + }), + t4: Honk.G1Point({ + x: uint256(0x237f8dc54d796c2c1080bf7444d9cf948b20e3386af8f6f9f1a0d753b8e8cb62), + y: uint256(0x0760e9f1c4ef20c02e419e2e24a79ee417b5bc6ba04f667f29eeba1c23e849a3) + }), + id1: Honk.G1Point({ + x: uint256(0x047fb5e65fa50b75618e63d061948e0325a6c31691dba46575a9b1b29770e774), + y: uint256(0x1bb8f3358642934eb9d84d6d62d58ac1eda0116841220f07c4b349836ea5e85b) + }), + id2: Honk.G1Point({ + x: uint256(0x24871428617e352c457baae5b042bdc725b03efd9d6df4d231d33981078771bb), + y: uint256(0x0bd1f451e556b203a9c0a02d10cd5f2956586a44cb62526c7546f33a42278d67) + }), + id3: Honk.G1Point({ + x: uint256(0x19c3aa8241ea7d90a20902dca543902f43d83b1892e2a50da0c954e9bcf2a19e), + y: uint256(0x116ccacc53166cc5ea29a6097a731c168217262671d5a3aeadef7c9dd19ef5cf) + }), + id4: Honk.G1Point({ + x: uint256(0x24a599d525d2d50d57089e1c9006a3ced7a51235a2582080c8032619c8cd7516), + y: uint256(0x02a2b6e766a64bf55c9440d614259d4d946dd1df37877a95a68eb20dde98e153) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6), + y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x0d06cb4e0a5020818541df1279f0b68ba33f90fbc20e8ca797fc34067d3e3de8), + y: uint256(0x0d7631821cd9c64a626e7390e0f45830e473b9b84bb54fbfadf7985083663052) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); + +// SmallSubgroupIPA opening-claim layout — mirrors SMALL_IPA_CLAIMS in +// barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa_utils.hpp. +uint256 constant NUM_SMALL_IPA_OPENING_CLAIMS = 5; +uint256 constant SMALL_IPA_BOUNDARY_OPENING_IDX = 3; +uint256 constant NUM_SMALL_IPA_TRANSCRIPT_EVALS = 4; +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly ("memory-safe") { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly ("memory-safe") { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 29; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// Must match UltraFlavor_Generated::EntityId order. +enum WIRE { + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + Q_LOOKUP, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + Q_M, + Q_R, + Q_O, + Q_C, + Q_L, + Q_4, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) external pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[6] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[7] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[0] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[1] = acc; + } + + // Contribution 4: z_perm initialization check (lagrange_first * z_perm = 0) + { + Fr acc = (wire(p, WIRE.LAGRANGE_FIRST) * wire(p, WIRE.Z_PERM)) * domainSep; + evals[2] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[3] = accumulatorNone; + evals[4] = accumulatorOne; + evals[5] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[11] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[12] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[13] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[12] = evals[12] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[13] = evals[13] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[15] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[16] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[17] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[18] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[19] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[14] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[20] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[24] = evals[24] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[28] = evals[28] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[NUM_SMALL_IPA_OPENING_CLAIMS] denominators; + Fr[NUM_SMALL_IPA_OPENING_CLAIMS] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = + ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // (0,0) is the canonical EIP-196 encoding of the identity. It is accepted here + // because polynomial commitments to identically-zero polynomials (e.g. unused + // selector or table polys) are legitimately the identity. On-curve validation + // (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles per EIP-196. + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly ("memory-safe") { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly ("memory-safe") { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, + 5 // pubInputsOffset = NUM_DISABLED_ROWS_IN_SUMCHECK + NUM_ZERO_ROWS = 4 + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + // Row-disabling polynomial: 1 - ∏_{i≥2}(1 - u_i) + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * (Fr.wrap(1) - tp.sumCheckUChallenges[i]); + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.s1; + commitments[3] = vk.s2; + commitments[4] = vk.s3; + commitments[5] = vk.s4; + commitments[6] = vk.id1; + commitments[7] = vk.id2; + commitments[8] = vk.id3; + commitments[9] = vk.id4; + commitments[10] = vk.lagrangeFirst; + commitments[11] = vk.lagrangeLast; + commitments[12] = vk.qLookup; + commitments[13] = vk.t1; + commitments[14] = vk.t2; + commitments[15] = vk.t3; + commitments[16] = vk.t4; + commitments[17] = vk.qm; + commitments[18] = vk.qr; + commitments[19] = vk.qo; + commitments[20] = vk.qc; + commitments[21] = vk.ql; + commitments[22] = vk.q4; + commitments[23] = vk.qArith; + commitments[24] = vk.qDeltaRange; + commitments[25] = vk.qElliptic; + commitments[26] = vk.qMemory; + commitments[27] = vk.qNnf; + commitments[28] = vk.qPoseidon2External; + commitments[29] = vk.qPoseidon2Internal; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Denominators 1/(z - point_i) for the five opening points {r, g*r, r, 1, r}. + mem.denominators[0] = ONE.div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = ONE.div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] = ONE.div(tp.shplonkZ - ONE); + mem.denominators[NUM_SMALL_IPA_OPENING_CLAIMS - 1] = mem.denominators[0]; + + // Iterate the opening claims in three segments — the inner loops can't be merged without an extra induction + // variable, which pushes us into stack-too-deep. + for (uint256 i = 0; i < SMALL_IPA_BOUNDARY_OPENING_IDX; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + + // Boundary slot: claimed value is hardcoded 0, so no constantTermAccumulator contribution. + { + Fr scalingFactor = mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] * mem.batchingChallenge; + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + } + + for (uint256 i = SMALL_IPA_BOUNDARY_OPENING_IDX + 1; i < NUM_SMALL_IPA_OPENING_CLAIMS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i - 1]; + } + + // Group per-claim batching scalars by commitment: [G], [A] (three openings), [Q]. + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = + mem.batchingScalars[1] + mem.batchingScalars[2] + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX]; + scalars[boundary + 2] = mem.batchingScalars[NUM_SMALL_IPA_OPENING_CLAIMS - 1]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Identity bases are accepted: VK selector/table polys may be identically zero, + // and the ecAdd/ecMul precompiles treat (0,0) as the additive identity per EIP-196. + // Soundness against an attacker substituting (0,0) for a non-zero commitment is + // upheld by sumcheck/Shplemini, which would fail on inconsistent evaluations. + + bool success = true; + assembly ("memory-safe") { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract OidcGoogleHonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/solidity/contracts/circuits/circuits.json b/solidity/contracts/circuits/circuits.json new file mode 100644 index 0000000..b27b2a5 --- /dev/null +++ b/solidity/contracts/circuits/circuits.json @@ -0,0 +1,13 @@ +{ + "version": "0.4.0", + "circuits": { + "bearer-link": { + "contract": "BearerLinkHonkVerifier", + "sha256": "1fe9789337c2b1ce5250fbde11f3ff3f9969972fe60dce12ba2b40f455a894c2" + }, + "oidc-google": { + "contract": "OidcGoogleHonkVerifier", + "sha256": "633b48d340c23933e0d6fcfa0267902466d87db656a8bda62d1b5ee3213c9a42" + } + } +} diff --git a/solidity/contracts/circuits/test/HonkVerifiers.t.sol b/solidity/contracts/circuits/test/HonkVerifiers.t.sol new file mode 100644 index 0000000..7d3afc1 --- /dev/null +++ b/solidity/contracts/circuits/test/HonkVerifiers.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {BearerLinkHonkVerifier} from "../BearerLinkHonkVerifier.sol"; +import {OidcGoogleHonkVerifier} from "../OidcGoogleHonkVerifier.sol"; +import {GooglePlatformVerifier, IGoogleJwtRoots} from "../../ceremony/GooglePlatformVerifier.sol"; +import {INotaryService} from "../../ceremony/INotaryService.sol"; +import {IHonkVerifier} from "../../ceremony/PlatformVerifierBase.sol"; +import {XPlatformVerifier} from "../../ceremony/XPlatformVerifier.sol"; + +/// @notice The vendored Honk verifiers are what the Platform Verifiers pin. +/// +/// @dev A bb verifier embeds its verification key as code and exposes no +/// getter, so nothing here can ask it WHICH circuit it answers for. What +/// it does say is the `logN` a wrong-length proof comes back with, and +/// the two circuits differ in it: that is the check that would catch a +/// release whose tarballs were swapped, or a vendor run that wrote one +/// circuit's verifier under the other's name. +contract HonkVerifiersTest is Test { + /// `Errors.ProofLengthWrongWithLogN` from the generated sources, which + /// only a Honk verifier raises. + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + + address constant OWNER = address(0xA11CE); + uint64 constant LIFETIME = 3600; + uint64 constant SKEW = 300; + + IHonkVerifier bearerLink; + IHonkVerifier oidcGoogle; + + function setUp() public { + bearerLink = IHonkVerifier(address(new BearerLinkHonkVerifier())); + oidcGoogle = IHonkVerifier(address(new OidcGoogleHonkVerifier())); + } + + /// The `logN` a verifier reports for its own circuit, read by handing + /// it a proof of the wrong length. + function _logN(IHonkVerifier verifier) private view returns (uint256) { + try verifier.verify("", new bytes32[](0)) returns (bool) { + revert("an empty proof verified"); + } catch (bytes memory reason) { + // The first four bytes of revert data ARE the selector; the rest + // is decoded below. + // forge-lint: disable-next-line(unsafe-typecast) + assertEq(bytes4(reason), ProofLengthWrongWithLogN.selector, "not a Honk verifier"); + bytes memory args = new bytes(reason.length - 4); + for (uint256 i = 0; i < args.length; ++i) { + args[i] = reason[i + 4]; + } + (uint256 logN,,) = abi.decode(args, (uint256, uint256, uint256)); + return logN; + } + } + + /// EIP-170: forge deploys under the default limit, and the sizes are + /// asserted rather than merely survived so a release that grows past it + /// names the number. + function test_verifiersFitUnderTheCodeSizeLimit() public view { + assertLe(address(bearerLink).code.length, 24_576, "bearer-link over EIP-170"); + assertLe(address(oidcGoogle).code.length, 24_576, "oidc-google over EIP-170"); + } + + function test_eachVerifierAnswersForItsOwnCircuit() public view { + uint256 bearer = _logN(bearerLink); + uint256 oidc = _logN(oidcGoogle); + assertGt(bearer, 0, "bearer-link reports no circuit size"); + assertGt(oidc, 0, "oidc-google reports no circuit size"); + assertNotEq(bearer, oidc, "both platforms would verify under one circuit"); + } + + /// A Platform Verifier pins its circuit's verifier by address and by the + /// code hash the chain reports for it — the real artifact, not a stub. + function test_platformVerifiersPinTheRealArtifacts() public { + XPlatformVerifier xImpl = new XPlatformVerifier(); + XPlatformVerifier x = XPlatformVerifier( + address( + new ERC1967Proxy( + address(xImpl), + abi.encodeCall( + XPlatformVerifier.initialize, + ( + OWNER, + INotaryService(address(0x0707)), + bearerLink, + address(bearerLink).codehash, + LIFETIME, + SKEW, + SKEW + ) + ) + ) + ) + ); + assertEq(address(x.honkVerifier()), address(bearerLink)); + assertEq(x.honkVerifierCodehash(), address(bearerLink).codehash); + + GooglePlatformVerifier gImpl = new GooglePlatformVerifier(); + GooglePlatformVerifier g = GooglePlatformVerifier( + address( + new ERC1967Proxy( + address(gImpl), + abi.encodeCall( + GooglePlatformVerifier.initialize, + ( + OWNER, + INotaryService(address(0)), + oidcGoogle, + address(oidcGoogle).codehash, + SKEW, + IGoogleJwtRoots(address(0x2007)) + ) + ) + ) + ) + ); + assertEq(address(g.honkVerifier()), address(oidcGoogle)); + assertEq(g.honkVerifierCodehash(), address(oidcGoogle).codehash); + assertNotEq(x.honkVerifierCodehash(), g.honkVerifierCodehash(), "one artifact for two circuits"); + } +} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index ca2a8d8..17e7673 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -48,6 +48,12 @@ fs_permissions = [ # written carries a `// forge-lint: disable-...` comment with its reason # instead, so the lint stays on for the next one. [lint] +# The vendored Honk verifiers are bb's output, not ours: the notes they draw +# (naming, unchecked casts) would be fixed in Aztec's generator, and a fix +# made here is undone by the next `scripts/vendor-circuit-verifiers.sh` +# run, whose `--check` in CI would refuse the edit first. They still compile +# under `-D` like everything else; only the linter skips them. +ignore = ["contracts/circuits/*HonkVerifier.sol"] # Foundry's default stops at `low`, which leaves the style lints off: naming, # named struct fields, unused and unaliased imports. They are on. `gas` and # `code-size` stay off because their advice -- assembly keccak, a custom error From 170a3c7ccbae9d150888c4327035e1f68550baf0 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 19:02:26 +0100 Subject: [PATCH 2/4] feat(contracts): cover the Honk verifiers and their libraries The two verifiers are the first covered contracts that link a library, so COVERED, vendor-artifacts.sh and the linked_bytecode docs now say so, and circuits::deploy_honk_verifier is the one call that deploys RelationsLib and ZKTranscriptLib, links them and returns the verifier's address. The circuits pin rides into the artifacts so a consumer can name a deploy after the release its verifier came from. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/README.md | 35 ++-- rust/contracts/src/artifacts.rs | 82 ++++++--- rust/contracts/src/bindings/circuits.rs | 64 +++++++ rust/contracts/src/bindings/mod.rs | 1 + rust/contracts/src/circuits.rs | 222 ++++++++++++++++++++++++ rust/contracts/src/deploy.rs | 7 +- rust/contracts/src/lib.rs | 10 +- rust/contracts/src/platform_verifier.rs | 33 +++- scripts/vendor-artifacts.sh | 22 ++- 9 files changed, 430 insertions(+), 46 deletions(-) create mode 100644 rust/contracts/src/bindings/circuits.rs create mode 100644 rust/contracts/src/circuits.rs diff --git a/rust/contracts/README.md b/rust/contracts/README.md index 9687ba2..499b398 100644 --- a/rust/contracts/README.md +++ b/rust/contracts/README.md @@ -3,9 +3,10 @@ Typed [alloy](https://github.com/alloy-rs/alloy) bindings, embedded forge artifacts, and deploy/upgrade helpers for the libid identity stack: the ceremony verification path (`NotaryService`, `CeremonyProofVerifier`, the -three launch Platform Verifiers it routes to, and `GoogleJwtRoots`, the -signing keys the `google/v1` verifier trusts), the naming system -(`IdentityNames`), and the deterministic deployment factory (`LibidFactory`). +three launch Platform Verifiers it routes to, the two UltraHonk verifiers +they pin, and `GoogleJwtRoots`, the signing keys the `google/v1` verifier +trusts), the naming system (`IdentityNames`), and the deterministic +deployment factory (`LibidFactory`). The compiled artifacts are vendored into the crate, so a consumer can deploy or upgrade the whole stack against a live network with **zero filesystem @@ -74,20 +75,25 @@ async fn main() -> Result<(), Box> { } ``` -## Example: deploy a Platform Verifier +## Example: deploy a Platform Verifier on its circuit's Honk verifier A Platform Verifier pins the bb-generated UltraHonk verifier for its circuit by address and by code hash, holds a Notary Service only if its profile notarizes anything, and caps its parameters. `platform_verifier::Initializer` knows those rules: it reads the code hash off the chain, refuses what the -contract would refuse, and builds the exact `initialize` call. The Honk -verifier comes from a `libid-circuits` release and is deployed beforehand. +contract would refuse, and builds the exact `initialize` call. + +The Honk verifier is vendored here too, from the pinned `libid-circuits` +release. `circuits::deploy_honk_verifier` deploys the two libraries it links +(`RelationsLib`, `ZKTranscriptLib`), links them in and deploys the verifier — +three transactions — and returns the address the initializer pins. ```rust,no_run use alloy::{primitives::Address, providers::ProviderBuilder}; use libid_contracts::{ bindings::ceremony::{CeremonyProofVerifier, XPlatformVerifier}, - platform_verifier::{deploy_platform_verifier, Initializer, TlsNotaryRoots}, + circuits::deploy_honk_verifier, + platform_verifier::{deploy_platform_verifier, Initializer, PlatformVerifier, TlsNotaryRoots}, Artifacts, }; @@ -97,8 +103,12 @@ async fn main() -> Result<(), Box> { .wallet(/* your signer */ todo!()) .connect_http("https://rpc.example.org".parse()?); let artifacts = Artifacts::embedded(); - let (owner, notary, honk_verifier, proof_verifier): (Address, Address, Address, Address) = - todo!(); + let (owner, notary, proof_verifier): (Address, Address, Address) = todo!(); + + // The circuit `x/v1` proves under is `bearer-link`; `PlatformVerifier::circuit` + // says so, and this deploys its verifier with the libraries linked. + let honk_verifier = + deploy_honk_verifier(&provider, &artifacts, PlatformVerifier::X.circuit(), None).await?; // `x/v1`: two notarized sessions, so a Notary Service is required. // `Initializer::Google` takes `GoogleRoots` instead — no Notary Service @@ -141,9 +151,10 @@ Other entry points: canonical cross-network factory where missing and deploy protocol proxies through it at name-derived CREATE3 addresses. - `deploy::load_linked_bytecode` (or `Artifacts::linked_bytecode`) — deploys - and links external libraries before returning the creation bytecode. Nothing - covered today links one; the UltraHonk verifiers the ceremony circuits bring - will. + and links external libraries before returning the creation bytecode; what + `circuits::deploy_honk_verifier` goes through. +- `circuits::version` — the `libid-circuits` release the vendored verifiers + came from, for a consumer that names a deployment after its artifact. - `platform_verifier::codehash_at` — the code hash `setTrustRoots` wants when a Platform Verifier is rotated onto a new circuit release. - `Artifacts::method_identifiers` — selector extraction from the vendored diff --git a/rust/contracts/src/artifacts.rs b/rust/contracts/src/artifacts.rs index 264813d..c44a8fd 100644 --- a/rust/contracts/src/artifacts.rs +++ b/rust/contracts/src/artifacts.rs @@ -39,11 +39,18 @@ pub const COVERED: &[(&str, &str)] = &[ ("CeremonyProofVerifier", "CeremonyProofVerifier"), ("ERC1967Proxy", "ERC1967Proxy"), ("GoogleJwtRoots", "GoogleJwtRoots"), - // ceremony: the launch Platform Verifiers (one per profile; the UltraHonk - // verifier each pins comes from the circuits release, not from here) + // ceremony: the launch Platform Verifiers (one per profile) ("XPlatformVerifier", "XPlatformVerifier"), ("GitHubPlatformVerifier", "GitHubPlatformVerifier"), ("GooglePlatformVerifier", "GooglePlatformVerifier"), + // circuits: the UltraHonk verifiers the Platform Verifiers pin, vendored + // from the libid-circuits release, each with the two libraries it links + ("BearerLinkHonkVerifier", "BearerLinkHonkVerifier"), + ("BearerLinkHonkVerifier", "RelationsLib"), + ("BearerLinkHonkVerifier", "ZKTranscriptLib"), + ("OidcGoogleHonkVerifier", "OidcGoogleHonkVerifier"), + ("OidcGoogleHonkVerifier", "RelationsLib"), + ("OidcGoogleHonkVerifier", "ZKTranscriptLib"), // identity ("IdentityNames", "IdentityNames"), // ens (deployed once per network, not CREATE3-canonical) @@ -82,17 +89,22 @@ impl Artifacts { /// The raw artifact JSON for `out/.sol/.json`. pub fn raw(&self, file: &str, contract: &str) -> Result { - let rel = format!("{file}.sol/{contract}.json"); + self.read_json(&format!("{file}.sol/{contract}.json")) + } + + /// Any JSON file at `rel` inside the source: an artifact, or the + /// `circuits.json` pin the vendor script copies in beside them. + pub(crate) fn read_json(&self, rel: &str) -> Result { let contents = match &self.source { Source::Embedded => EMBEDDED - .get_file(&rel) + .get_file(rel) .and_then(|f| f.contents_utf8()) .map(str::to_owned) .ok_or_else(|| Error::Artifact { detail: format!("no embedded artifact {rel}"), })?, Source::Dir(dir) => { - let path = dir.join(&rel); + let path = dir.join(rel); std::fs::read_to_string(&path).map_err(|e| Error::Artifact { detail: format!("failed to read artifact {}: {e}", path.display()), })? @@ -130,11 +142,12 @@ impl Artifacts { /// Creation bytecode with every external library it references deployed /// (recursively) through `provider` and linked in. Mirrors what forge does - /// automatically. Nothing covered today links a library; the UltraHonk - /// verifiers the ceremony circuits bring link `ZKTranscriptLib`, and this - /// is the path they will deploy through. For artifacts with no link - /// references this behaves like [`Self::bytecode_named`] (no transaction - /// is sent). + /// automatically. The two UltraHonk verifiers are what links a library + /// today — `RelationsLib` and `ZKTranscriptLib`, vendored beside each — + /// and [`deploy_honk_verifier`](crate::circuits::deploy_honk_verifier) + /// is the one call that takes them through here and deploys the result. + /// For artifacts with no link references this behaves like + /// [`Self::bytecode_named`] (no transaction is sent). /// /// `sender` opts into explicit nonce management (see /// [`deploy_contract_from`](crate::deploy::deploy_contract_from)). @@ -207,8 +220,8 @@ mod tests { use super::*; /// Every covered contract's creation bytecode is present and non-empty. - /// Only the hex is checked here so a future artifact with link - /// placeholders still passes; linking is the anvil tests' business. + /// Only the hex is checked here so the artifacts with link placeholders + /// (the Honk verifiers) pass too; linking is the anvil tests' business. #[test] fn every_covered_contract_has_bytecode() { let artifacts = Artifacts::embedded(); @@ -223,23 +236,50 @@ mod tests { } } - /// Contracts without link references decode straight to bytes — which is - /// every covered contract today, so this doubles as the check that none of - /// them silently grew a library dependency the vendor script must follow. + /// Contracts without link references decode straight to bytes, and the + /// ones with them are exactly the two Honk verifiers — so this doubles + /// as the check that nothing else silently grew a library dependency, + /// and that every library a linked contract names is covered under its + /// own file, where the vendor script and the linker look for it. #[test] - fn unlinked_contracts_decode() { + fn unlinked_contracts_decode_and_linked_ones_are_the_honk_verifiers() { let artifacts = Artifacts::embedded(); + let mut linked = Vec::new(); for &(file, contract) in COVERED { - if artifacts - .link_references(file, contract) - .unwrap() - .is_empty() - { + let refs = artifacts.link_references(file, contract).unwrap(); + if refs.is_empty() { let bytecode = artifacts .bytecode_named(file, contract) .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}")); assert!(!bytecode.is_empty()); + continue; + } + linked.push((file, contract)); + let err = artifacts.bytecode_named(file, contract).unwrap_err(); + assert!( + err.to_string().contains("unresolved link references"), + "{file}.sol:{contract}: {err}" + ); + for (path, libs) in &refs { + let stem = std::path::Path::new(path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap(); + for library in libs.as_object().unwrap().keys() { + assert!( + COVERED.contains(&(stem, library.as_str())), + "{file}.sol:{contract} links {stem}.sol:{library}, which is not covered" + ); + } } } + linked.sort_unstable(); + assert_eq!( + linked, + [ + ("BearerLinkHonkVerifier", "BearerLinkHonkVerifier"), + ("OidcGoogleHonkVerifier", "OidcGoogleHonkVerifier"), + ] + ); } } diff --git a/rust/contracts/src/bindings/circuits.rs b/rust/contracts/src/bindings/circuits.rs new file mode 100644 index 0000000..4f3afd8 --- /dev/null +++ b/rust/contracts/src/bindings/circuits.rs @@ -0,0 +1,64 @@ +//! Bindings for the bb-generated UltraHonk verifiers in +//! `solidity/contracts/circuits`: the one call a Platform Verifier makes of +//! them, and the error that says which circuit a deployed one answers for. + +/// Bindings for a bb-generated UltraHonk verifier — `BearerLinkHonkVerifier` +/// and `OidcGoogleHonkVerifier`, one interface for both. +/// +/// `verify` is the whole surface a Platform Verifier uses +/// (`IHonkVerifier` in `PlatformVerifierBase.sol`). The error is the one +/// thing a deployed verifier says about itself: it embeds its verification +/// key as code constants and exposes no getter, so `logN` from a +/// wrong-length proof is how a test tells a real verifier over the right +/// circuit from a contract that merely has code. +#[allow(unused_attributes)] +mod honk_verifier_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface HonkVerifier { + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + external + view + returns (bool); + + /// Raised by `verify` before anything else when the proof is + /// not the length the circuit's `logN` implies. + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + } + } +} + +pub use honk_verifier_inner::HonkVerifier; + +#[cfg(test)] +mod tests { + use alloy::sol_types::SolCall; + + use super::*; + use crate::{ + circuits::Circuit, + Artifacts, + }; + + /// `verify` is what a Platform Verifier calls; a vendored verifier + /// without that selector would be pinned and revert at the first + /// user's proof. + #[test] + fn every_circuit_verifier_answers_verify() { + let artifacts = Artifacts::embedded(); + for circuit in Circuit::ALL { + let methods = artifacts.method_identifiers(circuit.contract()).unwrap(); + let found = methods + .get(HonkVerifier::verifyCall::SIGNATURE) + .unwrap_or_else(|| panic!("{} has no verify", circuit.contract())); + assert_eq!( + *found, + alloy::hex::encode(HonkVerifier::verifyCall::SELECTOR), + "{}.verify", + circuit.contract() + ); + } + } +} diff --git a/rust/contracts/src/bindings/mod.rs b/rust/contracts/src/bindings/mod.rs index e6d8cef..244f290 100644 --- a/rust/contracts/src/bindings/mod.rs +++ b/rust/contracts/src/bindings/mod.rs @@ -2,6 +2,7 @@ //! sources in `solidity/contracts`. One module per contract directory. pub mod ceremony; +pub mod circuits; pub mod ens; pub mod factory; pub mod identity; diff --git a/rust/contracts/src/circuits.rs b/rust/contracts/src/circuits.rs new file mode 100644 index 0000000..d0a6b43 --- /dev/null +++ b/rust/contracts/src/circuits.rs @@ -0,0 +1,222 @@ +//! The ceremony circuits' UltraHonk verifiers: which circuit each platform +//! proves under, and a deploy that links the libraries a bb verifier needs. +//! +//! A Honk verifier is not written in this repository. bb derives it from a +//! circuit's verification key, `libid-circuits` runs bb and ships the +//! Solidity in its release tarballs, and `scripts/vendor-circuit-verifiers.sh` +//! downloads the pinned release — checked against digests committed in +//! `solidity/contracts/circuits/circuits.json` — formats it and commits it +//! under `solidity/contracts/circuits`. From there it is a contract like any +//! other: `forge build` compiles it and `scripts/vendor-artifacts.sh` embeds +//! it, so a consumer deploys it from [`Artifacts::embedded`] with no `bb`. +//! +//! bb emits `RelationsLib` and `ZKTranscriptLib` as external libraries, so a +//! verifier's creation code carries a placeholder per call site until each +//! library is deployed and its address linked in. [`deploy_honk_verifier`] +//! does all of that and returns the verifier's address, which is what a +//! [`platform_verifier::Initializer`](crate::platform_verifier::Initializer) +//! pins — by address and by the code hash it reads off the chain. + +use alloy::{ + primitives::Address, + providers::Provider, +}; + +use crate::{ + artifacts::Artifacts, + deploy::deploy_contract_from, + error::{ + Error, + Result, + }, +}; + +/// One ceremony circuit, and so one vendored Honk verifier. +/// +/// Two, not three: `oidc-google` proves the Google ID Token, and +/// `bearer-link` ties a token exchange to an identity for X and GitHub +/// alike, because their statements are the same. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Circuit { + /// The token-exchange circuit, shared by the `x/v1` and `github/v1` + /// profiles. + BearerLink, + /// The Google OIDC circuit, for `google/v1`. + OidcGoogle, +} + +impl Circuit { + /// Every circuit the launch platforms verify under. + pub const ALL: [Self; 2] = [Self::BearerLink, Self::OidcGoogle]; + + /// The circuit's directory in the `libid-circuits` release, which is + /// also its tarball's name and its key in `circuits.json`. + pub const fn name(self) -> &'static str { + match self { + Self::BearerLink => "bearer-link", + Self::OidcGoogle => "oidc-google", + } + } + + /// The vendored contract, which is also its `.sol` file and its entry + /// in [`COVERED`](crate::artifacts::COVERED). bb names every verifier + /// `HonkVerifier`; `libid-circuits` renames each so two can share a + /// project and one artifact path names one circuit. + pub const fn contract(self) -> &'static str { + match self { + Self::BearerLink => "BearerLinkHonkVerifier", + Self::OidcGoogle => "OidcGoogleHonkVerifier", + } + } +} + +/// The external libraries every bb verifier links, vendored beside it under +/// its own `.sol` file (the shape `linkReferences` names them in). +pub const LIBRARIES: [&str; 2] = ["RelationsLib", "ZKTranscriptLib"]; + +/// The `libid-circuits` release the vendored verifiers came from, read from +/// the pin `scripts/vendor-artifacts.sh` copies in as `circuits.json`. +/// +/// A Honk verifier IS its verification key, so a consumer that names a +/// deployment after its artifact — a CREATE3 name, say — wants this in the +/// name: a new circuits release is a different contract and must land at a +/// different address rather than silently replace the old one. +/// +/// Errors for an [`Artifacts::from_dir`] over a raw forge `out/`, which +/// carries no pin. +pub fn version(artifacts: &Artifacts) -> Result { + let pin: serde_json::Value = artifacts.read_json("circuits.json")?; + pin["version"] + .as_str() + .map(str::to_owned) + .ok_or_else(|| Error::Artifact { + detail: "circuits.json has no version".into(), + }) +} + +/// Deploy `circuit`'s Honk verifier with its libraries linked, and return +/// its address. +/// +/// Each library is deployed first and its address substituted into the +/// verifier's creation code, then the verifier itself; deploying the +/// placeholder would produce a contract that reverts on every proof. Three +/// transactions per circuit. The libraries are not shared between the two +/// circuits: each verifier's file carries its own copy, and forge links a +/// verifier only against the libraries of its own file. +/// +/// The address is what a Platform Verifier initializer takes as +/// `honk_verifier`; the code hash it pins beside it is read off the chain by +/// [`Initializer::call`](crate::platform_verifier::Initializer::call). +/// +/// `sender` opts into explicit nonce management (see +/// [`deploy_contract_from`]). +pub async fn deploy_honk_verifier( + provider: &P, + artifacts: &Artifacts, + circuit: Circuit, + sender: Option
, +) -> Result
{ + let contract = circuit.contract(); + let bytecode = artifacts + .linked_bytecode(provider, contract, contract, sender) + .await?; + deploy_contract_from( + provider, + bytecode, + &format!("{contract} ({} circuit)", circuit.name()), + sender, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::artifacts::COVERED; + + /// Every verifier and every library it links is one the crate vendors, + /// under the verifier's own file — a library missing from the artifacts + /// could only deploy with its placeholder left in. + #[test] + fn every_verifier_and_its_libraries_are_covered() { + let artifacts = Artifacts::embedded(); + for circuit in Circuit::ALL { + let contract = circuit.contract(); + assert!( + COVERED.contains(&(contract, contract)), + "{contract} is not in COVERED" + ); + for library in LIBRARIES { + assert!( + COVERED.contains(&(contract, library)), + "{contract}.sol:{library} is not in COVERED" + ); + } + + // The artifact links exactly LIBRARIES, and nothing under another + // file: a verifier that linked a library the list does not name + // would deploy with a placeholder left in. + let refs = artifacts.link_references(contract, contract).unwrap(); + let mut linked: Vec<(String, String)> = refs + .iter() + .flat_map(|(path, libs)| { + let stem = std::path::Path::new(path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or_else(|| panic!("{contract}: bad library path {path}")) + .to_owned(); + libs.as_object() + .into_iter() + .flatten() + .map(move |(name, _)| (stem.clone(), name.clone())) + }) + .collect(); + linked.sort(); + let mut expected: Vec<(String, String)> = LIBRARIES + .iter() + .map(|lib| (contract.to_owned(), (*lib).to_owned())) + .collect(); + expected.sort(); + assert_eq!(linked, expected, "{contract} links something else"); + } + } + + /// The two circuits are distinct artifacts: a shared one would wire + /// both platforms to one verification key. + #[test] + fn the_two_circuits_are_different_artifacts() { + let artifacts = Artifacts::embedded(); + let [bearer, oidc] = Circuit::ALL; + assert_ne!(bearer.contract(), oidc.contract()); + assert_ne!( + artifacts + .bytecode_hex(bearer.contract(), bearer.contract()) + .unwrap(), + artifacts + .bytecode_hex(oidc.contract(), oidc.contract()) + .unwrap() + ); + } + + /// The enum and the pin name the same circuits under the same + /// contracts; the pin is what the vendor script follows, the enum what + /// a consumer deploys by. + #[test] + fn the_enum_matches_the_pin() { + let artifacts = Artifacts::embedded(); + let pin: serde_json::Value = artifacts.read_json("circuits.json").unwrap(); + let circuits = pin["circuits"].as_object().expect("circuits object"); + assert_eq!(circuits.len(), Circuit::ALL.len()); + for circuit in Circuit::ALL { + let entry = &circuits[circuit.name()]; + assert_eq!(entry["contract"].as_str(), Some(circuit.contract())); + let digest = entry["sha256"].as_str().expect("sha256 string"); + assert_eq!(digest.len(), 64, "{}: not a sha256", circuit.name()); + } + let version = version(&artifacts).unwrap(); + assert!( + version.split('.').count() == 3, + "circuits.json version '{version}' is not major.minor.patch" + ); + } +} diff --git a/rust/contracts/src/deploy.rs b/rust/contracts/src/deploy.rs index 5e05a26..d5ca5be 100644 --- a/rust/contracts/src/deploy.rs +++ b/rust/contracts/src/deploy.rs @@ -230,9 +230,10 @@ pub async fn upgrade_uups( /// artifacts with no `linkReferences` this behaves like /// [`Artifacts::bytecode_named`] and sends nothing. /// -/// Kept although nothing covered today links a library: the bb-generated -/// UltraHonk verifiers link `ZKTranscriptLib`, and the ones the ceremony -/// circuits release will deploy through this path when they land. +/// The bb-generated UltraHonk verifiers are what goes through here: each +/// links `RelationsLib` and `ZKTranscriptLib`, and +/// [`deploy_honk_verifier`](crate::circuits::deploy_honk_verifier) is the +/// call that links and deploys one. pub async fn load_linked_bytecode( provider: &P, artifacts: &Artifacts, diff --git a/rust/contracts/src/lib.rs b/rust/contracts/src/lib.rs index 4503d15..fcee4c7 100644 --- a/rust/contracts/src/lib.rs +++ b/rust/contracts/src/lib.rs @@ -7,8 +7,9 @@ //! consumer talks to: the ceremony verification path (`NotaryService`, //! `CeremonyProofVerifier`, the three launch Platform Verifiers it routes //! to, and `GoogleJwtRoots`, the Google signing keys the `google/v1` -//! verifier trusts), the naming system (`IdentityNames`), and the -//! deterministic factory. Kept in lockstep with the Solidity sources in +//! verifier trusts), the naming system (`IdentityNames`), the +//! deterministic factory, and the UltraHonk verifiers the Platform +//! Verifiers pin. Kept in lockstep with the Solidity sources in //! `solidity/contracts`. //! - [`artifacts`] — the compiled creation bytecode, link references, and //! method identifiers of every deployable contract, embedded at compile time @@ -26,12 +27,17 @@ //! contract serves which platform, and an `initialize` call built with the //! Honk verifier's code hash read off chain and the rules //! `PlatformVerifierBase` enforces checked first. +//! - [`circuits`] — the ceremony circuits' UltraHonk verifiers, vendored +//! from the pinned `libid-circuits` release: which circuit a platform +//! proves under, and a deploy that links the libraries a bb verifier +//! needs. //! //! Signing is the consumer's concern: every helper takes a provider you have //! already wired with a wallet. pub mod artifacts; pub mod bindings; +pub mod circuits; pub mod deploy; mod error; pub mod factory; diff --git a/rust/contracts/src/platform_verifier.rs b/rust/contracts/src/platform_verifier.rs index 701b698..5e113d5 100644 --- a/rust/contracts/src/platform_verifier.rs +++ b/rust/contracts/src/platform_verifier.rs @@ -13,10 +13,13 @@ //! [`deploy_platform_verifier`] puts the implementation behind a fresh //! ERC1967 proxy with it. //! -//! The Honk verifier itself is not this crate's to deploy: it is -//! bb-generated from a `libid-circuits` release verification key, and a -//! Platform Verifier pins whichever one governance selected, by address AND -//! by code hash. +//! The Honk verifier a Platform Verifier pins is vendored here too +//! ([`circuits`](crate::circuits)): bb-generated in `libid-circuits` from +//! the circuit's verification key, deployed with its libraries linked by +//! [`deploy_honk_verifier`](crate::circuits::deploy_honk_verifier). Which +//! circuit a platform proves under is [`PlatformVerifier::circuit`]; the +//! contract pins whichever address governance names, by address AND by +//! code hash. use alloy::{ primitives::{ @@ -34,6 +37,7 @@ use crate::{ GooglePlatformVerifier, TlsNotaryPlatformVerifier, }, + circuits::Circuit, deploy::deploy_behind_proxy, error::{ Error, @@ -92,6 +96,15 @@ impl PlatformVerifier { keccak256(self.platform().as_bytes()) } + /// The ceremony circuit this platform's proofs are made under, and so + /// which vendored Honk verifier its `honk_verifier` should be. + pub const fn circuit(self) -> Circuit { + match self { + Self::X | Self::GitHub => Circuit::BearerLink, + Self::Google => Circuit::OidcGoogle, + } + } + /// Whether the profile notarizes any session, and so whether its /// verifier holds a Notary Service. `CeremonyProfile.attestationCount` /// is two for the TLSNotary profiles and zero for Google; the @@ -394,6 +407,18 @@ mod tests { assert_eq!(PlatformVerifier::ALL.len(), libid_profiles::LAUNCH.len()); } + /// Every circuit has a platform proving under it: a vendored verifier + /// no platform pins would be dead weight in every consumer's binary. + #[test] + fn every_circuit_serves_a_platform() { + for circuit in Circuit::ALL { + assert!( + PlatformVerifier::ALL.iter().any(|v| v.circuit() == circuit), + "{circuit:?} serves no platform" + ); + } + } + /// Every verifier's contract is one the crate vendors. #[test] fn every_verifier_is_covered() { diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index c5bf008..af3364a 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -6,8 +6,10 @@ # rust/contracts/artifacts/.sol/.json, pruned to the fields the # crate reads: bytecode.object, bytecode.linkReferences, methodIdentifiers. # Libraries referenced through linkReferences are followed transitively and -# vendored too (none of the covered contracts links one today; the Honk -# verifiers the ceremony circuits will bring do). +# vendored too: the two Honk verifiers link RelationsLib and ZKTranscriptLib, +# and both are listed below as well so the list and the crate's COVERED agree +# line for line. The circuits pin rides along as circuits.json, so the crate +# can say which libid-circuits release its verifiers came from. # # The result is NOT committed: rust/contracts/artifacts is gitignored and # regenerated on demand. Run this before any cargo command in rust/ — the @@ -24,6 +26,7 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OUT="$REPO_ROOT/solidity/out" DEST="$REPO_ROOT/rust/contracts/artifacts" +CIRCUITS_PIN="$REPO_ROOT/solidity/contracts/circuits/circuits.json" # ":" — the artifact lives at out/.sol/.json. # Keep in sync with the covered-contract list in rust/contracts/src/artifacts.rs. @@ -33,11 +36,20 @@ ARTIFACTS=( "CeremonyProofVerifier:CeremonyProofVerifier" "ERC1967Proxy:ERC1967Proxy" "GoogleJwtRoots:GoogleJwtRoots" - # ceremony: the launch Platform Verifiers (one per profile; the UltraHonk - # verifier each pins comes from the circuits release, not from here) + # ceremony: the launch Platform Verifiers (one per profile) "XPlatformVerifier:XPlatformVerifier" "GitHubPlatformVerifier:GitHubPlatformVerifier" "GooglePlatformVerifier:GooglePlatformVerifier" + # circuits: the UltraHonk verifiers the Platform Verifiers pin, vendored + # from the libid-circuits release by scripts/vendor-circuit-verifiers.sh, + # each with the two libraries it links (bb emits them as external + # libraries, so they are deployed contracts the verifier is linked to) + "BearerLinkHonkVerifier:BearerLinkHonkVerifier" + "BearerLinkHonkVerifier:RelationsLib" + "BearerLinkHonkVerifier:ZKTranscriptLib" + "OidcGoogleHonkVerifier:OidcGoogleHonkVerifier" + "OidcGoogleHonkVerifier:RelationsLib" + "OidcGoogleHonkVerifier:ZKTranscriptLib" # identity "IdentityNames:IdentityNames" # ens (deployed once per network, not CREATE3-canonical; embedded so a @@ -106,6 +118,8 @@ while [[ ${#queue[@]} -gt 0 ]]; do | "\($p):\(.)"' "$src") done +cp "$CIRCUITS_PIN" "$STAGE/circuits.json" + rm -rf "$DEST" mkdir -p "$(dirname "$DEST")" cp -R "$STAGE" "$DEST" From 33af8d51be148a827c750f6fc57ad36b6dafb829 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 19:02:39 +0100 Subject: [PATCH 3/4] test(contracts): pin the Platform Verifiers to real Honk verifiers on anvil The stand-in is gone: each Platform Verifier initializes against its circuit's verifier, the initializer's code hash is the chain's, and a wrong-length proof reads logN back so the two circuits are proven distinct, which is the check that catches a mixed-up vk. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/tests/anvil.rs | 136 ++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 16 deletions(-) diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index bcc1100..7715003 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -439,13 +439,14 @@ async fn deploys_the_ens_resolver_with_its_constructor_arguments() { /// (e) The three Platform Verifiers through `deploy_platform_verifier`, on /// the collaborators they pin: the Notary Service (the two TLSNotary ones), -/// the JWT root list (Google), and a Honk verifier — stood in for by any -/// contract with code, because `initialize` pins a code hash and never -/// calls `verify`. Each comes back initialized as the views say, registers -/// with the Proof Verifier, and the ceilings the crate restates are the -/// contract's. Then the rules: an initializer the wrapper refuses is one -/// the contract refuses too, and a Honk verifier with no code is caught -/// before any transaction. +/// the JWT root list (Google), and the real Honk verifier for each one's +/// circuit, deployed with its libraries linked. The code hash the +/// initializer computes is the one the chain reports for that verifier, +/// and the one the contract records. Each comes back initialized as the +/// views say, registers with the Proof Verifier, and the ceilings the crate +/// restates are the contract's. Then the rules: an initializer the wrapper +/// refuses is one the contract refuses too, and a Honk verifier with no +/// code is caught before any transaction. #[tokio::test] async fn deploys_and_initializes_every_platform_verifier() { use alloy::{ @@ -458,6 +459,10 @@ async fn deploys_and_initializes_every_platform_verifier() { GooglePlatformVerifier, TlsNotaryPlatformVerifier, }, + circuits::{ + deploy_honk_verifier, + Circuit, + }, platform_verifier::{ codehash_at, deploy_platform_verifier, @@ -511,27 +516,41 @@ async fn deploys_and_initializes_every_platform_verifier() { ) .await .unwrap(); - let honk = deploy_contract( - &provider, - artifacts.bytecode("WTIA9").unwrap(), - "stand-in Honk verifier", - ) - .await - .unwrap(); + // The real verifiers, one per circuit, libraries linked. The hash a + // Platform Verifier pins is read off the chain, never computed from the + // vendored bytes: it is what the chain holds for the artifact. + let bearer_link = + deploy_honk_verifier(&provider, &artifacts, Circuit::BearerLink, None) + .await + .unwrap(); + let oidc_google = + deploy_honk_verifier(&provider, &artifacts, Circuit::OidcGoogle, None) + .await + .unwrap(); + let honk_at = |circuit: Circuit| match circuit { + Circuit::BearerLink => bearer_link, + Circuit::OidcGoogle => oidc_google, + }; + let honk = bearer_link; let honk_codehash = codehash_at(&provider, honk).await.unwrap(); assert_ne!(honk_codehash, keccak256([])); + assert_ne!( + honk_codehash, + codehash_at(&provider, oidc_google).await.unwrap(), + "one artifact for two circuits" + ); let tls = TlsNotaryRoots { owner: deployer, notary_service: notary_proxy, - honk_verifier: honk, + honk_verifier: bearer_link, proof_lifetime: libid_profiles::PROOF_LIFETIME_SECONDS_X, max_future_attestation_skew: libid_profiles::MAX_FUTURE_ATTESTATION_SKEW_SECONDS, future_observation_allowance: 300, }; let google = GoogleRoots { owner: deployer, - honk_verifier: honk, + honk_verifier: oidc_google, future_observation_allowance: 7200, jwt_roots: roots_proxy, }; @@ -543,6 +562,21 @@ async fn deploys_and_initializes_every_platform_verifier() { Initializer::Google(google), ] { let verifier = init.verifier(); + // The initializer pins its circuit's verifier, and computes the hash + // the chain reports for it — `EXTCODEHASH`, `keccak256` of the + // runtime code — which is what `initialize` then checks. + let honk = honk_at(verifier.circuit()); + assert_eq!( + init.honk_verifier(), + honk, + "{verifier:?} pins the wrong circuit" + ); + let honk_codehash = keccak256(provider.get_code_at(honk).await.unwrap()); + assert_eq!( + init.call(&provider).await.unwrap().honk_verifier_codehash(), + honk_codehash, + "{verifier:?}: the initializer computed a hash the chain does not hold" + ); let proxy = deploy_platform_verifier(&provider, &artifacts, &init, None) .await .unwrap_or_else(|e| panic!("{verifier:?}: {e}")); @@ -706,3 +740,73 @@ async fn deploys_and_initializes_every_platform_verifier() { "{err}" ); } + +/// (f) The two Honk verifiers through `deploy_honk_verifier`: each lands +/// with its libraries linked, under EIP-170, and answers for its OWN +/// circuit. A bb verifier has no getter for its verification key; the one +/// thing it says about itself is the `logN` a wrong-length proof comes back +/// with. That separates a real verifier from a contract that merely has +/// code, and the two circuits from each other — the check that would catch +/// a release whose tarballs were swapped, or a vendor run that wrote one +/// circuit's verifier under the other's name. +#[tokio::test] +async fn deploys_the_linked_honk_verifiers_over_their_own_circuits() { + use alloy::{ + primitives::Bytes, + sol_types::SolError, + }; + use libid_contracts::{ + bindings::circuits::HonkVerifier, + circuits::{ + deploy_honk_verifier, + version, + Circuit, + }, + platform_verifier::codehash_at, + }; + + let provider = test_provider(); + let artifacts = Artifacts::embedded(); + assert!(!version(&artifacts).unwrap().is_empty()); + + let mut log_n = Vec::new(); + for circuit in Circuit::ALL { + let address = deploy_honk_verifier(&provider, &artifacts, circuit, None) + .await + .unwrap_or_else(|e| panic!("{circuit:?}: {e}")); + let code = provider.get_code_at(address).await.unwrap(); + assert!(!code.is_empty(), "{circuit:?} has no code at {address:#x}"); + // anvil runs the default limit, so deploying at all is the EIP-170 + // proof; the number is asserted so a release that grows past it + // says so here rather than in a failed deploy. + assert!( + code.len() <= 24_576, + "{circuit:?} is {} bytes, over EIP-170", + code.len() + ); + assert_eq!( + codehash_at(&provider, address).await.unwrap(), + keccak256(&code) + ); + + let err = HonkVerifier::new(address, &provider) + .verify(Bytes::new(), Vec::new()) + .call() + .await + .expect_err("an empty proof is the wrong length"); + let data = err + .as_revert_data() + .expect("the verifier reverted with data"); + let decoded = HonkVerifier::ProofLengthWrongWithLogN::abi_decode(&data) + .expect("only a Honk verifier raises ProofLengthWrongWithLogN"); + assert!( + decoded.logN > U256::ZERO, + "{circuit:?} reports no circuit size" + ); + log_n.push(decoded.logN); + } + assert_ne!( + log_n[0], log_n[1], + "both platforms would verify under one circuit" + ); +} From 29ca9dc8aa26f86c2ab6cf79d20e396d60396462 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 19:18:15 +0100 Subject: [PATCH 4/4] build: vendor the Honk verifiers in CI instead of committing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two .sol files are libid-circuits' release asset; circuits.json is the pin. Committing them made this repo a second copy of what that release already publishes, guarded by a --check that only existed because the copies did. Now the forge-build action runs the vendor script before every build, test, dry-run and publish, and each download is held to the pinned sha256 — the guarantee the copies stood in for. Locally: scripts/vendor-circuit-verifiers.sh before the first forge build. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .github/actions/forge-build/action.yml | 23 +- .github/workflows/ci.yml | 21 +- .gitignore | 8 + README.md | 39 +- rust/contracts/README.md | 7 +- rust/contracts/src/circuits.rs | 9 +- scripts/vendor-artifacts.sh | 7 + scripts/vendor-circuit-verifiers.sh | 47 +- .../circuits/BearerLinkHonkVerifier.sol | 2494 ----------------- .../circuits/OidcGoogleHonkVerifier.sol | 2494 ----------------- solidity/foundry.toml | 8 +- 11 files changed, 92 insertions(+), 5065 deletions(-) delete mode 100644 solidity/contracts/circuits/BearerLinkHonkVerifier.sol delete mode 100644 solidity/contracts/circuits/OidcGoogleHonkVerifier.sol diff --git a/.github/actions/forge-build/action.yml b/.github/actions/forge-build/action.yml index 5ed4396..deb335b 100644 --- a/.github/actions/forge-build/action.yml +++ b/.github/actions/forge-build/action.yml @@ -1,10 +1,11 @@ -# Install Foundry, restore the forge artifact cache, and build the contracts. +# Install Foundry, vendor the Honk verifiers, restore the forge artifact cache, +# and build the contracts. # -# Four jobs need `forge build` before they can do anything else: solidity runs -# the tests over it, handles-drift shells to `forge fmt`, rust vendors the -# artifacts into the crate, and ts generates bindings from them. Repeating the -# install + cache + build stanza in each job is how the copies drift, so the -# sequence lives here once. +# Every job that needs `forge build` — solidity runs the tests over it, rust +# vendors the artifacts into the crate, ts generates bindings from them, and +# the publish dry-run and both publish jobs do the last two for real — runs +# this composite. Repeating the install + vendor + cache + build stanza in each +# job is how the copies drift, so the sequence lives here once. # # The cache key is the SAME in every job that uses this action — it is derived # only from inputs.foundry-version and the checked-out sources — so whichever @@ -30,6 +31,16 @@ runs: with: version: ${{ inputs.foundry-version }} + # The Honk verifiers are not in the checkout: they are gitignored, and + # scripts/vendor-circuit-verifiers.sh downloads them from the pinned + # libid-circuits release, refusing any tarball whose sha256 is not the + # literal in solidity/contracts/circuits/circuits.json. So every build, + # test, dry-run and publish compiles a download the pin has just checked. + # Before the cache step, so the vendored sources are in the key's hash. + - name: Vendor circuit verifiers + shell: bash + run: ./scripts/vendor-circuit-verifiers.sh + # `solidity/lib/**` hashes the checked-out submodule trees, so bumping a # submodule pointer changes the key. The foundry version is in the key too: # artifacts embed solc metadata, and a stale `out/` from another compiler diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b69154..3c871f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,11 @@ name: CI # cache key, and whichever run warms the cache first makes the build a fast # no-op everywhere else. # -# Neither generated tree is committed (rust/contracts/artifacts and -# ts/packages/contracts/src/abis are gitignored), so every job that compiles -# the crate or the package regenerates it first — publishing included. +# Nothing generated is committed: the vendored Honk verifiers, +# rust/contracts/artifacts and ts/packages/contracts/src/abis are all +# gitignored. The forge-build composite vendors the verifiers before it +# builds, and every job that compiles the crate or the package regenerates +# its tree first — publishing included. # # Job ids are stable on purpose (solidity, generated-tables, rust, ts, dco): # the release/publish jobs at the bottom of this file hang their `needs:` off @@ -93,9 +95,9 @@ jobs: # Generated files match their source. handles.json generates the platform # constants and the handle vector table for every language; a drift between # the source and a committed output leaves every per-language test green - # while the languages disagree with each other. The vendored Honk verifiers - # are the same shape: circuits.json pins a libid-circuits release by digest, - # and the committed Solidity must be exactly what that release ships. + # while the languages disagree with each other. The Honk verifiers are not + # checked here: nothing of theirs is committed but the pin, and the + # forge-build composite holds every download to it. # --------------------------------------------------------------------------- generated-tables: name: Generated tables @@ -120,13 +122,6 @@ jobs: - name: Generated ceremony profiles are current run: python3 scripts/regen-ceremony-profiles.py --check - # Downloads the pinned release and checks it against the committed - # digests before comparing, so this fails on a hand edit, a stale - # vendor after a pin move, and a release asset that changed under its - # tag alike. - - name: Vendored Honk verifiers match the circuits pin - run: scripts/vendor-circuit-verifiers.sh --check - # --------------------------------------------------------------------------- # Rust: vendored artifacts, formatting, clippy, tests. Needs forge twice # over — the vendored artifacts are generated from a fresh `forge build`, diff --git a/.gitignore b/.gitignore index d9fb3a5..584324f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,13 @@ rust/contracts/artifacts/ ts/packages/contracts/src/abis/ ts/packages/contracts/src/calls/ +# Vendored from the pinned libid-circuits release, never committed: the Honk +# verifiers are that repository's release asset, and circuits.json beside them +# (the version and each tarball's sha256, which IS committed) is the pin every +# download is checked against. CI's forge-build action vendors them before +# every build, test, dry-run and publish. Locally, before `forge build`: +# scripts/vendor-circuit-verifiers.sh # -> solidity/contracts/circuits/*HonkVerifier.sol +solidity/contracts/circuits/*HonkVerifier.sol + # Python bytecode from the generator scripts. __pycache__/ diff --git a/README.md b/README.md index 321ef6a..0d6ce0f 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ solidity/ # Foundry project root contracts/ ceremony/ # NotaryService, CeremonyProofVerifier, Platform Verifiers, # GoogleJwtRoots - circuits/ # the UltraHonk verifiers the Platform Verifiers pin, - # vendored from the pinned libid-circuits release + circuits/ # the UltraHonk verifiers the Platform Verifiers pin: + # circuits.json pins a libid-circuits release, the + # Solidity is vendored from it and not committed identity/ # IdentityNames, handle normalization factory/ # LibidFactory: deterministic CREATE3 deployment WTIA9.sol # wrapped TIA @@ -33,14 +34,18 @@ scripts/ ```sh git submodule update --init --recursive +scripts/vendor-circuit-verifiers.sh # -> solidity/contracts/circuits/*HonkVerifier.sol cd solidity forge build forge test ``` -`forge build` is the input to the two generated trees, neither of which is -committed. Generate them once after cloning, and again after any change to a -contract they cover: +Nothing generated is committed. The Honk verifiers are downloaded from the +pinned [libid-circuits](https://github.com/libid-org/libid-circuits) release +(see [Circuit verifiers](#circuit-verifiers)), so a fresh clone vendors them +before its first `forge build`, which needs curl, jq, tar and forge. `forge +build` is in turn the input to the two generated trees. Generate them once +after cloning, and again after any change to a contract they cover: ```sh scripts/vendor-artifacts.sh # -> rust/contracts/artifacts (the crate embeds @@ -50,9 +55,10 @@ pnpm -C ts codegen # -> ts/packages/contracts/src/abis (tsc reads # these, so `pnpm -C ts build` needs them) ``` -CI runs both in every job that compiles the crate or the package, and again in -the publish jobs — the published crate and npm package carry the generated -output even though git does not. +CI runs all three before every build, test, dry-run and publish: the +forge-build action vendors the verifiers before it builds, and every job that +compiles the crate or the package regenerates its tree — the published crate +and npm package carry the generated output even though git does not. ## Handle vectors @@ -74,9 +80,11 @@ fails when any of them drifts from `handles.json`. The ceremony circuits' UltraHonk verifiers are not written here. `bb` derives each from its circuit's verification key, and [libid-circuits](https://github.com/libid-org/libid-circuits) runs `bb` and -ships the Solidity in its release tarballs. `solidity/contracts/circuits/` -holds that Solidity, formatted, as a source like any other: `forge build` -compiles it and the crate embeds it, so no consumer runs `bb`. +ships the Solidity in its release tarballs. `scripts/vendor-circuit-verifiers.sh` +downloads it into `solidity/contracts/circuits/`, formatted, where `forge +build` compiles it and the crate embeds it, so no consumer runs `bb`. The +files are gitignored: they are another repository's release asset, and the +pin says which bytes they must be. `solidity/contracts/circuits/circuits.json` is the pin — the release version and each tarball's sha256, committed here and checked against every download. @@ -84,12 +92,13 @@ To move it, download the new release's tarballs, take their digests with `shasum -a 256`, write the version and the digests into `circuits.json`, then: ```sh -scripts/vendor-circuit-verifiers.sh # rewrite the verifiers from the pin -scripts/vendor-circuit-verifiers.sh --check # verify the committed files match it +scripts/vendor-circuit-verifiers.sh # rewrite the verifiers from the pin ``` -CI's generated-tables job runs the check; a release cannot ship a verifier -that is not what the pinned circuits release shipped. +CI's forge-build action runs the same script before every build, test, +dry-run and publish, refusing any tarball whose digest is not the pin's; a +release cannot ship a verifier that is not what the pinned circuits release +shipped. ## Releasing diff --git a/rust/contracts/README.md b/rust/contracts/README.md index 499b398..43fcbf1 100644 --- a/rust/contracts/README.md +++ b/rust/contracts/README.md @@ -12,9 +12,10 @@ The compiled artifacts are vendored into the crate, so a consumer can deploy or upgrade the whole stack against a live network with **zero filesystem dependencies at runtime**. They are generated, not committed: `scripts/vendor-artifacts.sh` produces `artifacts/` from `solidity/` and CI -runs it before every build, test and publish. Working in this repo, run it -once after cloning — the crate embeds the directory with `include_dir!`, so -until it exists `cargo build` fails at macro expansion. Signing stays on the +runs it before every build, test and publish. Working in this repo, run +`scripts/vendor-circuit-verifiers.sh` and then it once after cloning — the +Honk verifiers are vendored too, and the crate embeds the directory with +`include_dir!`, so until it exists `cargo build` fails at macro expansion. Signing stays on the consumer's side: every helper is generic over an alloy `Provider` you have already wired with a wallet. diff --git a/rust/contracts/src/circuits.rs b/rust/contracts/src/circuits.rs index d0a6b43..3444e88 100644 --- a/rust/contracts/src/circuits.rs +++ b/rust/contracts/src/circuits.rs @@ -5,10 +5,11 @@ //! circuit's verification key, `libid-circuits` runs bb and ships the //! Solidity in its release tarballs, and `scripts/vendor-circuit-verifiers.sh` //! downloads the pinned release — checked against digests committed in -//! `solidity/contracts/circuits/circuits.json` — formats it and commits it -//! under `solidity/contracts/circuits`. From there it is a contract like any -//! other: `forge build` compiles it and `scripts/vendor-artifacts.sh` embeds -//! it, so a consumer deploys it from [`Artifacts::embedded`] with no `bb`. +//! `solidity/contracts/circuits/circuits.json` — formats it and writes it +//! under `solidity/contracts/circuits`, gitignored and vendored again before +//! every build. From there it is a contract like any other: `forge build` +//! compiles it and `scripts/vendor-artifacts.sh` embeds it, so a consumer +//! deploys it from [`Artifacts::embedded`] with no `bb`. //! //! bb emits `RelationsLib` and `ZKTranscriptLib` as external libraries, so a //! verifier's creation code carries a placeholder per call site until each diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index af3364a..3a76e4c 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -67,6 +67,13 @@ fi command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } +# The Honk verifiers are vendored, not committed, and a build without them +# fails in the test that imports them; name the missing step instead. +while read -r contract; do + [[ -f "$REPO_ROOT/solidity/contracts/circuits/$contract.sol" ]] || + { echo "no $contract.sol under solidity/contracts/circuits; run scripts/vendor-circuit-verifiers.sh first" >&2; exit 1; } +done < <(jq -r '.circuits[].contract' "$CIRCUITS_PIN") + echo "==> forge build" (cd "$REPO_ROOT/solidity" && forge build) diff --git a/scripts/vendor-circuit-verifiers.sh b/scripts/vendor-circuit-verifiers.sh index 3e5da96..9d65dbf 100755 --- a/scripts/vendor-circuit-verifiers.sh +++ b/scripts/vendor-circuit-verifiers.sh @@ -23,24 +23,23 @@ # (`assembly ("memory-safe")` on every assembly block, for via_ir, and the # rename off bb's fixed `HonkVerifier`); `forge fmt` is deliberately left to # the consumer, because libid-circuits carries no Foundry toolchain. So the -# committed file is fmt(shipped) plus the banner below, and `--check` -# regenerates it and fails on any difference — which is what makes the -# committed bytes checkable rather than trusted. +# written file is fmt(shipped) plus the banner below. # -# The sources ARE committed, like HandleVectors.sol: forge, the crate and the -# npm package build from the tree with no network, and CI's generated-tables -# job runs `--check` so a hand edit or a stale vendor cannot outlive a PR. -# The compiled artifacts are not; scripts/vendor-artifacts.sh regenerates -# them from `forge build` like every other contract's. +# The sources are NOT committed: they are gitignored like the forge +# artifacts and the npm ABIs, because they are another repository's release +# asset and the pin already says which bytes they must be. CI's forge-build +# action runs this script before every `forge build` — tests, dry-runs and +# publishes included — and a clone runs it once before its first build. So +# every build starts from a download the pin has just checked, and there is +# no committed copy for a hand edit or a stale vendor to live in. # # Moving the pin: download the new release's tarballs, read their sha256 # with `shasum -a 256` (compare against the release page, not against a # manifest fetched by a script), write the version and the digests into -# circuits.json, run this script, commit both. +# circuits.json, run this script, commit circuits.json. # # Usage: -# scripts/vendor-circuit-verifiers.sh # regenerate from the pin -# scripts/vendor-circuit-verifiers.sh --check # fail if a committed source drifts +# scripts/vendor-circuit-verifiers.sh # write the verifiers from the pin # # Requires curl, jq, tar, forge and shasum or sha256sum. set -euo pipefail @@ -52,13 +51,10 @@ DEST="$SOLIDITY/$DEST_REL" PIN="$DEST/circuits.json" RELEASES="https://github.com/libid-org/libid-circuits/releases/download" -MODE="write" -while [[ $# -gt 0 ]]; do - case "$1" in - --check) MODE="check"; shift ;; - *) echo "unknown argument: $1" >&2; exit 2 ;; - esac -done +if [[ $# -gt 0 ]]; then + echo "unknown argument: $1" >&2 + exit 2 +fi for tool in curl jq tar forge; do command -v "$tool" >/dev/null || { echo "$tool is required" >&2; exit 1; } @@ -137,17 +133,4 @@ while IFS=$'\t' read -r circuit contract want; do echo "==> $circuit -> $DEST_REL/$contract.sol" done < <(jq -r '.circuits | to_entries[] | "\(.key)\t\(.value.contract)\t\(.value.sha256)"' "$PIN") -if [[ "$MODE" == "check" ]]; then - drifted=0 - for staged in "$STAGE"/*.sol; do - name="$(basename "$staged")" - if ! cmp -s "$staged" "$DEST/$name"; then - echo "$DEST_REL/$name is not what the pin vendors; run scripts/vendor-circuit-verifiers.sh" >&2 - drifted=1 - fi - done - [[ "$drifted" == 0 ]] || exit 1 - echo "==> the committed verifiers match the pin" -else - cp "$STAGE"/*.sol "$DEST/" -fi +cp "$STAGE"/*.sol "$DEST/" diff --git a/solidity/contracts/circuits/BearerLinkHonkVerifier.sol b/solidity/contracts/circuits/BearerLinkHonkVerifier.sol deleted file mode 100644 index 5243f43..0000000 --- a/solidity/contracts/circuits/BearerLinkHonkVerifier.sol +++ /dev/null @@ -1,2494 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Aztec -// Vendored from libid-circuits v0.4.0 (libid-circuits-0.4.0-bearer-link.tar.gz) by scripts/vendor-circuit-verifiers.sh. Do not edit. -// The pin is contracts/circuits/circuits.json; `forge fmt` is the only change to what shipped. -pragma solidity >=0.8.21; - -uint256 constant N = 65536; -uint256 constant LOG_N = 16; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 72; -uint256 constant VK_HASH = 0x1d161afb536683d31a3e426db0feaa30de8be89cc45510579f361266c20e078f; - -library HonkVerificationKey { - function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { - Honk.VerificationKey memory vk = Honk.VerificationKey({ - circuitSize: uint256(65536), - logCircuitSize: uint256(16), - publicInputsSize: uint256(72), - ql: Honk.G1Point({ - x: uint256(0x21d8f7a12fca729bc6bfe60cdf2dacfa950154e11529a824e2a84c9c8b7ae5ad), - y: uint256(0x28526fc2bac460041b557dc083b1ba45ed48b9ce11dc54d9693eb115cb835bc7) - }), - qr: Honk.G1Point({ - x: uint256(0x13a030594cd4cc26b066d930f003f59d30f98f6bbdf7f510de7a22f3b4ca91c6), - y: uint256(0x08e14f589148732c712804948a65aac5814ecd254929d1ecefe207128c9d0ee8) - }), - qo: Honk.G1Point({ - x: uint256(0x0207cb7dbdfdc49d7b07079b19e5c24a1f7d200eb8f9f57c38359b850b63d2d9), - y: uint256(0x0979437546d53eb53b21ea07b3b0ff9003d0d8b94a6ba1b74675f002ea21734c) - }), - q4: Honk.G1Point({ - x: uint256(0x17df214e1867d539648dd94ba8e71f81b97c13ec3c457d3b2d497943a4727bce), - y: uint256(0x2fa8b49ccb8c6ec2fa37724a64df40f4770121664d50d0e13aff1d943e9c2027) - }), - qm: Honk.G1Point({ - x: uint256(0x2fcade2ba858ac130be60566c5af5f18ab533cbbd5fe183597eaaa3ece8c4ec8), - y: uint256(0x07217d0d916b000076b0c6ac589c2d45ac5a70912e1dd2917af547e66bcc6b96) - }), - qc: Honk.G1Point({ - x: uint256(0x0550a650096cce86a8ed1b2d93bf65af5868b8cc0a49ca67049bed72fc3202ae), - y: uint256(0x11f6c27fe423a81c335666e00629650287e537fbdafc58c743d15cde0f44caef) - }), - qLookup: Honk.G1Point({ - x: uint256(0x16a5c54dfac02744e9c8edfdd812df41f6692fac5c03dd51eb643339ab9a9685), - y: uint256(0x09e7164978342671c70e53407dca7f0cf80c2d5e1654332763ccef010cd1a7d2) - }), - qArith: Honk.G1Point({ - x: uint256(0x2f9b8f15ab3501c467b1c074e5007a1a40b43828694ecd2845a7019189535765), - y: uint256(0x223c92802d53bf81bf1066e0d7426a040f571b924003d0d5d97d4f80d906bdfe) - }), - qDeltaRange: Honk.G1Point({ - x: uint256(0x1dd5af2bc33313bae90b5e7be3abfc093d49d2f4b5f1d75e4301ef6eb9db59e8), - y: uint256(0x03c30084892bd7e01fdfabf86ffef536543353ff37900b15ae41808531b2c06c) - }), - qElliptic: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qMemory: Honk.G1Point({ - x: uint256(0x1f40d0a610240bdf14f4ff3df8a91eb858a2e8aed1b5774b96e0aee86d5db953), - y: uint256(0x20bfe881117eea3f1b75bba4eb194625fc2eb5951c61d2ea1b57a3dc37bc3f5a) - }), - qNnf: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qPoseidon2External: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qPoseidon2Internal: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - s1: Honk.G1Point({ - x: uint256(0x16d07df09b0938e7d0e5710914c491e9a578073dcc215f902f63467bbb873ef4), - y: uint256(0x190deff8947ca1880195384fbea386f313eb72fc156c1427949488b876c59580) - }), - s2: Honk.G1Point({ - x: uint256(0x303e3355a805c782712e3d48c80041d70232a553552bf6fbc9e7b2ecc5fa7b38), - y: uint256(0x2586f8d270c7d1061f4c8627132ea1aeda84747d3d2a8669e0f3930549ffbb5e) - }), - s3: Honk.G1Point({ - x: uint256(0x2328b0f8447c3392021727560158f659a44b0021bf1016d22cbddd6b9c847154), - y: uint256(0x222967c63ccdb06aeb6e9cc6b72d2931ce49286a59f1c5f4638231ce5a656713) - }), - s4: Honk.G1Point({ - x: uint256(0x20eb4577881ab617c1561beb2e099801da537d9f68066698120969821d55df24), - y: uint256(0x12ab1f967fc802acc18e0acc95b26631087258af8a60c0ec21e3a9862b5a44d9) - }), - t1: Honk.G1Point({ - x: uint256(0x2007e7f9cf246e2fff5241dcad09f774e3febf4a5528b4f654a1c53332eee70e), - y: uint256(0x0b93b63b9a2f1b6803aa832ea402038ed701919a50b87f27ea8ab3fecd6e0bc1) - }), - t2: Honk.G1Point({ - x: uint256(0x22b74d79893277da758e319b9254ac723a8bb788a04e9f990e93e24328ef44cc), - y: uint256(0x20fc82a77859ebc3187ff8cce5278805710a6fd48b606045f81b2b652c8c2882) - }), - t3: Honk.G1Point({ - x: uint256(0x10ee38b08d8cb65a4bb9f16fe31932970faa33b310bc0e4a583db8d88f448335), - y: uint256(0x21ff5d9dbda6ecd2b8cb8df09d01108535d127413664ae2b7d480f3a89d7c0da) - }), - t4: Honk.G1Point({ - x: uint256(0x265434c6d481bb44c0003077c2cf237884af36f0a7ca62cf3f8ad4c83b45bd32), - y: uint256(0x0ad46a441ccbfbeb58b2d4b90bc050752f8ba1bbaafca67426e72d99189dbbba) - }), - id1: Honk.G1Point({ - x: uint256(0x07c4cba2626a34ce9919d17f5ad08277a6f1d5d80927e0603789f50b1f008693), - y: uint256(0x2b3fd005d61848b09996ce01e4abad7cf88cffa461227921c622825169c079e6) - }), - id2: Honk.G1Point({ - x: uint256(0x2909d03ff49e1a1830812a909ade2355b29dd92e055e4346de9ef947113028ef), - y: uint256(0x0bdcd9f3e9c8f432d346d3a983e34f896379cb1c312114a010a3b729891ebba2) - }), - id3: Honk.G1Point({ - x: uint256(0x2e019d7c64b6adbda9ce45ee8b0ae4c7bc10773396342554d9b98de7dbf53748), - y: uint256(0x056a601f09e639cc189181bc7809f5af103dd62633d48d2c2a642e5277074eaa) - }), - id4: Honk.G1Point({ - x: uint256(0x199b3a2b71a2cd6d7c59bb9c5201b75b12f32baa0b02d840e468f46f2eff9934), - y: uint256(0x27710345ab4b3a536e76ea99da95b0501778c34b2e14340d706b21a89b0a08d8) - }), - lagrangeFirst: Honk.G1Point({ - x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6), - y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c) - }), - lagrangeLast: Honk.G1Point({ - x: uint256(0x1fde62f333621ba020dd4ad0ec877585e009583273d4a1de45a55638c7cfade9), - y: uint256(0x244ac512eca15ea8b3fa8f9e5e0daefcfd9365414269dddd7f75cb95473fbfc5) - }) - }); - return vk; - } -} - -pragma solidity ^0.8.27; - -interface IVerifier { - function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); -} - -/** - * @notice Library of error codes - * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier - */ -library Errors { - error ValueGeLimbMax(); - error ValueGeGroupOrder(); - error ValueGeFieldOrder(); - - error InvertOfZero(); - error NotPowerOfTwo(); - error ModExpFailed(); - - error ProofLengthWrong(); - error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); - error PublicInputsLengthWrong(); - error SumcheckFailed(); - error ShpleminiFailed(); - - error PointAtInfinity(); - - error ConsistencyCheckFailed(); - error GeminiChallengeInSubgroup(); -} - -type Fr is uint256; - -using {add as +} for Fr global; -using {sub as -} for Fr global; -using {mul as *} for Fr global; - -using {notEqual as !=} for Fr global; -using {equal as ==} for Fr global; - -uint256 constant SUBGROUP_SIZE = 256; -uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order -uint256 constant P = MODULUS; -Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); -Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); -Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); -Fr constant ONE = Fr.wrap(1); -Fr constant ZERO = Fr.wrap(0); - -// SmallSubgroupIPA opening-claim layout — mirrors SMALL_IPA_CLAIMS in -// barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa_utils.hpp. -uint256 constant NUM_SMALL_IPA_OPENING_CLAIMS = 5; -uint256 constant SMALL_IPA_BOUNDARY_OPENING_IDX = 3; -uint256 constant NUM_SMALL_IPA_TRANSCRIPT_EVALS = 4; -// Instantiation - -library FrLib { - bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; - - function invert(Fr value) internal view returns (Fr) { - uint256 v = Fr.unwrap(value); - require(v != 0, Errors.InvertOfZero()); - - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), v) - mstore(add(free, 0x80), sub(MODULUS, 2)) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function pow(Fr base, uint256 v) internal view returns (Fr) { - uint256 b = Fr.unwrap(base); - // Only works for power of 2 - require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), b) - mstore(add(free, 0x80), v) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function div(Fr numerator, Fr denominator) internal view returns (Fr) { - unchecked { - return numerator * invert(denominator); - } - } - - function sqr(Fr value) internal pure returns (Fr) { - unchecked { - return value * value; - } - } - - function unwrap(Fr value) internal pure returns (uint256) { - unchecked { - return Fr.unwrap(value); - } - } - - function neg(Fr value) internal pure returns (Fr) { - unchecked { - return Fr.wrap(MODULUS - Fr.unwrap(value)); - } - } - - function from(uint256 value) internal pure returns (Fr) { - unchecked { - require(value < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(value); - } - } - - function fromBytes32(bytes32 value) internal pure returns (Fr) { - unchecked { - uint256 v = uint256(value); - require(v < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(v); - } - } - - function toBytes32(Fr value) internal pure returns (bytes32) { - unchecked { - return bytes32(Fr.unwrap(value)); - } - } -} - -// Free functions -function add(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function mul(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function sub(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); - } -} - -function notEqual(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) != Fr.unwrap(b); - } -} - -function equal(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) == Fr.unwrap(b); - } -} - -uint256 constant CONST_PROOF_SIZE_LOG_N = 25; - -uint256 constant NUMBER_OF_SUBRELATIONS = 29; -uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; -uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; -uint256 constant NUMBER_OF_ENTITIES = 41; -// The number of entities added for ZK (gemini_masking_poly) -uint256 constant NUM_MASKING_POLYNOMIALS = 1; -uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_UNSHIFTED = 36; -uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_TO_BE_SHIFTED = 5; -uint256 constant PAIRING_POINTS_SIZE = 8; - -uint256 constant FIELD_ELEMENT_SIZE = 0x20; -uint256 constant GROUP_ELEMENT_SIZE = 0x40; - -// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) -uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; - -// Must match UltraFlavor_Generated::EntityId order. -enum WIRE { - SIGMA_1, - SIGMA_2, - SIGMA_3, - SIGMA_4, - ID_1, - ID_2, - ID_3, - ID_4, - LAGRANGE_FIRST, - LAGRANGE_LAST, - Q_LOOKUP, - TABLE_1, - TABLE_2, - TABLE_3, - TABLE_4, - Q_M, - Q_R, - Q_O, - Q_C, - Q_L, - Q_4, - Q_ARITH, - Q_RANGE, - Q_ELLIPTIC, - Q_MEMORY, - Q_NNF, - Q_POSEIDON2_EXTERNAL, - Q_POSEIDON2_INTERNAL, - W_L, - W_R, - W_O, - W_4, - Z_PERM, - LOOKUP_INVERSES, - LOOKUP_READ_COUNTS, - LOOKUP_READ_TAGS, - W_L_SHIFT, - W_R_SHIFT, - W_O_SHIFT, - W_4_SHIFT, - Z_PERM_SHIFT -} - -library Honk { - struct G1Point { - uint256 x; - uint256 y; - } - - struct VerificationKey { - // Misc Params - uint256 circuitSize; - uint256 logCircuitSize; - uint256 publicInputsSize; - // Selectors - G1Point qm; - G1Point qc; - G1Point ql; - G1Point qr; - G1Point qo; - G1Point q4; - G1Point qLookup; // Lookup - G1Point qArith; // Arithmetic widget - G1Point qDeltaRange; // Delta Range sort - G1Point qMemory; // Memory - G1Point qNnf; // Non-native Field - G1Point qElliptic; // Auxillary - G1Point qPoseidon2External; - G1Point qPoseidon2Internal; - // Copy constraints - G1Point s1; - G1Point s2; - G1Point s3; - G1Point s4; - // Copy identity - G1Point id1; - G1Point id2; - G1Point id3; - G1Point id4; - // Precomputed lookup table - G1Point t1; - G1Point t2; - G1Point t3; - G1Point t4; - // Fixed first and last - G1Point lagrangeFirst; - G1Point lagrangeLast; - } - - struct RelationParameters { - // challenges - Fr eta; - Fr beta; - Fr gamma; - // derived - Fr publicInputsDelta; - } - - struct Proof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // Free wires - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Lookup helpers - Permutations - G1Point zPerm; - // Lookup helpers - logup - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Sumcheck - Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - G1Point shplonkQ; - G1Point kzgQuotient; - } - - /// forge-lint: disable-next-item(pascal-case-struct) - struct ZKProof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) - G1Point geminiMaskingPoly; - // Commitments to wire polynomials - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Commitments to logup witness polynomials - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Commitment to grand permutation polynomial - G1Point zPerm; - G1Point[3] libraCommitments; - // Sumcheck - Fr libraSum; - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr libraEvaluation; - Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - Fr[4] libraPolyEvals; - G1Point shplonkQ; - G1Point kzgQuotient; - } -} - -// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest -/// forge-lint: disable-next-item(pascal-case-struct) -struct ZKTranscript { - // Oink - Honk.RelationParameters relationParameters; - Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] - Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; - // Sumcheck - Fr libraChallenge; - Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; - // Shplemini - Fr rho; - Fr geminiR; - Fr shplonkNu; - Fr shplonkZ; - // Derived - Fr publicInputsDelta; -} - -library ZKTranscriptLib { - function generateTranscript( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - uint256 logN - ) external pure returns (ZKTranscript memory t) { - Fr previousChallenge; - (t.relationParameters, previousChallenge) = - generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); - - (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); - - (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); - (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); - (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); - - (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); - - (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); - - (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); - - (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); - return t; - } - - function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { - uint256 challengeU256 = uint256(Fr.unwrap(challenge)); - // Split into two equal 127-bit chunks (254/2) - uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits - uint256 hi = challengeU256 >> 127; - first = FrLib.from(lo); - second = FrLib.from(hi); - } - - function generateRelationParametersChallenges( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - Fr previousChallenge - ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { - (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); - - (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); - } - - function generateEtaChallenge( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize - ) internal pure returns (Fr eta, Fr previousChallenge) { - // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) - bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); - round0[0] = bytes32(vkHash); - - for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { - require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); - round0[1 + i] = publicInputs[i]; - } - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); - } - - // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) - round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); - round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); - - // Create the first challenge - // Note: w4 is added to the challenge later on - round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); - round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); - round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); - round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); - round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); - round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); - - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); - (eta,) = splitChallenge(previousChallenge); - } - - function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) - { - bytes32[7] memory round1; - round1[0] = FrLib.toBytes32(previousChallenge); - round1[1] = bytes32(proof.lookupReadCounts.x); - round1[2] = bytes32(proof.lookupReadCounts.y); - round1[3] = bytes32(proof.lookupReadTags.x); - round1[4] = bytes32(proof.lookupReadTags.y); - round1[5] = bytes32(proof.w4.x); - round1[6] = bytes32(proof.w4.y); - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); - (beta, gamma) = splitChallenge(nextPreviousChallenge); - } - - // Alpha challenges non-linearise the gate contributions - function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) - { - // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup - uint256[5] memory alpha0; - alpha0[0] = Fr.unwrap(previousChallenge); - alpha0[1] = proof.lookupInverses.x; - alpha0[2] = proof.lookupInverses.y; - alpha0[3] = proof.zPerm.x; - alpha0[4] = proof.zPerm.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); - Fr alpha; - (alpha,) = splitChallenge(nextPreviousChallenge); - - // Compute powers of alpha for batching subrelations - alphas[0] = alpha; - for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { - alphas[i] = alphas[i - 1] * alpha; - } - } - - function generateGateChallenges(Fr previousChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) - { - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); - (gateChallenges[0],) = splitChallenge(previousChallenge); - for (uint256 i = 1; i < logN; i++) { - gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; - } - nextPreviousChallenge = previousChallenge; - } - - function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr libraChallenge, Fr nextPreviousChallenge) - { - // 2 comm, 1 sum, 1 challenge - uint256[4] memory challengeData; - challengeData[0] = Fr.unwrap(previousChallenge); - challengeData[1] = proof.libraCommitments[0].x; - challengeData[2] = proof.libraCommitments[0].y; - challengeData[3] = Fr.unwrap(proof.libraSum); - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); - (libraChallenge,) = splitChallenge(nextPreviousChallenge); - } - - function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) - { - for (uint256 i = 0; i < logN; i++) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; - univariateChal[0] = prevChallenge; - - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; - } - prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); - - (sumcheckChallenges[i],) = splitChallenge(prevChallenge); - } - nextPreviousChallenge = prevChallenge; - } - - // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) - function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr rho, Fr nextPreviousChallenge) - { - uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; - rhoChallengeElements[0] = Fr.unwrap(prevChallenge); - uint256 i; - for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { - rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); - } - rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); - i += 1; - rhoChallengeElements[i] = proof.libraCommitments[1].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; - i += 2; - rhoChallengeElements[i] = proof.libraCommitments[2].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); - (rho,) = splitChallenge(nextPreviousChallenge); - } - - function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr geminiR, Fr nextPreviousChallenge) - { - uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); - gR[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 0; i < logN - 1; i++) { - gR[1 + i * 2] = proof.geminiFoldComms[i].x; - gR[2 + i * 2] = proof.geminiFoldComms[i].y; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); - - (geminiR,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr shplonkNu, Fr nextPreviousChallenge) - { - uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); - shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 1; i <= logN; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); - } - - uint256 libraIdx = 0; - for (uint256 i = logN + 1; i <= logN + 4; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); - libraIdx++; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); - (shplonkNu,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr shplonkZ, Fr nextPreviousChallenge) - { - uint256[3] memory shplonkZChallengeElements; - shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); - - shplonkZChallengeElements[1] = proof.shplonkQ.x; - shplonkZChallengeElements[2] = proof.shplonkQ.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); - (shplonkZ,) = splitChallenge(nextPreviousChallenge); - } - - function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { - uint256 boundary = 0x0; - - // Pairing point object - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); - // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 - require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); - p.pairingPointObject[i] = FrLib.from(limb); - boundary += FIELD_ELEMENT_SIZE; - } - - // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) - p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Commitments - p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Lookup / Permutation Helper Commitments - p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - // Sumcheck univariates - for (uint256 i = 0; i < logN; i++) { - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - } - - // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) - for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { - p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - - p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Gemini - // Read gemini fold univariates - for (uint256 i = 0; i < logN - 1; i++) { - p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - } - - // Read gemini a evaluations - for (uint256 i = 0; i < logN; i++) { - p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - for (uint256 i = 0; i < 4; i++) { - p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - // Shplonk - p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - // KZG - p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - } -} - -library RelationsLib { - struct EllipticParams { - // Points - Fr x_1; - Fr y_1; - Fr x_2; - Fr y_2; - Fr y_3; - Fr x_3; - // push accumulators into memory - Fr x_double_identity; - } - - // Parameters used within the Memory Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct MemParams { - Fr memory_record_check; - Fr partial_record_check; - Fr next_gate_access_type; - Fr record_delta; - Fr index_delta; - Fr adjacent_values_match_if_adjacent_indices_match; - Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; - Fr access_check; - Fr next_gate_access_type_is_boolean; - Fr ROM_consistency_check_identity; - Fr RAM_consistency_check_identity; - Fr timestamp_delta; - Fr RAM_timestamp_check_identity; - Fr memory_identity; - Fr index_is_monotonically_increasing; - } - - // Parameters used within the Non-Native Field Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct NnfParams { - Fr limb_subproduct; - Fr non_native_field_gate_1; - Fr non_native_field_gate_2; - Fr non_native_field_gate_3; - Fr limb_accumulator_1; - Fr limb_accumulator_2; - Fr nnf_identity; - } - - struct PoseidonExternalParams { - Fr s1; - Fr s2; - Fr s3; - Fr s4; - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr t0; - Fr t1; - Fr t2; - Fr t3; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr q_pos_by_scaling; - } - - struct PoseidonInternalParams { - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr u_sum; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr s1; - Fr q_pos_by_scaling; - } - - Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) - uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; - - // Constants for the Non-native Field relation - Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); - Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); - - function accumulateRelationEvaluations( - Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, - Fr powPartialEval - ) external pure returns (Fr accumulator) { - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; - - // Accumulate all relations in Ultra Honk - each with varying number of subrelations - accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); - - // batch the subrelations with the precomputed alpha powers to obtain the full honk relation - accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); - } - - /** - * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids - * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code - * editors, and thus is noisy. - */ - function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { - return p[uint256(_wire)]; - } - - /** - * Ultra Arithmetic Relation - * - */ - function accumulateArithmeticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - // Relation 0 - Fr q_arith = wire(p, WIRE.Q_ARITH); - { - Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); - - Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; - accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) - + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); - accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); - accum = accum * q_arith; - accum = accum * domainSep; - evals[6] = accum; - } - - // Relation 1 - { - Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); - accum = accum * (q_arith - Fr.wrap(2)); - accum = accum * (q_arith - ONE); - accum = accum * q_arith; - accum = accum * domainSep; - evals[7] = accum; - } - } - - function accumulatePermutationRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr grand_product_numerator; - Fr grand_product_denominator; - - { - Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; - num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); - - grand_product_numerator = num; - } - { - Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; - den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); - - grand_product_denominator = den; - } - - // Contribution 2 - { - Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; - - acc = acc - - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) - * grand_product_denominator); - acc = acc * domainSep; - evals[0] = acc; - } - - // Contribution 3 - { - Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; - evals[1] = acc; - } - - // Contribution 4: z_perm initialization check (lagrange_first * z_perm = 0) - { - Fr acc = (wire(p, WIRE.LAGRANGE_FIRST) * wire(p, WIRE.Z_PERM)) * domainSep; - evals[2] = acc; - } - } - - function accumulateLogDerivativeLookupRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr table_term; - Fr lookup_term; - - // Calculate the write term (the table accumulation) - // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) - + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); - } - - // Calculate the read term - // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); - Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); - Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); - - lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) - + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); - } - - Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; - Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; - - Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) - - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); - - // Inverse calculated correctly relation - Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; - accumulatorNone = accumulatorNone * domainSep; - - // Inverse - Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; - - Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); - - Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; - - evals[3] = accumulatorNone; - evals[4] = accumulatorOne; - evals[5] = read_tag_boolean_relation * domainSep; - } - - function accumulateDeltaRangeRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr minus_one = ZERO - ONE; - Fr minus_two = ZERO - Fr.wrap(2); - Fr minus_three = ZERO - Fr.wrap(3); - - // Compute wire differences - Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); - Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); - Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); - Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); - - // Contribution 6 - { - Fr acc = delta_1; - acc = acc * (delta_1 + minus_one); - acc = acc * (delta_1 + minus_two); - acc = acc * (delta_1 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[8] = acc; - } - - // Contribution 7 - { - Fr acc = delta_2; - acc = acc * (delta_2 + minus_one); - acc = acc * (delta_2 + minus_two); - acc = acc * (delta_2 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[9] = acc; - } - - // Contribution 8 - { - Fr acc = delta_3; - acc = acc * (delta_3 + minus_one); - acc = acc * (delta_3 + minus_two); - acc = acc * (delta_3 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[10] = acc; - } - - // Contribution 9 - { - Fr acc = delta_4; - acc = acc * (delta_4 + minus_one); - acc = acc * (delta_4 + minus_two); - acc = acc * (delta_4 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[11] = acc; - } - } - - function accumulateEllipticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - EllipticParams memory ep; - ep.x_1 = wire(p, WIRE.W_R); - ep.y_1 = wire(p, WIRE.W_O); - - ep.x_2 = wire(p, WIRE.W_L_SHIFT); - ep.y_2 = wire(p, WIRE.W_4_SHIFT); - ep.y_3 = wire(p, WIRE.W_O_SHIFT); - ep.x_3 = wire(p, WIRE.W_R_SHIFT); - - Fr q_sign = wire(p, WIRE.Q_L); - Fr q_is_double = wire(p, WIRE.Q_M); - - // Contribution 10 point addition, x-coordinate check - // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 - Fr x_diff = (ep.x_2 - ep.x_1); - Fr y1_sqr = (ep.y_1 * ep.y_1); - { - // Move to top - Fr partialEval = domainSep; - - Fr y2_sqr = (ep.y_2 * ep.y_2); - Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; - Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); - x_add_identity = x_add_identity * x_diff * x_diff; - x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; - - evals[12] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 11 point addition, x-coordinate check - // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 - { - Fr y1_plus_y3 = ep.y_1 + ep.y_3; - Fr y_diff = ep.y_2 * q_sign - ep.y_1; - Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; - evals[13] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 10 point doubling, x-coordinate check - // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 - // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 - { - Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; - Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; - y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; - Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); - - // NOTE: pushed into memory (stack >:'( ) - ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; - - Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - evals[12] = evals[12] + acc; - } - - // Contribution 11 point doubling, y-coordinate check - // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 - { - Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; - Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); - evals[13] = evals[13] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - } - } - - function accumulateMemoryRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - MemParams memory ap; - - // Compute eta powers locally - Fr eta_two = rp.eta * rp.eta; - Fr eta_three = eta_two * rp.eta; - - /** - * MEMORY - * - * A RAM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) - * * v: `value` of memory cell being accessed - * * a: `access` type of record. read: 0 = read, 1 = write - * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three - * - * A ROM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three - * - * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + - * selectors, depending on whether the gate is a RAM read/write or a ROM read - * - * | gate type | i | v2/t | v | a | r | - * | --------- | -- | ----- | -- | -- | -- | - * | ROM | w1 | w2 | w3 | -- | w4 | - * | RAM | w1 | w2 | w3 | qc | w4 | - * - * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on - * `w2` to fix its value) - * - * - */ - - /** - * Memory Record Check - * Partial degree: 1 - * Total degree: 4 - * - * A ROM/ROM access gate can be evaluated with the identity: - * - * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 - * - * For ROM gates, qc = 0 - */ - ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); - ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); - ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 - ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); - - /** - * Contribution 13 & 14 - * ROM Consistency Check - * Partial degree: 1 - * Total degree: 4 - * - * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of - * records that are sorted. - * - * We apply the following checks for the sorted records: - * - * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 - * 2. index values for adjacent records are monotonically increasing - * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} - * - */ - ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); - ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); - - ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 - - ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 - - evals[15] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - evals[16] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - - ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 - - /** - * Contributions 15,16,17 - * RAM Consistency Check - * - * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` - * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. - * This is validated by requiring `access` to be boolean - * - * For two adjacent entries in the sorted list if _both_ - * A) index values match - * B) adjacent access value is 0 (i.e. next gate is a READ) - * then - * C) both values must match. - * The gate boolean check is - * (A && B) => C === !(A && B) || C === !A || !B || C - * - * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized - * with a WRITE operation. - */ - Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 - ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 - - // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta - // deg 1 or 4 - ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); - ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; - - Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); - ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = - (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 - - // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the - // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't - // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access - // type is correct, to cover this edge case - // deg 2 or 4 - ap.next_gate_access_type_is_boolean = - ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; - - // Putting it all together... - evals[17] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation - * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 - evals[18] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 - evals[19] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 - - ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 - - /** - * RAM Timestamp Consistency Check - * - * | w1 | w2 | w3 | w4 | - * | index | timestamp | timestamp_check | -- | - * - * Let delta_index = index_{i + 1} - index_{i} - * - * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i - * Else timestamp_check = 0 - */ - ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); - ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 - - /** - * Complete Contribution 12 - * The complete RAM/ROM memory identity - * Partial degree: - */ - ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 - ap.memory_identity = - ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 - ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 - ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 - - // (deg 3 or 9) + (deg 4) + (deg 3) - ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 - evals[14] = ap.memory_identity; - } - - function accumulateNnfRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - NnfParams memory ap; - - /** - * Contribution 12 - * Non native field arithmetic gate 2 - * deg 4 - * - * _ _ - * / _ _ _ 14 \ - * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | - * \_ _/ - * - * - */ - ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); - ap.non_native_field_gate_2 = - (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); - - ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; - ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); - ap.non_native_field_gate_1 = ap.limb_subproduct; - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); - - ap.non_native_field_gate_3 = ap.limb_subproduct; - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); - - Fr non_native_field_identity = - ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; - non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); - - // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm - // deg 2 - ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); - ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); - - // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm - // deg 2 - ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); - ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); - - Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; - limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 - - ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; - ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); - evals[20] = ap.nnf_identity; - } - - function accumulatePoseidonExternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonExternalParams memory ep; - - ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); - ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); - ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); - - ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; - ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; - ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; - ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; - // matrix mul v = M_E * u with 14 additions - ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 - ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 - ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 - // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 - ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 - // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 - ep.v4 = ep.t1 + ep.t1; - ep.v4 = ep.v4 + ep.v4 + ep.t3; - // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 - ep.v2 = ep.t0 + ep.t0; - ep.v2 = ep.v2 + ep.v2 + ep.t2; - // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 - ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 - ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 - - ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; - evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); - - evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); - - evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); - - evals[24] = evals[24] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - function accumulatePoseidonInternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonInternalParams memory ip; - - Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ - FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), - FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), - FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), - FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) - ]; - - // add round constants - ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - - // apply s-box round - ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; - ip.u2 = wire(p, WIRE.W_R); - ip.u3 = wire(p, WIRE.W_O); - ip.u4 = wire(p, WIRE.W_4); - - // matrix mul with v = M_I * u 4 muls and 7 additions - ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; - - ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; - - ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; - evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); - - ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; - evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); - - ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; - evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); - - ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; - evals[28] = evals[28] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - // Batch subrelation evaluations using precomputed powers of alpha - // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array - function scaleAndBatchSubrelations( - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges - ) internal pure returns (Fr accumulator) { - accumulator = evaluations[0]; - - for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { - accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; - } - } -} - -library CommitmentSchemeLib { - using FrLib for Fr; - - // Avoid stack too deep - struct ShpleminiIntermediates { - Fr unshiftedScalar; - Fr shiftedScalar; - Fr unshiftedScalarNeg; - Fr shiftedScalarNeg; - // Scalar to be multiplied by [1]₁ - Fr constantTermAccumulator; - // Accumulator for powers of rho - Fr batchingChallenge; - // Linear combination of multilinear (sumcheck) evaluations and powers of rho - Fr batchedEvaluation; - Fr[NUM_SMALL_IPA_OPENING_CLAIMS] denominators; - Fr[NUM_SMALL_IPA_OPENING_CLAIMS] batchingScalars; - // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr posInvertedDenominator; - // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr negInvertedDenominator; - // ν^{2i} * 1/(z - r^{2^i}) - Fr scalingFactorPos; - // ν^{2i+1} * 1/(z + r^{2^i}) - Fr scalingFactorNeg; - // Fold_i(r^{2^i}) reconstructed by Verifier - Fr[] foldPosEvaluations; - } - - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 - function computeFoldPosEvaluations( - Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, - Fr batchedEvalAccumulator, - Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, - Fr[] memory geminiEvalChallengePowers, - uint256 logSize - ) internal view returns (Fr[] memory) { - Fr[] memory foldPosEvaluations = new Fr[](logSize); - for (uint256 i = logSize; i > 0; --i) { - Fr challengePower = geminiEvalChallengePowers[i - 1]; - Fr u = sumcheckUChallenges[i - 1]; - - Fr batchedEvalRoundAcc = - ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] - * (challengePower * (ONE - u) - u)); - // Divide by the denominator - batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); - - batchedEvalAccumulator = batchedEvalRoundAcc; - foldPosEvaluations[i - 1] = batchedEvalRoundAcc; - } - return foldPosEvaluations; - } - - function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { - Fr[] memory squares = new Fr[](logN); - squares[0] = r; - for (uint256 i = 1; i < logN; ++i) { - squares[i] = squares[i - 1].sqr(); - } - return squares; - } -} - -uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q - -// Fr utility - -function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { - scalar = FrLib.fromBytes32(bytes32(proofSection)); -} - -// EC Point utilities -function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { - uint256 x = uint256(bytes32(proofSection[0x00:0x20])); - uint256 y = uint256(bytes32(proofSection[0x20:0x40])); - require(x < Q && y < Q, Errors.ValueGeGroupOrder()); - - // (0,0) is the canonical EIP-196 encoding of the identity. It is accepted here - // because polynomial commitments to identically-zero polynomials (e.g. unused - // selector or table polys) are legitimately the identity. On-curve validation - // (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles per EIP-196. - point = Honk.G1Point({x: x, y: y}); -} - -function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { - // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. - if (point.y != 0) { - point.y = Q - point.y; - } - return point; -} - -/** - * Convert the pairing points to G1 points. - * - * The pairing points are serialised as an array of 2 limbs representing two points - * (P0 and P1, used for lhs and rhs of pairing operation). - * - * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. - * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] - * - * @param pairingPoints The pairing points to convert. - * @return lhs P0 point - * @return rhs P1 point - */ -function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) - pure - returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) -{ - // P0 (lhs): x = lo | (hi << 136) - uint256 lhsX = Fr.unwrap(pairingPoints[0]); - lhsX |= Fr.unwrap(pairingPoints[1]) << 136; - - uint256 lhsY = Fr.unwrap(pairingPoints[2]); - lhsY |= Fr.unwrap(pairingPoints[3]) << 136; - - // P1 (rhs): x = lo | (hi << 136) - uint256 rhsX = Fr.unwrap(pairingPoints[4]); - rhsX |= Fr.unwrap(pairingPoints[5]) << 136; - - uint256 rhsY = Fr.unwrap(pairingPoints[6]); - rhsY |= Fr.unwrap(pairingPoints[7]) << 136; - - // Reconstructed coordinates must be < Q to prevent malleability. - // Without this, two different limb encodings could map to the same curve point - // (via mulmod reduction in on-curve checks) but produce different transcript hashes. - require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); - - lhs.x = lhsX; - lhs.y = lhsY; - rhs.x = rhsX; - rhs.y = rhsY; -} - -/** - * Hash the pairing inputs from the present verification context with those extracted from the public inputs. - * - * @param proofPairingPoints Pairing points from the proof - (public inputs). - * @param accLhs Accumulator point for the left side - result of shplemini. - * @param accRhs Accumulator point for the right side - result of shplemini. - * @return recursionSeparator The recursion separator - generated from hashing the above. - */ -function generateRecursionSeparator( - Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, - Honk.G1Point memory accLhs, - Honk.G1Point memory accRhs -) pure returns (Fr recursionSeparator) { - // hash the proof aggregated X - // hash the proof aggregated Y - // hash the accum X - // hash the accum Y - - (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); - - uint256[8] memory recursionSeparatorElements; - - // Proof points - recursionSeparatorElements[0] = proofLhs.x; - recursionSeparatorElements[1] = proofLhs.y; - recursionSeparatorElements[2] = proofRhs.x; - recursionSeparatorElements[3] = proofRhs.y; - - // Accumulator points - recursionSeparatorElements[4] = accLhs.x; - recursionSeparatorElements[5] = accLhs.y; - recursionSeparatorElements[6] = accRhs.x; - recursionSeparatorElements[7] = accRhs.y; - - recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); -} - -/** - * G1 Mul with Separator - * Using the ecAdd and ecMul precompiles - * - * @param basePoint The point to multiply. - * @param other The other point to add. - * @param recursionSeperator The separator to use for the multiplication. - * @return `(recursionSeperator * basePoint) + other`. - */ -function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) - view - returns (Honk.G1Point memory) -{ - Honk.G1Point memory result; - - result = ecMul(recursionSeperator, basePoint); - result = ecAdd(result, other); - - return result; -} - -/** - * G1 Mul - * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. - * - * @param value The value to multiply the point by. - * @param point The point to multiply. - * @return result The result of the multiplication. - */ -function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write the point into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | point.x - // free + 0x20| point.y - mstore(free, mload(point)) - mstore(add(free, 0x20), mload(add(point, 0x20))) - // Write the scalar into memory (one 32 byte word) - // Memory layout: - // Address | value - // free + 0x40| value - mstore(add(free, 0x40), value) - - // Call the ecMul precompile, it takes in the following - // [point.x, point.y, scalar], and returns the result back into the free memory location. - let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) - if iszero(success) { - revert(0, 0) - } - // Copy the result of the multiplication back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x60)) - } - - return result; -} - -/** - * G1 Add - * Takes two G1 points and uses the ecAdd precompile to return the result. - * - * @param lhs The left hand side of the addition. - * @param rhs The right hand side of the addition. - * @return result The result of the addition. - */ -function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write lhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | lhs.x - // free + 0x20| lhs.y - mstore(free, mload(lhs)) - mstore(add(free, 0x20), mload(add(lhs, 0x20))) - - // Write rhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free + 0x40| rhs.x - // free + 0x60| rhs.y - mstore(add(free, 0x40), mload(rhs)) - mstore(add(free, 0x60), mload(add(rhs, 0x20))) - - // Call the ecAdd precompile, it takes in the following - // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. - let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) - if iszero(success) { revert(0, 0) } - - // Copy the result of the addition back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x80)) - } - - return result; -} - -function rejectPointAtInfinity(Honk.G1Point memory point) pure { - require((point.x | point.y) != 0, Errors.PointAtInfinity()); -} - -/** - * Check if pairing point limbs are all zero (default/infinity). - * Default pairing points indicate no recursive verification occurred. - */ -function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { - uint256 acc = 0; - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - acc |= Fr.unwrap(pairingPoints[i]); - } - return acc == 0; -} - -function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { - bytes memory input = abi.encodePacked( - rhs.x, - rhs.y, - // Fixed G2 point - uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), - uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), - uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), - uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), - lhs.x, - lhs.y, - // G2 point from VK - uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), - uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), - uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), - uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) - ); - - (bool success, bytes memory result) = address(0x08).staticcall(input); - decodedResult = success && abi.decode(result, (bool)); -} - -abstract contract BaseZKHonkVerifier is IVerifier { - using FrLib for Fr; - - struct PairingInputs { - Honk.G1Point P_0; - Honk.G1Point P_1; - } - - struct SmallSubgroupIpaIntermediates { - Fr[SUBGROUP_SIZE] challengePolyLagrange; - Fr challengePolyEval; - Fr lagrangeFirst; - Fr lagrangeLast; - Fr rootPower; - Fr[SUBGROUP_SIZE] denominators; // this has to disappear - Fr diff; - } - - // Constants for proof length calculation (matching UltraKeccakZKFlavor) - uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; - uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points - uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements - uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations - - uint256 internal constant LIBRA_COMMITMENTS = 3; - uint256 internal constant LIBRA_EVALUATIONS = 4; - uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; - - uint256 internal constant SHIFTED_COMMITMENTS_START = 30; - uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; - - uint256 internal immutable $N; - uint256 internal immutable $LOG_N; - uint256 internal immutable $VK_HASH; - uint256 internal immutable $NUM_PUBLIC_INPUTS; - uint256 internal immutable $MSMSize; - - constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { - $N = _N; - $LOG_N = _logN; - $VK_HASH = _vkHash; - $NUM_PUBLIC_INPUTS = _numPublicInputs; - $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; - } - - function verify(bytes calldata proof, bytes32[] calldata publicInputs) - public - view - override - returns (bool verified) - { - // Calculate expected proof size based on $LOG_N - uint256 expectedProofSize = calculateProofSize($LOG_N); - - // Check the received proof is the expected size where each field element is 32 bytes - require( - proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) - ); - - Honk.VerificationKey memory vk = loadVerificationKey(); - Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); - - require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); - - // Generate the fiat shamir challenges for the whole protocol - ZKTranscript memory t = - ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); - - // Derive public input delta - t.relationParameters.publicInputsDelta = computePublicInputDelta( - publicInputs, - p.pairingPointObject, - t.relationParameters.beta, - t.relationParameters.gamma, - 5 // pubInputsOffset = NUM_DISABLED_ROWS_IN_SUMCHECK + NUM_ZERO_ROWS = 4 + 1 - ); - - // Sumcheck - require(verifySumcheck(p, t), Errors.SumcheckFailed()); - require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); - - verified = true; - } - - function computePublicInputDelta( - bytes32[] memory publicInputs, - Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, - Fr beta, - Fr gamma, - uint256 offset - ) internal view returns (Fr publicInputDelta) { - Fr numerator = Fr.wrap(1); - Fr denominator = Fr.wrap(1); - - Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); - Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); - - { - for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { - Fr pubInput = FrLib.fromBytes32(publicInputs[i]); - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - Fr pubInput = pairingPointObject[i]; - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - } - - // Fr delta = numerator / denominator; // TOOO: batch invert later? - publicInputDelta = FrLib.div(numerator, denominator); - } - - function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { - Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 - Fr powPartialEvaluation = Fr.wrap(1); - - // We perform sumcheck reductions over log n rounds ( the multivariate degree ) - for (uint256 round; round < $LOG_N; ++round) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; - Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; - require(totalSum == roundTargetSum, Errors.SumcheckFailed()); - - Fr roundChallenge = tp.sumCheckUChallenges[round]; - - // Update the round target for the next rounf - roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); - powPartialEvaluation = - powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); - } - - // Last round - // For ZK flavors: sumcheckEvaluations has 42 elements - // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations - Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; - for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { - relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 - } - Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( - relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation - ); - - // Row-disabling polynomial: 1 - ∏_{i≥2}(1 - u_i) - Fr evaluation = Fr.wrap(1); - for (uint256 i = 2; i < $LOG_N; i++) { - evaluation = evaluation * (Fr.wrap(1) - tp.sumCheckUChallenges[i]); - } - - grandHonkRelationSum = - grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; - verified = (grandHonkRelationSum == roundTargetSum); - } - - // Return the new target sum for the next sumcheck round - function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) - internal - view - returns (Fr targetSum) - { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) - ]; - - // To compute the next target sum, we evaluate the given univariate at a point u (challenge). - - // Performing Barycentric evaluations - // Compute B(x) - Fr numeratorValue = Fr.wrap(1); - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); - } - - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); - } - - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; - } - - // Scale the sum by the value of B(x) - targetSum = targetSum * numeratorValue; - } - - function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) - internal - view - returns (bool verified) - { - CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack - - // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size - Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); - // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings - Fr[] memory scalars = new Fr[]($MSMSize); - Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); - - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); - - mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); - mem.shiftedScalar = - tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); - - scalars[0] = Fr.wrap(1); - commitments[0] = proof.shplonkQ; - - /* Batch multivariate opening claims, shifted and unshifted - * The vector of scalars is populated as follows: - * \f[ - * \left( - * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) - * \right) - * \f] - * - * The following vector is concatenated to the vector of commitments: - * \f[ - * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} - * \f] - * - * Simultaneously, the evaluation of the multilinear polynomial - * \f[ - * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} - * \f] - * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. - * - * This approach minimizes the number of iterations over the commitments to multilinear polynomials - * and eliminates the need to store the powers of \f$ \rho \f$. - */ - // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] - // Start batching challenge at 1, not rho, to match non-ZK pattern - mem.batchingChallenge = Fr.wrap(1); - mem.batchedEvaluation = Fr.wrap(0); - - mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); - mem.shiftedScalarNeg = mem.shiftedScalar.neg(); - - // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) - for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { - scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; - mem.batchedEvaluation = mem.batchedEvaluation - + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - // g commitments are accumulated at r - // For each of the to be shifted commitments perform the shift in place by - // adding to the unshifted value. - // We do so, as the values are to be used in batchMul later, and as - // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. - // Applied to w1, w2, w3, w4 and zPerm - for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { - uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; - uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; - - scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); - mem.batchedEvaluation = - mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - - commitments[1] = proof.geminiMaskingPoly; - - commitments[2] = vk.s1; - commitments[3] = vk.s2; - commitments[4] = vk.s3; - commitments[5] = vk.s4; - commitments[6] = vk.id1; - commitments[7] = vk.id2; - commitments[8] = vk.id3; - commitments[9] = vk.id4; - commitments[10] = vk.lagrangeFirst; - commitments[11] = vk.lagrangeLast; - commitments[12] = vk.qLookup; - commitments[13] = vk.t1; - commitments[14] = vk.t2; - commitments[15] = vk.t3; - commitments[16] = vk.t4; - commitments[17] = vk.qm; - commitments[18] = vk.qr; - commitments[19] = vk.qo; - commitments[20] = vk.qc; - commitments[21] = vk.ql; - commitments[22] = vk.q4; - commitments[23] = vk.qArith; - commitments[24] = vk.qDeltaRange; - commitments[25] = vk.qElliptic; - commitments[26] = vk.qMemory; - commitments[27] = vk.qNnf; - commitments[28] = vk.qPoseidon2External; - commitments[29] = vk.qPoseidon2Internal; - - // Accumulate proof points - commitments[30] = proof.w1; - commitments[31] = proof.w2; - commitments[32] = proof.w3; - commitments[33] = proof.w4; - commitments[34] = proof.zPerm; - commitments[35] = proof.lookupInverses; - commitments[36] = proof.lookupReadCounts; - commitments[37] = proof.lookupReadTags; - - /* Batch gemini claims from the prover - * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from - * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars - * - * 1. Moves the vector - * \f[ - * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) - * \f] - * to the 'commitments' vector. - * - * 2. Computes the scalars: - * \f[ - * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} - * \f] - * and places them into the 'scalars' vector. - * - * 3. Accumulates the summands of the constant term: - * \f[ - * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} - * \f] - * and adds them to the 'constant_term_accumulator'. - */ - - // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 - Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( - tp.sumCheckUChallenges, - mem.batchedEvaluation, - proof.geminiAEvaluations, - powers_of_evaluation_challenge, - $LOG_N - ); - - mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; - mem.constantTermAccumulator = - mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); - - mem.batchingChallenge = tp.shplonkNu.sqr(); - uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; - - // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; - // Compute scalar multipliers for each fold commitment - for (uint256 i = 0; i < $LOG_N - 1; ++i) { - bool dummy_round = i >= ($LOG_N - 1); - - if (!dummy_round) { - // Update inverted denominators - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); - - // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] - mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; - mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; - scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); - - // Accumulate the const term contribution given by - // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) - Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; - accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; - mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; - } - // Update the running power of v - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; - - commitments[boundary + i] = proof.geminiFoldComms[i]; - } - - boundary += $LOG_N - 1; - - // Denominators 1/(z - point_i) for the five opening points {r, g*r, r, 1, r}. - mem.denominators[0] = ONE.div(tp.shplonkZ - tp.geminiR); - mem.denominators[1] = ONE.div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); - mem.denominators[2] = mem.denominators[0]; - mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] = ONE.div(tp.shplonkZ - ONE); - mem.denominators[NUM_SMALL_IPA_OPENING_CLAIMS - 1] = mem.denominators[0]; - - // Iterate the opening claims in three segments — the inner loops can't be merged without an extra induction - // variable, which pushes us into stack-too-deep. - for (uint256 i = 0; i < SMALL_IPA_BOUNDARY_OPENING_IDX; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; - } - - // Boundary slot: claimed value is hardcoded 0, so no constantTermAccumulator contribution. - { - Fr scalingFactor = mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] * mem.batchingChallenge; - mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - } - - for (uint256 i = SMALL_IPA_BOUNDARY_OPENING_IDX + 1; i < NUM_SMALL_IPA_OPENING_CLAIMS; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i - 1]; - } - - // Group per-claim batching scalars by commitment: [G], [A] (three openings), [Q]. - scalars[boundary] = mem.batchingScalars[0]; - scalars[boundary + 1] = - mem.batchingScalars[1] + mem.batchingScalars[2] + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX]; - scalars[boundary + 2] = mem.batchingScalars[NUM_SMALL_IPA_OPENING_CLAIMS - 1]; - - for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { - commitments[boundary++] = proof.libraCommitments[i]; - } - - commitments[boundary] = Honk.G1Point({x: 1, y: 2}); - scalars[boundary++] = mem.constantTermAccumulator; - - require( - checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), - Errors.ConsistencyCheckFailed() - ); - - Honk.G1Point memory quotient_commitment = proof.kzgQuotient; - - commitments[boundary] = quotient_commitment; - scalars[boundary] = tp.shplonkZ; // evaluation challenge - - PairingInputs memory pair; - pair.P_0 = batchMul(commitments, scalars); - pair.P_1 = negateInplace(quotient_commitment); - - // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) - if (!arePairingPointsDefault(proof.pairingPointObject)) { - Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); - (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = - convertPairingPointsToG1(proof.pairingPointObject); - - // Validate the points from the proof are on the curve - rejectPointAtInfinity(P_0_other); - rejectPointAtInfinity(P_1_other); - - // accumulate with aggregate points in proof - pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); - pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); - } - - return pairing(pair.P_0, pair.P_1); - } - - function checkEvalsConsistency( - Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, - Fr geminiR, - Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, - Fr libraEval - ) internal view returns (bool check) { - Fr one = Fr.wrap(1); - Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; - require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); - - SmallSubgroupIpaIntermediates memory mem; - mem.challengePolyLagrange[0] = one; - for (uint256 round = 0; round < $LOG_N; round++) { - uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; - mem.challengePolyLagrange[currIdx] = one; - for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { - mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; - } - } - - mem.rootPower = one; - mem.challengePolyEval = Fr.wrap(0); - for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { - mem.denominators[idx] = mem.rootPower * geminiR - one; - mem.denominators[idx] = mem.denominators[idx].invert(); - mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; - mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; - } - - Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); - mem.challengePolyEval = mem.challengePolyEval * numerator; - mem.lagrangeFirst = mem.denominators[0] * numerator; - mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; - - mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; - - mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) - * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); - mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; - - check = mem.diff == Fr.wrap(0); - } - - // This implementation is the same as above with different constants - function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) - internal - view - returns (Honk.G1Point memory result) - { - uint256 limit = $MSMSize; - - // Identity bases are accepted: VK selector/table polys may be identically zero, - // and the ecAdd/ecMul precompiles treat (0,0) as the additive identity per EIP-196. - // Soundness against an attacker substituting (0,0) for a non-zero commitment is - // upheld by sumcheck/Shplemini, which would fail on inconsistent evaluations. - - bool success = true; - assembly ("memory-safe") { - let free := mload(0x40) - - let count := 0x01 - for {} lt(count, add(limit, 1)) { count := add(count, 1) } { - // Get loop offsets - let base_base := add(base, mul(count, 0x20)) - let scalar_base := add(scalars, mul(count, 0x20)) - - mstore(add(free, 0x40), mload(mload(base_base))) - mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) - // Add scalar - mstore(add(free, 0x80), mload(scalar_base)) - - success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) - // accumulator = accumulator + accumulator_2 - success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) - } - - // Return the result - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - } - - require(success, Errors.ShpleminiFailed()); - } - - // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) - function calculateProofSize(uint256 logN) internal pure returns (uint256) { - // Witness and Libra commitments - uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments - proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking - - // Sumcheck - proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates - proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations - - // Libra and Gemini - proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval - proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations - proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations - - // PCS commitments - proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments - proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments - - // Pairing points - proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs - - return proofLength * 32; - } - - function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); -} - -contract BearerLinkHonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { - function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { - return HonkVerificationKey.loadVerificationKey(); - } -} diff --git a/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol b/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol deleted file mode 100644 index ecb41dc..0000000 --- a/solidity/contracts/circuits/OidcGoogleHonkVerifier.sol +++ /dev/null @@ -1,2494 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Aztec -// Vendored from libid-circuits v0.4.0 (libid-circuits-0.4.0-oidc-google.tar.gz) by scripts/vendor-circuit-verifiers.sh. Do not edit. -// The pin is contracts/circuits/circuits.json; `forge fmt` is the only change to what shipped. -pragma solidity >=0.8.21; - -uint256 constant N = 262144; -uint256 constant LOG_N = 18; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 64; -uint256 constant VK_HASH = 0x1b50bbf6d8ea6efc7ecc2547c25b285511704a10d9247466e84045095d9c3f77; - -library HonkVerificationKey { - function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { - Honk.VerificationKey memory vk = Honk.VerificationKey({ - circuitSize: uint256(262144), - logCircuitSize: uint256(18), - publicInputsSize: uint256(64), - ql: Honk.G1Point({ - x: uint256(0x08515254b0b2bbf647e56ed57459e7a64c16f3671ec0b40040e24cae2de769a1), - y: uint256(0x19f82d3334546ad161637f7a51fb2c842c4315275c4024da44f52d59f951dedb) - }), - qr: Honk.G1Point({ - x: uint256(0x047e99ebaa0002607e7ada10258f79a9be3a1cf6690e32cbc8f489afffd4c63d), - y: uint256(0x16a2041afe3ccc04b269627d512fa7136afff3deceae5fae49c274810b12d83d) - }), - qo: Honk.G1Point({ - x: uint256(0x2e316e89831c2ad1377c299e1aae33c18341e31b82b802a14147286cd3eb7ca0), - y: uint256(0x17a7383dffd125bdbabfb45c8ac10e6b8864a7c6a925e20bc8e377d7b39f7d43) - }), - q4: Honk.G1Point({ - x: uint256(0x0a8c8e60875cb0f659516ed955c124adb212f0a695c007a453d26dd721e940c1), - y: uint256(0x16339174b877c03cbc7c85674096efb16b9ea5471b01133e0a12d436893fce9d) - }), - qm: Honk.G1Point({ - x: uint256(0x1285dd2ef272db30649cdfa1f2262fba3c35983ef4deceb0e5f093dc613ab6cf), - y: uint256(0x07bc7a4cc600b01a4a4022cdf42a1d0f41abb867b213cc84070ca6a436b05017) - }), - qc: Honk.G1Point({ - x: uint256(0x2704cb2db8dbb2a6229383fe1f7a0c26e18edbef580dabd94ecc101a8c000566), - y: uint256(0x24eab2285697340ecc7c0485a41d95a5069cbbc721e32bbb6ecbaac8732b185a) - }), - qLookup: Honk.G1Point({ - x: uint256(0x25182937a03a6c2ba9e263545ff28fd79302eb44a72cceb2425965d5420e0b0c), - y: uint256(0x0b55f00da16bf0d0e7eb2e1d212f16adc4139020526e1890051eed208b567f68) - }), - qArith: Honk.G1Point({ - x: uint256(0x259c3a410a4c16e2c0d60e909c5d80c0c45a2e9d534883a342c6df4866d2554c), - y: uint256(0x25fc94235774734903ef66a6100c7cb95f8e7700c83d6ea7df8088c25d6ef1b4) - }), - qDeltaRange: Honk.G1Point({ - x: uint256(0x2002cc2742d0b1b60e9089886601cb515bd32e1631efffa3015c98d2dcdac063), - y: uint256(0x15fb091ccd4254bb539e726a6568bc91aed65bbe74f606fb3f047c463164aa4a) - }), - qElliptic: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qMemory: Honk.G1Point({ - x: uint256(0x23b5b2077446236b5198c270fd8fa54bbb416f214fc52db278f594de8a1e1b7d), - y: uint256(0x1951fc00779e95dad7e2f6413ef993be7bc5edcd75d4b009ff361d0ca5248909) - }), - qNnf: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qPoseidon2External: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - qPoseidon2Internal: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000) - }), - s1: Honk.G1Point({ - x: uint256(0x1757d1f99e0fe4f46de6bb139aa0815888f75288adb232a1bf753022fc77a656), - y: uint256(0x2aecb68071e83250e9b3ffca574a9c99616badaa9fe6266fbcebace96fb82ad5) - }), - s2: Honk.G1Point({ - x: uint256(0x07897ea026c26a2ebfc2aeb63275bd76a72ce2455d899a0f4f8731e8a736a8ad), - y: uint256(0x1d6f2d522f4daffb155914a653aa7440c6ff435c5537b305a9c4d9566d00adad) - }), - s3: Honk.G1Point({ - x: uint256(0x02fe36d7c36b00aaa67ff577f6be31aa9e419f21d556ac171540505b16ecdbb0), - y: uint256(0x11652f3a5f31f4a13badb6b22b42ffe2d1efc5ba169b01040d48d01df499b7c0) - }), - s4: Honk.G1Point({ - x: uint256(0x0d6dd4196a4c3f7dd85026ee6ee6561f4f05c218815ce16b89fb47a042549b94), - y: uint256(0x21c9d576510a4d224c41368265e95747ff7307df314fbe1cafe75f59c5876361) - }), - t1: Honk.G1Point({ - x: uint256(0x022c2e257702c8227a744d35983564a1efbed263b14f025945c36ed229fc1ddd), - y: uint256(0x27f456a22cc2a694bad2998fb70241483b2d5d6d27197a646ee1ba3abc344ea6) - }), - t2: Honk.G1Point({ - x: uint256(0x10c31588d8dfb80f7abac5634e2b9b6e58b36031cbb75ac8e2febebe9417cd45), - y: uint256(0x24b3f9578867af5610a9b7d9a989c6fadb0b87a86aaabd191bf0f63003b5ada4) - }), - t3: Honk.G1Point({ - x: uint256(0x2914a41cda5920827a6e63a413040ac1de2883ec93c1751fa3240eef56bb7c24), - y: uint256(0x024dab849a0daf31508ff240bca405c355fdaf562cc016bf7d19298a98d40575) - }), - t4: Honk.G1Point({ - x: uint256(0x237f8dc54d796c2c1080bf7444d9cf948b20e3386af8f6f9f1a0d753b8e8cb62), - y: uint256(0x0760e9f1c4ef20c02e419e2e24a79ee417b5bc6ba04f667f29eeba1c23e849a3) - }), - id1: Honk.G1Point({ - x: uint256(0x047fb5e65fa50b75618e63d061948e0325a6c31691dba46575a9b1b29770e774), - y: uint256(0x1bb8f3358642934eb9d84d6d62d58ac1eda0116841220f07c4b349836ea5e85b) - }), - id2: Honk.G1Point({ - x: uint256(0x24871428617e352c457baae5b042bdc725b03efd9d6df4d231d33981078771bb), - y: uint256(0x0bd1f451e556b203a9c0a02d10cd5f2956586a44cb62526c7546f33a42278d67) - }), - id3: Honk.G1Point({ - x: uint256(0x19c3aa8241ea7d90a20902dca543902f43d83b1892e2a50da0c954e9bcf2a19e), - y: uint256(0x116ccacc53166cc5ea29a6097a731c168217262671d5a3aeadef7c9dd19ef5cf) - }), - id4: Honk.G1Point({ - x: uint256(0x24a599d525d2d50d57089e1c9006a3ced7a51235a2582080c8032619c8cd7516), - y: uint256(0x02a2b6e766a64bf55c9440d614259d4d946dd1df37877a95a68eb20dde98e153) - }), - lagrangeFirst: Honk.G1Point({ - x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6), - y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c) - }), - lagrangeLast: Honk.G1Point({ - x: uint256(0x0d06cb4e0a5020818541df1279f0b68ba33f90fbc20e8ca797fc34067d3e3de8), - y: uint256(0x0d7631821cd9c64a626e7390e0f45830e473b9b84bb54fbfadf7985083663052) - }) - }); - return vk; - } -} - -pragma solidity ^0.8.27; - -interface IVerifier { - function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); -} - -/** - * @notice Library of error codes - * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier - */ -library Errors { - error ValueGeLimbMax(); - error ValueGeGroupOrder(); - error ValueGeFieldOrder(); - - error InvertOfZero(); - error NotPowerOfTwo(); - error ModExpFailed(); - - error ProofLengthWrong(); - error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); - error PublicInputsLengthWrong(); - error SumcheckFailed(); - error ShpleminiFailed(); - - error PointAtInfinity(); - - error ConsistencyCheckFailed(); - error GeminiChallengeInSubgroup(); -} - -type Fr is uint256; - -using {add as +} for Fr global; -using {sub as -} for Fr global; -using {mul as *} for Fr global; - -using {notEqual as !=} for Fr global; -using {equal as ==} for Fr global; - -uint256 constant SUBGROUP_SIZE = 256; -uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order -uint256 constant P = MODULUS; -Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); -Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); -Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); -Fr constant ONE = Fr.wrap(1); -Fr constant ZERO = Fr.wrap(0); - -// SmallSubgroupIPA opening-claim layout — mirrors SMALL_IPA_CLAIMS in -// barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa_utils.hpp. -uint256 constant NUM_SMALL_IPA_OPENING_CLAIMS = 5; -uint256 constant SMALL_IPA_BOUNDARY_OPENING_IDX = 3; -uint256 constant NUM_SMALL_IPA_TRANSCRIPT_EVALS = 4; -// Instantiation - -library FrLib { - bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; - - function invert(Fr value) internal view returns (Fr) { - uint256 v = Fr.unwrap(value); - require(v != 0, Errors.InvertOfZero()); - - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), v) - mstore(add(free, 0x80), sub(MODULUS, 2)) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function pow(Fr base, uint256 v) internal view returns (Fr) { - uint256 b = Fr.unwrap(base); - // Only works for power of 2 - require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), b) - mstore(add(free, 0x80), v) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function div(Fr numerator, Fr denominator) internal view returns (Fr) { - unchecked { - return numerator * invert(denominator); - } - } - - function sqr(Fr value) internal pure returns (Fr) { - unchecked { - return value * value; - } - } - - function unwrap(Fr value) internal pure returns (uint256) { - unchecked { - return Fr.unwrap(value); - } - } - - function neg(Fr value) internal pure returns (Fr) { - unchecked { - return Fr.wrap(MODULUS - Fr.unwrap(value)); - } - } - - function from(uint256 value) internal pure returns (Fr) { - unchecked { - require(value < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(value); - } - } - - function fromBytes32(bytes32 value) internal pure returns (Fr) { - unchecked { - uint256 v = uint256(value); - require(v < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(v); - } - } - - function toBytes32(Fr value) internal pure returns (bytes32) { - unchecked { - return bytes32(Fr.unwrap(value)); - } - } -} - -// Free functions -function add(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function mul(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function sub(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); - } -} - -function notEqual(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) != Fr.unwrap(b); - } -} - -function equal(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) == Fr.unwrap(b); - } -} - -uint256 constant CONST_PROOF_SIZE_LOG_N = 25; - -uint256 constant NUMBER_OF_SUBRELATIONS = 29; -uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; -uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; -uint256 constant NUMBER_OF_ENTITIES = 41; -// The number of entities added for ZK (gemini_masking_poly) -uint256 constant NUM_MASKING_POLYNOMIALS = 1; -uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_UNSHIFTED = 36; -uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_TO_BE_SHIFTED = 5; -uint256 constant PAIRING_POINTS_SIZE = 8; - -uint256 constant FIELD_ELEMENT_SIZE = 0x20; -uint256 constant GROUP_ELEMENT_SIZE = 0x40; - -// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) -uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; - -// Must match UltraFlavor_Generated::EntityId order. -enum WIRE { - SIGMA_1, - SIGMA_2, - SIGMA_3, - SIGMA_4, - ID_1, - ID_2, - ID_3, - ID_4, - LAGRANGE_FIRST, - LAGRANGE_LAST, - Q_LOOKUP, - TABLE_1, - TABLE_2, - TABLE_3, - TABLE_4, - Q_M, - Q_R, - Q_O, - Q_C, - Q_L, - Q_4, - Q_ARITH, - Q_RANGE, - Q_ELLIPTIC, - Q_MEMORY, - Q_NNF, - Q_POSEIDON2_EXTERNAL, - Q_POSEIDON2_INTERNAL, - W_L, - W_R, - W_O, - W_4, - Z_PERM, - LOOKUP_INVERSES, - LOOKUP_READ_COUNTS, - LOOKUP_READ_TAGS, - W_L_SHIFT, - W_R_SHIFT, - W_O_SHIFT, - W_4_SHIFT, - Z_PERM_SHIFT -} - -library Honk { - struct G1Point { - uint256 x; - uint256 y; - } - - struct VerificationKey { - // Misc Params - uint256 circuitSize; - uint256 logCircuitSize; - uint256 publicInputsSize; - // Selectors - G1Point qm; - G1Point qc; - G1Point ql; - G1Point qr; - G1Point qo; - G1Point q4; - G1Point qLookup; // Lookup - G1Point qArith; // Arithmetic widget - G1Point qDeltaRange; // Delta Range sort - G1Point qMemory; // Memory - G1Point qNnf; // Non-native Field - G1Point qElliptic; // Auxillary - G1Point qPoseidon2External; - G1Point qPoseidon2Internal; - // Copy constraints - G1Point s1; - G1Point s2; - G1Point s3; - G1Point s4; - // Copy identity - G1Point id1; - G1Point id2; - G1Point id3; - G1Point id4; - // Precomputed lookup table - G1Point t1; - G1Point t2; - G1Point t3; - G1Point t4; - // Fixed first and last - G1Point lagrangeFirst; - G1Point lagrangeLast; - } - - struct RelationParameters { - // challenges - Fr eta; - Fr beta; - Fr gamma; - // derived - Fr publicInputsDelta; - } - - struct Proof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // Free wires - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Lookup helpers - Permutations - G1Point zPerm; - // Lookup helpers - logup - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Sumcheck - Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - G1Point shplonkQ; - G1Point kzgQuotient; - } - - /// forge-lint: disable-next-item(pascal-case-struct) - struct ZKProof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) - G1Point geminiMaskingPoly; - // Commitments to wire polynomials - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Commitments to logup witness polynomials - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Commitment to grand permutation polynomial - G1Point zPerm; - G1Point[3] libraCommitments; - // Sumcheck - Fr libraSum; - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr libraEvaluation; - Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - Fr[4] libraPolyEvals; - G1Point shplonkQ; - G1Point kzgQuotient; - } -} - -// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest -/// forge-lint: disable-next-item(pascal-case-struct) -struct ZKTranscript { - // Oink - Honk.RelationParameters relationParameters; - Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] - Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; - // Sumcheck - Fr libraChallenge; - Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; - // Shplemini - Fr rho; - Fr geminiR; - Fr shplonkNu; - Fr shplonkZ; - // Derived - Fr publicInputsDelta; -} - -library ZKTranscriptLib { - function generateTranscript( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - uint256 logN - ) external pure returns (ZKTranscript memory t) { - Fr previousChallenge; - (t.relationParameters, previousChallenge) = - generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); - - (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); - - (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); - (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); - (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); - - (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); - - (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); - - (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); - - (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); - return t; - } - - function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { - uint256 challengeU256 = uint256(Fr.unwrap(challenge)); - // Split into two equal 127-bit chunks (254/2) - uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits - uint256 hi = challengeU256 >> 127; - first = FrLib.from(lo); - second = FrLib.from(hi); - } - - function generateRelationParametersChallenges( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - Fr previousChallenge - ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { - (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); - - (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); - } - - function generateEtaChallenge( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize - ) internal pure returns (Fr eta, Fr previousChallenge) { - // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) - bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); - round0[0] = bytes32(vkHash); - - for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { - require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); - round0[1 + i] = publicInputs[i]; - } - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); - } - - // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) - round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); - round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); - - // Create the first challenge - // Note: w4 is added to the challenge later on - round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); - round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); - round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); - round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); - round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); - round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); - - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); - (eta,) = splitChallenge(previousChallenge); - } - - function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) - { - bytes32[7] memory round1; - round1[0] = FrLib.toBytes32(previousChallenge); - round1[1] = bytes32(proof.lookupReadCounts.x); - round1[2] = bytes32(proof.lookupReadCounts.y); - round1[3] = bytes32(proof.lookupReadTags.x); - round1[4] = bytes32(proof.lookupReadTags.y); - round1[5] = bytes32(proof.w4.x); - round1[6] = bytes32(proof.w4.y); - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); - (beta, gamma) = splitChallenge(nextPreviousChallenge); - } - - // Alpha challenges non-linearise the gate contributions - function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) - { - // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup - uint256[5] memory alpha0; - alpha0[0] = Fr.unwrap(previousChallenge); - alpha0[1] = proof.lookupInverses.x; - alpha0[2] = proof.lookupInverses.y; - alpha0[3] = proof.zPerm.x; - alpha0[4] = proof.zPerm.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); - Fr alpha; - (alpha,) = splitChallenge(nextPreviousChallenge); - - // Compute powers of alpha for batching subrelations - alphas[0] = alpha; - for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { - alphas[i] = alphas[i - 1] * alpha; - } - } - - function generateGateChallenges(Fr previousChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) - { - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); - (gateChallenges[0],) = splitChallenge(previousChallenge); - for (uint256 i = 1; i < logN; i++) { - gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; - } - nextPreviousChallenge = previousChallenge; - } - - function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr libraChallenge, Fr nextPreviousChallenge) - { - // 2 comm, 1 sum, 1 challenge - uint256[4] memory challengeData; - challengeData[0] = Fr.unwrap(previousChallenge); - challengeData[1] = proof.libraCommitments[0].x; - challengeData[2] = proof.libraCommitments[0].y; - challengeData[3] = Fr.unwrap(proof.libraSum); - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); - (libraChallenge,) = splitChallenge(nextPreviousChallenge); - } - - function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) - { - for (uint256 i = 0; i < logN; i++) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; - univariateChal[0] = prevChallenge; - - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; - } - prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); - - (sumcheckChallenges[i],) = splitChallenge(prevChallenge); - } - nextPreviousChallenge = prevChallenge; - } - - // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) - function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr rho, Fr nextPreviousChallenge) - { - uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; - rhoChallengeElements[0] = Fr.unwrap(prevChallenge); - uint256 i; - for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { - rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); - } - rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); - i += 1; - rhoChallengeElements[i] = proof.libraCommitments[1].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; - i += 2; - rhoChallengeElements[i] = proof.libraCommitments[2].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); - (rho,) = splitChallenge(nextPreviousChallenge); - } - - function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr geminiR, Fr nextPreviousChallenge) - { - uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); - gR[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 0; i < logN - 1; i++) { - gR[1 + i * 2] = proof.geminiFoldComms[i].x; - gR[2 + i * 2] = proof.geminiFoldComms[i].y; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); - - (geminiR,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr shplonkNu, Fr nextPreviousChallenge) - { - uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); - shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 1; i <= logN; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); - } - - uint256 libraIdx = 0; - for (uint256 i = logN + 1; i <= logN + 4; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); - libraIdx++; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); - (shplonkNu,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr shplonkZ, Fr nextPreviousChallenge) - { - uint256[3] memory shplonkZChallengeElements; - shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); - - shplonkZChallengeElements[1] = proof.shplonkQ.x; - shplonkZChallengeElements[2] = proof.shplonkQ.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); - (shplonkZ,) = splitChallenge(nextPreviousChallenge); - } - - function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { - uint256 boundary = 0x0; - - // Pairing point object - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); - // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 - require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); - p.pairingPointObject[i] = FrLib.from(limb); - boundary += FIELD_ELEMENT_SIZE; - } - - // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) - p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Commitments - p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Lookup / Permutation Helper Commitments - p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - // Sumcheck univariates - for (uint256 i = 0; i < logN; i++) { - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - } - - // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) - for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { - p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - - p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Gemini - // Read gemini fold univariates - for (uint256 i = 0; i < logN - 1; i++) { - p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - } - - // Read gemini a evaluations - for (uint256 i = 0; i < logN; i++) { - p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - for (uint256 i = 0; i < 4; i++) { - p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - // Shplonk - p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - // KZG - p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - } -} - -library RelationsLib { - struct EllipticParams { - // Points - Fr x_1; - Fr y_1; - Fr x_2; - Fr y_2; - Fr y_3; - Fr x_3; - // push accumulators into memory - Fr x_double_identity; - } - - // Parameters used within the Memory Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct MemParams { - Fr memory_record_check; - Fr partial_record_check; - Fr next_gate_access_type; - Fr record_delta; - Fr index_delta; - Fr adjacent_values_match_if_adjacent_indices_match; - Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; - Fr access_check; - Fr next_gate_access_type_is_boolean; - Fr ROM_consistency_check_identity; - Fr RAM_consistency_check_identity; - Fr timestamp_delta; - Fr RAM_timestamp_check_identity; - Fr memory_identity; - Fr index_is_monotonically_increasing; - } - - // Parameters used within the Non-Native Field Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct NnfParams { - Fr limb_subproduct; - Fr non_native_field_gate_1; - Fr non_native_field_gate_2; - Fr non_native_field_gate_3; - Fr limb_accumulator_1; - Fr limb_accumulator_2; - Fr nnf_identity; - } - - struct PoseidonExternalParams { - Fr s1; - Fr s2; - Fr s3; - Fr s4; - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr t0; - Fr t1; - Fr t2; - Fr t3; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr q_pos_by_scaling; - } - - struct PoseidonInternalParams { - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr u_sum; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr s1; - Fr q_pos_by_scaling; - } - - Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) - uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; - - // Constants for the Non-native Field relation - Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); - Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); - - function accumulateRelationEvaluations( - Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, - Fr powPartialEval - ) external pure returns (Fr accumulator) { - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; - - // Accumulate all relations in Ultra Honk - each with varying number of subrelations - accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); - - // batch the subrelations with the precomputed alpha powers to obtain the full honk relation - accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); - } - - /** - * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids - * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code - * editors, and thus is noisy. - */ - function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { - return p[uint256(_wire)]; - } - - /** - * Ultra Arithmetic Relation - * - */ - function accumulateArithmeticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - // Relation 0 - Fr q_arith = wire(p, WIRE.Q_ARITH); - { - Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); - - Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; - accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) - + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); - accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); - accum = accum * q_arith; - accum = accum * domainSep; - evals[6] = accum; - } - - // Relation 1 - { - Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); - accum = accum * (q_arith - Fr.wrap(2)); - accum = accum * (q_arith - ONE); - accum = accum * q_arith; - accum = accum * domainSep; - evals[7] = accum; - } - } - - function accumulatePermutationRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr grand_product_numerator; - Fr grand_product_denominator; - - { - Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; - num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); - - grand_product_numerator = num; - } - { - Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; - den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); - - grand_product_denominator = den; - } - - // Contribution 2 - { - Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; - - acc = acc - - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) - * grand_product_denominator); - acc = acc * domainSep; - evals[0] = acc; - } - - // Contribution 3 - { - Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; - evals[1] = acc; - } - - // Contribution 4: z_perm initialization check (lagrange_first * z_perm = 0) - { - Fr acc = (wire(p, WIRE.LAGRANGE_FIRST) * wire(p, WIRE.Z_PERM)) * domainSep; - evals[2] = acc; - } - } - - function accumulateLogDerivativeLookupRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr table_term; - Fr lookup_term; - - // Calculate the write term (the table accumulation) - // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) - + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); - } - - // Calculate the read term - // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); - Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); - Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); - - lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) - + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); - } - - Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; - Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; - - Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) - - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); - - // Inverse calculated correctly relation - Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; - accumulatorNone = accumulatorNone * domainSep; - - // Inverse - Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; - - Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); - - Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; - - evals[3] = accumulatorNone; - evals[4] = accumulatorOne; - evals[5] = read_tag_boolean_relation * domainSep; - } - - function accumulateDeltaRangeRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr minus_one = ZERO - ONE; - Fr minus_two = ZERO - Fr.wrap(2); - Fr minus_three = ZERO - Fr.wrap(3); - - // Compute wire differences - Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); - Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); - Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); - Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); - - // Contribution 6 - { - Fr acc = delta_1; - acc = acc * (delta_1 + minus_one); - acc = acc * (delta_1 + minus_two); - acc = acc * (delta_1 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[8] = acc; - } - - // Contribution 7 - { - Fr acc = delta_2; - acc = acc * (delta_2 + minus_one); - acc = acc * (delta_2 + minus_two); - acc = acc * (delta_2 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[9] = acc; - } - - // Contribution 8 - { - Fr acc = delta_3; - acc = acc * (delta_3 + minus_one); - acc = acc * (delta_3 + minus_two); - acc = acc * (delta_3 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[10] = acc; - } - - // Contribution 9 - { - Fr acc = delta_4; - acc = acc * (delta_4 + minus_one); - acc = acc * (delta_4 + minus_two); - acc = acc * (delta_4 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[11] = acc; - } - } - - function accumulateEllipticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - EllipticParams memory ep; - ep.x_1 = wire(p, WIRE.W_R); - ep.y_1 = wire(p, WIRE.W_O); - - ep.x_2 = wire(p, WIRE.W_L_SHIFT); - ep.y_2 = wire(p, WIRE.W_4_SHIFT); - ep.y_3 = wire(p, WIRE.W_O_SHIFT); - ep.x_3 = wire(p, WIRE.W_R_SHIFT); - - Fr q_sign = wire(p, WIRE.Q_L); - Fr q_is_double = wire(p, WIRE.Q_M); - - // Contribution 10 point addition, x-coordinate check - // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 - Fr x_diff = (ep.x_2 - ep.x_1); - Fr y1_sqr = (ep.y_1 * ep.y_1); - { - // Move to top - Fr partialEval = domainSep; - - Fr y2_sqr = (ep.y_2 * ep.y_2); - Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; - Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); - x_add_identity = x_add_identity * x_diff * x_diff; - x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; - - evals[12] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 11 point addition, x-coordinate check - // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 - { - Fr y1_plus_y3 = ep.y_1 + ep.y_3; - Fr y_diff = ep.y_2 * q_sign - ep.y_1; - Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; - evals[13] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 10 point doubling, x-coordinate check - // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 - // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 - { - Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; - Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; - y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; - Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); - - // NOTE: pushed into memory (stack >:'( ) - ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; - - Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - evals[12] = evals[12] + acc; - } - - // Contribution 11 point doubling, y-coordinate check - // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 - { - Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; - Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); - evals[13] = evals[13] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - } - } - - function accumulateMemoryRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - MemParams memory ap; - - // Compute eta powers locally - Fr eta_two = rp.eta * rp.eta; - Fr eta_three = eta_two * rp.eta; - - /** - * MEMORY - * - * A RAM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) - * * v: `value` of memory cell being accessed - * * a: `access` type of record. read: 0 = read, 1 = write - * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three - * - * A ROM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three - * - * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + - * selectors, depending on whether the gate is a RAM read/write or a ROM read - * - * | gate type | i | v2/t | v | a | r | - * | --------- | -- | ----- | -- | -- | -- | - * | ROM | w1 | w2 | w3 | -- | w4 | - * | RAM | w1 | w2 | w3 | qc | w4 | - * - * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on - * `w2` to fix its value) - * - * - */ - - /** - * Memory Record Check - * Partial degree: 1 - * Total degree: 4 - * - * A ROM/ROM access gate can be evaluated with the identity: - * - * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 - * - * For ROM gates, qc = 0 - */ - ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); - ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); - ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 - ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); - - /** - * Contribution 13 & 14 - * ROM Consistency Check - * Partial degree: 1 - * Total degree: 4 - * - * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of - * records that are sorted. - * - * We apply the following checks for the sorted records: - * - * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 - * 2. index values for adjacent records are monotonically increasing - * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} - * - */ - ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); - ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); - - ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 - - ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 - - evals[15] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - evals[16] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - - ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 - - /** - * Contributions 15,16,17 - * RAM Consistency Check - * - * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` - * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. - * This is validated by requiring `access` to be boolean - * - * For two adjacent entries in the sorted list if _both_ - * A) index values match - * B) adjacent access value is 0 (i.e. next gate is a READ) - * then - * C) both values must match. - * The gate boolean check is - * (A && B) => C === !(A && B) || C === !A || !B || C - * - * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized - * with a WRITE operation. - */ - Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 - ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 - - // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta - // deg 1 or 4 - ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); - ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; - - Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); - ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = - (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 - - // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the - // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't - // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access - // type is correct, to cover this edge case - // deg 2 or 4 - ap.next_gate_access_type_is_boolean = - ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; - - // Putting it all together... - evals[17] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation - * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 - evals[18] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 - evals[19] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 - - ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 - - /** - * RAM Timestamp Consistency Check - * - * | w1 | w2 | w3 | w4 | - * | index | timestamp | timestamp_check | -- | - * - * Let delta_index = index_{i + 1} - index_{i} - * - * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i - * Else timestamp_check = 0 - */ - ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); - ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 - - /** - * Complete Contribution 12 - * The complete RAM/ROM memory identity - * Partial degree: - */ - ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 - ap.memory_identity = - ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 - ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 - ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 - - // (deg 3 or 9) + (deg 4) + (deg 3) - ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 - evals[14] = ap.memory_identity; - } - - function accumulateNnfRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - NnfParams memory ap; - - /** - * Contribution 12 - * Non native field arithmetic gate 2 - * deg 4 - * - * _ _ - * / _ _ _ 14 \ - * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | - * \_ _/ - * - * - */ - ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); - ap.non_native_field_gate_2 = - (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); - - ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; - ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); - ap.non_native_field_gate_1 = ap.limb_subproduct; - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); - - ap.non_native_field_gate_3 = ap.limb_subproduct; - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); - - Fr non_native_field_identity = - ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; - non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); - - // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm - // deg 2 - ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); - ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); - - // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm - // deg 2 - ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); - ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); - - Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; - limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 - - ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; - ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); - evals[20] = ap.nnf_identity; - } - - function accumulatePoseidonExternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonExternalParams memory ep; - - ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); - ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); - ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); - - ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; - ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; - ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; - ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; - // matrix mul v = M_E * u with 14 additions - ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 - ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 - ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 - // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 - ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 - // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 - ep.v4 = ep.t1 + ep.t1; - ep.v4 = ep.v4 + ep.v4 + ep.t3; - // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 - ep.v2 = ep.t0 + ep.t0; - ep.v2 = ep.v2 + ep.v2 + ep.t2; - // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 - ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 - ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 - - ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; - evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); - - evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); - - evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); - - evals[24] = evals[24] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - function accumulatePoseidonInternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonInternalParams memory ip; - - Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ - FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), - FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), - FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), - FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) - ]; - - // add round constants - ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - - // apply s-box round - ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; - ip.u2 = wire(p, WIRE.W_R); - ip.u3 = wire(p, WIRE.W_O); - ip.u4 = wire(p, WIRE.W_4); - - // matrix mul with v = M_I * u 4 muls and 7 additions - ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; - - ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; - - ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; - evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); - - ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; - evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); - - ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; - evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); - - ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; - evals[28] = evals[28] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - // Batch subrelation evaluations using precomputed powers of alpha - // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array - function scaleAndBatchSubrelations( - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges - ) internal pure returns (Fr accumulator) { - accumulator = evaluations[0]; - - for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { - accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; - } - } -} - -library CommitmentSchemeLib { - using FrLib for Fr; - - // Avoid stack too deep - struct ShpleminiIntermediates { - Fr unshiftedScalar; - Fr shiftedScalar; - Fr unshiftedScalarNeg; - Fr shiftedScalarNeg; - // Scalar to be multiplied by [1]₁ - Fr constantTermAccumulator; - // Accumulator for powers of rho - Fr batchingChallenge; - // Linear combination of multilinear (sumcheck) evaluations and powers of rho - Fr batchedEvaluation; - Fr[NUM_SMALL_IPA_OPENING_CLAIMS] denominators; - Fr[NUM_SMALL_IPA_OPENING_CLAIMS] batchingScalars; - // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr posInvertedDenominator; - // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr negInvertedDenominator; - // ν^{2i} * 1/(z - r^{2^i}) - Fr scalingFactorPos; - // ν^{2i+1} * 1/(z + r^{2^i}) - Fr scalingFactorNeg; - // Fold_i(r^{2^i}) reconstructed by Verifier - Fr[] foldPosEvaluations; - } - - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 - function computeFoldPosEvaluations( - Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, - Fr batchedEvalAccumulator, - Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, - Fr[] memory geminiEvalChallengePowers, - uint256 logSize - ) internal view returns (Fr[] memory) { - Fr[] memory foldPosEvaluations = new Fr[](logSize); - for (uint256 i = logSize; i > 0; --i) { - Fr challengePower = geminiEvalChallengePowers[i - 1]; - Fr u = sumcheckUChallenges[i - 1]; - - Fr batchedEvalRoundAcc = - ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] - * (challengePower * (ONE - u) - u)); - // Divide by the denominator - batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); - - batchedEvalAccumulator = batchedEvalRoundAcc; - foldPosEvaluations[i - 1] = batchedEvalRoundAcc; - } - return foldPosEvaluations; - } - - function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { - Fr[] memory squares = new Fr[](logN); - squares[0] = r; - for (uint256 i = 1; i < logN; ++i) { - squares[i] = squares[i - 1].sqr(); - } - return squares; - } -} - -uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q - -// Fr utility - -function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { - scalar = FrLib.fromBytes32(bytes32(proofSection)); -} - -// EC Point utilities -function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { - uint256 x = uint256(bytes32(proofSection[0x00:0x20])); - uint256 y = uint256(bytes32(proofSection[0x20:0x40])); - require(x < Q && y < Q, Errors.ValueGeGroupOrder()); - - // (0,0) is the canonical EIP-196 encoding of the identity. It is accepted here - // because polynomial commitments to identically-zero polynomials (e.g. unused - // selector or table polys) are legitimately the identity. On-curve validation - // (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles per EIP-196. - point = Honk.G1Point({x: x, y: y}); -} - -function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { - // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. - if (point.y != 0) { - point.y = Q - point.y; - } - return point; -} - -/** - * Convert the pairing points to G1 points. - * - * The pairing points are serialised as an array of 2 limbs representing two points - * (P0 and P1, used for lhs and rhs of pairing operation). - * - * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. - * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] - * - * @param pairingPoints The pairing points to convert. - * @return lhs P0 point - * @return rhs P1 point - */ -function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) - pure - returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) -{ - // P0 (lhs): x = lo | (hi << 136) - uint256 lhsX = Fr.unwrap(pairingPoints[0]); - lhsX |= Fr.unwrap(pairingPoints[1]) << 136; - - uint256 lhsY = Fr.unwrap(pairingPoints[2]); - lhsY |= Fr.unwrap(pairingPoints[3]) << 136; - - // P1 (rhs): x = lo | (hi << 136) - uint256 rhsX = Fr.unwrap(pairingPoints[4]); - rhsX |= Fr.unwrap(pairingPoints[5]) << 136; - - uint256 rhsY = Fr.unwrap(pairingPoints[6]); - rhsY |= Fr.unwrap(pairingPoints[7]) << 136; - - // Reconstructed coordinates must be < Q to prevent malleability. - // Without this, two different limb encodings could map to the same curve point - // (via mulmod reduction in on-curve checks) but produce different transcript hashes. - require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); - - lhs.x = lhsX; - lhs.y = lhsY; - rhs.x = rhsX; - rhs.y = rhsY; -} - -/** - * Hash the pairing inputs from the present verification context with those extracted from the public inputs. - * - * @param proofPairingPoints Pairing points from the proof - (public inputs). - * @param accLhs Accumulator point for the left side - result of shplemini. - * @param accRhs Accumulator point for the right side - result of shplemini. - * @return recursionSeparator The recursion separator - generated from hashing the above. - */ -function generateRecursionSeparator( - Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, - Honk.G1Point memory accLhs, - Honk.G1Point memory accRhs -) pure returns (Fr recursionSeparator) { - // hash the proof aggregated X - // hash the proof aggregated Y - // hash the accum X - // hash the accum Y - - (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); - - uint256[8] memory recursionSeparatorElements; - - // Proof points - recursionSeparatorElements[0] = proofLhs.x; - recursionSeparatorElements[1] = proofLhs.y; - recursionSeparatorElements[2] = proofRhs.x; - recursionSeparatorElements[3] = proofRhs.y; - - // Accumulator points - recursionSeparatorElements[4] = accLhs.x; - recursionSeparatorElements[5] = accLhs.y; - recursionSeparatorElements[6] = accRhs.x; - recursionSeparatorElements[7] = accRhs.y; - - recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); -} - -/** - * G1 Mul with Separator - * Using the ecAdd and ecMul precompiles - * - * @param basePoint The point to multiply. - * @param other The other point to add. - * @param recursionSeperator The separator to use for the multiplication. - * @return `(recursionSeperator * basePoint) + other`. - */ -function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) - view - returns (Honk.G1Point memory) -{ - Honk.G1Point memory result; - - result = ecMul(recursionSeperator, basePoint); - result = ecAdd(result, other); - - return result; -} - -/** - * G1 Mul - * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. - * - * @param value The value to multiply the point by. - * @param point The point to multiply. - * @return result The result of the multiplication. - */ -function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write the point into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | point.x - // free + 0x20| point.y - mstore(free, mload(point)) - mstore(add(free, 0x20), mload(add(point, 0x20))) - // Write the scalar into memory (one 32 byte word) - // Memory layout: - // Address | value - // free + 0x40| value - mstore(add(free, 0x40), value) - - // Call the ecMul precompile, it takes in the following - // [point.x, point.y, scalar], and returns the result back into the free memory location. - let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) - if iszero(success) { - revert(0, 0) - } - // Copy the result of the multiplication back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x60)) - } - - return result; -} - -/** - * G1 Add - * Takes two G1 points and uses the ecAdd precompile to return the result. - * - * @param lhs The left hand side of the addition. - * @param rhs The right hand side of the addition. - * @return result The result of the addition. - */ -function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write lhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | lhs.x - // free + 0x20| lhs.y - mstore(free, mload(lhs)) - mstore(add(free, 0x20), mload(add(lhs, 0x20))) - - // Write rhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free + 0x40| rhs.x - // free + 0x60| rhs.y - mstore(add(free, 0x40), mload(rhs)) - mstore(add(free, 0x60), mload(add(rhs, 0x20))) - - // Call the ecAdd precompile, it takes in the following - // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. - let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) - if iszero(success) { revert(0, 0) } - - // Copy the result of the addition back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x80)) - } - - return result; -} - -function rejectPointAtInfinity(Honk.G1Point memory point) pure { - require((point.x | point.y) != 0, Errors.PointAtInfinity()); -} - -/** - * Check if pairing point limbs are all zero (default/infinity). - * Default pairing points indicate no recursive verification occurred. - */ -function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { - uint256 acc = 0; - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - acc |= Fr.unwrap(pairingPoints[i]); - } - return acc == 0; -} - -function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { - bytes memory input = abi.encodePacked( - rhs.x, - rhs.y, - // Fixed G2 point - uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), - uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), - uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), - uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), - lhs.x, - lhs.y, - // G2 point from VK - uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), - uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), - uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), - uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) - ); - - (bool success, bytes memory result) = address(0x08).staticcall(input); - decodedResult = success && abi.decode(result, (bool)); -} - -abstract contract BaseZKHonkVerifier is IVerifier { - using FrLib for Fr; - - struct PairingInputs { - Honk.G1Point P_0; - Honk.G1Point P_1; - } - - struct SmallSubgroupIpaIntermediates { - Fr[SUBGROUP_SIZE] challengePolyLagrange; - Fr challengePolyEval; - Fr lagrangeFirst; - Fr lagrangeLast; - Fr rootPower; - Fr[SUBGROUP_SIZE] denominators; // this has to disappear - Fr diff; - } - - // Constants for proof length calculation (matching UltraKeccakZKFlavor) - uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; - uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points - uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements - uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations - - uint256 internal constant LIBRA_COMMITMENTS = 3; - uint256 internal constant LIBRA_EVALUATIONS = 4; - uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; - - uint256 internal constant SHIFTED_COMMITMENTS_START = 30; - uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; - - uint256 internal immutable $N; - uint256 internal immutable $LOG_N; - uint256 internal immutable $VK_HASH; - uint256 internal immutable $NUM_PUBLIC_INPUTS; - uint256 internal immutable $MSMSize; - - constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { - $N = _N; - $LOG_N = _logN; - $VK_HASH = _vkHash; - $NUM_PUBLIC_INPUTS = _numPublicInputs; - $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; - } - - function verify(bytes calldata proof, bytes32[] calldata publicInputs) - public - view - override - returns (bool verified) - { - // Calculate expected proof size based on $LOG_N - uint256 expectedProofSize = calculateProofSize($LOG_N); - - // Check the received proof is the expected size where each field element is 32 bytes - require( - proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) - ); - - Honk.VerificationKey memory vk = loadVerificationKey(); - Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); - - require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); - - // Generate the fiat shamir challenges for the whole protocol - ZKTranscript memory t = - ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); - - // Derive public input delta - t.relationParameters.publicInputsDelta = computePublicInputDelta( - publicInputs, - p.pairingPointObject, - t.relationParameters.beta, - t.relationParameters.gamma, - 5 // pubInputsOffset = NUM_DISABLED_ROWS_IN_SUMCHECK + NUM_ZERO_ROWS = 4 + 1 - ); - - // Sumcheck - require(verifySumcheck(p, t), Errors.SumcheckFailed()); - require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); - - verified = true; - } - - function computePublicInputDelta( - bytes32[] memory publicInputs, - Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, - Fr beta, - Fr gamma, - uint256 offset - ) internal view returns (Fr publicInputDelta) { - Fr numerator = Fr.wrap(1); - Fr denominator = Fr.wrap(1); - - Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); - Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); - - { - for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { - Fr pubInput = FrLib.fromBytes32(publicInputs[i]); - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - Fr pubInput = pairingPointObject[i]; - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - } - - // Fr delta = numerator / denominator; // TOOO: batch invert later? - publicInputDelta = FrLib.div(numerator, denominator); - } - - function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { - Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 - Fr powPartialEvaluation = Fr.wrap(1); - - // We perform sumcheck reductions over log n rounds ( the multivariate degree ) - for (uint256 round; round < $LOG_N; ++round) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; - Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; - require(totalSum == roundTargetSum, Errors.SumcheckFailed()); - - Fr roundChallenge = tp.sumCheckUChallenges[round]; - - // Update the round target for the next rounf - roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); - powPartialEvaluation = - powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); - } - - // Last round - // For ZK flavors: sumcheckEvaluations has 42 elements - // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations - Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; - for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { - relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 - } - Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( - relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation - ); - - // Row-disabling polynomial: 1 - ∏_{i≥2}(1 - u_i) - Fr evaluation = Fr.wrap(1); - for (uint256 i = 2; i < $LOG_N; i++) { - evaluation = evaluation * (Fr.wrap(1) - tp.sumCheckUChallenges[i]); - } - - grandHonkRelationSum = - grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; - verified = (grandHonkRelationSum == roundTargetSum); - } - - // Return the new target sum for the next sumcheck round - function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) - internal - view - returns (Fr targetSum) - { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) - ]; - - // To compute the next target sum, we evaluate the given univariate at a point u (challenge). - - // Performing Barycentric evaluations - // Compute B(x) - Fr numeratorValue = Fr.wrap(1); - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); - } - - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); - } - - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; - } - - // Scale the sum by the value of B(x) - targetSum = targetSum * numeratorValue; - } - - function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) - internal - view - returns (bool verified) - { - CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack - - // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size - Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); - // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings - Fr[] memory scalars = new Fr[]($MSMSize); - Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); - - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); - - mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); - mem.shiftedScalar = - tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); - - scalars[0] = Fr.wrap(1); - commitments[0] = proof.shplonkQ; - - /* Batch multivariate opening claims, shifted and unshifted - * The vector of scalars is populated as follows: - * \f[ - * \left( - * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) - * \right) - * \f] - * - * The following vector is concatenated to the vector of commitments: - * \f[ - * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} - * \f] - * - * Simultaneously, the evaluation of the multilinear polynomial - * \f[ - * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} - * \f] - * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. - * - * This approach minimizes the number of iterations over the commitments to multilinear polynomials - * and eliminates the need to store the powers of \f$ \rho \f$. - */ - // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] - // Start batching challenge at 1, not rho, to match non-ZK pattern - mem.batchingChallenge = Fr.wrap(1); - mem.batchedEvaluation = Fr.wrap(0); - - mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); - mem.shiftedScalarNeg = mem.shiftedScalar.neg(); - - // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) - for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { - scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; - mem.batchedEvaluation = mem.batchedEvaluation - + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - // g commitments are accumulated at r - // For each of the to be shifted commitments perform the shift in place by - // adding to the unshifted value. - // We do so, as the values are to be used in batchMul later, and as - // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. - // Applied to w1, w2, w3, w4 and zPerm - for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { - uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; - uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; - - scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); - mem.batchedEvaluation = - mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - - commitments[1] = proof.geminiMaskingPoly; - - commitments[2] = vk.s1; - commitments[3] = vk.s2; - commitments[4] = vk.s3; - commitments[5] = vk.s4; - commitments[6] = vk.id1; - commitments[7] = vk.id2; - commitments[8] = vk.id3; - commitments[9] = vk.id4; - commitments[10] = vk.lagrangeFirst; - commitments[11] = vk.lagrangeLast; - commitments[12] = vk.qLookup; - commitments[13] = vk.t1; - commitments[14] = vk.t2; - commitments[15] = vk.t3; - commitments[16] = vk.t4; - commitments[17] = vk.qm; - commitments[18] = vk.qr; - commitments[19] = vk.qo; - commitments[20] = vk.qc; - commitments[21] = vk.ql; - commitments[22] = vk.q4; - commitments[23] = vk.qArith; - commitments[24] = vk.qDeltaRange; - commitments[25] = vk.qElliptic; - commitments[26] = vk.qMemory; - commitments[27] = vk.qNnf; - commitments[28] = vk.qPoseidon2External; - commitments[29] = vk.qPoseidon2Internal; - - // Accumulate proof points - commitments[30] = proof.w1; - commitments[31] = proof.w2; - commitments[32] = proof.w3; - commitments[33] = proof.w4; - commitments[34] = proof.zPerm; - commitments[35] = proof.lookupInverses; - commitments[36] = proof.lookupReadCounts; - commitments[37] = proof.lookupReadTags; - - /* Batch gemini claims from the prover - * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from - * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars - * - * 1. Moves the vector - * \f[ - * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) - * \f] - * to the 'commitments' vector. - * - * 2. Computes the scalars: - * \f[ - * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} - * \f] - * and places them into the 'scalars' vector. - * - * 3. Accumulates the summands of the constant term: - * \f[ - * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} - * \f] - * and adds them to the 'constant_term_accumulator'. - */ - - // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 - Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( - tp.sumCheckUChallenges, - mem.batchedEvaluation, - proof.geminiAEvaluations, - powers_of_evaluation_challenge, - $LOG_N - ); - - mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; - mem.constantTermAccumulator = - mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); - - mem.batchingChallenge = tp.shplonkNu.sqr(); - uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; - - // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; - // Compute scalar multipliers for each fold commitment - for (uint256 i = 0; i < $LOG_N - 1; ++i) { - bool dummy_round = i >= ($LOG_N - 1); - - if (!dummy_round) { - // Update inverted denominators - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); - - // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] - mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; - mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; - scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); - - // Accumulate the const term contribution given by - // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) - Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; - accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; - mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; - } - // Update the running power of v - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; - - commitments[boundary + i] = proof.geminiFoldComms[i]; - } - - boundary += $LOG_N - 1; - - // Denominators 1/(z - point_i) for the five opening points {r, g*r, r, 1, r}. - mem.denominators[0] = ONE.div(tp.shplonkZ - tp.geminiR); - mem.denominators[1] = ONE.div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); - mem.denominators[2] = mem.denominators[0]; - mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] = ONE.div(tp.shplonkZ - ONE); - mem.denominators[NUM_SMALL_IPA_OPENING_CLAIMS - 1] = mem.denominators[0]; - - // Iterate the opening claims in three segments — the inner loops can't be merged without an extra induction - // variable, which pushes us into stack-too-deep. - for (uint256 i = 0; i < SMALL_IPA_BOUNDARY_OPENING_IDX; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; - } - - // Boundary slot: claimed value is hardcoded 0, so no constantTermAccumulator contribution. - { - Fr scalingFactor = mem.denominators[SMALL_IPA_BOUNDARY_OPENING_IDX] * mem.batchingChallenge; - mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - } - - for (uint256 i = SMALL_IPA_BOUNDARY_OPENING_IDX + 1; i < NUM_SMALL_IPA_OPENING_CLAIMS; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i - 1]; - } - - // Group per-claim batching scalars by commitment: [G], [A] (three openings), [Q]. - scalars[boundary] = mem.batchingScalars[0]; - scalars[boundary + 1] = - mem.batchingScalars[1] + mem.batchingScalars[2] + mem.batchingScalars[SMALL_IPA_BOUNDARY_OPENING_IDX]; - scalars[boundary + 2] = mem.batchingScalars[NUM_SMALL_IPA_OPENING_CLAIMS - 1]; - - for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { - commitments[boundary++] = proof.libraCommitments[i]; - } - - commitments[boundary] = Honk.G1Point({x: 1, y: 2}); - scalars[boundary++] = mem.constantTermAccumulator; - - require( - checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), - Errors.ConsistencyCheckFailed() - ); - - Honk.G1Point memory quotient_commitment = proof.kzgQuotient; - - commitments[boundary] = quotient_commitment; - scalars[boundary] = tp.shplonkZ; // evaluation challenge - - PairingInputs memory pair; - pair.P_0 = batchMul(commitments, scalars); - pair.P_1 = negateInplace(quotient_commitment); - - // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) - if (!arePairingPointsDefault(proof.pairingPointObject)) { - Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); - (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = - convertPairingPointsToG1(proof.pairingPointObject); - - // Validate the points from the proof are on the curve - rejectPointAtInfinity(P_0_other); - rejectPointAtInfinity(P_1_other); - - // accumulate with aggregate points in proof - pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); - pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); - } - - return pairing(pair.P_0, pair.P_1); - } - - function checkEvalsConsistency( - Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, - Fr geminiR, - Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, - Fr libraEval - ) internal view returns (bool check) { - Fr one = Fr.wrap(1); - Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; - require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); - - SmallSubgroupIpaIntermediates memory mem; - mem.challengePolyLagrange[0] = one; - for (uint256 round = 0; round < $LOG_N; round++) { - uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; - mem.challengePolyLagrange[currIdx] = one; - for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { - mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; - } - } - - mem.rootPower = one; - mem.challengePolyEval = Fr.wrap(0); - for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { - mem.denominators[idx] = mem.rootPower * geminiR - one; - mem.denominators[idx] = mem.denominators[idx].invert(); - mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; - mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; - } - - Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); - mem.challengePolyEval = mem.challengePolyEval * numerator; - mem.lagrangeFirst = mem.denominators[0] * numerator; - mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; - - mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; - - mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) - * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); - mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; - - check = mem.diff == Fr.wrap(0); - } - - // This implementation is the same as above with different constants - function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) - internal - view - returns (Honk.G1Point memory result) - { - uint256 limit = $MSMSize; - - // Identity bases are accepted: VK selector/table polys may be identically zero, - // and the ecAdd/ecMul precompiles treat (0,0) as the additive identity per EIP-196. - // Soundness against an attacker substituting (0,0) for a non-zero commitment is - // upheld by sumcheck/Shplemini, which would fail on inconsistent evaluations. - - bool success = true; - assembly ("memory-safe") { - let free := mload(0x40) - - let count := 0x01 - for {} lt(count, add(limit, 1)) { count := add(count, 1) } { - // Get loop offsets - let base_base := add(base, mul(count, 0x20)) - let scalar_base := add(scalars, mul(count, 0x20)) - - mstore(add(free, 0x40), mload(mload(base_base))) - mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) - // Add scalar - mstore(add(free, 0x80), mload(scalar_base)) - - success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) - // accumulator = accumulator + accumulator_2 - success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) - } - - // Return the result - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - } - - require(success, Errors.ShpleminiFailed()); - } - - // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) - function calculateProofSize(uint256 logN) internal pure returns (uint256) { - // Witness and Libra commitments - uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments - proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking - - // Sumcheck - proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates - proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations - - // Libra and Gemini - proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval - proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations - proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations - - // PCS commitments - proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments - proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments - - // Pairing points - proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs - - return proofLength * 32; - } - - function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); -} - -contract OidcGoogleHonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { - function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { - return HonkVerificationKey.loadVerificationKey(); - } -} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 17e7673..0d3e71c 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -49,10 +49,10 @@ fs_permissions = [ # instead, so the lint stays on for the next one. [lint] # The vendored Honk verifiers are bb's output, not ours: the notes they draw -# (naming, unchecked casts) would be fixed in Aztec's generator, and a fix -# made here is undone by the next `scripts/vendor-circuit-verifiers.sh` -# run, whose `--check` in CI would refuse the edit first. They still compile -# under `-D` like everything else; only the linter skips them. +# (naming, unchecked casts) would be fixed in Aztec's generator, and the +# files are not committed: `scripts/vendor-circuit-verifiers.sh` writes them +# before every build, so a fix made here does not outlive the next run. They +# still compile under `-D` like everything else; only the linter skips them. ignore = ["contracts/circuits/*HonkVerifier.sol"] # Foundry's default stops at `low`, which leaves the style lints off: naming, # named struct fields, unused and unaliased imports. They are on. `gas` and