diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..983e89ba0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' + +[target.wasm32-wasip2] +runner = 'wasmtime' diff --git a/.github/scripts/wasm-target-test-build.sh b/.github/scripts/wasm-target-test-build.sh deleted file mode 100644 index 3c42427cd..000000000 --- a/.github/scripts/wasm-target-test-build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -GIT_ROOT=$(pwd) - -cd /tmp - -# create test project -cargo new foobar -cd foobar - -# set rust-toolchain same as "sonobe" -cp "${GIT_ROOT}/rust-toolchain" . - -# add wasm32-* targets -rustup target add wasm32-unknown-unknown wasm32-wasip1 - -# add dependencies -cargo add --path "${GIT_ROOT}/frontends" --features wasm, parallel -cargo add --path "${GIT_ROOT}/folding-schemes" --features parallel -cargo add getrandom --features wasm_js --target wasm32-unknown-unknown - -# test build for wasm32-* targets -cargo build --release --target wasm32-unknown-unknown -cargo build --release --target wasm32-wasip1 -# Emscripten would require to fetch the `emcc` tooling. Hence we don't build the lib as a dep for it. -# cargo build --release --target wasm32-unknown-emscripten - -# delete test project -cd ../ -rm -rf foobar diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4b70d0cc..9ca52f736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ name: CI Check on: + workflow_dispatch: merge_group: pull_request: push: @@ -36,44 +37,45 @@ concurrency: jobs: test: if: github.event.pull_request.draft == false - name: Test + name: Test ${{ matrix.target }} (${{ matrix.features }}) runs-on: ubuntu-latest strategy: matrix: - feature_set: [basic] include: - - feature_set: basic - features: --features default,light-test + # x64: both parallel and no-parallel + - target: x86_64-unknown-linux-gnu + features: parallel + args: "--features parallel" + - target: x86_64-unknown-linux-gnu + features: no-parallel + args: "" + # wasm: no-parallel only + - target: wasm32-unknown-unknown + features: no-parallel + args: "" + - target: wasm32-wasip2 + features: no-parallel + args: "" steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: noir-lang/noirup@v0.1.3 - with: - toolchain: 0.36.0 - - name: Download Circom - run: | - mkdir -p $HOME/bin - curl -sSfL https://github.com/iden3/circom/releases/download/v2.1.6/circom-linux-amd64 -o $HOME/bin/circom - chmod +x $HOME/bin/circom - echo "$HOME/bin" >> $GITHUB_PATH - - name: Download solc - run: | - curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc - chmod +x /usr/local/bin/solc - - name: Execute compile.sh to generate .r1cs and .wasm from .circom - run: ./experimental-frontends/src/circom/test_folder/compile.sh - - name: Execute compile.sh to generate .json from noir - run: ./experimental-frontends/src/noir/test_folder/compile.sh - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --workspace --no-default-features ${{ matrix.features }} - - name: Run Doc-tests - uses: actions-rs/cargo@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - command: test - args: --doc + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + - name: Install wasm-bindgen-cli + if: matrix.target == 'wasm32-unknown-unknown' + run: cargo install wasm-bindgen-cli + - name: Install wasmtime-cli + if: matrix.target == 'wasm32-wasip2' + run: cargo install wasmtime-cli + - name: Test sonobe-primitives + run: cargo test --release -p sonobe-primitives --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test sonobe-fs + run: cargo test --release -p sonobe-fs --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test sonobe-ivc + run: cargo test --release -p sonobe-ivc --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test documentation examples + run: cargo test --doc --target ${{ matrix.target }} ${{ matrix.args }} build: if: github.event.pull_request.draft == false @@ -82,79 +84,21 @@ jobs: strategy: matrix: target: + - x86_64-unknown-linux-gnu - wasm32-unknown-unknown - - wasm32-wasip1 - # Ignoring until clear usage is required - # - wasm32-unknown-emscripten - - steps: - - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - override: false - default: true - - name: Add target - run: rustup target add ${{ matrix.target }} - - name: Wasm-compat experimental-frontends build - uses: actions-rs/cargo@v1 - with: - command: build - args: -p experimental-frontends --no-default-features --target ${{ matrix.target }} --features "wasm, parallel" - - name: Wasm-compat folding-schemes build - uses: actions-rs/cargo@v1 - with: - command: build - args: -p folding-schemes --no-default-features --target ${{ matrix.target }} --features "default,light-test" - - name: Run wasm-compat script - run: | - chmod +x .github/scripts/wasm-target-test-build.sh - .github/scripts/wasm-target-test-build.sh - shell: bash - - examples: - if: github.event.pull_request.draft == false - name: Run examples & examples tests - runs-on: ubuntu-latest + - wasm32-wasip2 steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: noir-lang/noirup@v0.1.3 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: 0.36.0 - - name: Download Circom - run: | - mkdir -p $HOME/bin - curl -sSfL https://github.com/iden3/circom/releases/download/v2.1.6/circom-linux-amd64 -o $HOME/bin/circom - chmod +x $HOME/bin/circom - echo "$HOME/bin" >> $GITHUB_PATH - - name: Download solc - run: | - curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc - chmod +x /usr/local/bin/solc - - name: Execute compile.sh to generate .r1cs and .wasm from .circom - run: ./experimental-frontends/src/circom/test_folder/compile.sh - - name: Execute compile.sh to generate .json from noir - run: ./experimental-frontends/src/noir/test_folder/compile.sh - - name: Run examples tests - run: cargo test --examples - - name: Run examples - run: cargo run --release --example 2>&1 | grep -E '^ ' | xargs -n1 cargo run --release --example - - # run the benchmarks with the flag `--no-run` to ensure that they compile, - # but without executing them. - bench: - if: github.event.pull_request.draft == false - name: Bench compile - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1 - with: - command: bench - args: -p folding-schemes --no-run + - name: Build sonobe-primitives + run: cargo build -p sonobe-primitives --target ${{ matrix.target }} + - name: Build sonobe-fs + run: cargo build -p sonobe-fs --target ${{ matrix.target }} + - name: Build sonobe-ivc + run: cargo build -p sonobe-ivc --target ${{ matrix.target }} fmt: if: github.event.pull_request.draft == false @@ -162,41 +106,33 @@ jobs: timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: Swatinem/rust-cache@v2 - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - command: fmt - args: --all --check + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Run rustfmt + run: cargo fmt --all --check clippy: if: github.event.pull_request.draft == false - name: Clippy lint checks + name: Clippy (${{ matrix.target }}) runs-on: ubuntu-latest strategy: matrix: - feature_set: [basic, wasm] - include: - - feature_set: basic - features: --features default - # We only want to test `experimental-frontends` package with `wasm` feature. - - feature_set: wasm - features: -p experimental-frontends --features wasm,parallel --target wasm32-unknown-unknown + target: + - x86_64-unknown-linux-gnu + - wasm32-unknown-unknown + - wasm32-wasip2 steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: components: clippy + targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - name: Add target - run: rustup target add wasm32-unknown-unknown - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features ${{ matrix.features }} -- -D warnings + run: cargo clippy --workspace --all-targets --target ${{ matrix.target }} -- -D warnings typos: if: github.event.pull_request.draft == false diff --git a/.gitignore b/.gitignore index d3ba383d1..71f19e54b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,6 @@ /target -Cargo.lock -# Circom generated files -experimental-frontends/src/circom/test_folder/*_js/ *.r1cs *.sym - -# Noir generated files -experimental-frontends/src/noir/test_folder/*/target/* - -# generated contracts data -solidity-verifiers/generated -examples/*.sol -examples/*.calldata -examples/*.inputs *.serialized */*.serialized diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..da61a4ad9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2710 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "k256", + "thiserror", +] + +[[package]] +name = "alloy-eip7928" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b12337f74cbfa451cb04dac173974814a6ff463079e1793aa09600ba8813ab" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "once_cell", + "thiserror", +] + +[[package]] +name = "alloy-primitives" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256", + "paste", + "ruint", + "rustc-hash", + "sha3 0.11.0", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "alloy-rlp-derive", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc66f96ebe2a17a499475b4f94791d379817592ef494171586967ffdc6f95db" +dependencies = [ + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-r1cs-std", + "ark-std 0.6.0", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b3409b1846fe459d19c95df039481575ac6d5842ae63858ad75cc31219bfc1" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-r1cs-std", + "ark-relations", + "ark-serialize 0.6.0", + "ark-snark", + "ark-std 0.6.0", + "blake2", + "blake3", + "derivative", + "digest 0.10.7", + "fnv", + "merlin", + "num-bigint", + "rayon", + "sha2", + "tracing", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8352a2b2aedf6ba2cc38f7520fc51191d518dde96175c729af19f2d059f191c4" +dependencies = [ + "ahash", + "ark-ff 0.6.0", + "ark-poly 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "educe", + "fnv", + "hashbrown 0.17.1", + "itertools 0.14.0", + "num-bigint", + "num-integer", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-groth16" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a293328aa422e65527e285614ce5d1dceb0bd7b8b18d18b1b63191ee1f74cb41" +dependencies = [ + "ark-crypto-primitives", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-poly 0.6.0", + "ark-relations", + "ark-serialize 0.6.0", + "ark-snark", + "ark-std 0.6.0", + "rayon", +] + +[[package]] +name = "ark-grumpkin" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c969a12ceed8881e5c66277dabd3e7a5aa2c72c9370f3f42eca3f2dd33a21a" +dependencies = [ + "ark-bn254 0.6.0", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-r1cs-std", + "ark-std 0.6.0", +] + +[[package]] +name = "ark-pallas" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26dbfc18163d3313389ce5d515aa675bb694b8ef64744006c5a2c88439c7133" +dependencies = [ + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-r1cs-std", + "ark-std 0.6.0", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-poly" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f55af10b672002b8d953e230282c51206842e20e5791a94432219b4201de5c" +dependencies = [ + "ahash", + "ark-ff 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "educe", + "fnv", + "hashbrown 0.17.1", + "rayon", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291f1c6628bfcac79b0dc2adbe401aa9100e2e96daa971645e0b18fc94de9a98" +dependencies = [ + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-relations", + "ark-std 0.6.0", + "educe", + "itertools 0.14.0", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4c11c797a64b8a23e22bf4e77bf582ac27bb21395e3183a9a506ba2561e9f9" +dependencies = [ + "ark-ff 0.6.0", + "ark-poly 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "foldhash 0.1.5", + "indexmap 2.14.0", + "rayon", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "rayon", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-snark" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdb461d2be9b2bd6f303c79fffc89f5858790a7b4d33257bca3178e2c071fb9" +dependencies = [ + "ark-ff 0.6.0", + "ark-relations", + "ark-serialize 0.6.0", + "ark-std 0.6.0", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "colored", + "num-traits", + "rand 0.8.6", + "rayon", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "askama" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" +dependencies = [ + "askama_parser", + "basic-toml", + "glob", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.118", +] + +[[package]] +name = "askama_macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" +dependencies = [ + "rustc-hash", + "serde", + "serde_derive", + "unicode-ident", + "winnow", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "aurora-engine-modexp" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak 0.1.6", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "revm" +version = "40.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823da6e5509bb8e5dcd91295870e494917a030ad506fc83301f3f08ad8b15b17" +dependencies = [ + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database", + "revm-database-interface", + "revm-handler", + "revm-inspector", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-bytecode" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b378c2653331fe60969d9745e802cd773d82a20d8aaced914dfcf26ab8f0d9" +dependencies = [ + "bitvec", + "revm-primitives", +] + +[[package]] +name = "revm-context" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafa298114f3cab706945de14c04e73e6e6d7896302e4183dae273f968e52f80" +dependencies = [ + "bitvec", + "cfg-if", + "derive-where", + "revm-bytecode", + "revm-context-interface", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-context-interface" +version = "19.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9c13f1dfc79425931fd184b6bd373dfac7baba50859b01107d5c0e20549cbb" +dependencies = [ + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-database" +version = "15.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69c3ce73454a09ef89a66177239d7c4f5f697227ae27254c99451866603b19d" +dependencies = [ + "derive_more", + "revm-bytecode", + "revm-database-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-database-interface" +version = "12.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2656187f9f9c22ef9dd9300ed71aeaeca3506a6a0a229a07f264649b960d68" +dependencies = [ + "auto_impl", + "either", + "revm-primitives", + "revm-state", + "thiserror", +] + +[[package]] +name = "revm-handler" +version = "20.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce1d66037ca1394128313bb995fa9f50d834927a389386bb34f8f0ef914648f" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database-interface", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-inspector" +version = "21.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe3635d3411e8318849546570ca0220e783443319e28f5397c9f80b05bf4344" +dependencies = [ + "auto_impl", + "either", + "revm-context", + "revm-database-interface", + "revm-handler", + "revm-interpreter", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-interpreter" +version = "37.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae56c57ddca1f5c4abd443f826f1b3e49a86a528e7e1ea0fc207cdc4671a37e" +dependencies = [ + "revm-bytecode", + "revm-context-interface", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-precompile" +version = "36.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191db865091e07ecb80b12ce3048192c76071ca3d2b0a315b111b271cd4ced37" +dependencies = [ + "ark-bls12-381", + "ark-bn254 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "cfg-if", + "k256", + "p256", + "revm-context-interface", + "revm-primitives", + "ripemd", + "sha2", +] + +[[package]] +name = "revm-primitives" +version = "24.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5102d804892908d4ebf68da29b8562895922dffa26c230ff2c4dadcf93916f" +dependencies = [ + "alloy-primitives", + "once_cell", +] + +[[package]] +name = "revm-state" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eff6067185cf80932e06f6a9c8045b012ecb6f99a8d6edc618ec2792373e14" +dependencies = [ + "alloy-eip7928", + "bitflags", + "nonmax", + "revm-bytecode", + "revm-primitives", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ruint" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" +dependencies = [ + "alloy-rlp", + "proptest", + "rand 0.8.6", + "rand 0.9.4", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "time", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sonobe-fs" +version = "0.1.0" +dependencies = [ + "ark-bn254 0.6.0", + "ark-crypto-primitives", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-pallas", + "ark-poly 0.6.0", + "ark-r1cs-std", + "ark-relations", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "getrandom", + "itertools 0.14.0", + "num-bigint", + "rayon", + "sonobe-primitives", + "thiserror", + "wasm-bindgen-test", +] + +[[package]] +name = "sonobe-ivc" +version = "0.1.0" +dependencies = [ + "ark-bn254 0.6.0", + "ark-crypto-primitives", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-grumpkin", + "ark-r1cs-std", + "ark-relations", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "askama", + "getrandom", + "num-bigint", + "sha3 0.10.9", + "sonobe-fs", + "sonobe-primitives", + "sonobe-snarks", + "thiserror", + "wasm-bindgen-test", +] + +[[package]] +name = "sonobe-primitives" +version = "0.1.0" +dependencies = [ + "ark-bn254 0.6.0", + "ark-crypto-primitives", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-grumpkin", + "ark-pallas", + "ark-poly 0.6.0", + "ark-r1cs-std", + "ark-relations", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "getrandom", + "hashbrown 0.17.1", + "itertools 0.14.0", + "num-bigint", + "num-integer", + "num-traits", + "rayon", + "revm", + "serde", + "serde_json", + "sha3 0.10.9", + "thiserror", + "wasm-bindgen-test", +] + +[[package]] +name = "sonobe-snarks" +version = "0.1.0" +dependencies = [ + "ark-bn254 0.6.0", + "ark-ec 0.6.0", + "ark-ff 0.6.0", + "ark-groth16", + "ark-grumpkin", + "ark-poly 0.6.0", + "ark-relations", + "ark-serialize 0.6.0", + "ark-snark", + "ark-std 0.6.0", + "askama", + "getrandom", + "hashbrown 0.17.1", + "itertools 0.14.0", + "rayon", + "sonobe-primitives", + "thiserror", + "wasm-bindgen-test", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index f7f00d21a..abfdacf5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,91 +1,53 @@ [workspace] members = [ - "folding-schemes", - "solidity-verifiers", - "cli", - "experimental-frontends", + "crates/primitives", + "crates/fs", + "crates/ivc", + "crates/snarks", ] resolver = "2" - -[patch.crates-io] -# Update ark-groth16 to latest git version -ark-groth16 = { git = "https://github.com/arkworks-rs/groth16", rev = "b3b4a15" } - -# Required dependencies for latest ark-groth16 -ark-ff = { git = "https://github.com/arkworks-rs/algebra" } -ark-ec = { git = "https://github.com/arkworks-rs/algebra" } -ark-serialize = { git = "https://github.com/arkworks-rs/algebra" } -ark-poly = { git = "https://github.com/arkworks-rs/algebra" } -ark-relations = { git = "https://github.com/arkworks-rs/snark" } -ark-snark = { git = "https://github.com/arkworks-rs/snark" } -ark-crypto-primitives = { git = "https://github.com/flyingnobita/crypto-primitives", rev = "f559264" } -ark-r1cs-std = { git = "https://github.com/flyingnobita/r1cs-std_yelhousni", rev = "b4bab0c" } # "perf/sw-updated" branch -ark-std = { git = "https://github.com/arkworks-rs/std" } -ark-poly-commit = { git = "https://github.com/arkworks-rs/poly-commit" } - - -# Curve crates also need git versions -ark-bn254 = { git = "https://github.com/arkworks-rs/algebra" } -ark-grumpkin = { git = "https://github.com/arkworks-rs/algebra" } -ark-pallas = { git = "https://github.com/arkworks-rs/algebra" } -ark-vesta = { git = "https://github.com/arkworks-rs/algebra" } -ark-mnt4-298 = { git = "https://github.com/arkworks-rs/algebra" } -ark-mnt6-298 = { git = "https://github.com/arkworks-rs/algebra" } - -[patch."https://github.com/arkworks-rs/circom-compat"] -ark-circom = { git = "https://github.com/dmpierre/circom-compat", rev = "0dfd773c" } - [workspace.package] -edition = "2021" +edition = "2024" license = "MIT" repository = "https://github.com/privacy-scaling-explorations/sonobe/" +rust-version = "1.85.1" [workspace.dependencies] -acvm = { git = "https://github.com/winderica/noir", rev = "fc9e99", default-features = false } # "arkworks-next" branch -askama = { version = "0.12.0", default-features = false } -clap = { version = "4.4" } -clap-verbosity-flag = { version = "2.1" } -criterion = { version = "0.5" } -env_logger = { version = "0.10" } -getrandom = { version = "0.2" } -log = { version = "0.4" } -noname = { git = "https://github.com/dmpierre/noname", rev = "c34f17" } +askama = { version = "0.16.0" } +hashbrown = { version = "0.17" } +itertools = { version = "0.14.0" } num-bigint = { version = "0.4.3" } num-integer = { version = "0.1" } -pprof = { version = "0.13" } -serde = { version = "^1.0.0" } -serde_json = { version = "^1.0.0" } +num-traits = { version = "0.2" } sha3 = { version = "0.10" } -rand = { version = "0.8.5" } rayon = { version = "1" } -revm = { version = "19.5.0", default-features = false } -rust-crypto = { version = "0.2" } -thiserror = { version = "1.0" } -tokio = "1.44.1" -wasmer = { version = "6.1.0-rc.2", default-features = false } +revm = { version = "40.0.3", default-features = false } +serde = { version = "1.0" } +serde_json = { version = "1.0" } +thiserror = { version = "2.0.16" } +wasm-bindgen-test = { version = "0.3" } # Arkworks family -ark-bn254 = { version = "^0.5.0", default-features = false } -ark-circom = { git = "https://github.com/arkworks-rs/circom-compat", default-features = false } -ark-crypto-primitives = { version = "^0.5.0", default-features = false } -ark-ec = { version = "^0.5.0", default-features = false } -ark-ff = { version = "^0.5.0", default-features = false } -ark-groth16 = { version = "^0.5.0" } -ark-grumpkin = { version = "^0.5.0", default-features = false } -ark-mnt4-298 = { version = "^0.5.0" } -ark-mnt6-298 = { version = "^0.5.0" } -ark-pallas = { version = "^0.5.0" } -ark-poly = { version = "^0.5.0", default-features = false } -ark-poly-commit = { version = "^0.5.0" } -ark-r1cs-std = { version = "^0.5.0", default-features = false } -ark-relations = { version = "^0.5.0", default-features = false } -ark-serialize = { version = "^0.5.0" } -ark-snark = { version = "^0.5.0", default-features = false } -ark-std = { version = "^0.5.0", default-features = false } -ark-vesta = { version = "^0.5.0" } +ark-crypto-primitives = { version = "0.6.0", default-features = false } +ark-ec = { version = "0.6.0", default-features = false } +ark-ff = { version = "0.6.0", default-features = false } +ark-groth16 = { version = "0.6.0", default-features = false } +ark-poly = { version = "0.6.0", default-features = false } +ark-r1cs-std = { version = "0.6.0", default-features = false } +ark-relations = { version = "0.6.0", default-features = false } +ark-serialize = { version = "0.6.0", default-features = false } +ark-snark = { version = "0.6.0", default-features = false } +ark-std = { version = "0.6.0", default-features = false } + +# Ark curves +ark-bn254 = { version = "0.6.0", default-features = false } +ark-grumpkin = { version = "0.6.0", default-features = false } +ark-pallas = { version = "0.6.0", default-features = false } +ark-vesta = { version = "0.6.0", default-features = false } # Local crates -experimental-frontends = { path = "experimental-frontends" } -folding-schemes = { path = "folding-schemes" } -solidity-verifiers = { path = "solidity-verifiers" } +sonobe-primitives = { path = "crates/primitives", default-features = false } +sonobe-fs = { path = "crates/fs", default-features = false } +sonobe-ivc = { path = "crates/ivc", default-features = false } +sonobe-snarks = { path = "crates/snarks", default-features = false } diff --git a/README.md b/README.md index bb9ee6f10..b7d7564e3 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,40 @@ -# sonobe +# Sonobe -Experimental folding schemes library implemented jointly by [0xPARC](https://0xparc.org/) and [PSE](https://pse.dev). +Experimental folding/accumulation schemes library implemented jointly by [0xPARC](https://0xparc.org/) and [PSE](https://pse.dev). - + -Sonobe is a modular library to fold arithmetic circuit instances in an Incremental Verifiable computation (IVC) style. It features multiple folding schemes and decider setups, allowing users to pick the scheme which best fits their needs. -

-Sonobe is conceived as an exploratory effort with the aim to push forward the practical side of folding schemes and advancing towards onchain (EVM) verification. -

-"The Sonobe module is one of the many units used to build modular origami. The popularity of Sonobe modular origami models derives from the simplicity of folding the modules, the sturdy and easy assembly, and the flexibility of the system." +## What is folding? -
+Folding/accumulation schemes are a cryptographic primitive that empowers recursive zero-knowledge proofs by merging multiple instances of a computation into a single instance of the same computation. -> **Warning**: experimental code, do not use in production.
-> The code has not been audited. Several optimizations are also pending. Our focus so far has been on implementing the Nova, HyperNova and ProtoGalaxy schemes, all with the CycleFold approach; and achieving the onchain (in EVM) verification of the folding proofs. +For more details about folding schemes, please refer to our [documentation](./docs/Folding.md). +## Features -## Schemes implemented +In Sonobe, we aim to provide _modular_, _secure_, _performant_, and _easy to use_ implementations of folding schemes. + +- **Modularity**: As our main priority, Sonobe features multiple folding schemes, commitment schemes, deciders (a.k.a. proof compression SNARKs), and frontends/DSLs, allowing users to pick the combination which best fits their needs. It provides compilers that are able to build higher level primitives such as Incremental Verifiable Computation (IVC) and Proof-Carrying Data (PCD) from arbitrary folding schemes. +- **Security**: + +In addition, Sonobe is conceived as an exploratory effort with the aim to push forward the practical side of folding schemes and advancing towards onchain (EVM) verification. + +_"The [Sonobe module](https://en.wikipedia.org/wiki/Sonobe) is one of the many units used to build modular origami. The popularity of Sonobe modular origami models derives from the simplicity of folding the modules, the sturdy and easy assembly, and the flexibility of the system."_ + +> **Warning**: experimental code, do not use in production. +> +> The code has not been audited. Several optimizations are also pending. Expect breaking changes. + + +## Supported schemes + +Below is the support matrix of different schemes implemented in Sonobe: + +| Folding Schemes | Folding-to-IVC compilers | Folding-to-PCD compilers | Deciders | Commitment Schemes | Frontends | +|---|---|---|---|---|---| +| Nova[^nova] | | | | | | +| | | | | | | +| | | | | | | Folding schemes implemented: @@ -24,8 +42,6 @@ Folding schemes implemented: - [CycleFold: Folding-scheme-based recursive arguments over a cycle of elliptic curves](https://eprint.iacr.org/2023/1192.pdf), Abhiram Kothapalli, Srinath Setty. 2023 - [HyperNova: Recursive arguments for customizable constraint systems](https://eprint.iacr.org/2023/573.pdf), Abhiram Kothapalli, Srinath Setty. 2023 - [ProtoGalaxy: Efficient ProtoStar-style folding of multiple instances](https://eprint.iacr.org/2023/1106.pdf), Liam Eagen, Ariel Gabizon. 2023 - - ## Frontends Frontends allow to define the circuit to be folded (ie. `FCircuit`). @@ -37,10 +53,12 @@ More details about the frontend interface and the experimental frontends can be ## Usage -Import the library: +Declare the libraries as dependencies in your `Cargo.toml`: ```toml [dependencies] -folding-schemes = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "folding-schemes"} +sonobe-fs = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "sonobe-fs" } +sonobe-ivc = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "sonobe-ivc" } +sonobe-primitives = { git = "https://github.com/privacy-scaling-explorations/sonobe", package = "sonobe-primitives" } ``` Available packages: @@ -50,7 +68,6 @@ Available packages: Available features: - `parallel` enables some parallelization optimizations available in the crate. It is enabled by default. -- `light-test` disables part of the DeciderEthCircuit various circuits (which accounts for ~9M constraints) so that the tests involving those circuits can run faster. Do not use it outside tests. This feature is disabled by default. Examples of usage can be found at the [examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples) directory. @@ -67,7 +84,7 @@ Once the IVC iterations are completed, the IVC proof is compressed into the Deci

- +

Where $w_i$ are the external witnesses used at each iterative step. @@ -87,14 +104,14 @@ The development flow using Sonobe looks like: 4. Generate the decider verifier

- +

The folding scheme and decider used can be swapped with a few lines of code (eg. switching from a Decider that uses two Spartan proofs over a cycle of curves, to a Decider that uses a single Groth16 proof over the BN254 to be verified in an Ethereum smart contract). The [Sonobe docs](https://privacy-scaling-explorations.github.io/sonobe-docs/) contain more details about the usage and design of the library. -Complete examples can be found at [folding-schemes/examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples) +Complete examples can be found at [folding-schemes/examples](https://github.com/privacy-scaling-explorations/sonobeAcknowledgments/tree/main/examples) ## License @@ -107,3 +124,8 @@ This project builds on top of multiple [arkworks](https://github.com/arkworks-rs The Solidity templates used in `nova_cyclefold_verifier.sol`, use [iden3](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs)'s Groth16 implementation and a KZG10 Solidity template adapted from [weijiekoh/libkzg](https://github.com/weijiekoh/libkzg). In addition to the direct code contributors who make this repository possible, this project has been made possible by many conversations with [Srinath Setty](https://github.com/srinathsetty), [Lev Soukhanov](https://github.com/levs57), [Matej Penciak](https://github.com/mpenciak), [Adrian Hamelink](https://github.com/adr1anh), [François Garillot](https://github.com/huitseeker), [Daniel Marin](https://github.com/danielmarinq), [Han Jian](https://github.com/han0110), [Wyatt Benno](https://github.com/wyattbenno777), [Niсolas Gailly](https://github.com/nikkolasg) and [Nalin Bhardwaj](https://github.com/nalinbhardwaj), to whom we are grateful. + + +## Citations + +[^nova]: \ No newline at end of file diff --git a/benches/README.md b/benches/README.md deleted file mode 100644 index 6f998097e..000000000 --- a/benches/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# benchmarks -*Note: we're starting to benchmark & profile Sonobe, current results are pre-optimizations.* - -- Benchmark - - Run: `cargo bench` - - To run a specific benchmark, for example Nova's benchmark, run: `cargo bench --bench=nova` -- Profiling - - eg. `cargo bench --bench=nova -- --profile-time 3` - - diff --git a/benches/common.rs b/benches/common.rs deleted file mode 100644 index 163dbbfc6..000000000 --- a/benches/common.rs +++ /dev/null @@ -1,53 +0,0 @@ -use criterion::*; - -use folding_schemes::{ - frontend::{utils::CustomFCircuit, FCircuit}, - Curve, Error, FoldingScheme, -}; - -pub(crate) fn bench_ivc_opt< - C1: Curve, - C2: Curve, - FS: FoldingScheme>, ->( - c: &mut Criterion, - name: String, - n: usize, - prep_param: FS::PreprocessorParam, -) -> Result<(), Error> { - let fcircuit_size = 1 << n; // 2^n - - let f_circuit = CustomFCircuit::::new(fcircuit_size)?; - - let mut rng = rand::rngs::OsRng; - - // prepare the FS prover & verifier params - let fs_params = FS::preprocess(&mut rng, &prep_param)?; - - let z_0 = vec![C1::ScalarField::from(3_u32)]; - let mut fs = FS::init(&fs_params, f_circuit, z_0)?; - - // warmup steps - for _ in 0..5 { - fs.prove_step(rng, (), None)?; - } - - let mut group = c.benchmark_group(format!( - "{} - FCircuit: {} (2^{}) constraints", - name, fcircuit_size, n - )); - group.significance_level(0.1).sample_size(10); - group.bench_function("prove_step", |b| { - b.iter(|| -> Result<_, _> { black_box(fs.clone()).prove_step(rng, (), None) }) - }); - - // verify the IVCProof - let ivc_proof = fs.ivc_proof(); - group.bench_function("verify", |b| { - b.iter(|| -> Result<_, _> { - FS::verify(black_box(fs_params.1.clone()), black_box(ivc_proof.clone())) - }) - }); - group.finish(); - Ok(()) -} diff --git a/benches/hypernova.rs b/benches/hypernova.rs deleted file mode 100644 index d3f4421ca..000000000 --- a/benches/hypernova.rs +++ /dev/null @@ -1,84 +0,0 @@ -use criterion::*; -use pprof::criterion::{Output, PProfProfiler}; - -use ark_bn254::{Fr as bn_Fr, G1Projective as bn_G}; -use ark_grumpkin::Projective as grumpkin_G; -use ark_pallas::{Fr as pallas_Fr, Projective as pallas_G}; -use ark_vesta::Projective as vesta_G; - -use folding_schemes::{ - commitment::pedersen::Pedersen, - folding::{hypernova::HyperNova, nova::PreprocessorParam}, - frontend::{utils::CustomFCircuit, FCircuit}, - transcript::poseidon::poseidon_canonical_config, -}; - -mod common; -use common::bench_ivc_opt; - -fn bench_hypernova_ivc(c: &mut Criterion) { - let poseidon_config = poseidon_canonical_config::(); - - // iterate over the powers of n - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = PreprocessorParam::new(poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - pallas_G, - vesta_G, - HyperNova< - pallas_G, - vesta_G, - CustomFCircuit, - Pedersen, - Pedersen, - 1, - 1, - false, - >, - >( - c, - "HyperNova - Pallas-Vesta curves".to_string(), - *n, - prep_param, - ) - .unwrap(); - } - - let poseidon_config = poseidon_canonical_config::(); - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = PreprocessorParam::new(poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - bn_G, - grumpkin_G, - HyperNova< - bn_G, - grumpkin_G, - CustomFCircuit, - Pedersen, - Pedersen, - 1, - 1, - false, - >, - >( - c, - "HyperNova - BN254-Grumpkin curves".to_string(), - *n, - prep_param, - ) - .unwrap(); - } -} - -criterion_group! { - name = benches; - config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); - targets = bench_hypernova_ivc -} -criterion_main!(benches); diff --git a/benches/nova.rs b/benches/nova.rs deleted file mode 100644 index eed5419e2..000000000 --- a/benches/nova.rs +++ /dev/null @@ -1,75 +0,0 @@ -use criterion::*; -use pprof::criterion::{Output, PProfProfiler}; - -use ark_bn254::{Fr as bn_Fr, G1Projective as bn_G}; -use ark_grumpkin::Projective as grumpkin_G; -use ark_pallas::{Fr as pallas_Fr, Projective as pallas_G}; -use ark_vesta::Projective as vesta_G; - -use folding_schemes::{ - commitment::pedersen::Pedersen, - folding::nova::{Nova, PreprocessorParam}, - frontend::{utils::CustomFCircuit, FCircuit}, - transcript::poseidon::poseidon_canonical_config, -}; - -mod common; -use common::bench_ivc_opt; - -fn bench_nova_ivc(c: &mut Criterion) { - let poseidon_config = poseidon_canonical_config::(); - - // iterate over the powers of n - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = PreprocessorParam::new(poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - pallas_G, - vesta_G, - Nova< - pallas_G, - vesta_G, - CustomFCircuit, - Pedersen, - Pedersen, - false, - >, - >(c, "Nova - Pallas-Vesta curves".to_string(), *n, prep_param) - .unwrap(); - } - - let poseidon_config = poseidon_canonical_config::(); - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = PreprocessorParam::new(poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - bn_G, - grumpkin_G, - Nova< - bn_G, - grumpkin_G, - CustomFCircuit, - Pedersen, - Pedersen, - false, - >, - >( - c, - "Nova - BN254-Grumpkin curves".to_string(), - *n, - prep_param, - ) - .unwrap(); - } -} - -criterion_group! { - name = benches; - config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); - targets = bench_nova_ivc -} -criterion_main!(benches); diff --git a/benches/protogalaxy.rs b/benches/protogalaxy.rs deleted file mode 100644 index ace36c354..000000000 --- a/benches/protogalaxy.rs +++ /dev/null @@ -1,78 +0,0 @@ -use criterion::*; -use pprof::criterion::{Output, PProfProfiler}; - -use ark_bn254::{Fr as bn_Fr, G1Projective as bn_G}; -use ark_grumpkin::Projective as grumpkin_G; -use ark_pallas::{Fr as pallas_Fr, Projective as pallas_G}; -use ark_vesta::Projective as vesta_G; - -use folding_schemes::{ - commitment::pedersen::Pedersen, - folding::protogalaxy::ProtoGalaxy, - frontend::{utils::CustomFCircuit, FCircuit}, - transcript::poseidon::poseidon_canonical_config, -}; - -mod common; -use common::bench_ivc_opt; - -fn bench_protogalaxy_ivc(c: &mut Criterion) { - let poseidon_config = poseidon_canonical_config::(); - - // iterate over the powers of n - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = (poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - pallas_G, - vesta_G, - ProtoGalaxy< - pallas_G, - vesta_G, - CustomFCircuit, - Pedersen, - Pedersen, - >, - >( - c, - "ProtoGalaxy - Pallas-Vesta curves".to_string(), - *n, - prep_param, - ) - .unwrap(); - } - - let poseidon_config = poseidon_canonical_config::(); - for n in [0_usize, 14, 16, 18, 19, 20, 21, 22].iter() { - let fcircuit_size = 1 << n; // 2^n - let fcircuit = CustomFCircuit::::new(fcircuit_size).unwrap(); - let prep_param = (poseidon_config.clone(), fcircuit); - - bench_ivc_opt::< - bn_G, - grumpkin_G, - ProtoGalaxy< - bn_G, - grumpkin_G, - CustomFCircuit, - Pedersen, - Pedersen, - >, - >( - c, - "ProtoGalaxy - BN254-Grumpkin curves".to_string(), - *n, - prep_param, - ) - .unwrap(); - } -} - -criterion_group! { - name = benches; - config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); - targets = bench_protogalaxy_ivc -} -criterion_main!(benches); diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index d24b0b054..000000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "solidity-verifiers-cli" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-serialize = { workspace = true } -solidity-verifiers = { workspace = true } -clap = { workspace = true, features = ["derive", "string"] } -clap-verbosity-flag = { workspace = true } -env_logger = { workspace = true } - -[features] -default = ["parallel"] -parallel = ["solidity-verifiers/parallel"] \ No newline at end of file diff --git a/cli/README.md b/cli/README.md deleted file mode 100644 index d34b2026b..000000000 --- a/cli/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Solidity Verifiers CLI - -Solidity Verifiers CLI is a Command-Line Interface (CLI) tool to generate the Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0xPARC teams. - -Solidity Verifiers CLI is released under the MIT license, but notice that the Solidity template for the Groth16 verification has GPL-3.0 license, hence the generated Solidity verifiers that use the Groth16 template will have that license too. - -## Supported Protocols - -Solidity Verifier currently supports the generation of Solidity smart contracts for the verification of proofs in the following Zero Knowledge protocols: - -- **Groth16:** - - Efficient and succinct zero-knowledge proof system. - - Template credit: [Jordi Baylina - Groth16 Verifier Template](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs) - -- **KZG:** - - Uses the Kate-Zaverucha-Goldberg polynomial commitment scheme. - - Template credit: [weijiekoh - KZG10 Verifier Contract](https://github.com/weijiekoh/libkzg/blob/master/sol/KZGVerifier.sol) - -- **Nova + CycleFold Decider:** - - Implements the decider circuit verification for the Nova proof system in conjunction with the CycleFold protocol optimization. - - Template inspiration and setup credit: [Han - revm/Solidity Contract Testing Functions](https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/tree/main) - -## Usage - -```bash -solidity-verifiers-cli [OPTIONS] -p -k -o -``` - -A real use case (which was used to test the tool itself): -`solidity-verifiers-cli -p groth16 -k ./solidity-verifiers/assets/G16_test_vk` -This would generate a Groth16 verifier contract for the given G16 verifier key (which consists of the G16_Vk only) and store this contract in `$pwd`. - -### Options: - -v, --verbose: Increase logging verbosity - -q, --quiet: Decrease logging verbosity - -p, --protocol : Selects the protocol for which to generate the Decider circuit Solidity Verifier (possible values: groth16, kzg, nova-cyclefold) - -o, --out : Sets the output path for all generated artifacts - -k, --protocol-vk : Sets the input path for the file containing the verifier key required by the protocol chosen such that the verification contract can be generated. - --pragma : Selects the Solidity compiler version to be set in the Solidity Verifier contract artifact - -h, --help: Print help (see a summary with '-h') - -V, --version: Print version - -## License -Solidity Verifier CLI is released under the MIT license, but notice that the Solidity template for the Groth16 verification has GPL-3.0 license, hence the generated Solidity verifiers will have that license too. - -## Contributing -Feel free to explore, use, and contribute to Solidity Verifiers CLI as we strive to enhance privacy and scalability in the blockchain space! -We welcome contributions to Solidity Verifiers CLI! If you encounter any issues, have feature requests, or want to contribute to the codebase, please check out the GitHub repository and follow the guidelines outlined in the contributing documentation. diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index 29479b58e..000000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,40 +0,0 @@ -use ark_serialize::Write; -use clap::Parser; -use settings::Cli; -use std::path::Path; -use std::{fs, io}; - -mod settings; - -fn create_or_open_then_write>(path: &Path, content: &T) -> Result<(), io::Error> { - let mut file = fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(path)?; - file.write_all(content.as_ref()) -} - -fn main() { - let cli = Cli::parse(); - - // generate a subscriber with the desired log level - env_logger::builder() - .format_timestamp_secs() - .filter_level(cli.verbosity.log_level_filter()) - .init(); - - let out_path = cli.out; - - // Fetch the exact protocol for which we need to generate the Decider verifier contract. - let protocol = cli.protocol; - // Fetch the protocol data passed by the user from the file. - let protocol_vk = std::fs::read(cli.protocol_vk).unwrap(); - - // Generate the Solidity Verifier contract for the selected protocol with the given data. - create_or_open_then_write( - &out_path, - &protocol.render(&protocol_vk, cli.pragma).unwrap(), - ) - .unwrap(); -} diff --git a/cli/src/settings.rs b/cli/src/settings.rs deleted file mode 100644 index 158ae1a13..000000000 --- a/cli/src/settings.rs +++ /dev/null @@ -1,113 +0,0 @@ -use ark_serialize::SerializationError; -use clap::{Parser, ValueEnum}; -use solidity_verifiers::{ - Groth16VerifierKey, KZG10VerifierKey, NovaCycleFoldVerifierKey, ProtocolVerifierKey, -}; -use std::{env, fmt::Display, path::PathBuf}; - -fn get_default_out_path() -> PathBuf { - let mut path = env::current_dir().unwrap(); - path.push("verifier.sol"); - path -} - -#[derive(Debug, Copy, Clone, ValueEnum)] -pub(crate) enum Protocol { - Groth16, - Kzg, - NovaCycleFold, -} - -impl Display for Protocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -// Would be nice to link this to the `Template` or `ProtocolVerifierKey` traits. -// Sadly, this requires Boxing with `dyn` or similar which would complicate the code more than is actually required. -impl Protocol { - pub(crate) fn render( - &self, - data: &[u8], - pragma: Option, - ) -> Result, SerializationError> { - match self { - Self::Groth16 => Ok(Groth16VerifierKey::deserialize_protocol_verifier_key(data)? - .render_as_template(pragma)), - - Self::Kzg => Ok(KZG10VerifierKey::deserialize_protocol_verifier_key(data)? - .render_as_template(pragma)), - Self::NovaCycleFold => Ok(NovaCycleFoldVerifierKey::deserialize_protocol_verifier_key( - data, - )? - .render_as_template(pragma)), - } - } -} - -const ABOUT: &str = "A Command-Line Interface (CLI) tool to generate the Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. -"; - -const LONG_ABOUT: &str = " - _____ ______ ______ ______ ______ ______ ______ -| |__| || |__| || |__| || |__| || |__| || |__| || |__| | -| () || () || () || () || () || () || () | -|______||______||______||______||______||______||______| - ______ ______ -| |__| | ____ _ _ _ _ _ | |__| | -| () | / ___| ___ | (_) __| (_) |_ _ _ | () | -|______| \\___ \\ / _ \\| | |/ _` | | __| | | | |______| - ______ ___) | (_) | | | (_| | | |_| |_| | ______ -| |__| | |____/ \\___/|_|_|\\__,_|_|\\__|\\__, | | |__| | -| () | __ __ _ __ _ |___/ | () | -|______| \\ \\ / /__ _ __(_)/ _(_) ___ _ __ |______| - ______ \\ \\ / / _ \\ '__| | |_| |/ _ \\ '__| ______ -| |__| | \\ V / __/ | | | _| | __/ | | |__| | -| () | \\_/ \\___|_| |_|_| |_|\\___|_| | () | -|______| |______| - ______ ______ ______ ______ ______ ______ ______ -| |__| || |__| || |__| || |__| || |__| || |__| || |__| | -| () || () || () || () || () || () || () | -|______||______||______||______||______||______||______| - -Welcome to Solidity Verifiers CLI, a Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0xPARC teams. - -Solidity Verifiers CLI is released under the MIT license, but notice that the Solidity template for the Groth16 verification has GPL-3.0 license, hence the generated Solidity verifiers that use the Groth16 template will have that license too. - -Solidity Verifier currently supports the generation of Solidity smart contracts for the verification of proofs in the following Zero Knowledge protocols: - - Groth16: - Efficient and succinct zero-knowledge proof system. - - KZG: - Uses the Kate-Zaverucha-Goldberg polynomial commitment scheme. - - Nova + CycleFold Decider: - Implements the decider circuit verification for the Nova proof system in conjunction with the CycleFold protocol optimization. -"; -#[derive(Debug, Parser)] -#[command(author = "0xPARC & PSE", version, about = ABOUT, long_about = Some(LONG_ABOUT))] -#[command(propagate_version = true)] -/// A tool to create Solidity Contracts which act as verifiers for the major Folding Schemes implemented -/// within the `sonobe` repo. -pub(crate) struct Cli { - #[command(flatten)] - pub verbosity: clap_verbosity_flag::Verbosity, - - /// Selects the protocol for which we want to generate the Solidity Verifier contract. - #[arg(short = 'p', long, value_enum, rename_all = "lower")] - pub protocol: Protocol, - - #[arg(short = 'o', long, default_value=get_default_out_path().into_os_string())] - /// Sets the output path for all the artifacts generated by the command. - pub out: PathBuf, - - #[arg(short = 'k', long)] - /// Sets the input path for the file containing the verifier key required by the protocol chosen such that the verification contract can be generated. - pub protocol_vk: PathBuf, - - /// Selects the Solidity compiler version to be set in the Solidity Verifier contract artifact. - #[arg(long, default_value=None)] - pub pragma: Option, -} diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml new file mode 100644 index 000000000..b955cc775 --- /dev/null +++ b/crates/fs/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sonobe-fs" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } +ark-ec = { workspace = true } +ark-ff = { workspace = true, features = ["asm"] } +ark-poly = { workspace = true } +ark-r1cs-std = { workspace = true } +ark-relations = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +ark-serialize = { workspace = true } +itertools = { workspace = true } +num-bigint = { workspace = true, features = ["rand"] } +thiserror = { workspace = true } +rayon = { workspace = true } + +sonobe-primitives = { workspace = true } + +[dev-dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-pallas = { workspace = true, features = ["curve", "r1cs"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[features] +default = [] +parallel = [ + "sonobe-primitives/parallel", +] diff --git a/crates/fs/src/definitions/algorithms.rs b/crates/fs/src/definitions/algorithms.rs new file mode 100644 index 000000000..d5f9c37a4 --- /dev/null +++ b/crates/fs/src/definitions/algorithms.rs @@ -0,0 +1,120 @@ +//! Traits that define out-of-circuit widgets for folding scheme algorithms +//! (preprocessing, key generation, proof generation, proof verification, and +//! deciding). + +use ark_std::{borrow::Borrow, rand::RngCore}; +use sonobe_primitives::{relations::Relation, transcripts::Transcript}; + +use super::{FoldingSchemeDef, errors::Error, keys::DeciderKey}; + +/// [`FoldingSchemePreprocessor`] is the trait for folding scheme preprocessor. +pub trait FoldingSchemePreprocessor: FoldingSchemeDef { + /// [`FoldingSchemePreprocessor::preprocess`] defines the preprocessing + /// algorithm, which is a randomized algorithm that takes as input the + /// config / parameterization `config` of the folding scheme (e.g., size + /// bounds of the folding scheme) and outputs the public parameters. + /// + /// Here, the randomness source is controlled by `rng`. + /// + /// The security parameter is implicitly specified by the size of underlying + /// fields and groups. + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; +} + +/// [`FoldingSchemeKeyGenerator`] is the trait for folding scheme key generator. +pub trait FoldingSchemeKeyGenerator: FoldingSchemeDef { + /// [`FoldingSchemeKeyGenerator::generate_keys`] defines the key generation + /// algorithm, which is a deterministic algorithm that takes as input the + /// public parameters `pp` and the arithmetization `arith`, and outputs a + /// prover key and a verifier key. + fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; +} + +/// [`FoldingSchemeProver`] is the trait for folding scheme prover. +pub trait FoldingSchemeProver: FoldingSchemeDef { + /// [`FoldingSchemeProver::prove`] defines the proof generation algorithm, + /// which is a (probably) randomized algorithm that takes as input the + /// prover key `pk`, the transcript `transcript` between the prover and the + /// verifier, `M` running witnesses `Ws`, `M` running instances `Us`, `N` + /// incoming witnesses `ws`, and `N` incoming instances `us`, and outputs + /// the folded witness and instance, the proof, and the challenges. + /// + /// Here, although the challenges can usually be derived by `transcript` and + /// thus do not necessarily need to be returned for verification, we still + /// have the prover return them explicitly so that they can be used for the + /// construction of CycleFold circuits in our CycleFold-based folding-to-IVC + /// compiler without re-deriving them from the transcript. + /// + /// The prover may further use `rng` as the randomness source, e.g., for + /// the hiding/zero-knowledge property. + #[allow(non_snake_case, clippy::type_complexity)] + fn prove( + pk: &::ProverKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; M], + Us: &[impl Borrow; M], + ws: &[impl Borrow; N], + us: &[impl Borrow; N], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof), Error>; +} + +/// [`FoldingSchemeVerifier`] is the trait for folding scheme verifier. +pub trait FoldingSchemeVerifier: FoldingSchemeDef { + /// [`FoldingSchemeVerifier::verify`] defines the proof verification + /// algorithm, which is a deterministic algorithm that takes as input the + /// verifier key `vk`, the transcript `transcript` between the prover and + /// the verifier, `M` running instances `Us`, `N` incoming instances `us`, + /// and the proof `proof`, and outputs the folded instance. + #[allow(non_snake_case)] + fn verify( + vk: &::VerifierKey, + transcript: &mut impl Transcript, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + ) -> Result; +} + +/// [`FoldingSchemeDecider`] is the trait for folding scheme decider. +pub trait FoldingSchemeDecider: FoldingSchemeDef { + /// [`FoldingSchemeDecider::decide_running`] defines the deciding algorithm + /// for running witness-instance pairs, which is a deterministic algorithm + /// that takes as input the decider key `dk`, a running witness `W` and a + /// running instance `U`, and outputs whether the witness-instance pair + /// satisfies the running relation. + #[allow(non_snake_case)] + fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { + Relation::::check_relation(dk, W, U) + } + + /// [`FoldingSchemeDecider::decide_running`] defines the deciding algorithm + /// for incoming witness-instance pairs, which is a deterministic algorithm + /// that takes as input the decider key `dk`, an incoming witness `W` and an + /// incoming instance `U`, and outputs whether the witness-instance pair + /// satisfies the incoming relation. + fn decide_incoming(dk: &Self::DeciderKey, w: &Self::IW, u: &Self::IU) -> Result<(), Error> { + Relation::::check_relation(dk, w, u) + } +} + +impl FoldingSchemeDecider for FS {} + +/// [`FoldingSchemeOps`] is a convenience super-trait bundling all algorithms. +pub trait FoldingSchemeOps: + FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} + +impl FoldingSchemeOps for FS where + FS: FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} diff --git a/crates/fs/src/definitions/circuits.rs b/crates/fs/src/definitions/circuits.rs new file mode 100644 index 000000000..9dc4452df --- /dev/null +++ b/crates/fs/src/definitions/circuits.rs @@ -0,0 +1,93 @@ +//! Traits that define in-circuit gadgets for folding scheme algorithms, mainly +//! for proof verification. + +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + commitments::CommitmentDefGadget, relations::RelationGadget, transcripts::TranscriptGadget, +}; + +use super::{FoldingSchemeDefGadget, algorithms::FoldingSchemeOps}; + +/// [`FoldingSchemePartialVerifierGadget`] is the partial in-circuit verifier. +/// +/// For schemes that have circuit-unfriendly parts in their verification, the +/// implementation can choose to only implement this partial verifier gadget and +/// use some other techniques for the remaining verification work. +/// For example, group-based folding schemes can defer the expensive elliptic +/// curve operations on commitments to an external CycleFold circuit. +pub trait FoldingSchemePartialVerifierGadget: + FoldingSchemeDefGadget> +{ + /// [`FoldingSchemePartialVerifierGadget::verify_hinted`] defines the proof + /// verification gadget that matches its out-of-circuit widget + /// [`crate::FoldingSchemeVerifier::verify`]. + /// + /// The implementation is allowed to create hints for the missing parts of + /// the verification that are not performed inside the constraint system, + /// and it is unnecessary to constrain these hints inside the circuit. + /// However, it is the caller's responsibility to ensure that these hints + /// are later verified using other techniques (e.g., CycleFold helper). + #[allow(non_snake_case)] + fn verify_hinted( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: [&Self::RU; M], + us: [&Self::IU; N], + proof: &Self::Proof, + ) -> Result; +} + +/// [`FoldingSchemeFullVerifierGadget`] is the full in-circuit verifier. +/// +/// Extends [`FoldingSchemePartialVerifierGadget`] by performing everything +/// required for proof verification inside the constraint system. +pub trait FoldingSchemeFullVerifierGadget: + FoldingSchemePartialVerifierGadget +{ + /// [`FoldingSchemeFullVerifierGadget::verify`] defines the proof + /// verification gadget that matches its out-of-circuit widget + /// [`crate::FoldingSchemeVerifier::verify`]. + /// + /// Unlike [`FoldingSchemePartialVerifierGadget::verify_hinted`], the + /// implementation is expected to perform all necessary verification steps + /// and constrain all required variables inside the circuit. + #[allow(non_snake_case)] + fn verify( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: [&Self::RU; M], + us: [&Self::IU; N], + proof: &Self::Proof, + ) -> Result; +} + +pub trait FoldingSchemeDeciderGadget: + FoldingSchemeDefGadget< + DeciderKey: RelationGadget + RelationGadget, +> +{ + #[allow(non_snake_case)] + fn decide_running( + dk: &Self::DeciderKey, + W: &Self::RW, + U: &Self::RU, + ) -> Result<(), SynthesisError> { + RelationGadget::::check_relation(dk, W, U) + } + + fn decide_incoming( + dk: &Self::DeciderKey, + w: &Self::IW, + u: &Self::IU, + ) -> Result<(), SynthesisError> { + RelationGadget::::check_relation(dk, w, u) + } +} + +impl< + FS: FoldingSchemeDefGadget< + DeciderKey: RelationGadget + RelationGadget, + >, +> FoldingSchemeDeciderGadget for FS +{ +} diff --git a/crates/fs/src/definitions/errors.rs b/crates/fs/src/definitions/errors.rs new file mode 100644 index 000000000..3879aa9b4 --- /dev/null +++ b/crates/fs/src/definitions/errors.rs @@ -0,0 +1,44 @@ +//! Error definitions for folding schemes. + +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + arithmetizations::Error as ArithError, commitments::Error as CommitmentError, +}; +use thiserror::Error; + +/// [`Error`] enumerates possible errors during folding scheme operations. +#[derive(Debug, Error)] +pub enum Error { + /// [`Error::ArithError`] indicates an error from the underlying constraint + /// system. + #[error(transparent)] + ArithError(#[from] ArithError), + /// [`Error::CommitmentError`] indicates an error from the underlying + /// commitment scheme. + #[error(transparent)] + CommitmentError(#[from] CommitmentError), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. + #[error(transparent)] + SynthesisError(#[from] SynthesisError), + /// [`Error::Unsupported`] indicates that a certain use case is not + /// supported. + #[error("Unsupported use case: {0}")] + Unsupported(String), + /// [`Error::DomainCreationFailure`] indicates a failure in creating + /// evaluation domains. + #[error("Failed to create domain")] + DomainCreationFailure, + /// [`Error::IndivisibleByVanishingPoly`] indicates that a polynomial is + /// not divisible by the vanishing polynomial of a certain domain. + #[error("Indivisible by vanishing polynomial")] + IndivisibleByVanishingPoly, + /// [`Error::UnsatisfiedRelation`] indicates that a certain relation is not + /// satisfied. + #[error("Unsatisfied relation: {0}")] + UnsatisfiedRelation(String), + /// [`Error::InvalidPublicParameters`] indicates that the provided public + /// parameters are invalid. + #[error("Invalid public parameters: {0}")] + InvalidPublicParameters(String), +} diff --git a/crates/fs/src/definitions/instances.rs b/crates/fs/src/definitions/instances.rs new file mode 100644 index 000000000..191774b66 --- /dev/null +++ b/crates/fs/src/definitions/instances.rs @@ -0,0 +1,102 @@ +//! Traits and abstractions for folding scheme instances. + +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, select::CondSelectGadget}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_std::fmt::Debug; +use sonobe_primitives::{ + arithmetizations::ArithConfig, + commitments::{CommitmentDef, CommitmentDefGadget}, + transcripts::{Absorbable, AbsorbableVar}, + utils::dummy::Dummy, +}; + +use super::utils::TaggedVec; + +/// [`FoldingInstance`] defines the operations that a folding scheme's instance +/// should support. +pub trait FoldingInstance: + Clone + Debug + PartialEq + Eq + Absorbable + for<'a> Dummy<&'a ArithConfig> +{ + /// [`FoldingInstance::commitments`] returns the commitments contained in + /// the instance. + // TODO (@winderica): consider the scenario where the instance has multiple + // commitments of different types. + fn commitments(&self) -> Vec; + + /// [`FoldingInstance::public_inputs`] returns the reference to the public + /// inputs contained in the instance. + fn public_inputs(&self) -> &[CM::Scalar]; +} + +/// [`PlainInstance`] is a vector of field elements that are the statements / +/// public inputs to a constraint system. +/// We provide this type for folding schemes that support such simple instances, +/// enabling compatibility with the definition of accumulation schemes (i.e., +/// running x plain -> running). +/// +/// To distinguish it from the witness vector, we use a tagged vector with tag +/// `'u'` for it. +pub type PlainInstance = TaggedVec; + +impl Dummy<&ArithConfig> for PlainInstance { + fn dummy(cfg: &ArithConfig) -> Self { + vec![V::default(); cfg.n_public_inputs].into() + } +} + +impl FoldingInstance for PlainInstance { + fn commitments(&self) -> Vec { + vec![] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + self + } +} + +/// [`FoldingInstanceVar`] is the in-circuit variable of [`FoldingInstance`]. +pub trait FoldingInstanceVar: + AllocVar + + GR1CSVar> + + AbsorbableVar + + CondSelectGadget +{ + /// [`FoldingInstanceVar::commitments`] returns the commitments contained in + /// the instance variable. + fn commitments(&self) -> Vec<&CM::CommitmentVar>; + + /// [`FoldingInstanceVar::public_inputs`] returns the reference to the + /// public inputs contained in the instance variable. + fn public_inputs(&self) -> &Vec; + + /// [`FoldingInstanceVar::new_witness_with_public_inputs`] allocates a + /// folding instance in the circuit as a witness variable, with the given + /// pre-allocated public inputs. + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result; +} + +impl FoldingInstanceVar for PlainInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![] + } + + fn public_inputs(&self) -> &Vec { + self + } + + fn new_witness_with_public_inputs( + _cs: impl Into>, + _u: &Self::Value, + x: Vec, + ) -> Result { + Ok(Self(x)) + } +} + +/// [`PlainInstanceVar`] is the in-circuit variable of [`PlainInstance`]. +// TODO (@winderica): use a different tag? +pub type PlainInstanceVar = PlainInstance; diff --git a/crates/fs/src/definitions/keys.rs b/crates/fs/src/definitions/keys.rs new file mode 100644 index 000000000..d430c4efd --- /dev/null +++ b/crates/fs/src/definitions/keys.rs @@ -0,0 +1,23 @@ +//! Traits and abstractions for folding scheme keys. + +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use sonobe_primitives::arithmetizations::ArithConfig; + +/// [`DeciderKey`] defines the information that a folding scheme's decider key +/// should include or provide access to. +pub trait DeciderKey: CanonicalSerialize + CanonicalDeserialize { + /// [`DeciderKey::ProverKey`] is the type of the prover key contained in the + /// decider key. + type ProverKey; + /// [`DeciderKey::VerifierKey`] is the type of the verifier key contained in + /// the decider key. + type VerifierKey: Clone; + + /// [`DeciderKey::to_pk`] returns the reference to the prover key. + fn to_pk(&self) -> &Self::ProverKey; + /// [`DeciderKey::to_vk`] returns the reference to the verifier key. + fn to_vk(&self) -> &Self::VerifierKey; + /// [`DeciderKey::to_arith_config`] returns the constraint system + /// configuration. + fn to_arith_config(&self) -> ArithConfig; +} diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs new file mode 100644 index 000000000..9c0c56213 --- /dev/null +++ b/crates/fs/src/definitions/mod.rs @@ -0,0 +1,149 @@ +//! Shared traits for folding schemes, including definitions of related +//! cryptographic objects and algorithms in and out of circuit. + +pub mod algorithms; +pub mod circuits; +pub mod errors; +pub mod instances; +pub mod keys; +pub mod utils; +pub mod variants; +pub mod witnesses; + +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; +use sonobe_primitives::{ + algebra::field::SonobeField, + arithmetizations::{Arith, ArithConfig}, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget}, + relations::{Relation, WitnessInstanceSampler}, + utils::dummy::Dummy, +}; + +use self::{ + errors::Error, + instances::{FoldingInstance, FoldingInstanceVar}, + keys::DeciderKey, + witnesses::FoldingWitness, +}; +use crate::FoldingWitnessVar; + +/// [`FoldingSchemeDef`] provides the core type definitions of a folding scheme. +/// +/// A folding scheme is a cryptographic primitive that folds multiple instances +/// of computations into a single instance while preserving the validity of the +/// computations. +/// More specifically, a folding scheme in general considers two relations `R1` +/// and `R2`. +/// The folding prover folds `M` witness-instance pairs satisfying `R1` and `N` +/// witness-instance pairs satisfying `R2` into a single witness-instance pair +/// satisfying `R1`, along with a proof that the folding was done correctly. +/// The folding verifier folds `M` instances of `R1` and `N` instances of `R2` +/// into a single instance of `R1` under the help of the proof. +/// +/// While folding schemes can be applied in various contexts, we primarily focus +/// on their use in constructing recursive proof systems, and thus we refer to +/// `R1` as the "running relation" and `R2` as the "incoming relation" in the +/// codebase. +/// A witness-instance pair `(W, U)` of type `(RW, RU)` for `R1` is called a +/// "running" witness-instance pair, while a witness-instance pair `(w, u)` of +/// type `(IW, IU)` for `R2` is called an "incoming" witness-instance pair. +/// +/// Different folding schemes support different running and incoming relations, +/// as well as the number of witness-instance pairs that can be folded at once. +pub trait FoldingSchemeDef { + /// [`FoldingSchemeDef::CM`] is the commitment scheme used by the folding + /// scheme. + type CM: CommitmentDef; + /// [`FoldingSchemeDef::RW`] is the type of running witness. + type RW: FoldingWitness; + /// [`FoldingSchemeDef::RU`] is the type of running instance. + type RU: FoldingInstance; + /// [`FoldingSchemeDef::IW`] is the type of incoming witness. + type IW: FoldingWitness; + /// [`FoldingSchemeDef::IU`] is the type of incoming instance. + type IU: FoldingInstance; + /// [`FoldingSchemeDef::TranscriptField`] is the field type used in the + /// transcript of the folding scheme. + type TranscriptField: SonobeField; + /// [`FoldingSchemeDef::Arith`] is the constraint system supported by the + /// folding scheme. + type Arith: Arith; + /// [`FoldingSchemeDef::Config`] is the type of configuration required to + /// generate the public parameters of the folding scheme. + type Config; + /// [`FoldingSchemeDef::PublicParam`] is the type of public parameters of + /// the folding scheme. + type PublicParam; + /// [`FoldingSchemeDef::DeciderKey`] is the type of decider key of the + /// folding scheme, which is used to determine the satisfiability of a + /// witness-instance pair. + type DeciderKey: DeciderKey + + Clone + + Relation + + Relation + + WitnessInstanceSampler + + WitnessInstanceSampler< + Self::IW, + Self::IU, + Source = AssignmentsOwned<::Scalar>, + Error = Error, + >; + /// [`FoldingSchemeDef::Challenge`] is the type of challenge generated + /// during the folding process. + type Challenge; + /// [`FoldingSchemeDef::Proof`] is the type of proof generated by the + /// folding prover. + type Proof: Clone + for<'a> Dummy<&'a ArithConfig>; +} + +/// [`FoldingSchemeDefGadget`] specifies the in-circuit associated types for a +/// folding scheme gadget. +pub trait FoldingSchemeDefGadget { + /// [`FoldingSchemeDefGadget::Widget`] points to the out-of-circuit folding + /// scheme widget. + type Widget: FoldingSchemeDef; + + /// [`FoldingSchemeDefGadget::CM`] is the commitment scheme gadget. + type CM: CommitmentDefGadget::CM>; + type RW: FoldingWitnessVar::RW>; + /// [`FoldingSchemeDefGadget::RU`] is the type of in-circuit running + /// instance variable. + type RU: FoldingInstanceVar::RU>; + type IW: FoldingWitnessVar::IW>; + /// [`FoldingSchemeDefGadget::IU`] is the type of in-circuit incoming + /// instance variable. + type IU: FoldingInstanceVar::IU>; + + type Arith: AllocVar< + ::Arith, + ::ConstraintField, + >; + + /// [`FoldingSchemeDefGadget::VerifierKey`] is the type of in-circuit + /// verifier key variable. + type VerifierKey; + type DeciderKey: AllocVar< + ::DeciderKey, + ::ConstraintField, + >; + + /// [`FoldingSchemeDefGadget::Challenge`] is the type of in-circuit + /// challenge variable. + type Challenge: AllocVar< + ::Challenge, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = ::Challenge, + >; + /// [`FoldingSchemeDefGadget::Proof`] is the type of in-circuit proof + /// variable. + type Proof: AllocVar< + ::Proof, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = ::Proof, + >; +} diff --git a/crates/fs/src/definitions/utils.rs b/crates/fs/src/definitions/utils.rs new file mode 100644 index 000000000..d7a69ec43 --- /dev/null +++ b/crates/fs/src/definitions/utils.rs @@ -0,0 +1,152 @@ +//! Utility types shared across folding scheme definitions. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + ops::{Deref, DerefMut}, + slice::Iter, + vec::IntoIter, +}; +use rayon::{ + iter::{IntoParallelIterator, IntoParallelRefIterator}, + slice::Iter as RayonIter, + vec::IntoIter as RayonIntoIter, +}; +use sonobe_primitives::transcripts::{Absorbable, AbsorbableVar}; + +/// [`TaggedVec`] is a wrapper around a vector that additionally carries a +/// compile-time `char` tag. +/// +/// This is used to create nominally distinct vector types that are structurally +/// identical. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TaggedVec(pub Vec); + +impl Deref for TaggedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TaggedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for TaggedVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl IntoIterator for TaggedVec { + type Item = V; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, V, const TAG: char> IntoIterator for &'a TaggedVec { + type Item = &'a V; + type IntoIter = Iter<'a, V>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoParallelIterator for TaggedVec { + type Item = V; + + type Iter = RayonIntoIter; + + fn into_par_iter(self) -> Self::Iter { + self.0.into_par_iter() + } +} + +impl<'a, V: Sync, const TAG: char> IntoParallelIterator for &'a TaggedVec { + type Iter = RayonIter<'a, V>; + + type Item = &'a V; + + fn into_par_iter(self) -> Self::Iter { + self.0.par_iter() + } +} + +impl From> for Vec { + fn from(val: TaggedVec) -> Self { + val.0 + } +} + +impl Absorbable for TaggedVec { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest) + } +} + +impl, const TAG: char> AbsorbableVar for TaggedVec { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest) + } +} + +impl, Y, const TAG: char> AllocVar, F> + for TaggedVec +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(Self) + } +} + +impl, const TAG: char> CondSelectGadget + for TaggedVec +{ + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.len() != false_value.len() { + return Err(SynthesisError::Unsatisfiable); + } + true_value + .iter() + .zip(false_value.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>() + .map(Self) + } +} + +impl, const TAG: char> GR1CSVar for TaggedVec { + type Value = TaggedVec; + + fn cs(&self) -> ConstraintSystemRef { + self.0.cs() + } + + fn value(&self) -> Result { + self.0.value().map(TaggedVec) + } +} diff --git a/crates/fs/src/definitions/variants.rs b/crates/fs/src/definitions/variants.rs new file mode 100644 index 000000000..5d4ba2f2c --- /dev/null +++ b/crates/fs/src/definitions/variants.rs @@ -0,0 +1,68 @@ +//! Traits that define variants of folding schemes based on different underlying +//! mathematical structures. + +use sonobe_primitives::{ + algebra::group::CF2, + commitments::{CommitmentDef, GroupBasedCommitment}, +}; + +use crate::{ + FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, FoldingSchemeOps, + FoldingSchemePartialVerifierGadget, +}; + +/// [`GroupBasedFoldingSchemePrimaryDef`] defines a folding scheme based on +/// groups (elliptic curves), whose transcript field is the scalar field of its +/// group-based commitment scheme. +pub trait GroupBasedFoldingSchemePrimaryDef: + FoldingSchemeDef< + CM: GroupBasedCommitment, + TranscriptField = <::CM as CommitmentDef>::Scalar, + > +{ + /// [`GroupBasedFoldingSchemePrimaryDef::Gadget`] is the in-circuit gadget + /// that defines the folding scheme. + type Gadget: FoldingSchemeDefGadget::Gadget2>; +} + +/// [`GroupBasedFoldingSchemePrimary`] is a convenience trait that combines the +/// definition [`GroupBasedFoldingSchemePrimaryDef`] and operations +/// [`FoldingSchemeOps`]. +pub trait GroupBasedFoldingSchemePrimary: + GroupBasedFoldingSchemePrimaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemePrimary for FS where + FS: GroupBasedFoldingSchemePrimaryDef> +{ +} + +/// [`GroupBasedFoldingSchemeSecondaryDef`] defines a folding scheme based on +/// groups (elliptic curves), whose transcript field is the base field of its +/// group-based commitment scheme. +pub trait GroupBasedFoldingSchemeSecondaryDef: + FoldingSchemeDef< + CM: GroupBasedCommitment, + TranscriptField = CF2<<::CM as CommitmentDef>::Commitment>, + > +{ + /// [`GroupBasedFoldingSchemeSecondaryDef::Gadget`] is the in-circuit gadget + /// that defines the folding scheme. + type Gadget: FoldingSchemeDefGadget::Gadget1>; +} + +/// [`GroupBasedFoldingSchemeSecondary`] is a convenience trait that combines +/// the definition [`GroupBasedFoldingSchemeSecondaryDef`] and operations +/// [`FoldingSchemeOps`]. +pub trait GroupBasedFoldingSchemeSecondary: + GroupBasedFoldingSchemeSecondaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemeSecondary for FS where + FS: GroupBasedFoldingSchemeSecondaryDef> +{ +} diff --git a/crates/fs/src/definitions/witnesses.rs b/crates/fs/src/definitions/witnesses.rs new file mode 100644 index 000000000..aecc50934 --- /dev/null +++ b/crates/fs/src/definitions/witnesses.rs @@ -0,0 +1,50 @@ +//! Traits and abstractions for folding scheme witnesses. + +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; +use ark_std::fmt::Debug; +use sonobe_primitives::{ + arithmetizations::ArithConfig, + commitments::{CommitmentDef, CommitmentDefGadget}, + utils::dummy::Dummy, +}; + +use super::utils::TaggedVec; + +/// [`FoldingWitness`] defines the operations that a folding scheme's witness +/// should support. +pub trait FoldingWitness: Debug + for<'a> Dummy<&'a ArithConfig> {} + +/// [`PlainWitness`] is a vector of field elements that are the witnesses to a +/// constraint system. +/// We provide this type for folding schemes that support such simple witnesses, +/// enabling compatibility with the definition of accumulation schemes (i.e., +/// running x plain -> running). +/// +/// To distinguish it from the instance vector, we use a tagged vector with tag +/// `'w'` for it. +pub type PlainWitness = TaggedVec; + +impl Dummy<&ArithConfig> for PlainWitness { + fn dummy(cfg: &ArithConfig) -> Self { + vec![V::default(); cfg.n_witnesses].into() + } +} + +impl FoldingWitness for PlainWitness {} + +/// [`FoldingWitnessVar`] is the in-circuit variable of [`FoldingWitness`]. +pub trait FoldingWitnessVar: + AllocVar + + GR1CSVar> +{ +} + +impl FoldingWitnessVar for T where + T: AllocVar + + GR1CSVar> +{ +} + +/// [`PlainWitnessVar`] is the in-circuit variable of [`PlainWitness`]. +// TODO (@winderica): use a different tag? +pub type PlainWitnessVar = PlainWitness; diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs new file mode 100644 index 000000000..85159d33b --- /dev/null +++ b/crates/fs/src/lib.rs @@ -0,0 +1,135 @@ +#![warn(missing_docs)] + +//! Folding scheme definition and implementations. +//! +//! This crate provides the traits for folding schemes, the out-of-circuit +//! widgets and the in-circuit gadgets of their algorithms, and their associated +//! structures (such as keys, instances, and witnesses) in [`definitions`]. +//! +//! Concrete constructions of the following folding schemes are then implemented +//! as submodules: +//! - [`Nova`](nova) +//! - [`HyperNova`](hypernova) +//! - [`Mova`](mova) +//! - [`Ova`](ova) +//! - [`ProtoGalaxy`](protogalaxy) +//! +//! Each scheme module mirrors the same directory layout: +//! - `algorithms/`: Implementations for the following algorithms: +//! - Preprocessing/Setup: [`FoldingSchemePreprocessor`] +//! - Key generation: [`FoldingSchemeKeyGenerator`] +//! - Proof generation: [`FoldingSchemeProver`] +//! - Proof verification: [`FoldingSchemeVerifier`] +//! - `circuits/`: In-circuit (partial / full) gadgets, mainly for verification. +//! - `instances/`: Instance types. +//! - `keys/`: Key types. +//! - `witnesses/`: Witness types. + +pub mod definitions; +pub mod nova; + +pub use self::definitions::{ + FoldingSchemeDef, FoldingSchemeDefGadget, + algorithms::{ + FoldingSchemeDecider, FoldingSchemeKeyGenerator, FoldingSchemeOps, + FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, + }, + circuits::{FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget}, + errors::Error, + instances::{FoldingInstance, FoldingInstanceVar, PlainInstance, PlainInstanceVar}, + keys::DeciderKey, + utils::TaggedVec, + variants::{ + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondary, GroupBasedFoldingSchemeSecondaryDef, + }, + witnesses::{FoldingWitness, FoldingWitnessVar, PlainWitness, PlainWitnessVar}, +}; + +#[cfg(test)] +mod tests { + use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; + use ark_std::{error::Error, rand::Rng, sync::Arc}; + use sonobe_primitives::{ + circuits::{ArithExtractor, AssignmentsOwned}, + commitments::CommitmentDef, + relations::WitnessInstanceSampler, + transcripts::{ + Transcript, + griffin::{GriffinParams, sponge::GriffinSponge}, + }, + }; + + use super::*; + + #[allow(non_snake_case)] + pub fn test_folding_scheme, const M: usize, const N: usize>( + config: FS::Config, + circuit: impl ConstraintSynthesizer<::Scalar>, + assignments_vec: Vec::Scalar>>, + mut rng: impl Rng, + ) -> Result<(), Box> + where + FS::Arith: From::Scalar>>, + { + let pp = FS::preprocess(config, &mut rng)?; + + let mut cs = ArithExtractor::new(); + cs.execute_synthesizer(circuit)?; + let arith = cs.arith()?; + let dk = FS::generate_keys(pp, arith)?; + let pk = dk.to_pk(); + let vk = dk.to_vk(); + + let mut Ws = vec![]; + let mut Us = vec![]; + for _ in 0..M { + let (W, U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; + FS::decide_running(&dk, &W, &U)?; + Ws.push(W); + Us.push(U); + } + let mut Ws = Ws.try_into().unwrap(); + let mut Us = Us.try_into().unwrap(); + + let config = Arc::new(GriffinParams::new(16, 5, 9)); + + let mut transcript_p = GriffinSponge::new(config.clone()); + let mut transcript_v = GriffinSponge::new(config); + + for assignments in assignments_vec { + let mut ws = vec![]; + let mut us = vec![]; + for _ in 0..N { + let (w, u) = WitnessInstanceSampler::::sample( + &dk, + assignments.clone(), + &mut rng, + )?; + FS::decide_incoming(&dk, &w, &u)?; + ws.push(w); + us.push(u); + } + let ws = ws.try_into().unwrap(); + let us = us.try_into().unwrap(); + + let (WW, UU, pi) = FS::prove(pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; + FS::decide_running(&dk, &WW, &UU)?; + assert_eq!(FS::verify(vk, &mut transcript_v, &Us, &us, &pi)?, UU); + + for i in 0..M { + let (W, U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; + FS::decide_running(&dk, &W, &U)?; + Ws[i] = W; + Us[i] = U; + } + if M != 0 { + let idx = rng.gen_range(0..M); + Ws[idx] = WW; + Us[idx] = UU; + } + } + + Ok(()) + } +} diff --git a/crates/fs/src/nova/algorithms/key_generator.rs b/crates/fs/src/nova/algorithms/key_generator.rs new file mode 100644 index 000000000..f9d1420d6 --- /dev/null +++ b/crates/fs/src/nova/algorithms/key_generator.rs @@ -0,0 +1,26 @@ +//! Key generation for Nova. + +use ark_std::sync::Arc; +use sonobe_primitives::{ + algebra::field::SonobeField, + arithmetizations::Arith, + commitments::{CommitmentKey, GroupBasedCommitment}, +}; + +use crate::{Error, FoldingSchemeKeyGenerator, nova::AbstractNova}; + +impl FoldingSchemeKeyGenerator + for AbstractNova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints.max(cfg.n_witnesses) { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} diff --git a/crates/fs/src/nova/algorithms/mod.rs b/crates/fs/src/nova/algorithms/mod.rs new file mode 100644 index 000000000..263d3cd42 --- /dev/null +++ b/crates/fs/src/nova/algorithms/mod.rs @@ -0,0 +1,6 @@ +//! Implementations folding scheme algorithms for Nova. + +pub mod key_generator; +pub mod preprocessor; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/nova/algorithms/preprocessor.rs b/crates/fs/src/nova/algorithms/preprocessor.rs new file mode 100644 index 000000000..70d143f58 --- /dev/null +++ b/crates/fs/src/nova/algorithms/preprocessor.rs @@ -0,0 +1,15 @@ +//! Preprocessing for Nova. + +use ark_std::rand::RngCore; +use sonobe_primitives::{algebra::field::SonobeField, commitments::GroupBasedCommitment}; + +use crate::{Error, FoldingSchemePreprocessor, nova::AbstractNova}; + +impl FoldingSchemePreprocessor + for AbstractNova +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} diff --git a/crates/fs/src/nova/algorithms/prover.rs b/crates/fs/src/nova/algorithms/prover.rs new file mode 100644 index 000000000..3201d0c71 --- /dev/null +++ b/crates/fs/src/nova/algorithms/prover.rs @@ -0,0 +1,150 @@ +//! Proof generation for Nova. + +use ark_ff::{Field, One}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, ops::Mul, rand::RngCore}; +#[cfg(not(feature = "parallel"))] +use itertools::Itertools; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::{field::SonobeField, ops::bits::FromBits}, + arithmetizations::r1cs::R1CS, + circuits::{Assignments, AssignmentsOwned}, + commitments::GroupBasedCommitment, + transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + nova::{AbstractNova, NovaKey}, +}; + +fn cross_term<'a, F: Field>( + arith: &R1CS, + z1: impl Into>, + z2: impl Into>, + #[cfg(feature = "parallel")] e: impl IndexedParallelIterator>, + #[cfg(not(feature = "parallel"))] e: impl Iterator>, +) -> Result, Error> { + let z1 = z1.into(); + let z2 = z2.into(); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = arith.evaluate_r1cs(AssignmentsOwned::from(( + z1.constant + z2.constant, + cfg_iter!(z1.public) + .zip_eq(z2.public) + .map(|(a, b)| *a + b) + .collect(), + cfg_iter!(z1.private) + .zip_eq(z2.private) + .map(|(a, b)| *a + b) + .collect(), + )))?; + Ok(cfg_into_iter!(v) + .zip_eq(e) + .map(|(a, b)| a - b.borrow()) + .collect()) +} + +impl FoldingSchemeProver<1, 1> + for AbstractNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + let (z1, z2) = ((U.u, &U.x[..], &W.w[..]), (One::one(), &u.x[..], &w.w[..])); + let t = cross_term(&pk.arith, z1, z2, cfg_iter!(W.e))?; + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = transcript.add(&U).add(&u).add(&cm_t).challenge_bits(B); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let WW = Self::RW { + e: cfg_iter!(W.e) + .zip_eq(&t) + .map(|(a, b)| rho * b + a) + .collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w) + .zip_eq(&w.w) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + w.r_w * rho, + }; + let UU = Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip_eq(&u.x) + .map(|(a, b)| rho * b + a) + .collect(), + }; + Ok((WW, UU, cm_t)) + } +} + +impl FoldingSchemeProver<2, 0> + for AbstractNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + [W1, W2]: &[impl Borrow; 2], + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + _: &[impl Borrow; 0], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<2, 0>), Error> { + let (W1, U1) = (W1.borrow(), U1.borrow()); + let (W2, U2) = (W2.borrow(), U2.borrow()); + + let (z1, z2) = ((U1.u, &U1.x[..], &W1.w[..]), (U2.u, &U2.x[..], &W2.w[..])); + let e = cfg_iter!(W1.e).zip_eq(&W2.e).map(|(a, b)| *a + b); + let t = cross_term(&pk.arith, z1, z2, e)?; + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = transcript.add(&(U1, U2)).add(&cm_t).challenge_bits(B); + let rho = CM::Scalar::from_bits_le(&rho_bits); + let rho_squared = rho * rho; + + let WW = Self::RW { + e: cfg_iter!(W1.e) + .zip_eq(&t) + .zip_eq(&W2.e) + .map(|((a, b), c)| rho_squared * c + rho * b + a) + .collect(), + r_e: W1.r_e + r_t * rho + W2.r_e * rho_squared, + w: cfg_iter!(W1.w) + .zip_eq(&W2.w) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W1.r_w + W2.r_w * rho, + }; + let UU = Self::RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip_eq(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }; + Ok((WW, UU, cm_t)) + } +} diff --git a/crates/fs/src/nova/algorithms/verifier.rs b/crates/fs/src/nova/algorithms/verifier.rs new file mode 100644 index 000000000..9596c031f --- /dev/null +++ b/crates/fs/src/nova/algorithms/verifier.rs @@ -0,0 +1,71 @@ +//! Proof verification for Nova. + +use ark_std::{borrow::Borrow, cfg_iter, ops::Mul}; +#[cfg(not(feature = "parallel"))] +use itertools::Itertools; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::{field::SonobeField, ops::bits::FromBits}, + commitments::GroupBasedCommitment, + transcripts::Transcript, +}; + +use crate::{Error, FoldingSchemeVerifier, nova::AbstractNova}; + +impl FoldingSchemeVerifier<1, 1> + for AbstractNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm_t: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = transcript.add(&U).add(&u).add(cm_t).challenge_bits(B); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip_eq(&u.x) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + +impl FoldingSchemeVerifier<2, 0> + for AbstractNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + cm_t: &Self::Proof<2, 0>, + ) -> Result { + let (U1, U2) = (U1.borrow(), U2.borrow()); + + let rho_bits = transcript.add(&(U1, U2)).add(cm_t).challenge_bits(B); + let rho = CM::Scalar::from_bits_le(&rho_bits); + let rho_squared = rho * rho; + + Ok(Self::RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip_eq(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} diff --git a/crates/fs/src/nova/circuits/mod.rs b/crates/fs/src/nova/circuits/mod.rs new file mode 100644 index 000000000..8d54a8eb9 --- /dev/null +++ b/crates/fs/src/nova/circuits/mod.rs @@ -0,0 +1,3 @@ +//! In-circuit gadgets for Nova. + +pub mod verifier; diff --git a/crates/fs/src/nova/circuits/verifier.rs b/crates/fs/src/nova/circuits/verifier.rs new file mode 100644 index 000000000..d60dc6a81 --- /dev/null +++ b/crates/fs/src/nova/circuits/verifier.rs @@ -0,0 +1,140 @@ +//! Partial and full in-circuit verifier implementations for Nova. + +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, groups::CurveVar}; +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + algebra::ops::bits::FromBitsGadget, + commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, + transcripts::TranscriptGadget, +}; + +use crate::{ + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, nova::AbstractNovaGadget, +}; + +impl FoldingSchemePartialVerifierGadget<1, 1> for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = transcript.add(&U)?.add(&u)?.add(proof)?.challenge_bits(B)?; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + if U.x.len() != u.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness(U.cm_e.cs().or(proof.cs()).or(rho.cs()), || { + Ok(U.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + cm_w: CM::CommitmentVar::new_witness(U.cm_w.cs().or(u.cm_w.cs()).or(rho.cs()), || { + Ok(U.cm_w.value().unwrap_or_default() + + u.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} + +impl FoldingSchemePartialVerifierGadget<2, 0> for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U1, U2]: [&Self::RU; 2], + _: [&Self::IU; 0], + proof: &Self::Proof<2, 0>, + ) -> Result { + let rho_bits = transcript.add(&(U1, U2))?.add(proof)?.challenge_bits(B)?; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + if U1.x.len() != U2.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + + Ok(Self::RU { + u: (U2.u.clone() * &rho + &U1.u) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness( + U1.cm_e.cs().or(U2.cm_e.cs()).or(proof.cs()).or(rho.cs()), + || { + let rho = rho.value().unwrap_or_default(); + Ok(U1.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho + + U2.cm_e.value().unwrap_or_default() * rho * rho) + }, + )?, + cm_w: CM::CommitmentVar::new_witness( + U1.cm_w.cs().or(U2.cm_w.cs()).or(rho.cs()), + || { + Ok(U1.cm_w.value().unwrap_or_default() + + U2.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + x: U1 + .x + .iter() + .zip(&U2.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> for AbstractNovaGadget +where + CM: CommitmentDefGadget, + CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, +{ + #[allow(non_snake_case)] + fn verify( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = transcript.add(&U)?.add(&u)?.add(proof)?.challenge_bits(B)?; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + if U.x.len() != u.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: proof.scalar_mul_le(rho_bits.iter())? + &U.cm_e, + cm_w: u.cm_w.scalar_mul_le(rho_bits.iter())? + &U.cm_w, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} diff --git a/crates/fs/src/nova/instances/circuits.rs b/crates/fs/src/nova/instances/circuits.rs new file mode 100644 index 000000000..4a3b61f37 --- /dev/null +++ b/crates/fs/src/nova/instances/circuits.rs @@ -0,0 +1,225 @@ +//! In-circuit variables for Nova instances. + +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; + +use super::{IncomingInstance, RunningInstance}; +use crate::FoldingInstanceVar; + +/// [`RunningInstanceVar`] defines Nova's running instance variable. +#[derive(Clone, Debug, PartialEq)] +pub struct RunningInstanceVar { + /// [`RunningInstanceVar::cm_e`] is the error term commitment. + pub cm_e: CM::CommitmentVar, + /// [`RunningInstanceVar::u`] is the constant term. + pub u: CM::ScalarVar, + /// [`RunningInstanceVar::cm_w`] is the witness commitment. + pub cm_w: CM::CommitmentVar, + /// [`RunningInstanceVar::x`] is the vector of public inputs (to the + /// circuit). + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for RunningInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningInstance { cm_e, u, cm_w, x } = v.borrow(); + Ok(Self { + cm_e: AllocVar::new_variable(cs.clone(), || Ok(cm_e), mode)?, + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + cm_w: AllocVar::new_variable(cs.clone(), || Ok(cm_w), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningInstanceVar { + type Value = RunningInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm_e + .cs() + .or(self.u.cs()) + .or(self.cm_w.cs()) + .or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(RunningInstance { + cm_e: self.cm_e.value()?, + u: self.u.value()?, + cm_w: self.cm_w.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for RunningInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.u.absorb_into(dest)?; + self.x.absorb_into(dest)?; + self.cm_e.absorb_into(dest)?; + self.cm_w.absorb_into(dest) + } +} + +impl CondSelectGadget for RunningInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + cm_e: cond.select(&true_value.cm_e, &false_value.cm_e)?, + u: cond.select(&true_value.u, &false_value.u)?, + cm_w: cond.select(&true_value.cm_w, &false_value.cm_w)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for RunningInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm_e, &self.cm_w] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + cm_e: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_e))?, + u: AllocVar::new_witness(cs.clone(), || Ok(&u.u))?, + cm_w: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_w))?, + x, + }) + } +} + +/// [`IncomingInstanceVar`] defines Nova's incoming instance variable. +#[derive(Clone, Debug, PartialEq)] +pub struct IncomingInstanceVar { + /// [`IncomingInstanceVar::cm_w`] is the witness commitment. + pub cm_w: CM::CommitmentVar, + /// [`IncomingInstanceVar::x`] is the vector of public inputs (to the + /// circuit). + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for IncomingInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let IncomingInstance { cm_w, x } = v.borrow(); + Ok(Self { + cm_w: AllocVar::new_variable(cs.clone(), || Ok(cm_w), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for IncomingInstanceVar { + type Value = IncomingInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm_w.cs().or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(IncomingInstance { + cm_w: self.cm_w.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for IncomingInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.x.absorb_into(dest)?; + self.cm_w.absorb_into(dest) + } +} + +impl CondSelectGadget for IncomingInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + cm_w: cond.select(&true_value.cm_w, &false_value.cm_w)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for IncomingInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + cm_w: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_w))?, + x, + }) + } +} diff --git a/crates/fs/src/nova/instances/mod.rs b/crates/fs/src/nova/instances/mod.rs new file mode 100644 index 000000000..e47264ab9 --- /dev/null +++ b/crates/fs/src/nova/instances/mod.rs @@ -0,0 +1,90 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! instances. + +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, transcripts::Absorbable, + utils::dummy::Dummy, +}; + +use crate::FoldingInstance; + +pub mod circuits; + +/// [`RunningInstance`] defines Nova's running instance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningInstance { + /// [`RunningInstance::cm_e`] is the error term commitment. + pub cm_e: CM::Commitment, + /// [`RunningInstance::u`] is the constant term. + pub u: CM::Scalar, + /// [`RunningInstance::cm_w`] is the witness commitment. + pub cm_w: CM::Commitment, + /// [`RunningInstance::x`] is the vector of public inputs (to the circuit). + pub x: Vec, +} + +impl FoldingInstance for RunningInstance { + fn commitments(&self) -> Vec { + vec![self.cm_e.clone(), self.cm_w.clone()] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } +} + +impl Dummy<&ArithConfig> for RunningInstance { + fn dummy(cfg: &ArithConfig) -> Self { + Self { + cm_e: Default::default(), + u: Default::default(), + cm_w: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs], + } + } +} + +impl Absorbable for RunningInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.u.absorb_into(dest); + self.x.absorb_into(dest); + self.cm_e.absorb_into(dest); + self.cm_w.absorb_into(dest); + } +} + +/// [`IncomingInstance`] defines Nova's incoming instance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IncomingInstance { + /// [`IncomingInstance::cm_w`] is the witness commitment. + pub cm_w: CM::Commitment, + /// [`IncomingInstance::x`] is the vector of public inputs (to the circuit). + pub x: Vec, +} + +impl FoldingInstance for IncomingInstance { + fn commitments(&self) -> Vec { + vec![self.cm_w.clone()] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } +} + +impl Dummy<&ArithConfig> for IncomingInstance { + fn dummy(cfg: &ArithConfig) -> Self { + Self { + cm_w: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs], + } + } +} + +impl Absorbable for IncomingInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.x.absorb_into(dest); + self.cm_w.absorb_into(dest); + } +} diff --git a/crates/fs/src/nova/keys/circuits.rs b/crates/fs/src/nova/keys/circuits.rs new file mode 100644 index 000000000..c3c5c16d3 --- /dev/null +++ b/crates/fs/src/nova/keys/circuits.rs @@ -0,0 +1,72 @@ +use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{ + arithmetizations::{ + ArithGadget, ArithRelationGadget, + r1cs::{RelaxedInstance, RelaxedWitness}, + }, + commitments::{CommitmentDefGadget, CommitmentOpsGadget}, + relations::RelationGadget, +}; + +use super::super::{ + instances::circuits::{IncomingInstanceVar as IUVar, RunningInstanceVar as RUVar}, + witnesses::circuits::{IncomingWitnessVar as IWVar, RunningWitnessVar as RWVar}, +}; +use crate::nova::keys::NovaKey; + +#[derive(Clone)] +pub struct NovaKeyVar { + arith: A, + ck: CM::KeyVar, +} + +impl, CM: CommitmentDefGadget> + AllocVar, CM::ConstraintField> for NovaKeyVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let NovaKey { arith, ck } = v.borrow(); + Ok(Self { + arith: AllocVar::new_variable(cs.clone(), || Ok(arith.borrow()), mode)?, + ck: AllocVar::new_variable(cs.clone(), || Ok(ck.borrow()), mode)?, + }) + } +} + +impl RelationGadget, RUVar> for NovaKeyVar +where + A: for<'a> ArithRelationGadget< + RelaxedWitness<&'a [CM::ScalarVar]>, + RelaxedInstance<&'a [CM::ScalarVar]>, + >, + CM: CommitmentOpsGadget, +{ + fn check_relation(&self, w: &RWVar, u: &RUVar) -> Result<(), SynthesisError> { + self.arith.check_relation( + &RelaxedWitness { w: &w.w, e: &w.e }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &w.e, &w.r_e, &u.cm_e)?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + Ok(()) + } +} + +impl RelationGadget, IUVar> for NovaKeyVar +where + A: ArithRelationGadget, Vec>, + CM: CommitmentOpsGadget, +{ + fn check_relation(&self, w: &IWVar, u: &IUVar) -> Result<(), SynthesisError> { + self.arith.check_relation(&w.w, &u.x)?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + Ok(()) + } +} diff --git a/crates/fs/src/nova/keys/mod.rs b/crates/fs/src/nova/keys/mod.rs new file mode 100644 index 000000000..2a08a1452 --- /dev/null +++ b/crates/fs/src/nova/keys/mod.rs @@ -0,0 +1,150 @@ +//! Definitions of Nova keys and trait implementations for relation checks and +//! witness-instance sampling using Nova keys. + +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::{UniformRand, rand::RngCore, sync::Arc}; +use sonobe_primitives::{ + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{RelaxedInstance, RelaxedWitness}, + }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentOps}, + relations::{Relation, WitnessInstanceSampler}, +}; + +use super::{ + instances::{IncomingInstance as IU, RunningInstance as RU}, + witnesses::{IncomingWitness as IW, RunningWitness as RW}, +}; +use crate::{DeciderKey, Error, PlainInstance as PU, PlainWitness as PW}; + +pub mod circuits; + +/// [`NovaKey`] is Nova's decider key. +#[derive(Clone, CanonicalSerialize, CanonicalDeserialize)] +pub struct NovaKey { + pub(super) arith: Arc, + pub(super) ck: Arc, +} + +impl DeciderKey for NovaKey { + type ProverKey = Self; + type VerifierKey = (); + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> ArithConfig { + self.arith.config() + } +} + +impl Relation, RU> for NovaKey +where + A: for<'a> ArithRelation, RelaxedInstance<&'a [CM::Scalar]>>, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + self.arith.check_relation( + &RelaxedWitness { w: &w.w, e: &w.e }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + CM::open(&self.ck, &w.e, &w.r_e, &u.cm_e)?; + Ok(()) + } +} + +impl Relation, IU> for NovaKey +where + A: ArithRelation, Vec>, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &IW, u: &IU) -> Result<(), Self::Error> { + self.arith.check_relation(&w.w, &u.x)?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + Ok(()) + } +} + +impl Relation, PU> for NovaKey +where + A: ArithRelation, Vec>, + CM: CommitmentDef, +{ + type Error = Error; + + fn check_relation(&self, w: &PW, u: &PU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + Ok(()) + } +} + +impl WitnessInstanceSampler, IU> for NovaKey { + type Source = AssignmentsOwned; + type Error = Error; + + fn sample(&self, z: Self::Source, rng: impl RngCore) -> Result<(IW, IU), Error> { + let (w, x) = (z.private, z.public); + let (cm_w, r_w) = CM::commit(&self.ck, &w, rng)?; + Ok((IW { w, r_w }, IU { cm_w, x })) + } +} + +impl WitnessInstanceSampler, PU> + for NovaKey +{ + type Source = AssignmentsOwned; + type Error = Error; + + fn sample( + &self, + z: Self::Source, + _rng: impl RngCore, + ) -> Result<(PW, PU), Error> { + Ok((z.private.into(), z.public.into())) + } +} + +impl WitnessInstanceSampler, RU> for NovaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Source = (); + type Error = Error; + + fn sample(&self, _: Self::Source, mut rng: impl RngCore) -> Result<(RW, RU), Error> { + let cfg = self.arith.config(); + + let u = CM::Scalar::rand(&mut rng); + let x = (0..cfg.n_public_inputs) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let w = (0..cfg.n_witnesses) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w, e: &[] }, + &RelaxedInstance { x: &x, u: &u }, + )?; + + let (cm_w, r_w) = CM::commit(&self.ck, &w, &mut rng)?; + let (cm_e, r_e) = CM::commit(&self.ck, &e, &mut rng)?; + Ok((RW { w, r_w, e, r_e }, RU { cm_w, x, cm_e, u })) + } +} diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs new file mode 100644 index 000000000..11136fefa --- /dev/null +++ b/crates/fs/src/nova/mod.rs @@ -0,0 +1,186 @@ +//! This module implements the Nova folding scheme, which is introduced in this +//! [paper]. +//! +//! [paper]: https://eprint.iacr.org/2021/370.pdf + +use ark_r1cs_std::boolean::Boolean; +use ark_std::marker::PhantomData; +use sonobe_primitives::{ + algebra::{field::SonobeField, group::CF2}, + arithmetizations::r1cs::{R1CS, circuits::R1CSVar}, + commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, +}; + +use self::{ + instances::{ + IncomingInstance as IU, RunningInstance as RU, + circuits::{IncomingInstanceVar as IUVar, RunningInstanceVar as RUVar}, + }, + witnesses::{ + IncomingWitness as IW, RunningWitness as RW, + circuits::{IncomingWitnessVar as IWVar, RunningWitnessVar as RWVar}, + }, +}; +use crate::{ + FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondaryDef, + nova::keys::{NovaKey, circuits::NovaKeyVar}, +}; + +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod keys; +pub mod witnesses; + +// used for the RO challenges. +// From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness +// error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this +// case, we keep the size of S close to 2^128. +/// [`AbstractNova`] implements the Nova folding scheme which can operate on +/// both the primary and secondary curves. +pub struct AbstractNova { + _t: PhantomData<(CM, TF)>, +} + +/// [`Nova`] is the main Nova folding scheme on the primary curve. +pub type Nova = + AbstractNova::Scalar, CHALLENGE_BITS>; + +/// [`CycleFoldNova`] is the Nova folding scheme on the secondary curve which +/// can be used as the folding scheme for folding CycleFold instances. +pub type CycleFoldNova = + AbstractNova::Commitment>, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractNova +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = TF; + type Arith = R1CS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = NovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = CM::Commitment; +} + +/// [`AbstractNovaGadget`] is the in-circuit gadget for [`AbstractNova`]. +pub struct AbstractNovaGadget { + _vc: PhantomData, +} + +impl FoldingSchemeDefGadget + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + type Widget = AbstractNova; + + type Arith = R1CSVar; + type CM = CM; + type RW = RWVar; + type RU = RUVar; + type IW = IWVar; + type IU = IUVar; + type VerifierKey = (); + type DeciderKey = NovaKeyVar; + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = CM::CommitmentVar; +} + +impl GroupBasedFoldingSchemePrimaryDef + for AbstractNova +{ + type Gadget = AbstractNovaGadget; +} + +impl GroupBasedFoldingSchemeSecondaryDef + for AbstractNova, CHALLENGE_BITS> +{ + type Gadget = AbstractNovaGadget; +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fq, Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{ + error::Error, + rand::{RngCore, thread_rng}, + }; + use sonobe_primitives::{ + circuits::test_utils::{CircuitForTest, satisfying_assignments_for_test}, + commitments::pedersen::Pedersen, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::tests::test_folding_scheme; + + fn test_nova_opt( + rounds: usize, + mut rng: impl RngCore, + ) -> Result<(), Box> { + test_folding_scheme::, TF>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::, TF>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::, TF>, 2, 0>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::, TF>, 2, 0>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + Ok(()) + } + + #[test] + fn test_nova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_nova_opt::(10, &mut rng)?; + test_nova_opt::(10, &mut rng)?; + Ok(()) + } +} diff --git a/crates/fs/src/nova/witnesses/circuits.rs b/crates/fs/src/nova/witnesses/circuits.rs new file mode 100644 index 000000000..b85ea26f5 --- /dev/null +++ b/crates/fs/src/nova/witnesses/circuits.rs @@ -0,0 +1,109 @@ +//! In-circuit variables for Nova witnesses. + +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::commitments::CommitmentDefGadget; + +use super::{IncomingWitness, RunningWitness}; + +/// [`RunningWitnessVar`] defines Nova's running witness variable. +#[derive(Debug, PartialEq)] +pub struct RunningWitnessVar { + /// [`RunningWitnessVar::e`] is the error term. + pub e: Vec, + /// [`RunningWitnessVar::r_e`] is the randomness for the error term + /// commitment. + pub r_e: CM::RandomnessVar, + /// [`RunningWitnessVar::w`] is the vector of witnesses (to the circuit). + pub w: Vec, + /// [`RunningWitnessVar::r_w`] is the randomness for the witness commitment. + pub r_w: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for RunningWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningWitness { e, r_e, w, r_w } = v.borrow(); + Ok(Self { + e: AllocVar::new_variable(cs.clone(), || Ok(&e[..]), mode)?, + r_e: AllocVar::new_variable(cs.clone(), || Ok(r_e), mode)?, + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r_w: AllocVar::new_variable(cs.clone(), || Ok(r_w), mode)?, + }) + } +} + +impl GR1CSVar for RunningWitnessVar { + type Value = RunningWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.e + .cs() + .or(self.r_e.cs()) + .or(self.w.cs()) + .or(self.r_w.cs()) + } + + fn value(&self) -> Result { + Ok(RunningWitness { + e: self.e.value()?, + r_e: self.r_e.value()?, + w: self.w.value()?, + r_w: self.r_w.value()?, + }) + } +} + +/// [`IncomingWitnessVar`] defines Nova's incoming witness variable. +#[derive(Debug, PartialEq)] +pub struct IncomingWitnessVar { + /// [`IncomingWitnessVar::w`] is the vector of witnesses (to the circuit). + pub w: Vec, + /// [`IncomingWitnessVar::r_w`] is the randomness for the witness + /// commitment. + pub r_w: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for IncomingWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let IncomingWitness { w, r_w } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r_w: AllocVar::new_variable(cs.clone(), || Ok(r_w), mode)?, + }) + } +} + +impl GR1CSVar for IncomingWitnessVar { + type Value = IncomingWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r_w.cs()) + } + + fn value(&self) -> Result { + Ok(IncomingWitness { + w: self.w.value()?, + r_w: self.r_w.value()?, + }) + } +} diff --git a/crates/fs/src/nova/witnesses/mod.rs b/crates/fs/src/nova/witnesses/mod.rs new file mode 100644 index 000000000..2df4c86f2 --- /dev/null +++ b/crates/fs/src/nova/witnesses/mod.rs @@ -0,0 +1,56 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! witnesses. + +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, utils::dummy::Dummy, +}; + +use crate::FoldingWitness; + +pub mod circuits; + +/// [`RunningWitness`] defines Nova's running witness. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningWitness { + /// [`RunningWitness::e`] is the error term. + pub e: Vec, + /// [`RunningWitness::r_e`] is the randomness for the error term commitment. + pub r_e: CM::Randomness, + /// [`RunningWitness::w`] is the vector of witnesses (to the circuit). + pub w: Vec, + /// [`RunningWitness::r_w`] is the randomness for the witness commitment. + pub r_w: CM::Randomness, +} + +impl FoldingWitness for RunningWitness {} + +impl Dummy<&ArithConfig> for RunningWitness { + fn dummy(cfg: &ArithConfig) -> Self { + Self { + e: vec![Default::default(); cfg.n_constraints], + r_e: Default::default(), + w: vec![Default::default(); cfg.n_witnesses], + r_w: Default::default(), + } + } +} + +/// [`IncomingWitness`] defines Nova's incoming witness. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IncomingWitness { + /// [`IncomingWitness::w`] is the witness (to the circuit). + pub w: Vec, + /// [`IncomingWitness::r_w`] is the randomness for the witness commitment. + pub r_w: CM::Randomness, +} + +impl FoldingWitness for IncomingWitness {} + +impl Dummy<&ArithConfig> for IncomingWitness { + fn dummy(cfg: &ArithConfig) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses], + r_w: Default::default(), + } + } +} diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml new file mode 100644 index 000000000..0b5e8c191 --- /dev/null +++ b/crates/ivc/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "sonobe-ivc" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-bn254 = { workspace = true, features = ["curve"], optional = true } +ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } +ark-ec = { workspace = true } +ark-ff = { workspace = true, features = ["asm"] } +ark-r1cs-std = { workspace = true } +ark-relations = { workspace = true } +ark-serialize = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +askama = { workspace = true, optional = true } +num-bigint = { workspace = true, features = ["rand"] } +sha3 = { workspace = true } +thiserror = { workspace = true } + +sonobe-primitives = { workspace = true } +sonobe-fs = { workspace = true } +sonobe-snarks = { workspace = true } + +[dev-dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-grumpkin = { workspace = true, features = ["r1cs"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[features] +default = ["ark-std/print-trace", "evm"] +evm = [ + "dep:ark-bn254", + "dep:askama", + "sonobe-primitives/evm", + "sonobe-snarks/evm", +] +parallel = [ + "sonobe-fs/parallel", + "sonobe-snarks/parallel", +] \ No newline at end of file diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs new file mode 100644 index 000000000..cfbca4153 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs @@ -0,0 +1,4 @@ +//! Per-scheme adapters that implement [`super::CycleFoldCircuit`] for supported +//! folding schemes. + +pub mod nova; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs new file mode 100644 index 000000000..ce100bd60 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -0,0 +1,349 @@ +//! Nova CycleFold adapter that bridges Nova into the CycleFold IVC compiler. + +use ark_ff::{PrimeField, Zero}; +use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::{borrow::Borrow, iter::once}; +use sonobe_fs::{ + FoldingSchemeDefGadget, + nova::{CycleFoldNova, Nova}, +}; +#[cfg(feature = "evm")] +use sonobe_primitives::utils::evm::serialize::EVMSerialize; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + group::{CF1, CF2, SonobeCurve, emulated::EmulatedAffineVar}, + ops::bits::{FromBits, ToBitsGadgetExt}, + }, + circuits::WitnessToPublic, + commitments::GroupBasedCommitment, + transcripts::{ + Transcript, TranscriptGadget, + replay::{ReplayTranscript, ReplayTranscriptVar}, + }, +}; + +#[cfg(feature = "evm")] +use crate::compilers::cyclefold::evm_verifier::{DeciderFoldFragment, FoldingSchemeEVMExt}; +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// [`NovaCycleFoldCircuit`] defines CycleFold circuit for Nova. +pub struct NovaCycleFoldCircuit { + r: Vec, + points: Vec, +} + +impl Default + for NovaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); 2], + } + } +} + +impl CycleFoldCircuit> + for NovaCycleFoldCircuit +{ + fn verify_point_rlc(&self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let rho = FpVar::new_input(cs.clone(), || Ok(CF2::::from_bits_le(&self.r[..])))?; + let rho_bits = rho.to_n_bits_le(CHALLENGE_BITS)?; + + let points = Vec::::new_witness(cs.clone(), || Ok(&self.points[..]))?; + points.mark_as_public()?; + + (points[1].scalar_mul_le(rho_bits.iter())? + &points[0]).mark_as_public() + } +} + +impl FoldingSchemeCycleFoldExt<1, 1> + for Nova +{ + const N_CYCLEFOLDS: usize = 2; + + type CFCircuit = NovaCycleFoldCircuit; + + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + [U]: &[impl Borrow; 1], + [u]: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + mut transcript: ReplayTranscript>, + ) -> Vec { + let rho = transcript.challenge_bits(CHALLENGE_BITS); + vec![ + NovaCycleFoldCircuit { + r: rho.clone(), + points: vec![U.borrow().cm_e, *proof], + }, + NovaCycleFoldCircuit { + r: rho, + points: vec![U.borrow().cm_w, u.borrow().cm_w], + }, + ] + } + + #[allow(non_snake_case)] + fn to_cyclefold_inputs( + [U]: [::RU; 1], + [u]: [::IU; 1], + UU: ::RU, + proof: ::Proof<1, 1>, + mut transcript: ReplayTranscriptVar>, + ) -> Result>>>, SynthesisError> { + let mut rho = transcript.challenge_bits(CHALLENGE_BITS)?; + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + let rho = EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?; + Ok(vec![ + once(rho.clone()) + .chain( + [U.cm_e, proof, UU.cm_e] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho) + .chain( + [U.cm_w, u.cm_w, UU.cm_w] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + +impl FoldingSchemeCycleFoldExt<2, 0> + for Nova +{ + const N_CYCLEFOLDS: usize = 3; + + type CFCircuit = NovaCycleFoldCircuit; + + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + proof: &Self::Proof<2, 0>, + mut transcript: ReplayTranscript>, + ) -> Vec { + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + vec![ + NovaCycleFoldCircuit { + r: rho_bits.clone(), + points: vec![*proof, U2.borrow().cm_e], + }, + NovaCycleFoldCircuit { + r: rho_bits.clone(), + points: vec![U1.borrow().cm_e, U2.borrow().cm_e * rho + proof], + }, + NovaCycleFoldCircuit { + r: rho_bits, + points: vec![U1.borrow().cm_w, U2.borrow().cm_w], + }, + ] + } + + #[allow(non_snake_case)] + fn to_cyclefold_inputs( + [U1, U2]: [::RU; 2], + _: [::IU; 0], + UU: ::RU, + proof: ::Proof<2, 0>, + mut transcript: ReplayTranscriptVar>, + ) -> Result>>>, SynthesisError> { + let mut rho_bits = transcript.challenge_bits(CHALLENGE_BITS)?; + rho_bits.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + let rho = EmulatedFieldVar::from_bounded_bits_le( + &rho_bits, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?; + let cm_tmp = + EmulatedAffineVar::new_witness(U2.cm_e.cs().or(proof.cs()).or(rho_bits.cs()), || { + let rho_bits = rho_bits.value().unwrap_or_default(); + let rho = CM::Scalar::from_bits_le(&rho_bits); + Ok(proof.value().unwrap_or_default() + U2.cm_e.value().unwrap_or_default() * rho) + })?; + Ok(vec![ + once(rho.clone()) + .chain( + [proof, U2.cm_e, cm_tmp.clone()] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho.clone()) + .chain( + [U1.cm_e, cm_tmp, UU.cm_e] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho) + .chain( + [U1.cm_w, U2.cm_w, UU.cm_w] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + +/// [`NovaNovaIVC`] defines a CycleFold-based IVC using Nova as the primary +/// folding scheme and Nova as the secondary folding scheme. +pub type NovaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(feature = "evm")] +impl< + CM: GroupBasedCommitment, + const CHALLENGE_BITS: usize, +> FoldingSchemeEVMExt<1, 1> for Nova +{ + fn decider_fold_fragment() -> DeciderFoldFragment { + let challenge = "challenge"; + DeciderFoldFragment { + challenge: challenge.to_string(), + params: ["U_cm_e", "cm_t", "U_cm_w", "u_cm_w"] + .map(|p| format!("uint256[2] calldata {p}")) + .to_vec(), + body: [ + format!("uint256 rho = {challenge} & ((1 << {CHALLENGE_BITS}) - 1);"), + "uint256[2] memory cm_e = _ecAdd(U_cm_e, _ecMul(cm_t, rho));".to_string(), + "uint256[2] memory cm_w = _ecAdd(U_cm_w, _ecMul(u_cm_w, rho));".to_string(), + ] + .join("\n"), + commitments: ["cm_e", "cm_w"].map(ToString::to_string).to_vec(), + } + } + + #[allow(non_snake_case)] + fn verify_calldata( + [U]: &[impl Borrow; 1], + [u]: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + ) -> Result, sonobe_fs::Error> { + let (U, u) = (U.borrow(), u.borrow()); + Ok((&U.cm_e, proof, &U.cm_w, &u.cm_w).to_calldata()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Bn254, Fr, G1Projective as C1}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as C2; + use ark_std::{error::Error, rand::thread_rng, sync::Arc}; + #[cfg(feature = "evm")] + use askama::Template; + use sonobe_primitives::{ + circuits::test_utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + use sonobe_snarks::cp::legogroth16::LegoGroth16; + #[cfg(feature = "evm")] + use sonobe_snarks::cp::legogroth16::evm_verifier::LegoGroth16VerifierTemplate; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::{ + compilers::cyclefold::CycleFoldBasedIVCDecider, + tests::{test_decider, test_ivc}, + }; + #[cfg(feature = "evm")] + use crate::{ + compilers::cyclefold::evm_verifier::CycleFoldBasedIVCDeciderVerifierTemplate, + tests::test_decider_evm, + }; + + #[test] + fn test_nova_nova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + ) + } + + #[test] + fn test_nova_nova_decider() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_decider::< + CycleFoldBasedIVCDecider< + Nova>, + CycleFoldNova>, + GriffinSponge<_>, + LegoGroth16, + >, + _, + >( + (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + ) + } + + #[cfg(feature = "evm")] + #[test] + fn test_nova_nova_decider_evm() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_decider_evm::< + CycleFoldBasedIVCDecider< + Nova>, + CycleFoldNova>, + GriffinSponge, + LegoGroth16, + >, + _, + >( + (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + |(lego_vk, _, _, _, reference_state)| { + let lego_src = LegoGroth16VerifierTemplate { vk: lego_vk }.render()?; + let decider_src = CycleFoldBasedIVCDeciderVerifierTemplate::< + Nova>, + CircuitForTest, + >::new(lego_vk, reference_state) + .render()?; + Ok(vec![ + ("DeciderVerifier.sol".to_string(), decider_src), + ("LegoGroth16Verifier.sol".to_string(), lego_src), + ]) + }, + &mut rng, + ) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs new file mode 100644 index 000000000..37f4eed2c --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -0,0 +1,257 @@ +//! Augmented and CycleFold circuits for the CycleFold-based IVC compiler. + +use ark_ff::PrimeField; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::marker::PhantomData; +use sonobe_fs::{ + FoldingInstanceVar, FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, +}; +use sonobe_primitives::{ + algebra::group::SonobeCurve, + arithmetizations::ArithConfig, + circuits::{FCircuit, WitnessToPublic}, + commitments::CommitmentDef, + transcripts::{TranscriptGadget, recording::RecordingTranscriptVar}, + utils::dummy::Dummy, +}; + +use crate::compilers::cyclefold::FoldingSchemeCycleFoldExt; + +/// [`AugmentedCircuit`] defines an augmented version of the user's step circuit +/// which additionally verifies the folding proofs in-circuit. +pub struct AugmentedCircuit< + 'a, + FS1: GroupBasedFoldingSchemePrimary<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, + FC: FCircuit, + T: TranscriptGadget, +> { + _fs: PhantomData<(FS1, FS2)>, + hash_config: &'a T::Config, + arith1_config: &'a ArithConfig, + arith2_config: &'a ArithConfig, + step_circuit: &'a FC, +} + +impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> +where + FS1: GroupBasedFoldingSchemePrimary<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, + FC: FCircuit, + T: TranscriptGadget, +{ + /// [`AugmentedCircuit::new`] creates an instance of the augmented circuit + /// for the given step circuit. + pub fn new( + hash_config: &'a T::Config, + arith1_config: &'a ArithConfig, + arith2_config: &'a ArithConfig, + step_circuit: &'a FC, + ) -> Self { + Self { + _fs: PhantomData, + hash_config, + arith1_config, + arith2_config, + step_circuit, + } + } +} + +impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> +where + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FC: FCircuit::Scalar>, + T: TranscriptGadget, +{ + /// [`AugmentedCircuit::compute_next_state`] invokes the step circuit on the + /// current state and external inputs to compute the next state and external + /// outputs, and it additionally verifies the folding proofs in-circuit. + #[allow(non_snake_case, clippy::too_many_arguments)] + pub fn compute_next_state( + &self, + cs: ConstraintSystemRef, + pp_hash: FC::Field, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + external_inputs: FC::ExternalInputs, + U: &FS1::RU, + u: &FS1::IU, + proof: FS1::Proof<1, 1>, + cf_U: &FS2::RU, + cf_us: Vec, + cf_proofs: Vec>, + ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { + let hash = T::new_with_pp_hash( + self.hash_config.clone(), + &FpVar::new_witness(cs.clone(), || Ok(pp_hash))?, + )?; + let sponge = hash.separate_domain("sponge".as_ref())?; + let mut transcript = + RecordingTranscriptVar::new(hash.separate_domain("transcript".as_ref())?); + + let i = FpVar::new_witness(cs.clone(), || Ok(FC::Field::from(i as u64)))?; + let ii = &i + FpVar::one(); + + let is_basecase = i.is_zero()?; + + let initial_state = FC::StateVar::new_witness(cs.clone(), || Ok(initial_state))?; + let current_state = FC::StateVar::new_witness(cs.clone(), || Ok(current_state))?; + + let U_dummy = AllocVar::new_constant(cs.clone(), FS1::RU::dummy(self.arith1_config))?; + let U = AllocVar::new_witness(cs.clone(), || Ok(U))?; + let proof = AllocVar::new_witness(cs.clone(), || Ok(proof))?; + + let cf_U_dummy = AllocVar::new_constant(cs.clone(), FS2::RU::dummy(self.arith2_config))?; + let cf_U = AllocVar::new_witness(cs.clone(), || Ok(cf_U))?; + let cf_proofs = Vec::new_witness(cs.clone(), || Ok(cf_proofs))?; + + // 0. Check initial state consistency + initial_state.conditional_enforce_equal(¤t_state, &is_basecase)?; + + // 1. Fold primary instances. + // 1.a. Derive the public input to the primary (augmented) circuit in + // the `i-1`-th step, which is `u.x = H(i, z_0, z_i, U, cf_U)`. + let u_x = sponge + .clone() + .add(&i)? + .add(&initial_state)? + .add(¤t_state)? + .add(&U)? + .add(&cf_U)? + .get_field_element()?; + // 1.b. Construct the incoming instance `u` representing the `i-1`-th + // execution of primary (augmented) circuit with the derived public + // input. + let u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), u, vec![u_x])?; + // 1.c. Fold the primary running instance `U` and incoming instance `u` + // using the provided proof to obtain the next running instance + // `UU`. + let UU = FS1::Gadget::verify_hinted(&(), &mut transcript, [&U], [&u], &proof)?; + // 1.d. If this is the base case (`i = 0`), then we should instead use + // the dummy running instance as the next running instance. + let actual_UU = is_basecase.select(&U_dummy, &UU)?; + + // 2. Fold secondary instances. + // 2.a. Derive the public inputs to the secondary (CycleFold) + // circuits in the `i`-th step, which are obtained by calling + // the implementation of `FoldingSchemeCycleFoldExt`. + let cf_u_xs = FS1::to_cyclefold_inputs([U], [u], UU, proof, transcript.clone().into())?; + if [cf_us.len(), cf_u_xs.len(), cf_proofs.len()] != [FS1::N_CYCLEFOLDS; 3] { + return Err(SynthesisError::Unsatisfiable); + } + let mut cf_UU = cf_U; + for ((cf_u, cf_u_x), cf_proof) in cf_us.iter().zip(cf_u_xs).zip(&cf_proofs) { + // 2.b. Construct the incoming instance `cf_u` representing the + // corresponding execution of secondary (CycleFold) circuit + // with the derived public inputs. + let cf_u = + FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), cf_u, cf_u_x)?; + // 2.c. Fold the secondary incoming instance `cf_u` into the running + // instance `cf_UU` using the provided proof. + cf_UU = FS2::Gadget::verify(&(), &mut transcript, [&cf_UU], [&cf_u], cf_proof)?; + } + // 2.d. If this is the base case (`i = 0`), then we should instead use + // the dummy running instance as the next running instance. + let actual_cf_UU = is_basecase.select(&cf_U_dummy, &cf_UU)?; + + // 3. Update state by invoking the step circuit. + let (next_state, external_outputs) = + self.step_circuit + .synthesize_step(i, current_state, external_inputs)?; + + // 4. Compute public input `uu.x = H(i+1, z_0, z_{i+1}, UU, cf_UU)`. + let uu_x = sponge + .clone() + .add(&ii)? + .add(&initial_state)? + .add(&next_state)? + .add(&actual_UU)? + .add(&actual_cf_UU)? + .get_field_element()?; + uu_x.mark_as_public()?; + + if cs.is_in_setup_mode() { + Ok((self.step_circuit.dummy_state(), external_outputs)) + } else { + Ok((next_state.value()?, external_outputs)) + } + } +} + +impl<'a, FS1, FS2, FC, T> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC, T> +where + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FC: FCircuit::Scalar>, + T: TranscriptGadget, +{ + fn generate_constraints( + self, + cs: ConstraintSystemRef, + ) -> Result<(), SynthesisError> { + self.compute_next_state( + cs, + Default::default(), + 0, + &self.step_circuit.dummy_state(), + &self.step_circuit.dummy_state(), + self.step_circuit.dummy_external_inputs(), + &Dummy::dummy(self.arith1_config), + &Dummy::dummy(self.arith1_config), + Dummy::dummy(self.arith1_config), + &Dummy::dummy(self.arith2_config), + vec![Dummy::dummy(self.arith2_config); FS1::N_CYCLEFOLDS], + vec![Dummy::dummy(self.arith2_config); FS1::N_CYCLEFOLDS], + ) + .map(|_| ()) + } +} + +/// [`CycleFoldCircuit`] is the trait describing the deferred verification of +/// the folding proofs which is now expressed as a circuit on the secondary +/// curve. +pub trait CycleFoldCircuit: Sized + Default { + /// [`CycleFoldCircuit::verify_point_rlc`] verifies the deferred folding + /// proof in-circuit on the secondary curve, which is done by checking the + /// random linear combination of the commitments contained in the folding + /// instances. + fn verify_point_rlc(&self, cs: ConstraintSystemRef) -> Result<(), SynthesisError>; +} diff --git a/crates/ivc/src/compilers/cyclefold/evm_verifier.rs b/crates/ivc/src/compilers/cyclefold/evm_verifier.rs new file mode 100644 index 000000000..a67f5ffcf --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/evm_verifier.rs @@ -0,0 +1,63 @@ +//! Solidity codegen traits and templates for the on-chain decider verifier. + +use ark_bn254::Bn254; +use ark_std::{borrow::Borrow, marker::PhantomData}; +use askama::Template; +use sonobe_fs::{Error, FoldingSchemeVerifier}; +use sonobe_primitives::circuits::FCircuit; +use sonobe_snarks::cp::legogroth16::VerifierKey; + +pub struct DeciderFoldFragment { + pub challenge: String, + pub params: Vec, + pub body: String, + pub commitments: Vec, +} + +pub trait FoldingSchemeEVMExt: FoldingSchemeVerifier { + fn decider_fold_fragment() -> DeciderFoldFragment; + + #[allow(non_snake_case)] + fn verify_calldata( + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + ) -> Result, Error>; +} + +pub struct DeciderStateFragment { + pub type_name: String, + pub type_def: String, + pub shape_check_body: String, + pub flatten_body: String, +} + +pub trait FCircuitEVMExt: FCircuit { + fn decider_state_fragment(reference: &Self::State) -> DeciderStateFragment; +} + +/// [`CycleFoldBasedIVCDeciderVerifierTemplate`] is rendered as the contract +/// `DeciderVerifier` via [`askama`]. +#[derive(Template)] +#[template(path = "cyclefold_based_ivc_decider.sol.askama")] +pub struct CycleFoldBasedIVCDeciderVerifierTemplate< + 'a, + FS: FoldingSchemeEVMExt<1, 1>, + FC: FCircuitEVMExt, +> { + _t: PhantomData, + vk: &'a VerifierKey, + reference_state: &'a FC::State, +} + +impl<'a, FS: FoldingSchemeEVMExt<1, 1>, FC: FCircuitEVMExt> + CycleFoldBasedIVCDeciderVerifierTemplate<'a, FS, FC> +{ + pub fn new(vk: &'a VerifierKey, reference_state: &'a FC::State) -> Self { + Self { + _t: PhantomData, + vk, + reference_state, + } + } +} diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs new file mode 100644 index 000000000..8a28b9ff6 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -0,0 +1,875 @@ +//! Implementation of the CycleFold-based IVC compiler as described in this +//! [paper]. +//! +//! It turns any compatible folding scheme into a full IVC scheme by running the +//! primary circuit on one curve and a "CycleFold" circuit on the secondary +//! curve to handle emulated elliptic curve operations. +//! +//! [paper]: https://eprint.iacr.org/2023/1192.pdf + +use ark_ec::CurveGroup; +use ark_ff::field_hashers::hash_to_field; +use ark_r1cs_std::{ + alloc::AllocVar, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, +}; +use ark_serialize::CanonicalSerialize; +use ark_std::{ + any::TypeId, + borrow::Borrow, + io::{Error as IoError, Write}, + marker::PhantomData, + rand::RngCore, +}; +use sha3::{ + Shake128, + digest::{ExtendableOutput, Update}, +}; +use sonobe_fs::{ + DeciderKey, FoldingInstance, FoldingInstanceVar, FoldingSchemeDef, FoldingSchemeDefGadget, + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, + definitions::circuits::FoldingSchemeDeciderGadget, +}; +#[cfg(feature = "evm")] +use sonobe_primitives::utils::evm::serialize::EVMSerialize; +use sonobe_primitives::{ + algebra::{ + field::emulated::EmulatedFieldVar, + group::{CF1, CF2, SonobeCurve}, + }, + arithmetizations::{Arith, ArithConfig}, + circuits::{ + ArithExtractor, AssignmentsExtractor, FCircuit, WitnessToPublic, + cache::{CommitmentKeyCache, CommittedCache, RandomnessCache, UsizeSet}, + inputize::Inputize, + }, + commitments::{CommitmentDef, CommitmentDefGadget}, + relations::WitnessInstanceSampler, + transcripts::{ + Transcript, TranscriptGadget, + recording::{RecordingTranscript, RecordingTranscriptVar}, + replay::{ReplayTranscript, ReplayTranscriptVar}, + }, + utils::dummy::Dummy, +}; +use sonobe_snarks::cp::CPSNARK; + +use crate::{ + Error, IVCKeyGenerator, IVCPreprocessor, IVCProofCompressor, IVCProver, IVCTypes, IVCVerifier, + compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit}, +}; +#[cfg(feature = "evm")] +use crate::{IVCProofCompressorEVMExt, compilers::cyclefold::evm_verifier::FoldingSchemeEVMExt}; + +pub mod adapters; +pub mod circuits; +#[cfg(feature = "evm")] +pub mod evm_verifier; + +/// [`FoldingSchemeCycleFoldExt`] is the extension trait that a folding scheme +/// must implement to be used with the CycleFold compiler. +pub trait FoldingSchemeCycleFoldExt: + GroupBasedFoldingSchemePrimary +{ + /// [`FoldingSchemeCycleFoldExt::CFCircuit`] is the CycleFold circuit type + /// associated with the folding scheme. + type CFCircuit: CycleFoldCircuit::Commitment>>; + + /// [`FoldingSchemeCycleFoldExt::N_CYCLEFOLDS`] specifies how many CycleFold + /// operations are needed to verify the primary folding scheme's proof. + const N_CYCLEFOLDS: usize; + + /// [`FoldingSchemeCycleFoldExt::to_cyclefold_circuits`] creates CycleFold + /// circuits for verifying the point RLCs needed by the folding scheme. + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + transcript: ReplayTranscript::Commitment>>, + ) -> Vec; + + /// [`FoldingSchemeCycleFoldExt::to_cyclefold_inputs`] computes the inputs + /// to CycleFold circuits. + /// + /// This will be called by the augmented circuit on the primary curve. + #[allow(non_snake_case, clippy::type_complexity)] + fn to_cyclefold_inputs( + Us: [::RU; M], + us: [::IU; N], + UU: ::RU, + proof: ::Proof, + transcript: ReplayTranscriptVar::Commitment>>, + ) -> Result< + Vec< + Vec< + EmulatedFieldVar< + ::Scalar, + CF2<::Commitment>, + >, + >, + >, + SynthesisError, + >; +} + +/// [`Key`] is the prover / verifier key for the CycleFold-based IVC scheme. +#[derive(Clone)] +pub struct Key(pub DK1, pub DK2, pub T); + +/// [`Proof`] is the proof produced by the CycleFold compiler. +pub struct Proof( + pub FS1::RW, + pub FS1::RU, + pub FS1::IW, + pub FS1::IU, + pub FS2::RW, + pub FS2::RU, +); + +impl + Dummy<&Key> for Proof +{ + fn dummy(pk: &Key) -> Self { + let cfg1 = &pk.0.to_arith_config(); + let cfg2 = &pk.1.to_arith_config(); + Self( + FS1::RW::dummy(cfg1), + FS1::RU::dummy(cfg1), + FS1::IW::dummy(cfg1), + FS1::IU::dummy(cfg1), + FS2::RW::dummy(cfg2), + FS2::RU::dummy(cfg2), + ) + } +} + +/// [`CycleFoldBasedIVC`] is the main implementation of the IVC compiler based +/// on CycleFold. +/// +/// We consider two folding schemes `FS1` and `FS2`, where `FS1` is the folding +/// scheme on the primary curve and `FS2` is the folding scheme on the secondary +/// curve. +/// The user's step circuit is proven using `FS1`, and part of the verification +/// of `FS1`'s proof is offloaded to `FS2` using CycleFold. +/// +/// `T` is the transcript type used by the IVC prover and verifier. +pub struct CycleFoldBasedIVC { + _d: PhantomData<(FS1, FS2, T)>, +} + +impl IVCTypes for CycleFoldBasedIVC +where + FS1: FoldingSchemeCycleFoldExt<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, + T: Transcript::Commitment>>, +{ + type Field = ::Scalar; + + type Config = (FS1::Config, FS2::Config, T::Config); + + type PublicParam = (FS1::PublicParam, FS2::PublicParam, T::Config); + + type ProverKey = + Key; + + type VerifierKey = + Key; + + type Proof = Proof; +} + +impl IVCPreprocessor for CycleFoldBasedIVC +where + FS1: FoldingSchemeCycleFoldExt<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, + T: Transcript::Commitment>>, +{ + fn preprocess( + (cfg1, cfg2, hash_config): Self::Config, + mut rng: impl RngCore, + ) -> Result { + Ok(( + FS1::preprocess(cfg1, &mut rng)?, + FS2::preprocess(cfg2, &mut rng)?, + hash_config, + )) + } +} + +impl IVCKeyGenerator for CycleFoldBasedIVC +where + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Arith: From::Commitment>>>, + // TODO (@winderica): + // All folding schemes we currently support have an empty verifier + // key, so I used `()` here, but this should be generalized in the + // future. + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + T: Transcript::Commitment>, Config: CanonicalSerialize>, + T::Gadget: TranscriptGadget::Commitment>, Config = T::Config>, +{ + fn generate_keys>( + (pp1, pp2, hash_config): Self::PublicParam, + step_circuit: &FC, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { + // Run the CycleFold circuit to extract the arithmetization on the + // secondary curve. + let arith2 = { + let mut cs = ArithExtractor::new(); + cs.execute_fn(|cs| FS1::CFCircuit::default().verify_point_rlc(cs))?; + cs.arith::()? + }; + + // The augmented circuit depends on the configuration of itself. + // For instance, we are not aware of the number of constraints in the + // augmented circuit until we fix `arith1_config`, which requires us to + // provide the number of constraints in the augmented circuit. + // + // To break this circular dependency, we use a fixed-point iteration + // where we start from a default arithmetization and repeatedly update + // it until its configuration stabilizes. + let mut arith1_config = ArithConfig { + n_public_inputs: 1, + ..Default::default() + }; + let arith2_config = &arith2.config(); + + let arith1; + loop { + let new_arith1 = { + let mut cs = ArithExtractor::new(); + cs.execute_synthesizer(AugmentedCircuit::::new( + &hash_config, + &arith1_config, + arith2_config, + step_circuit, + ))?; + cs.arith::()? + }; + let new_arith1_config = new_arith1.config(); + if new_arith1_config == arith1_config { + arith1 = new_arith1; + break; + } + arith1_config = new_arith1_config; + } + + let dk1 = FS1::generate_keys(pp1, arith1)?; + let dk2 = FS2::generate_keys(pp2, arith2)?; + + struct HashMarshaller<'a>(&'a mut Shake128); + + impl Write for HashMarshaller<'_> { + #[inline] + fn write(&mut self, buf: &[u8]) -> Result { + self.0.update(buf); + Ok(buf.len()) + } + + #[inline] + fn flush(&mut self) -> Result<(), IoError> { + Ok(()) + } + } + + let pp_hash = { + let mut shake = Shake128::default(); + dk1.serialize_compressed(HashMarshaller(&mut shake))?; + dk2.serialize_compressed(HashMarshaller(&mut shake))?; + hash_config.serialize_compressed(HashMarshaller(&mut shake))?; + hash_to_field::<_, _, 128>(&mut shake.finalize_xof()) + }; + let reference_state = step_circuit.dummy_state(); + let key = Key(dk1, dk2, (hash_config, pp_hash, reference_state)); + + Ok((key.clone(), key)) + } +} + +impl IVCProver for CycleFoldBasedIVC +where + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + // TODO (@winderica): + // All folding schemes we currently support have an empty verifier + // key, so I used `()` here, but this should be generalized in the + // future. + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + T: Transcript::Commitment>>, + T::Gadget: TranscriptGadget::Commitment>, Config = T::Config>, +{ + #[allow(non_snake_case)] + fn prove>( + Key(dk1, dk2, (hash_config, pp_hash, _)): &Self::ProverKey, + step_circuit: &FC, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + external_inputs: FC::ExternalInputs, + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, + mut rng: impl RngCore, + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error> { + let hash = T::new_with_pp_hash(hash_config.clone(), *pp_hash); + let mut transcript = RecordingTranscript::new(hash.separate_domain("transcript".as_ref())); + + let arith1_config = &dk1.to_arith_config(); + let arith2_config = &dk2.to_arith_config(); + + let (mut WW, mut UU) = (Dummy::dummy(arith1_config), Dummy::dummy(arith1_config)); + let mut proof = Dummy::dummy(arith1_config); + let mut cf_us = vec![Dummy::dummy(arith2_config); FS1::N_CYCLEFOLDS]; + let mut cf_proofs = vec![Dummy::dummy(arith2_config); FS1::N_CYCLEFOLDS]; + let (mut cf_UU, mut cf_WW) = (Dummy::dummy(arith2_config), Dummy::dummy(arith2_config)); + + if i != 0 { + (WW, UU, proof) = FS1::prove( + dk1.to_pk(), + &mut transcript, + &[W], + &[U], + &[w], + &[u], + &mut rng, + )?; + + let cf_circuits = + FS1::to_cyclefold_circuits(&[U], &[u], &proof, transcript.clone().into()); + for (i, cf_circuit) in cf_circuits.into_iter().enumerate() { + let mut cs = AssignmentsExtractor::new(); + cs.execute_fn(|cs| cf_circuit.verify_point_rlc(cs))?; + + let (cf_w, cf_u) = dk2.sample(cs.assignments()?, &mut rng)?; + + (cf_WW, cf_UU, cf_proofs[i]) = FS2::prove( + dk2.to_pk(), + &mut transcript, + &[if i == 0 { cf_W } else { &cf_WW }], + &[if i == 0 { cf_U } else { &cf_UU }], + &[&cf_w], + &[&cf_u], + &mut rng, + )?; + cf_us[i] = cf_u; + } + } + + let mut cs = AssignmentsExtractor::new(); + let (next_state, external_outputs) = cs.execute_fn(|cs| { + let augmented_circuit = AugmentedCircuit::::new( + hash_config, + arith1_config, + arith2_config, + step_circuit, + ); + augmented_circuit.compute_next_state( + cs, + *pp_hash, + i, + initial_state, + current_state, + external_inputs, + U, + u, + proof, + cf_U, + cf_us, + cf_proofs, + ) + })?; + + let (ww, uu) = dk1.sample(cs.assignments()?, &mut rng)?; + + Ok(( + next_state, + external_outputs, + Proof(WW, UU, ww, uu, cf_WW, cf_UU), + )) + } +} + +impl IVCVerifier for CycleFoldBasedIVC +where + FS1: FoldingSchemeCycleFoldExt<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, + T: Transcript::Commitment>>, +{ + #[allow(non_snake_case)] + fn verify>( + Key(dk1, dk2, (hash_config, pp_hash, reference_state)): &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, + ) -> Result<(), Error> { + // Ensure the prover supplied `initial_state` and `current_state` have + // the same shape as `reference_state`'s, which is exactly what the + // augmented circuit was synthesized for. + // + // A state that merely re-groups the same flattened field elements (e.g. + // `[[x, y], []]` vs `[[x], [y]]`) has a different shape and is rejected + // here. + if !FC::same_state_shape(reference_state, initial_state) + || !FC::same_state_shape(reference_state, current_state) + { + return Err(Error::IVCVerificationFail); + } + + if i == 0 { + return (initial_state == current_state) + .then_some(()) + .ok_or(Error::IVCVerificationFail); + } + + let hash = T::new_with_pp_hash(hash_config.clone(), *pp_hash); + let mut sponge = hash.separate_domain("sponge".as_ref()); + + let u_x = sponge + .add(&i) + .add(initial_state) + .add(current_state) + .add(U) + .add(cf_U) + .get_field_element(); + + if u.public_inputs() != [u_x] { + return Err(Error::IVCVerificationFail); + } + + FS1::decide_running(dk1, W, U)?; + FS1::decide_incoming(dk1, w, u)?; + FS2::decide_running(dk2, cf_W, cf_U)?; + + Ok(()) + } +} + +pub struct CycleFoldBasedIVCDecider { + _p: PhantomData<(FS1, FS2, T, S)>, +} + +impl< + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Arith: From::Commitment>>>, + // TODO (@winderica): + // All folding schemes we currently support have an empty verifier + // key, so I used `()` here, but this should be generalized in the + // future. + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()> + + FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()> + + FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + T: Transcript< + CF1<::Commitment>, + Config: CanonicalSerialize, + Gadget: TranscriptGadget< + CF1<::Commitment>, + Config = T::Config, + >, + >, + S: CPSNARK< + Field = ::Scalar, + Relation = (FS1::Arith, UsizeSet), + CommitmentKey = ::Key, + Commitment = <::Commitment as CurveGroup>::Affine, + CommitmentOpening = ::Scalar, + Error = SynthesisError, + >, +> IVCProofCompressor for CycleFoldBasedIVCDecider +{ + type IVC = CycleFoldBasedIVC; + + type ProverKey = (S::ProverKey, ::VerifierKey); + + type VerifierKey = ( + S::VerifierKey, + ::VerifierKey, + T::Config, + ::Field, + FC::State, + ); + + type CompressedProof = ( + S::Proof, + FS1::RU, + FS1::IU, + FS1::Proof<1, 1>, + Vec<::Field>, + ); + + type Error = Error; + + fn preprocess_and_generate_keys::Field>>( + circuit: &FC, + ivc_vk: ::VerifierKey, + rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Self::Error> { + let cfg1 = &ivc_vk.0.to_arith_config(); + let cfg2 = &ivc_vk.1.to_arith_config(); + + let mut cs = ArithExtractor::new(); + { + let mut cache = cs.cache_map.borrow_mut(); + cache.insert( + TypeId::of::(), + Box::new(UsizeSet::default()), + ); + cache.insert( + TypeId::of::(), + Box::new(Vec::<::Key>::new()), + ); + } + + cs.execute_synthesizer(CycleFoldBasedIVCDeciderCircuit:: { + vk: &ivc_vk, + i: 0, + initial_state: &circuit.dummy_state(), + current_state: &circuit.dummy_state(), + proof: &Dummy::dummy(cfg1), + WW: &Dummy::dummy(cfg1), + U: &Dummy::dummy(cfg1), + u: &Dummy::dummy(cfg1), + cf_W: &Dummy::dummy(cfg2), + cf_U: &Dummy::dummy(cfg2), + })?; + let (committed_variable_indices, ck) = { + let mut cache = cs.cache_map.borrow_mut(); + let committed_variable_indices = *cache + .remove(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast::() + .map_err(|_| SynthesisError::AssignmentMissing)?; + let ck = *cache + .remove(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast::::Key>>() + .map_err(|_| SynthesisError::AssignmentMissing)?; + + (committed_variable_indices, ck) + }; + + let (pk, vk) = S::generate_keys((cs.arith()?, committed_variable_indices), &ck, rng)?; + + let vk = ( + vk, + ivc_vk.0.to_vk().clone(), + ivc_vk.2.0.clone(), + ivc_vk.2.1, + ivc_vk.2.2.clone(), + ); + + Ok(((pk, ivc_vk), vk)) + } + + fn prove::Field>>( + (pk, ivc_vk): &Self::ProverKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + Proof(W, U, w, u, cf_W, cf_U): &::Proof, + mut rng: impl RngCore, + ) -> Result, Self::Error> { + let hash = T::new_with_pp_hash(ivc_vk.2.0.clone(), ivc_vk.2.1); + // Record the transcript so the cached challenges can be returned in the + // compressed proof. + let mut transcript = RecordingTranscript::new(hash.separate_domain("transcript".as_ref())); + + let (WW, _, folding_proof) = FS1::prove( + ivc_vk.0.to_pk(), + &mut transcript, + &[W], + &[U], + &[w], + &[u], + &mut rng, + )?; + + let mut cs = AssignmentsExtractor::new(); + { + let mut cache = cs.cache_map.borrow_mut(); + cache.insert( + TypeId::of::(), + Box::new(UsizeSet::default()), + ); + cache.insert( + TypeId::of::(), + Box::new(Vec::<::Field>::new()), + ); + } + + cs.execute_synthesizer(CycleFoldBasedIVCDeciderCircuit:: { + vk: ivc_vk, + i, + initial_state, + current_state, + proof: &folding_proof, + WW: &WW, + U, + u, + cf_W, + cf_U, + })?; + let w = &cs.assignments.witness_assignment; + let x = &cs.assignments.instance_assignment; + let mut cache = cs.cache_map.borrow_mut(); + let o = *cache + .remove(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast::::Field>>() + .map_err(|_| SynthesisError::AssignmentMissing)?; + + let compressed_proof = S::prove(pk, &x[1..], w, &o, &mut rng)?; + + Ok(( + compressed_proof, + U.clone(), + u.clone(), + folding_proof, + transcript.cached_challenges, + )) + } + + fn verify::Field>>( + (vk, folding_vk, hash_config, pp_hash, reference_state): &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + (compressed_proof, U, u, folding_proof, challenges): &Self::CompressedProof, + ) -> Result<(), Self::Error> { + if !FC::same_state_shape(reference_state, initial_state) + || !FC::same_state_shape(reference_state, current_state) + { + return Err(Error::IVCVerificationFail); + } + + if i == 0 { + return (initial_state == current_state) + .then_some(()) + .ok_or(Error::IVCVerificationFail); + } + + let hash = T::new_with_pp_hash(hash_config.clone(), *pp_hash); + let mut transcript = hash.separate_domain("transcript".as_ref()); + + let UU = FS1::verify(folding_vk, &mut transcript, &[U], &[u], folding_proof)?; + let commitments = UU.commitments(); + + let x = &[ + vec![::Field::from(i as u64)], + FC::StateVar::inputize(initial_state), + FC::StateVar::inputize(current_state), + challenges.clone(), + commitments.iter().flat_map(<::CM as CommitmentDefGadget>::CommitmentVar::inputize).collect::>() + ] + .concat(); + let c = CurveGroup::normalize_batch(&commitments); + + S::verify(vk, x, &c, compressed_proof)?; + + Ok(()) + } +} + +#[cfg(feature = "evm")] +impl< + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()> + + FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Scalar: EVMSerialize, + Commitment: SonobeCurve::Scalar>, + >, + > + FoldingSchemeEVMExt<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()> + + FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + T: Transcript< + CF1<::Commitment>, + Config: CanonicalSerialize, + Gadget: TranscriptGadget< + CF1<::Commitment>, + Config = T::Config, + >, + >, + S: CPSNARK< + Field = ::Scalar, + Relation = (FS1::Arith, UsizeSet), + CommitmentKey = ::Key, + Commitment = <::Commitment as CurveGroup>::Affine, + CommitmentOpening = ::Scalar, + Proof: EVMSerialize, + Error = SynthesisError, + >, +> IVCProofCompressorEVMExt for CycleFoldBasedIVCDecider +{ + fn verify_calldata::Field>>( + (_vk, _folding_vk, _hash_config, _pp_hash, _reference_state): &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + (compressed_proof, U, u, folding_proof, challenges): &Self::CompressedProof, + ) -> Result, Self::Error> { + Ok(( + vec![::Field::from(i as u64)], + FC::StateVar::inputize(initial_state), + FC::StateVar::inputize(current_state), + &challenges, + FS1::verify_calldata(&[U], &[u], folding_proof)?, + compressed_proof, + ) + .to_calldata()) + } +} + +pub struct CycleFoldBasedIVCDeciderCircuit< + 'a, + FS1: FoldingSchemeDef, + FS2: FoldingSchemeDef, + T: Transcript, + FC: FCircuit, +> { + vk: &'a Key, + i: usize, + initial_state: &'a FC::State, + current_state: &'a FC::State, + proof: &'a FS1::Proof<1, 1>, + WW: &'a FS1::RW, + U: &'a FS1::RU, + u: &'a FS1::IU, + cf_W: &'a FS2::RW, + cf_U: &'a FS2::RU, +} + +impl< + 'a, + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()> + + FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeDeciderGadget, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, + >, + T: Transcript< + CF1<::Commitment>, + Gadget: TranscriptGadget< + CF1<::Commitment>, + Config = T::Config, + >, + >, + FC: FCircuit::Commitment>>, +> ConstraintSynthesizer for CycleFoldBasedIVCDeciderCircuit<'a, FS1, FS2, T, FC> +{ + fn generate_constraints( + self, + cs: ConstraintSystemRef, + ) -> Result<(), SynthesisError> { + let i = FpVar::new_input(cs.clone(), || Ok(FC::Field::from(self.i as u64)))?; + let initial_state = FC::StateVar::new_input(cs.clone(), || Ok(self.initial_state))?; + let current_state = FC::StateVar::new_input(cs.clone(), || Ok(self.current_state))?; + + let Key(dk1, dk2, (hash_config, pp_hash, _)) = &self.vk; + let dk1 = AllocVar::new_constant(cs.clone(), dk1)?; + let dk2 = AllocVar::new_constant(cs.clone(), dk2)?; + let pp_hash = FpVar::new_constant(cs.clone(), pp_hash)?; + + let WW = AllocVar::new_witness(cs.clone(), || Ok(self.WW))?; + let U = AllocVar::new_witness(cs.clone(), || Ok(self.U))?; + let u = AllocVar::new_witness(cs.clone(), || Ok(self.u))?; + let cf_W = AllocVar::new_witness(cs.clone(), || Ok(self.cf_W))?; + let cf_U = AllocVar::new_witness(cs.clone(), || Ok(self.cf_U))?; + let proof = AllocVar::new_witness(cs.clone(), || Ok(self.proof))?; + + i.enforce_not_equal(&FpVar::zero())?; + + let hash = T::Gadget::new_with_pp_hash(hash_config.clone(), &pp_hash)?; + let mut sponge = hash.separate_domain("sponge".as_ref())?; + let mut transcript = + RecordingTranscriptVar::new(hash.separate_domain("transcript".as_ref())?); + + let UU = FS1::Gadget::verify_hinted(&(), &mut transcript, [&U], [&u], &proof)?; + + transcript.cached_challenges.mark_as_public()?; + + FS1::Gadget::decide_running(&dk1, &WW, &UU)?; + FS2::Gadget::decide_running(&dk2, &cf_W, &cf_U)?; + + let u_x = sponge + .add(&i)? + .add(&initial_state)? + .add(¤t_state)? + .add(&U)? + .add(&cf_U)? + .get_field_element()?; + + u.public_inputs().enforce_equal(&vec![u_x])?; + + Ok(()) + } +} diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs new file mode 100644 index 000000000..95113ac8c --- /dev/null +++ b/crates/ivc/src/compilers/mod.rs @@ -0,0 +1,7 @@ +//! Compilers that transform a folding scheme into a full IVC scheme. +//! +//! We currently provide a compiler based on CycleFold, and in the future there +//! may be other compilers such as the naive one (on a single curve) which fits +//! well with hash-based folding schemes and the two curves one. + +pub mod cyclefold; diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs new file mode 100644 index 000000000..fd144be52 --- /dev/null +++ b/crates/ivc/src/lib.rs @@ -0,0 +1,717 @@ +#![warn(missing_docs)] + +//! Incremental Verifiable Computation (IVC) abstractions. +//! +//! This crate provides the [`IVC`] trait, which describes the common +//! interface for all IVC constructions, and [compilers] that turn a folding +//! scheme into a full IVC scheme. + +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; +use ark_serialize::SerializationError; +use ark_std::{error::Error as ErrorTrait, rand::RngCore}; +use sonobe_fs::Error as FoldingError; +use sonobe_primitives::{ + arithmetizations::Error as ArithError, circuits::FCircuit, utils::dummy::Dummy, +}; +use thiserror::Error; + +pub mod compilers; + +/// [`Error`] enumerates possible errors during the IVC operations. +#[derive(Debug, Error)] +pub enum Error { + /// [`Error::ArithError`] indicates an error from the underlying constraint + /// system. + #[error(transparent)] + ArithError(#[from] ArithError), + /// [`Error::SerializationError`] indicates an error during serialization. + #[error(transparent)] + SerializationError(#[from] SerializationError), + /// [`Error::FoldingError`] indicates an error from the underlying folding + /// scheme. + #[error(transparent)] + FoldingError(#[from] FoldingError), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. + #[error(transparent)] + SynthesisError(#[from] SynthesisError), + /// [`Error::IVCVerificationFail`] indicates that the IVC verification has + /// failed. + #[error("IVC verification failed")] + IVCVerificationFail, +} + +/// [`IVCTypes`] defines the associated types for an IVC scheme. +pub trait IVCTypes { + /// [`IVCTypes::Field`] defines the field over which the IVC scheme operates. + type Field: PrimeField; + + /// [`IVCTypes::Config`] defines the configuration of IVC. + /// + /// ### Examples + /// + /// In folding-based IVC schemes, this is usually the configuration of the + /// underlying folding scheme. + type Config; + + /// [`IVCTypes::PublicParam`] defines the public parameters of IVC. + type PublicParam; + + /// [`IVCTypes::ProverKey`] defines the prover key of IVC. + /// + /// ### Design Rationale + /// + /// It is parameterized by the step circuit type `FC`, because the one + /// prover key can only be used for one step circuit, and different step + /// circuits require different prover keys. + /// With `FC`, we can prevent the misuse of keys on the type level. + type ProverKey; + + /// [`IVCTypes::VerifierKey`] defines the verifier key of IVC. + /// + /// ### Design Rationale + /// + /// It is parameterized by the step circuit type `FC`, because the one + /// verifier key can only be used for one step circuit, and different step + /// circuits require different verifier keys. + /// With `FC`, we can prevent the misuse of keys on the type level. + type VerifierKey; + + /// [`IVCTypes::Proof`] defines the proof of IVC. + /// + /// ### Design Rationale + /// + /// It is parameterized by the step circuit type `FC`, because it might be + /// problematic if the prover generates a proof for one step circuit but the + /// verifier expects a proof for another step circuit. + /// With `FC`, we can prevent such inconsistencies on the type level. + /// + /// It should also implement `Dummy`, so that we can generate an initial + /// proof from the prover key in a uniform way across different IVC schemes. + type Proof: for<'a> Dummy<&'a Self::ProverKey>; +} + +/// [`IVCPreprocessor`] defines the preprocessing algorithm of IVC. +pub trait IVCPreprocessor: IVCTypes { + /// [`IVCPreprocessor::preprocess`] is a randomized algorithm that generates + /// public parameters for the IVC scheme under a given configuration. + /// + /// ### Function Signature + /// + /// [`IVCPreprocessor::preprocess`] takes as input + /// - `config`: the configuration for the IVC scheme, and + /// - `rng`: the randomness source. + /// + /// It returns + /// - an error if the preprocessing fails, or + /// - `Ok(pp)` otherwise, where + /// - `pp`: the public parameters. + /// + /// ### Usage + /// + /// See [`IVC`] for an example of how to use this method in the context of a + /// full IVC workflow. + /// + /// ### Notes + /// + /// - The security parameter is implicitly specified by the underlying + /// mathematical structures (e.g., field/group orders) and the algorithms + /// involved in the implementation. + /// - This is usually called once for one configuration + /// - The same public parameters can be reused for different step circuits, + /// as long as they conform to the configuration. + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; +} + +/// [`IVCKeyGenerator`] defines the key generation algorithm of IVC. +pub trait IVCKeyGenerator: IVCTypes { + /// [`IVCKeyGenerator::generate_keys`] is a deterministic algorithm that + /// generates a pair of prover and verifier keys for a given step circuit. + /// + /// ### Function Signature + /// + /// [`IVCKeyGenerator::generate_keys`] takes as input + /// - `pp`: the public parameters, and + /// - `step_circuit`: the step circuit. + /// + /// It outputs + /// - an error if the key generation fails, or + /// - `Ok((pk, vk))` otherwise, where + /// - `pk`: the prover key, and + /// - `vk`: the verifier key. + /// + /// ### Usage + /// + /// See [`IVC`] for an example of how to use this method in the context of a + /// full IVC workflow. + /// + /// ### Notes + /// + /// - This is usually called once for one step circuit. + /// - The same prover and verifier keys can be reused for different initial + /// states and external inputs, as long as the step circuit is the same. + #[allow(clippy::type_complexity)] + fn generate_keys>( + pp: Self::PublicParam, + step_circuit: &FC, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; +} + +/// [`IVCProver`] defines the proof generation algorithm of IVC. +pub trait IVCProver: IVCTypes { + /// [`IVCProver::prove`] is a (probably) randomized algorithm that proves + /// the (next) state is correctly derived from the initial state after + /// invoking the step circuit for the claimed number of steps. + /// Proof generation is done by executing the step circuit on the current + /// state to obtain the next state, and then updating an existing proof for + /// the current state to a new proof for the next state. + /// + /// ### Function Signature + /// + /// [`IVCProver::prove`] takes as input + /// - `pk`: the prover key, + /// - `step_circuit`: the step circuit, + /// - `i`: the current step, + /// - `initial_state`: the initial state, + /// - `current_state`: the current state, + /// - `external_inputs`: the external inputs, + /// - `current_proof`: the current proof attesting that `current_state` is + /// correctly derived from `initial_state` after `i` executions of + /// `step_circuit`, and + /// - `rng`: the randomness source. + /// + /// It outputs + /// - an error if the proof generation fails, or + /// - `Ok((next_state, external_outputs, next_proof))` otherwise, where + /// - `next_state`: the next state, + /// - `external_outputs`: the external outputs, and + /// - `next_proof`: the next proof attesting that `next_state` is + /// correctly derived from `initial_state` after `i+1` executions of + /// `step_circuit`. + /// + /// ### Usage + /// + /// See [`IVC`] for an example of how to use this method in the context of a + /// full IVC workflow. + /// + /// ### Design Rationale + /// + /// `external_inputs` is needed by [`IVCProver::prove`] since it is one of + /// the inputs to [`FCircuit::synthesize_step`]. + /// + /// Similarly, [`FCircuit::synthesize_step`] returns `external_outputs`, and + /// it needs to be returned by [`IVCProver::prove`] so that the caller can + /// use it. + #[allow(clippy::type_complexity, clippy::too_many_arguments)] + fn prove>( + pk: &Self::ProverKey, + step_circuit: &FC, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + external_inputs: FC::ExternalInputs, + current_proof: &Self::Proof, + rng: impl RngCore, + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; +} + +/// [`IVCVerifier`] defines the proof verification algorithm of IVC. +pub trait IVCVerifier: IVCTypes { + /// [`IVCVerifier::verify`] is a deterministic algorithm that checks the + /// current state is correctly derived from the initial state after invoking + /// the step circuit for the claimed number of steps, by verifying the IVC + /// proof. + /// + /// ### Function Signature + /// + /// [`IVCVerifier::verify`] takes as input + /// - `vk`: the verifier key, + /// - `i`: the current step, + /// - `initial_state`: the initial state, + /// - `current_state`: the current state, and + /// - `proof`: the proof. + /// + /// It outputs + /// - an error if the proof is invalid, or + /// - `Ok(())` otherwise. + /// + /// ### Usage + /// + /// See [`IVC`] for an example of how to use this method in the context of a + /// full IVC workflow. + fn verify>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + proof: &Self::Proof, + ) -> Result<(), Error>; +} + +/// [`IVCOps`] is a convenience super-trait bundling all algorithms. +pub trait IVCOps: IVCPreprocessor + IVCKeyGenerator + IVCProver + IVCVerifier {} + +impl IVCOps for I {} + +/// [`IVC`] is the main trait for an Incrementally Verifiable Computation (IVC) +/// scheme, which includes the type definitions and all the algorithms. +/// +/// ### Usage +/// +/// A concrete IVC scheme `I` that implements [`IVC`] can usually be used in the +/// following way: +/// +/// ```rust +/// use ark_std::rand::Rng; +/// use sonobe_ivc::{Error, IVC}; +/// use sonobe_primitives::{circuits::FCircuit, utils::dummy::Dummy}; +/// +/// fn ivc_usage>( +/// config: I::Config, +/// step_circuit: F, +/// initial_state: F::State, +/// external_inputs_vec: Vec, +/// mut rng: impl Rng, +/// ) -> Result<(), Error> { +/// let n_steps = external_inputs_vec.len(); +/// +/// // 1. Generate public parameters. +/// let pp = I::preprocess(config, &mut rng)?; +/// +/// // 2. Generate prover key and verifier key for the step circuit. +/// let (pk, vk) = I::generate_keys(pp, &step_circuit)?; +/// +/// let mut current_state = initial_state.clone(); +/// let mut current_proof = I::Proof::dummy(&pk); +/// +/// for (i, external_inputs) in external_inputs_vec.into_iter().enumerate() { +/// // 3. Generate the new state and proof from the current state and +/// // proof. +/// let (next_state, external_outputs, next_proof) = I::prove( +/// &pk, +/// &step_circuit, +/// i, +/// &initial_state, +/// ¤t_state, +/// external_inputs, +/// ¤t_proof, +/// &mut rng, +/// )?; +/// current_state = next_state; +/// current_proof = next_proof; +/// } +/// +/// // 4. Verify the final state and proof. +/// I::verify(&vk, n_steps, &initial_state, ¤t_state, ¤t_proof)?; +/// +/// Ok(()) +/// } +/// ``` +pub trait IVC: IVCTypes + IVCOps {} + +impl IVC for I {} + +pub trait IVCTypesGadget { + type Widget: IVCTypes; + + type VerifierKey; + + type ProofVar; +} + +/// [`IVCVerifier`] defines the proof verification algorithm of IVC. +pub trait IVCVerifierGadget: IVCTypesGadget { + fn verify( + vk: &Self::VerifierKey, + i: FpVar, + initial_state: &FC::StateVar, + current_state: &FC::StateVar, + proof: &Self::ProofVar, + ) -> Result<(), Error>; +} + +/// [`IVCStatefulProver`] is a convenience struct that implements a stateful IVC +/// prover who maintains running state across iterations, so that the user does +/// not need to manually track and pass in the current state and proof at each +/// step. +/// +/// ### Usage +/// +/// With [`IVCStatefulProver`], the IVC workflow can be simplified as follows: +/// +/// ```rust +/// use ark_std::rand::Rng; +/// use sonobe_ivc::{Error, IVC, IVCStatefulProver}; +/// use sonobe_primitives::{circuits::FCircuit, utils::dummy::Dummy}; +/// +/// fn ivc_usage>( +/// config: I::Config, +/// step_circuit: F, +/// initial_state: F::State, +/// external_inputs_vec: Vec, +/// mut rng: impl Rng, +/// ) -> Result<(), Error> { +/// let n_steps = external_inputs_vec.len(); +/// +/// // 1. Generate public parameters. +/// let pp = I::preprocess(config, &mut rng)?; +/// +/// // 2. Generate prover key and verifier key for the step circuit. +/// let (pk, vk) = I::generate_keys(pp, &step_circuit)?; +/// +/// let mut prover = IVCStatefulProver::<_, I>::new(&pk, &step_circuit, initial_state)?; +/// +/// for external_inputs in external_inputs_vec { +/// // 3. Generate the new state and proof from the current state and +/// // proof. +/// prover.prove_step(external_inputs, &mut rng)?; +/// } +/// +/// // 4. Verify the final state and proof. +/// I::verify( +/// &vk, +/// prover.i, +/// &prover.initial_state, +/// &prover.current_state, +/// &prover.current_proof, +/// )?; +/// +/// Ok(()) +/// } +/// ``` +pub struct IVCStatefulProver<'a, FC: FCircuit, I: IVC> { + pk: &'a I::ProverKey, + step_circuit: &'a FC, + pub i: usize, + pub initial_state: FC::State, + pub current_state: FC::State, + pub current_proof: I::Proof, +} + +impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { + /// [`IVCStatefulProver::new`] creates a new stateful IVC prover with the + /// given prover key `pk`, step circuit `step_circuit`, and initial state + /// `initial_state`. + pub fn new( + pk: &'a I::ProverKey, + step_circuit: &'a FC, + initial_state: FC::State, + ) -> Result { + Ok(Self { + step_circuit, + i: 0, + current_state: initial_state.clone(), + initial_state, + current_proof: I::Proof::dummy(pk), + pk, + }) + } + + /// [`IVCStatefulProver::prove_step`] performs one step of proving, updating + /// the internal state and proof, and returning the external outputs. + pub fn prove_step( + &mut self, + external_inputs: FC::ExternalInputs, + rng: impl RngCore, + ) -> Result { + let (next_state, external_outputs, next_proof) = I::prove( + self.pk, + self.step_circuit, + self.i, + &self.initial_state, + &self.current_state, + external_inputs, + &self.current_proof, + rng, + )?; + self.i += 1; + self.current_state = next_state; + self.current_proof = next_proof; + Ok(external_outputs) + } +} + +pub trait IVCProofCompressor { + /// [`IVCProofCompressor::IVC`] defines the underlying IVC scheme that the decider + /// compiles. + type IVC: IVC; + + /// [`IVCProofCompressor::ProverKey`] defines the prover key type for the decider. + type ProverKey; + /// [`IVCProofCompressor::VerifierKey`] defines the verifier key type for the decider. + type VerifierKey; + type CompressedProof; + + type Error: ErrorTrait + 'static; + + fn preprocess_and_generate_keys::Field>>( + circuit: &FC, + ivc_vk: ::VerifierKey, + rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Self::Error>; + + fn prove::Field>>( + pk: &Self::ProverKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + proof: &::Proof, + rng: impl RngCore, + ) -> Result, Self::Error>; + + fn verify::Field>>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + compressed_proof: &Self::CompressedProof, + ) -> Result<(), Self::Error>; +} + +/// [`IVCProofCompressorEVMExt`] extends [`IVCProofCompressor`] with the ability +/// to produce the calldata for the decider's EVM verifier contract. +#[cfg(feature = "evm")] +pub trait IVCProofCompressorEVMExt: IVCProofCompressor { + /// [`IVCProofCompressorEVMExt::verify_calldata`] builds the calldata for + /// the EVM verifier from the same inputs as [`IVCProofCompressor::verify`]. + fn verify_calldata::Field>>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + compressed_proof: &Self::CompressedProof, + ) -> Result, Self::Error>; +} + +#[cfg(test)] +mod tests { + use ark_std::{error::Error, rand::Rng}; + #[cfg(feature = "evm")] + use sonobe_primitives::{ + algebra::field::SonobeField, + circuits::test_utils::CircuitForTest, + utils::evm::{compiler::SolidityCompiler, harness::TestEVM}, + }; + + use super::*; + #[cfg(feature = "evm")] + use crate::compilers::cyclefold::evm_verifier::{DeciderStateFragment, FCircuitEVMExt}; + + #[cfg(feature = "evm")] + impl FCircuitEVMExt for CircuitForTest { + fn decider_state_fragment(_: &Self::State) -> DeciderStateFragment { + DeciderStateFragment { + // `[F; 1]` is represented onchain as a fixed-size `uint256[1]` + type_name: "uint256[1]".to_string(), + // No custom struct + type_def: String::new(), + // The array length already pins the shape + shape_check_body: String::new(), + // Already flat + flatten_body: "return z;".to_string(), + } + } + } + + fn test_manual_state_management>( + pk: &I::ProverKey, + vk: &I::VerifierKey, + step_circuit: &F, + initial_state: F::State, + external_inputs_vec: Vec, + mut rng: impl Rng, + ) -> Result<(F::State, I::Proof), Box> { + let mut current_state = initial_state.clone(); + let mut current_proof = I::Proof::dummy(pk); + + I::verify(vk, 0, &initial_state, ¤t_state, ¤t_proof)?; + + for (i, external_inputs) in external_inputs_vec.into_iter().enumerate() { + let (next_state, _, next_proof) = I::prove( + pk, + step_circuit, + i, + &initial_state, + ¤t_state, + external_inputs, + ¤t_proof, + &mut rng, + )?; + current_state = next_state; + current_proof = next_proof; + + I::verify(vk, i + 1, &initial_state, ¤t_state, ¤t_proof)?; + } + + Ok((current_state, current_proof)) + } + + fn test_auto_state_management>( + pk: &I::ProverKey, + vk: &I::VerifierKey, + step_circuit: &F, + initial_state: F::State, + external_inputs_vec: Vec, + mut rng: impl Rng, + ) -> Result<(F::State, I::Proof), Box> { + let mut prover = IVCStatefulProver::<_, I>::new(pk, step_circuit, initial_state)?; + + I::verify( + vk, + prover.i, + &prover.initial_state, + &prover.current_state, + &prover.current_proof, + )?; + + for external_inputs in external_inputs_vec { + prover.prove_step(external_inputs, &mut rng)?; + + I::verify( + vk, + prover.i, + &prover.initial_state, + &prover.current_state, + &prover.current_proof, + )?; + } + + Ok((prover.current_state, prover.current_proof)) + } + + pub fn test_ivc>( + config: I::Config, + step_circuit: F, + external_inputs_vec: Vec, + mut rng: impl Rng, + ) -> Result<(), Box> { + let pp = I::preprocess(config, &mut rng)?; + + let (pk, vk) = I::generate_keys(pp, &step_circuit)?; + + let initial_state = step_circuit.dummy_state(); + + test_auto_state_management::( + &pk, + &vk, + &step_circuit, + initial_state.clone(), + external_inputs_vec.clone(), + &mut rng, + )?; + + test_manual_state_management::( + &pk, + &vk, + &step_circuit, + initial_state, + external_inputs_vec, + &mut rng, + )?; + + Ok(()) + } + + pub fn test_decider< + D: IVCProofCompressor, + F: FCircuit::Field, ExternalInputs: Clone>, + >( + config: ::Config, + step_circuit: F, + external_inputs_vec: Vec, + mut rng: impl Rng, + ) -> Result<(), Box> { + let n = external_inputs_vec.len(); + + let pp = D::IVC::preprocess(config, &mut rng)?; + + let (pk, vk) = D::IVC::generate_keys(pp, &step_circuit)?; + + let initial_state = step_circuit.dummy_state(); + + let (current_state, current_proof) = test_auto_state_management::( + &pk, + &vk, + &step_circuit, + initial_state.clone(), + external_inputs_vec, + &mut rng, + )?; + + let (pk, vk) = D::preprocess_and_generate_keys(&step_circuit, vk, &mut rng)?; + + let proof = D::prove( + &pk, + n, + &initial_state, + ¤t_state, + ¤t_proof, + &mut rng, + )?; + + D::verify(&vk, n, &initial_state, ¤t_state, &proof)?; + + Ok(()) + } + + #[cfg(feature = "evm")] + pub fn test_decider_evm< + D: IVCProofCompressorEVMExt, + F: FCircuit::Field, ExternalInputs: Clone>, + >( + config: ::Config, + step_circuit: F, + external_inputs_vec: Vec, + sources_generator: impl Fn(&D::VerifierKey) -> Result, Box>, + mut rng: impl Rng, + ) -> Result<(), Box> { + let n = external_inputs_vec.len(); + + let pp = D::IVC::preprocess(config, &mut rng)?; + + let (pk, vk) = D::IVC::generate_keys(pp, &step_circuit)?; + + let initial_state = step_circuit.dummy_state(); + + let (current_state, current_proof) = test_auto_state_management::( + &pk, + &vk, + &step_circuit, + initial_state.clone(), + external_inputs_vec, + &mut rng, + )?; + + let (pk, vk) = D::preprocess_and_generate_keys(&step_circuit, vk, &mut rng)?; + + let proof = D::prove( + &pk, + n, + &initial_state, + ¤t_state, + ¤t_proof, + &mut rng, + )?; + + let solc = SolidityCompiler::default(); + assert!(solc.available()); + + let (bytecode, selectors) = solc.compile(sources_generator(&vk)?, "DeciderVerifier")?; + let mut evm = TestEVM::default(); + let addr = evm.deploy(&bytecode, ())?.unwrap(); + let sig = *selectors.get("verifyDeciderProof").unwrap(); + + assert!( + evm.view( + addr, + sig, + &D::verify_calldata::(&vk, n, &initial_state, ¤t_state, &proof)? + )? + .is_success() + ); + + Ok(()) + } +} diff --git a/crates/ivc/templates/cyclefold_based_ivc_decider.sol.askama b/crates/ivc/templates/cyclefold_based_ivc_decider.sol.askama new file mode 100644 index 000000000..8736e7d8d --- /dev/null +++ b/crates/ivc/templates/cyclefold_based_ivc_decider.sol.askama @@ -0,0 +1,107 @@ +{%- let numCommitments = vk.link_vk.c.len() - 1 -%} +{%- let numPublicInputs = vk.cc_vk.gamma_abc_g1_pub.len() - 1 -%} +{%- let stateLen = (numPublicInputs - 1 - numCommitments * 16 - 1) / 2 %} +{%- let fold = FS::decider_fold_fragment() %} +{%- let state = FC::decider_state_fragment(&reference_state) %} +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.35; + +import "LegoGroth16Verifier.sol"; + +{{ state.type_def|safe }} + +contract DeciderVerifier is LegoGroth16Verifier { + error PointRLCFailed(); + error BaseCaseMismatch(); + error StateShapeMismatch(); + + function verifyDeciderProof( + uint256 i, + {{ state.type_name }} calldata z_0, + {{ state.type_name }} calldata z_i, + uint256 {{ fold.challenge }}, + {%- for p in fold.params %} + {{ p }}, + {%- endfor %} + uint256[12] calldata proof + ) public view { + _checkStateShape(z_0); + _checkStateShape(z_i); + uint256[{{ stateLen }}] memory z_0_flattened = _flattenState(z_0); + uint256[{{ stateLen }}] memory z_i_flattened = _flattenState(z_i); + + if (i == 0) { + for (uint256 k = 0; k < {{ stateLen }}; k++) { + if (z_0_flattened[k] != z_i_flattened[k]) { + revert BaseCaseMismatch(); + } + } + return; + } + + // Scheme-emitted point RLC, which computes the folded commitments from + // the unfolded ones and the challenge. + {{ fold.body|indent(8)|safe }} + + uint256[{{ numCommitments * 2 }}] memory c = [ + {%- for cm in fold.commitments %} + {{ cm }}[0], {{ cm }}[1]{% if !loop.last %},{% endif %} + {%- endfor %} + ]; + + // x = [i, z_0.., z_i.., challenge, inputize(cm)]. + uint256[{{ numPublicInputs }}] memory x; + x[0] = i; + for (uint256 k = 0; k < {{ stateLen }}; k++) { + x[1 + k] = z_0_flattened[k]; + x[1 + {{ stateLen }} + k] = z_i_flattened[k]; + } + x[{{ 1 + 2 * stateLen }}] = {{ fold.challenge }}; + for (uint256 i = 0; i < {{ numCommitments * 2 }}; i++) { + for (uint256 k = 0; k < 8; k++) { + x[{{ 1 + 2 * stateLen + 1 }} + i * 8 + k] = (c[i] >> (32 * k)) & 0xFFFFFFFF; + } + } + + this.verifyProof(x, c, proof); + } + + function _checkStateShape({{ state.type_name }} calldata z) internal pure { + {{ state.shape_check_body|indent(8)|safe }} + } + + function _flattenState({{ state.type_name }} calldata z) internal pure returns (uint256[{{ stateLen }}] memory) { + {{ state.flatten_body|indent(8)|safe }} + } + + function _ecMul(uint256[2] calldata p, uint256 s) + internal + view + returns (uint256[2] memory r) + { + uint256[3] memory input = [p[0], p[1], s]; + bool ok; + assembly ("memory-safe") { + ok := staticcall(gas(), 0x07, input, 0x60, r, 0x40) + } + if (!ok) { + revert PointRLCFailed(); + } + } + + function _ecAdd(uint256[2] calldata a, uint256[2] memory b) + internal + view + returns (uint256[2] memory r) + { + uint256[4] memory input = [a[0], a[1], b[0], b[1]]; + bool ok; + assembly ("memory-safe") { + ok := staticcall(gas(), 0x06, input, 0x80, r, 0x40) + } + if (!ok) { + revert PointRLCFailed(); + } + } +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml new file mode 100644 index 000000000..f4c6e8dd1 --- /dev/null +++ b/crates/primitives/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "sonobe-primitives" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } +ark-ec = { workspace = true } +ark-ff = { workspace = true, features = ["asm"] } +ark-poly = { workspace = true } +ark-relations = { workspace = true } +ark-r1cs-std = { workspace = true } +ark-serialize = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +hashbrown = { workspace = true } +itertools = { workspace = true } +num-bigint = { workspace = true, features = ["rand"] } +num-integer = { workspace = true } +num-traits = { workspace = true } +rayon = { workspace = true } +revm = { workspace = true, optional = true } +serde = { workspace = true, optional = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } +sha3 = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +ark-grumpkin = { workspace = true, features = ["r1cs"] } +ark-pallas = { workspace = true, features = ["curve", "r1cs"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[features] +default = [] +evm = ["dep:revm", "dep:serde", "dep:serde_json", "ark-std/std", "hashbrown/serde"] +parallel = [ + "ark-crypto-primitives/parallel", + "ark-ec/parallel", + "ark-ff/parallel", + "ark-poly/parallel", + "ark-relations/parallel", + "ark-crypto-primitives/parallel", + "ark-r1cs-std/parallel", + "ark-serialize/parallel", + "ark-std/parallel", +] \ No newline at end of file diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs new file mode 100644 index 000000000..75a5db986 --- /dev/null +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -0,0 +1,1777 @@ +//! This module provides implementation of in-circuit variables for emulated +//! integers or field elements. +//! +//! This is useful when we want to express or perform operations over a ring or +//! field in a circuit defined over a different field. +//! +//! Note that the implementation here is dedicated to Sonobe's use cases and the +//! priorities are efficiency instead of generality or usability, e.g., the user +//! needs to manually ensure the variables do not overflow the field capacity. +//! Therefore, be cautious if you want to use it in other contexts. + +use ark_ff::{BigInteger, One, PrimeField, Zero}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + convert::ToBitsGadget, + fields::{FieldVar, fp::FpVar}, + prelude::EqGadget, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + cmp::{max, min}, + fmt::Debug, + marker::PhantomData, + ops::Index, +}; +use num_bigint::{BigInt, BigUint, Sign}; +use num_integer::Integer; +use num_traits::Signed; + +use crate::{ + algebra::{ + field::{SonobeField, TwoStageFieldVar}, + ops::{ + bits::{FromBitsGadget, ToBitsGadgetExt}, + eq::EquivalenceGadget, + matrix::{MatrixGadget, SparseMatrixVar}, + vector::VectorMulGadget, + }, + }, + transcripts::AbsorbableVar, +}; + +/// [`Bounds`] records the lower and upper bounds (inclusive) of an integer. +/// +/// When allocating an emulated field element, we need to decompose it into +/// several limbs, each represented as a variable in the constraint field. +/// Operations over the emulated field element are translated into operations +/// over its limbs. +/// After several operations, the limbs may grow larger than the capacity of the +/// constraint field, and to prevent that, we track the bounds of each limb +/// using this struct, so that we can take action before the limbs overflow. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Bounds(pub BigInt, pub BigInt); + +impl Bounds { + /// [`Bounds::zero`] returns the bounds `[0, 0]`. + pub fn zero() -> Self { + Self::default() + } +} + +impl Bounds { + /// [`Bounds::add`] computes the sum of two pairs of bounds. + pub fn add(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // For `z = x + y`, its lower bound is the sum of the lower bounds of + // `x` and `y`, and its upper bound is the sum of the upper bounds of + // `x` and `y`. + Self(&self.0 + &other.0, &self.1 + &other.1) + } + + /// [`Bounds::sub`] computes the difference of two pairs of bounds. + pub fn sub(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // For `z = x - y`, its lower bound is the difference of the lower bound + // of `x` and the upper bound of `y`, and its upper bound is the + // difference of the upper bound of `x` and the lower bound of `y`. + Self(&self.0 - &other.1, &self.1 - &other.0) + } + + /// [`Bounds::add_many`] computes the sum of multiple pairs of bounds. + pub fn add_many(limbs: &[Self]) -> Self { + Self( + limbs.iter().map(|l| &l.0).sum(), + limbs.iter().map(|l| &l.1).sum(), + ) + } + + /// [`Bounds::mul`] computes the product of two pairs of bounds. + pub fn mul(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // To compute the bounds of `z = x * y`, we need to take into account + // the signs of `x` and `y`. + // + // Therefore, we first compute the following 4 products formed by the + // possible combinations of the bounds of `x` and `y`: + let ll = &self.0 * &other.0; + let lu = &self.0 * &other.1; + let ul = &self.1 * &other.0; + let uu = &self.1 * &other.1; + + // `z`'s lower bound is the minimum of these products, and its upper + // bound is the maximum of these products. + Self( + min(min(&ll, &lu), min(&ul, &uu)).clone(), + max(max(&ll, &lu), max(&ul, &uu)).clone(), + ) + } + + /// [`Bounds::shl`] shifts the bounds left by `shift` bits, i.e., multiplies + /// the bounds by `2^shift`. + pub fn shl(&self, shift: usize) -> Self { + // Given `x`, the bounds of `x << shift` can simply be computed by + // shifting the bounds of `x`. + Self(&self.0 << shift, &self.1 << shift) + } + + /// [`Bounds::shr_narrower`] shifts the bounds right by `shift` bits, i.e., + /// divides the bounds by `2^shift` and rounds the lower bound up and the + /// upper bound down, which gives a narrower range. + pub fn shr_narrower(&self, shift: usize) -> Self { + let d = BigInt::from(1u64) << shift; + Self(self.0.div_ceil(&d), self.1.div_floor(&d)) + } + + /// [`Bounds::shr_wider`] shifts the bounds right by `shift` bits, i.e., + /// divides the bounds by `2^shift` and rounds the lower bound down and the + /// upper bound up, which gives a wider range. + pub fn shr_wider(&self, shift: usize) -> Self { + let d = BigInt::from(1u64) << shift; + Self(self.0.div_floor(&d), self.1.div_ceil(&d)) + } + + /// [`Bounds::filter_safe`] checks if the bounds fit within the capacity of + /// a prime field `F`, and returns `Some(self)` if so, or `None` otherwise. + pub fn filter_safe(self) -> Option { + // We restrict variables to be within a window of size `(|F| + 1) / 2`, + // and the window to be within `[-(|F| - 1) / 2, (|F| - 1) / 2]`. + let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); + (self.0 >= -&limit && self.1 <= limit && &self.1 - &self.0 <= limit).then_some(self) + } +} + +fn compose(limbs: impl Borrow<[F]>) -> BigInt { + let mut r = BigInt::zero(); + + for &limb in limbs.borrow().iter().rev() { + r <<= F::BITS_PER_LIMB; + r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { + BigInt::from_biguint(Sign::Minus, (-limb).into()) + } else { + BigInt::from_biguint(Sign::Plus, limb.into()) + }; + } + r +} + +/// [`LimbedVar`] represents an in-circuit variable for an emulated integer or +/// field element, whose value is decomposed into several limbs, each being +/// created as a [`FpVar`] in the constraint field and tracked with its bounds. +/// +/// The generic parameter `Cfg` can be used to customize the behavior of ops on +/// `LimbedVar`, for instance, by specifying the modulus when emulating a field +/// element. +/// +/// The const generic parameter `ALIGNED` indicates if the limbs are "aligned". +/// When allocating a [`LimbedVar`], each limb has a predefined bit-length, but +/// after several operations, the actual bit-length of each limb may grow beyond +/// that. +/// It is usually fine to have larger limbs, but if they becomes larger than the +/// field capacity, we can no longer do operations on them. +/// Therefore, we sometimes need to "align" the limbs, i.e., reduce each limb +/// back to the predefined bit-length. +/// We say the limbs are "aligned" if the actual bit-length of each limb equals +/// the predefined bit-length, and "unaligned" otherwise. +#[derive(Debug, Clone)] +pub struct LimbedVar { + _cfg: PhantomData, + pub(crate) limbs: Vec>, + bounds: Vec, +} + +/// [`EmulatedIntVar`] is a type alias for emulated integer variables. +/// +/// We only expose aligned variables because unaligned integer variables only +/// appear as intermediate results during computations. +pub type EmulatedIntVar = LimbedVar; +/// [`EmulatedFieldVar`] is a type alias for emulated field element variables. +/// +/// We only expose aligned variables because unaligned integer variables only +/// appear as intermediate results during computations. +pub type EmulatedFieldVar = LimbedVar; + +impl GR1CSVar for LimbedVar { + type Value = BigInt; // For integers, their values are `BigInt`. + + fn cs(&self) -> ConstraintSystemRef { + self.limbs.cs() + } + + fn value(&self) -> Result { + self.limbs.value().map(compose) + } +} + +impl GR1CSVar + for LimbedVar +{ + type Value = Target; // For field elements, their values are in `Target`. + + fn cs(&self) -> ConstraintSystemRef { + self.limbs.cs() + } + + fn value(&self) -> Result { + let v = compose(self.limbs.value()?); + bigint_to_field_element(v).ok_or(SynthesisError::Unsatisfiable) + } +} + +fn bigint_to_field_element(v: BigInt) -> Option { + let (sign, abs) = v.into_parts(); + if abs >= F::MODULUS.into() { + return None; + } + match sign { + Sign::Plus | Sign::NoSign => Some(F::from(abs)), + Sign::Minus => Some(-F::from(abs)), + } +} + +impl LimbedVar { + /// [`LimbedVar::new`] creates a new [`LimbedVar`] from the pre-allocated + /// limbs and their bounds. + pub fn new(limbs: Vec>, bounds: Vec) -> Self { + Self { + _cfg: PhantomData, + limbs, + bounds, + } + } + + /// [`LimbedVar::ubound`] computes the upper bound of the represented value + /// from the upper bounds of its limbs. + fn ubound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.1; + } + + r + } + + /// [`LimbedVar::lbound`] computes the lower bound of the represented value + /// from the lower bounds of its limbs. + fn lbound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.0; + } + + r + } +} + +#[derive(PartialEq)] +pub enum RangeCheckMode { + Loose, + Tight, +} + +impl LimbedVar { + pub fn alloc>( + cs: impl Into>, + f: impl FnOnce() -> Result, + alloc_mode: AllocationMode, + range_check_mode: RangeCheckMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let (x, Bounds(lb, ub)) = v.borrow(); + + if x < lb || x > ub { + return Err(SynthesisError::Unsatisfiable); + } + + let len = max(lb.bits(), ub.bits()) as usize; + let zero = BigInt::zero(); + let one = BigInt::one(); + let min = &one - (&one << len); + let max = (&one << len) - &one; + + let (lb, ub) = if range_check_mode == RangeCheckMode::Loose { + ( + if lb.is_negative() { &min } else { &zero }, + if ub.is_positive() { &max } else { &zero }, + ) + } else { + (lb, ub) + }; + + let x_is_neg = x.is_negative(); + let mut x_abs = x.magnitude().clone(); + let mask = (BigUint::one() << F::BITS_PER_LIMB) - BigUint::one(); + let mut limbs_abs = vec![]; + for _ in 0..len.div_ceil(F::BITS_PER_LIMB) { + limbs_abs.push(F::from(&x_abs & &mask)); + x_abs >>= F::BITS_PER_LIMB; + } + + let x_is_neg = if !lb.is_negative() { + Boolean::FALSE + } else if !ub.is_positive() { + Boolean::TRUE + } else { + Boolean::new_variable(cs.clone(), || Ok(x_is_neg), alloc_mode)? + }; + let limbs_abs = Vec::>::new_variable(cs, || Ok(limbs_abs), alloc_mode)?; + + let limbs = limbs_abs + .into_iter() + .map(|limb_abs| { + limb_abs.enforce_bit_length(F::BITS_PER_LIMB)?; + x_is_neg.select(&limb_abs.negate()?, &limb_abs) + }) + .collect::>()?; + + let bounds = compute_bounds(lb, ub, F::BITS_PER_LIMB); + + let var = Self::new(limbs, bounds); + + // At this point, we are confident that: + // * If `lb >= 0`, then `0 <= var <= 2^len - 1`. + // * If `ub <= 0`, then `-2^len + 1 <= var <= 0`. + // * Otherwise, `-2^len + 1 <= var <= 2^len - 1`. + // + // However, for soundness, we need to enforce `lb <= var <= ub`, which + // is already guaranteed only if: + // * `lb = 0` and `ub = 2^len - 1` + // * `lb = -2^len + 1` and `ub = 0` + // * `lb = -2^len + 1` and `ub = 2^len - 1` + // + // For other cases, we additionally check: + // * `var <= ub` + // * `var >= lb` + if !lb.is_zero() && lb != &min { + Self::constant(lb - &one).enforce_lt(&var)?; + } + if !ub.is_zero() && ub != &max { + var.enforce_lt(&Self::constant(ub + &one))?; + } + + Ok(var) + } +} + +impl LimbedVar { + /// [`LimbedVar::from_bounded_bits_le`] computes a `LimbedVar` from its + /// little-endian bits with explicitly supplied [`Bounds`]. + pub fn from_bounded_bits_le( + bits: &[Boolean], + bounds: Bounds, + ) -> Result { + Ok(Self::new( + bits.chunks(F::BITS_PER_LIMB) + .map(Boolean::le_bits_to_fp) + .collect::>()?, + compute_bounds(&bounds.0, &bounds.1, F::BITS_PER_LIMB), + )) + } + + /// [`LimbedVar::enforce_lt`] enforces `self` to be less than `other`, where + /// both should be aligned (as indicated by the const generic). + /// Adapted from the xJsnark [paper] and its [implementation]. + /// + /// [paper]: https://www.cs.yale.edu/homes/cpap/published/xjsnark.pdf + /// [implementation]: https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 + pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { + // Compute the difference between limbs of `other` and `self`. + // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by + // `0`, and an unknown limb by `?`. + // Then, for `self < other`, `delta` should look like: + // ? ? ... ? ? + 0 0 ... 0 0 + let delta = other.sub_unaligned(self)?; + let len = delta.limbs.len(); + + // If `delta` has no limb, the difference between `self` and `other` is + // zero, and thus `self < other` does not hold. + if len == 0 { + return Err(SynthesisError::Unsatisfiable); + } + + // `helper` is a vector of booleans that indicates if the corresponding + // limb of `delta` is the first (searching from MSB) positive limb. + // For example, if `delta` is: + // - + ... + - + 0 0 ... 0 0 + // <---- search in this direction -------- + // Then `helper` should be: + // F F ... F F T F F ... F F + let helper = { + let cs = delta.limbs.cs(); + let mut helper = vec![false; len]; + for i in (0..len).rev() { + let limb = delta.limbs[i].value().unwrap_or_default().into_bigint(); + if !limb.is_zero() && limb <= F::MODULUS_MINUS_ONE_DIV_TWO { + helper[i] = true; + break; + } + } + Vec::>::new_variable_with_inferred_mode(cs, || Ok(helper))? + }; + + // `p` is the first positive limb in `delta`. + let mut p = FpVar::::zero(); + // `r` is the sum of all bits in `helper`, which should be 1 when `self` + // is less than `other`, as there should be more than one positive limb + // in `delta`, and thus exactly one true bit in `helper`. + let mut r = FpVar::zero(); + for (b, d) in helper.into_iter().zip(delta.limbs) { + // Choose the limb `d` only if `b` is true. + p += b.select(&d, &FpVar::zero())?; + // Either `r` or `d` should be zero. + // Consider the same example as above: + // - + ... + - + 0 0 ... 0 0 + // F F ... F F T F F ... F F + // |-----------| + // `r = 0` in this range (before/when we meet the first positive limb) + // |---------| + // `d = 0` in this range (after we meet the first positive limb) + // This guarantees that for every bit after the true bit in `helper`, + // the corresponding limb in `delta` is zero. + r.mul_equals(&d, &FpVar::zero())?; + // Add the current bit to `r`. + r += FpVar::from(b); + } + + // Ensure that `r` is exactly 1. This guarantees that there is exactly + // one true value in `helper`. + r.enforce_equal(&FpVar::one())?; + + // Ensure that `p` is positive, i.e., `1 <= p <= (|F| - 1) / 2`. + // This guarantees that the true value in `helper` corresponds to a + // positive limb in `delta`. + // To this end, we check `0 <= p - 1 <= 2^x - 1`, where `2^x` should + // satisfy `max_ub <= 2^x <= (|F| - 1) / 2`. + // Hence, we compute `x` as the ceiling of `log2(max_ub)`, so the left + // inequality holds, and the right inequality also holds because: + // - `max_ub` is the upper bound of a limb in `delta` + // - `delta` is the difference between two aligned `LimbedVar`s, whose + // limbs have at most `F::BITS_PER_LIMB` bits, which is much smaller + // than the field capacity + // Thus, `log2(max_ub)` is at most `F::BITS_PER_LIMB + 1`, from which we + // can conclude `2^x << (|F| - 1) / 2`. + + // `unwrap` is safe here because `None` can only happen when `delta` has + // no limbs, which is already handled at the beginning of the function. + let max_ub = delta.bounds.iter().map(|b| &b.1).max().unwrap(); + if !max_ub.is_positive() { + // If the maximum upper bound of `delta`'s limbs is non-positive, + // then all limbs in `delta` are non-positive, violating the + // requirement of `self < other`. + return Err(SynthesisError::Unsatisfiable); + } + (p - FpVar::one()).enforce_bit_length(max_ub.bits() as usize)?; + + Ok(()) + } +} + +impl From> for LimbedVar { + fn from(v: LimbedVar) -> Self { + Self::new(v.limbs, v.bounds) + } +} + +impl LimbedVar { + /// [`LimbedVar::add_unaligned`] computes `self + other`, without aligning + /// the limbs. + pub fn add_unaligned( + &self, + other: &LimbedVar, + ) -> Result, SynthesisError> { + let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bounds::zero(); limbs.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + Ok(LimbedVar::new(limbs, bounds)) + } + + /// [`LimbedVar::sub_unaligned`] computes `self - other`, without aligning + /// the limbs. + pub fn sub_unaligned( + &self, + other: &LimbedVar, + ) -> Result, SynthesisError> { + let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bounds::zero(); limbs.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .sub(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] -= v; + } + Ok(LimbedVar::new(limbs, bounds)) + } + + /// [`LimbedVar::mul_unaligned`] computes `self * other`, without aligning + /// the limbs. + /// + /// Here we implement the `O(n)` approach described in Section IV.B.1 of + /// xJsnark's [paper] for non-constant operands. + pub fn mul_unaligned( + &self, + other: &LimbedVar, + ) -> Result, SynthesisError> { + let len = self.limbs.len() + other.limbs.len() - 1; + if self.limbs.is_constant() || other.limbs.is_constant() { + // Use the naive approach for constant operands, which costs no + // constraints. + let bounds = (0..len) + .map(|i| { + let start = max(i + 1, other.bounds.len()) - other.bounds.len(); + let end = min(i + 1, self.bounds.len()); + Bounds::add_many( + &(start..end) + .map(|j| self.bounds[j].mul(&other.bounds[i - j])) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + + let limbs = (0..len) + .map(|i| { + let start = max(i + 1, other.limbs.len()) - other.limbs.len(); + let end = min(i + 1, self.limbs.len()); + (start..end) + .map(|j| &self.limbs[j] * &other.limbs[i - j]) + .sum() + }) + .collect(); + return Ok(LimbedVar::new(limbs, bounds)); + } + // Compute the product `limbs` outside the circuit and provide it as + // hints. + let (limbs, bounds) = { + let cs = self.limbs.cs().or(other.limbs.cs()); + let mut limbs = vec![F::zero(); len]; + let mut bounds = vec![Bounds::zero(); len]; + for i in 0..self.limbs.len() { + for j in 0..other.limbs.len() { + limbs[i + j] += self.limbs[i].value().unwrap_or_default() + * other.limbs[j].value().unwrap_or_default(); + bounds[i + j] = bounds[i + j].add(&self.bounds[i].mul(&other.bounds[j])) + } + } + ( + Vec::new_variable_with_inferred_mode(cs, || Ok(limbs))?, + bounds + .into_iter() + .map(|b| b.filter_safe::()) + .collect::>() + .ok_or(SynthesisError::Unsatisfiable)?, + ) + }; + for c in 1..=len { + let c = F::from(c as u64); + let mut t = F::one(); + let mut c_powers = vec![]; + for _ in 0..len { + c_powers.push(t); + t *= c; + } + // `l = Σ self[i] c^i` + let l = self + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `r = Σ other[i] c^i` + let r = other + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `o = Σ z[i] c^i` + let o = limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // Enforce `o = l * r` + l.mul_equals(&r, &o)?; + } + + Ok(LimbedVar::new(limbs, bounds)) + } + + /// [`LimbedVar::enforce_equal_unaligned`] enforces the equality between + /// `self` and `other` that are not necessarily aligned. + /// + /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 + /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 + /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 + pub fn enforce_equal_unaligned( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { + // Equality between `self` and `other` can be reduced to the equality + // between `diff = self - other` and 0. + let diff = self.sub_unaligned(other)?; + + let mut carry = FpVar::zero(); + let mut carry_bounds = Bounds::zero(); + let mut group_bounds = Bounds::zero(); + let mut offset = 0; + // `unwrap` is safe as long as `F` is a prime field with `|F| > 2`. + let inv = F::from(BigUint::one() << F::BITS_PER_LIMB) + .inverse() + .unwrap(); + + // For each limb in `diff`, we first try to group its _bounds_ into + // `group_bounds`. + // If the new bounds do not overflow / underflow, we can safely group + // the _limb_. + // + // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` + // is `F::BITS_PER_LIMB`, the initial number of bits in a limb. + // This is just as what we do in grade school arithmetic, e.g., + // 5 9 + // x 7 3 + // ------------- + // 15 27 + // 35 63 + // ------------- <- When grouping 35, 15 + 63, and 27, we are computing + // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 + // Note that this is different from the concatenation `x_0 || x_1 ...`, + // since the bit-length of each limb is not necessarily the initial size + // `W`. + // + // Assume a grouped limb `v` consists of `k` original limbs. + // Then the lower `k * W` bits of `v` must be zero for equality to hold, + // which is checked by enforcing that `2^{k * W}` divides `v`. + // To this end, we compute the quotient `q = v / 2^{k * W}` and enforce + // `q` is small that doesn't cause the multiplication `q * 2^{k * W}` to + // overflow / underflow. + // + // Moreover, we need to take into account the carry from the previous + // grouped limb, i.e., we actually enforce `carry + v` is a multiple of + // `2^{k * W}`, and derive the next carry by computing the quotient `q`. + // + // We can further avoid storing `v` by updating the carry on the fly for + // each limb, i.e., `carry = (carry + limb) / 2^W`, until the virtual + // grouped limb `v` is finalized. + for (limb, bounds) in diff.limbs.iter().zip(&diff.bounds) { + if let Some(new_group_bounds) = group_bounds.add(&bounds.shl(offset)).filter_safe::() + { + carry = (carry + limb) * inv; + carry_bounds = carry_bounds.add(bounds).shr_narrower(F::BITS_PER_LIMB); + group_bounds = new_group_bounds; + offset += F::BITS_PER_LIMB; + } else { + // New bounds overflow / underflow, i.e., the current group is + // finalized. + + debug_assert!(carry_bounds.shl(offset).0 >= group_bounds.0); + debug_assert!(carry_bounds.shl(offset).1 <= group_bounds.1); + + // We ensure `carry` is small, i.e., `lb <= carry <= ub`, or + // equivalently, `0 <= carry - lb <= ub - lb`, which can be done + // by ensuring `carry - lb` is a `log2(ub - lb + 1)`-bit number. + (&carry + - bigint_to_field_element::(carry_bounds.0.clone()) + .ok_or(SynthesisError::Unsatisfiable)?) + .enforce_bit_length( + (&carry_bounds.1 - &carry_bounds.0 + BigInt::one()).bits() as usize + )?; + + carry = (carry + limb) * inv; + carry_bounds = carry_bounds.add(bounds).shr_narrower(F::BITS_PER_LIMB); + // The limb folded above starts the next group and consumes one + // division by `2^W`, exactly as the first limb does in the + // group-extension (`if`) branch. Therefore `offset` must be + // `F::BITS_PER_LIMB` (not `0`), and `group_bounds` must keep + // tracking the undivided value `carry * 2^offset`. + offset = F::BITS_PER_LIMB; + group_bounds = carry_bounds.shl(offset); + } + } + + carry.enforce_equal(&FpVar::zero())?; + + Ok(()) + } +} + +impl + LimbedVar +{ + /// [`LimbedVar::modulo`] computes `self % Target::MODULUS` and returns the + /// result as an aligned [`LimbedVar`]. + /// + /// Note that we allow emulated field elements to be larger than the modulus + /// temporarily during computations, but the final result must be reduced + /// modulo `Target::MODULUS`, and for efficiency, this needs to be done by + /// the caller explicitly. + pub fn modulo(&self) -> Result, SynthesisError> { + let cs = self.cs(); + let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); + // Provide the quotient and remainder as hints + let (q, mut r) = { + let v = compose(self.limbs.value().unwrap_or_default()); + let q = v.div_floor(&m); + let r = v - &q * &m; + let mode = if cs.is_none() { + AllocationMode::Constant + } else { + AllocationMode::Witness + }; + + ( + LimbedVar::alloc( + cs.clone(), + || { + let lb = self.lbound().div_floor(&m); + let ub = self.ubound().div_floor(&m); + Ok((q, Bounds(lb, ub))) + }, + mode, + RangeCheckMode::Loose, + )?, + LimbedVar::alloc( + cs.clone(), + || Ok((r, Bounds(Zero::zero(), m.clone()))), + mode, + RangeCheckMode::Loose, + )?, + ) + }; + + let m = LimbedVar::constant(m); + + // Enforce `self = q * m + r` + q.mul_unaligned(&m)? + .add_unaligned(&r)? + .enforce_equal_unaligned(self)?; + // Enforce `r < m` (and `r >= 0` already holds) + r.enforce_lt(&m)?; + r.bounds = compute_bounds( + &BigInt::zero(), + &(-Target::one()).into_bigint().into().into(), + Base::BITS_PER_LIMB, + ); + + Ok(r) + } + + /// [`LimbedVar::enforce_congruent`] enforce that `self` is congruent to + /// `other` modulo `Target::MODULUS`. + pub fn enforce_congruent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { + let cs = self.cs().or(other.cs()); + let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); + // Provide the quotient as hint + let q = LimbedVar::alloc( + cs.clone(), + || { + let x = compose(self.limbs.value().unwrap_or_default()); + let y = compose(other.limbs.value().unwrap_or_default()); + let lb = (self.lbound() - other.ubound()).div_floor(&m); + let ub = (self.ubound() - other.lbound()).div_floor(&m); + Ok(((x - y).div_floor(&m), Bounds(lb, ub))) + }, + if cs.is_none() { + AllocationMode::Constant + } else { + AllocationMode::Witness + }, + RangeCheckMode::Loose, + )?; + + let m = LimbedVar::constant(m); + + // Enforce `self - other = q * m` + self.sub_unaligned(other)? + .enforce_equal_unaligned(&q.mul_unaligned(&m)?) + } +} + +// The following lines are quite repetitive, but we have to implement them all +// to make the compiler happy. +impl EquivalenceGadget> + for LimbedVar +{ + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} + +impl EquivalenceGadget> + for LimbedVar +{ + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl EquivalenceGadget> + for LimbedVar +{ + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl EquivalenceGadget> + for LimbedVar +{ + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} + +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + +impl TryFrom> + for LimbedVar +{ + type Error = SynthesisError; + + fn try_from(v: LimbedVar) -> Result { + v.modulo() + } +} + +impl TwoStageFieldVar for LimbedVar { + type ValueField = Target; + type ConstraintField = Base; + type Intermediate = LimbedVar; + + fn additive_identity() -> Self { + Self::constant(BigInt::zero()) + } + + fn multiplicative_identity() -> Self { + Self::constant(BigInt::one()) + } +} + +// Only implement `EqGadget` for aligned variables. +impl EqGadget for LimbedVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut bits = vec![]; + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + bits.push(self.limbs[i].is_eq(&other.limbs[i])?); + } + if bits.is_empty() { + Ok(Boolean::TRUE) + } else { + Boolean::kary_and(&bits) + } + } + + fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + self.limbs[i].enforce_equal(&other.limbs[i])?; + } + Ok(()) + } + + fn conditional_enforce_equal( + &self, + other: &Self, + should_enforce: &Boolean, + ) -> Result<(), SynthesisError> { + if should_enforce.is_constant() { + if should_enforce.value()? { + return self.enforce_equal(other); + } else { + return Ok(()); // No constraint when should_enforce is false + } + } + self.is_eq(other)? + .conditional_enforce_equal(&Boolean::TRUE, should_enforce) + } +} + +impl FromBitsGadget for LimbedVar { + fn from_bits_le(bits: &[Boolean]) -> Result { + Self::from_bounded_bits_le( + bits, + Bounds( + BigInt::zero(), + (BigInt::one() << bits.len()) - BigInt::one(), + ), + ) + } +} + +impl CondSelectGadget for LimbedVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.limbs.len() != false_value.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if true_value.bounds.len() != false_value.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut limbs = vec![]; + let mut bounds = vec![]; + for i in 0..true_value.limbs.len() { + if true_value.bounds[i] != false_value.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + limbs.push(cond.select(&true_value.limbs[i], &false_value.limbs[i])?); + bounds.push(true_value.bounds[i].clone()); + } + Ok(Self { + _cfg: PhantomData, + limbs, + bounds, + }) + } +} + +impl ToBitsGadget for LimbedVar { + fn to_bits_le(&self) -> Result>, SynthesisError> { + for bound in &self.bounds { + if bound.0 < BigInt::zero() { + return Err(SynthesisError::Unsatisfiable); + } + } + Ok(self + .limbs + .iter() + .zip(&self.bounds) + .map(|(limb, bound)| limb.to_n_bits_le(bound.1.bits() as usize)) + .collect::, _>>()? + .concat()) + } +} + +impl AbsorbableVar for LimbedVar { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; + + self.to_bits_le()? + .chunks(bits_per_limb) + .try_for_each(|i| Boolean::le_bits_to_fp(i).map(|v| dest.push(v))) + } +} + +impl< + CF: SonobeField, + Cfg, + Other: Index>, + const LHS_ALIGNED: bool, + const RHS_ALIGNED: bool, +> VectorMulGadget for [(LimbedVar, usize)] +{ + type Output = LimbedVar; + + fn mul(&self, other: &Other) -> Result { + let len = self + .iter() + .map(|(value, index)| value.limbs.len() + other[*index].limbs.len() - 1) + .max() + .unwrap_or(0); + // This is a combination of `mul_unaligned` and `add_unaligned` + // that results in more flattened `LinearCombination`s. + // Consequently, `ConstraintSystem::inline_all_lcs` costs less + // time, thus making trusted setup and proof generation faster. + let bounds = (0..len) + .map(|i| { + Bounds::add_many( + &self + .iter() + .flat_map(|(value, index)| { + let start = + max(i + 1, other[*index].bounds.len()) - other[*index].bounds.len(); + let end = min(i + 1, value.bounds.len()); + (start..end).map(|j| value.bounds[j].mul(&other[*index].bounds[i - j])) + }) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + let limbs = (0..len) + .map(|i| { + self.iter() + .flat_map(|(value, index)| { + let start = + max(i + 1, other[*index].limbs.len()) - other[*index].limbs.len(); + let end = min(i + 1, value.limbs.len()); + (start..end).map(|j| &value.limbs[j] * &other[*index].limbs[i - j]) + }) + .sum() + }) + .collect(); + Ok(LimbedVar::new(limbs, bounds)) + } +} + +impl MatrixGadget> + for SparseMatrixVar> +{ + fn mul_vector( + &self, + v: &impl Index>, + ) -> Result>, SynthesisError> { + self.0.iter().map(|row| row.mul(v)).collect() + } +} + +fn compute_bounds(lb: &BigInt, ub: &BigInt, bits_per_limb: usize) -> Vec { + let len = max(lb.bits(), ub.bits()) as usize; + let (n_full_limbs, n_remaining_bits) = len.div_rem(&bits_per_limb); + + let mut bounds = vec![ + Bounds( + if lb.is_negative() { + BigInt::one() - (BigInt::one() << bits_per_limb) + } else { + BigInt::zero() + }, + if ub.is_positive() { + (BigInt::one() << bits_per_limb) - BigInt::one() + } else { + BigInt::zero() + }, + ); + n_full_limbs + ]; + + if !n_remaining_bits.is_zero() { + let d = BigInt::one() << (len - n_remaining_bits); + bounds.push(Bounds(lb.div_floor(&d), ub.div_ceil(&d))); + } + + bounds +} + +impl AllocVar<(BigInt, Bounds), F> for LimbedVar { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + Self::alloc(cs, f, mode, RangeCheckMode::Tight) + } + + fn new_constant( + _cs: impl Into>, + t: impl Borrow<(BigInt, Bounds)>, + ) -> Result { + let (x, Bounds(lb, ub)) = t.borrow(); + + if x < lb || x > ub { + return Err(SynthesisError::Unsatisfiable); + } + + // Ignore `lb` and `ub` from now on, as a constant `x` will be bounded + // by itself. + let bits = x + .magnitude() + .to_radix_le(2) + .into_iter() + .map(|i| i == 1) + .collect::>(); + + let (limbs, bounds) = bits + .chunks(F::BITS_PER_LIMB) + .map(F::BigInt::from_bits_le) + .map(|v| { + let v_field = if x.is_negative() { + -F::from(v) + } else { + F::from(v) + }; + let v_bigint = BigInt::from_biguint(x.sign(), v.into()); + (FpVar::constant(v_field), Bounds(v_bigint.clone(), v_bigint)) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + + Ok(Self::new(limbs, bounds)) + } +} + +impl AllocVar for LimbedVar { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + Self::new_variable( + cs, + || { + f().map(|v| { + ( + v.borrow().into_bigint().into().into(), + Bounds(Zero::zero(), (-G::one()).into_bigint().into().into()), + ) + }) + }, + mode, + ) + } +} + +impl LimbedVar { + /// [`LimbedVar::constant`] allocates a constant [`LimbedVar`] with value + /// `x`. + pub fn constant(x: BigInt) -> Self { + // `unwrap` below is safe because we are allocating a constant value, + // which is guaranteed to succeed. + Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bounds(x.clone(), x))).unwrap() + } +} + +macro_rules! impl_binary_op { + ( + $trait: ident, + $fn: ident, + |$lhs_i:tt : &$lhs:ty, $rhs_i:tt : &$rhs:ty| -> $out:ty $body:block, + ($($params:tt)+), + ) => { + impl<$($params)+> core::ops::$trait<&$rhs> for &$lhs + { + type Output = $out; + + fn $fn(self, other: &$rhs) -> Self::Output { + let $lhs_i = self; + let $rhs_i = other; + $body + } + } + + impl<$($params)+> core::ops::$trait<$rhs> for &$lhs + { + type Output = $out; + + fn $fn(self, other: $rhs) -> Self::Output { + core::ops::$trait::$fn(self, &other) + } + } + + impl<$($params)+> core::ops::$trait<&$rhs> for $lhs + { + type Output = $out; + + fn $fn(self, other: &$rhs) -> Self::Output { + core::ops::$trait::$fn(&self, other) + } + } + + impl<$($params)+> core::ops::$trait<$rhs> for $lhs + { + type Output = $out; + + fn $fn(self, other: $rhs) -> Self::Output { + core::ops::$trait::$fn(&self, &other) + } + } + } +} + +macro_rules! impl_assignment_op { + ( + $assign_trait: ident, + $assign_fn: ident, + |$lhs_i:tt : &mut $lhs:ty, $rhs_i:tt : &$rhs:ty| $body:block, + ($($params:tt)+), + ) => { + impl<$($params)+> core::ops::$assign_trait<$rhs> for $lhs + { + fn $assign_fn(&mut self, other: $rhs) { + core::ops::$assign_trait::$assign_fn(self, &other) + } + } + + impl<$($params)+> core::ops::$assign_trait<&$rhs> for $lhs + { + fn $assign_fn(&mut self, other: &$rhs) { + let $lhs_i = self; + let $rhs_i = other; + $body + } + } + } +} + +impl_binary_op!( + Add, + add, + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { + a.add_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const LHS_ALIGNED: bool, const RHS_ALIGNED: bool), +); + +impl_assignment_op!( + AddAssign, + add_assign, + |a: &mut LimbedVar, b: &LimbedVar| { + *a = a.add_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const ALIGNED: bool), +); + +impl_binary_op!( + Sub, + sub, + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { + a.sub_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), +); + +impl_assignment_op!( + SubAssign, + sub_assign, + |a: &mut LimbedVar, b: &LimbedVar| { + *a = a.sub_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), +); + +impl_binary_op!( + Mul, + mul, + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { + a.mul_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), +); + +impl_assignment_op!( + MulAssign, + mul_assign, + |a: &mut LimbedVar, b: &LimbedVar| { + *a = a.mul_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), +); + +#[cfg(test)] +mod tests { + use ark_ff::Field; + use ark_pallas::{Fq, Fr}; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{ + UniformRand, + error::Error, + rand::{Rng, thread_rng}, + }; + use num_bigint::RandBigInt; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + #[test] + fn test_eq() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let zero = LimbedVar::::new(vec![], vec![]); + let zero2 = LimbedVar::::new( + vec![ + FpVar::new_witness(cs.clone(), || { + Ok(Fr::from(BigUint::one() << Fr::BITS_PER_LIMB)) + })?, + FpVar::new_witness(cs.clone(), || Ok(-Fr::one()))?, + ], + vec![ + Bounds( + -(BigInt::one() << (Fr::BITS_PER_LIMB * 2)), + BigInt::one() << (Fr::BITS_PER_LIMB * 2), + ), + Bounds( + -(BigInt::one() << (Fr::BITS_PER_LIMB * 2)), + BigInt::one() << (Fr::BITS_PER_LIMB * 2), + ), + ], + ); + let zero3 = LimbedVar::::new( + vec![ + FpVar::new_witness(cs.clone(), || { + Ok(Fr::from(BigUint::one() << Fr::BITS_PER_LIMB)) + })?, + FpVar::new_witness(cs.clone(), || Ok(-Fr::one()))?, + ], + vec![ + Bounds( + BigInt::zero(), + BigInt::from_biguint(Sign::Plus, Fr::MODULUS_MINUS_ONE_DIV_TWO.into()), + ), + Bounds( + -BigInt::from_biguint(Sign::Plus, Fr::MODULUS_MINUS_ONE_DIV_TWO.into()), + BigInt::zero(), + ), + ], + ); + + zero.enforce_equal_unaligned(&zero2)?; + zero.enforce_equal_unaligned(&zero3)?; + + let rng = &mut thread_rng(); + + let n_limbs = 100; + + let coeffs = (0..n_limbs) + .map(|_| if rng.gen_bool(0.5) { + -Fr::one() + } else { + Fr::one() + } * Fr::from(rng.gen_biguint(Fr::BITS_PER_LIMB as u64 * 2 - 1))) + .collect::>(); + let unaligned = LimbedVar::::new( + Vec::new_witness(cs.clone(), || Ok(&coeffs[..]))?, + vec![ + Bounds( + -(BigInt::one() << (Fr::BITS_PER_LIMB * 2)), + BigInt::one() << (Fr::BITS_PER_LIMB * 2), + ); + n_limbs + ], + ); + + let aligned = EmulatedIntVar::new_witness(cs.clone(), || { + let v = compose(&coeffs[..]); + Ok(( + v, + Bounds( + BigInt::one() - (BigInt::one() << (Fr::BITS_PER_LIMB * 2 * n_limbs)), + (BigInt::one() << (Fr::BITS_PER_LIMB * 2 * n_limbs)) - BigInt::one(), + ), + )) + })?; + aligned.enforce_equal_unaligned(&unaligned)?; + + assert!(cs.is_satisfied()?); + + let mut unaligned_incorrect = unaligned.clone(); + unaligned_incorrect.limbs[0] = if coeffs[0].is_zero() { + FpVar::new_witness(cs.clone(), || Ok(Fr::one()))? + } else { + FpVar::new_witness(cs.clone(), || Ok(-coeffs[0]))? + }; + aligned.enforce_equal_unaligned(&unaligned_incorrect)?; + + assert!(!cs.is_satisfied()?); + + Ok(()) + } + + #[test] + fn test_enforce_equal_unaligned_rejects_multiple_of_modulus() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + // Base-`2^BITS_PER_LIMB` digits of `p = Fr::MODULUS`. + let mask = (BigUint::one() << Fr::BITS_PER_LIMB) - BigUint::one(); + let mut vals = vec![]; + + let mut t: BigUint = Fr::MODULUS.into(); + t <<= Fr::BITS_PER_LIMB; + while !t.is_zero() { + vals.push(Fr::from(&t & &mask)); + t >>= Fr::BITS_PER_LIMB; + } + assert_eq!(compose(&vals[..]) >> Fr::BITS_PER_LIMB, Fr::MODULUS.into()); + + let mut bounds = vec![Bounds(BigInt::zero(), BigInt::zero())]; + // The huge bound on the first digit forces the first group to finalize + // immediately + bounds.push(Bounds( + BigInt::zero(), + BigInt::one() << (Fr::MODULUS_BIT_SIZE - 2), + )); + bounds.resize( + vals.len(), + Bounds(BigInt::zero(), BigInt::one() << (Fr::BITS_PER_LIMB + 1)), + ); + + let v = EmulatedIntVar::new(Vec::new_witness(cs.clone(), || Ok(vals))?, bounds); + + assert_eq!(v.value()? >> Fr::BITS_PER_LIMB, Fr::MODULUS.into()); + assert!(cs.is_satisfied()?); + + v.enforce_equal_unaligned(&EmulatedIntVar::constant(Zero::zero()))?; + + // A non-zero multiple of `p` must NOT be accepted as equal to zero. + assert!(!cs.is_satisfied()?); + + Ok(()) + } + + #[test] + fn test_alloc() -> Result<(), Box> { + let rng = &mut thread_rng(); + + let size = 1024; + let zero = BigInt::zero(); + let max: BigInt = (BigInt::one() << size) - BigInt::one(); + + let mut bounds = vec![(zero.clone(), max.clone())]; + + bounds.push((-&max, zero.clone())); + bounds.push((-&max, max.clone())); + bounds.push((rng.gen_bigint_range(&-&max, &zero), zero.clone())); + bounds.push((zero.clone(), rng.gen_bigint_range(&zero, &max))); + bounds.push(( + rng.gen_bigint_range(&-&max, &zero), + rng.gen_bigint_range(&zero, &max), + )); + bounds.push({ + let lb = rng.gen_bigint_range(&-&max, &zero); + (lb.clone(), rng.gen_bigint_range(&lb, &zero)) + }); + bounds.push({ + let lb = rng.gen_bigint_range(&zero, &max); + (lb.clone(), rng.gen_bigint_range(&lb, &max)) + }); + + for (lb, ub) in bounds { + let mut v = vec![ + lb.clone(), + ub.clone(), + &lb + BigInt::one(), + &ub - BigInt::one(), + ]; + if BigInt::zero() >= lb && BigInt::zero() <= ub { + v.push(BigInt::zero()); + } + for _ in 0..10 { + v.push(rng.gen_bigint_range(&lb, &ub)); + } + for a in v { + let cs = ConstraintSystem::::new_ref(); + + let a_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok((a.clone(), Bounds(lb.clone(), ub.clone()))) + })?; + + let a_const = EmulatedIntVar::::constant(a.clone()); + + assert_eq!(a, a_var.value()?); + assert_eq!(a, a_const.value()?); + assert!(cs.is_satisfied()?); + } + } + + Ok(()) + } + + #[test] + fn test_mul_bigint() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let size = 2048; + + let rng = &mut thread_rng(); + let a = rng.gen_bigint(size as u64); + let b = rng.gen_bigint(size as u64); + let ab = &a * &b; + let aab = &a * &ab; + let abb = &ab * &b; + + let a_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok(( + a, + Bounds( + BigInt::one() - (BigInt::one() << size), + (BigInt::one() << size) - BigInt::one(), + ), + )) + })?; + let b_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok(( + b, + Bounds( + BigInt::one() - (BigInt::one() << size), + (BigInt::one() << size) - BigInt::one(), + ), + )) + })?; + let ab_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok(( + ab, + Bounds( + BigInt::one() - (BigInt::one() << (size * 2)), + (BigInt::one() << (size * 2)) - BigInt::one(), + ), + )) + })?; + let aab_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok(( + aab, + Bounds( + BigInt::one() - (BigInt::one() << (size * 3)), + (BigInt::one() << (size * 3)) - BigInt::one(), + ), + )) + })?; + let abb_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok(( + abb, + Bounds( + BigInt::one() - (BigInt::one() << (size * 3)), + (BigInt::one() << (size * 3)) - BigInt::one(), + ), + )) + })?; + + let neg_a_var = EmulatedFieldVar::constant(BigInt::zero()) - &a_var; + let neg_b_var = EmulatedFieldVar::constant(BigInt::zero()) - &b_var; + let neg_ab_var = EmulatedFieldVar::constant(BigInt::zero()) - &ab_var; + let neg_aab_var = EmulatedFieldVar::constant(BigInt::zero()) - &aab_var; + let neg_abb_var = EmulatedFieldVar::constant(BigInt::zero()) - &abb_var; + + a_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&ab_var)?; + neg_a_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&ab_var)?; + a_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&neg_ab_var)?; + neg_a_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&neg_ab_var)?; + + a_var + .mul_unaligned(&ab_var)? + .enforce_equal_unaligned(&aab_var)?; + neg_a_var + .mul_unaligned(&neg_ab_var)? + .enforce_equal_unaligned(&aab_var)?; + a_var + .mul_unaligned(&neg_ab_var)? + .enforce_equal_unaligned(&neg_aab_var)?; + neg_a_var + .mul_unaligned(&ab_var)? + .enforce_equal_unaligned(&neg_aab_var)?; + + ab_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&abb_var)?; + neg_ab_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&abb_var)?; + ab_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&neg_abb_var)?; + neg_ab_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&neg_abb_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_mul_fq() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut thread_rng(); + let a = Fq::rand(rng); + let b = Fq::rand(rng); + let ab = a * b; + let aab = a * ab; + let abb = ab * b; + + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let b_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(b))?; + let ab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(ab))?; + let aab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(aab))?; + let abb_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(abb))?; + + let neg_a_var = EmulatedFieldVar::constant(BigInt::zero()) - &a_var; + let neg_b_var = EmulatedFieldVar::constant(BigInt::zero()) - &b_var; + let neg_ab_var = EmulatedFieldVar::constant(BigInt::zero()) - &ab_var; + let neg_aab_var = EmulatedFieldVar::constant(BigInt::zero()) - &aab_var; + let neg_abb_var = EmulatedFieldVar::constant(BigInt::zero()) - &abb_var; + + a_var.mul_unaligned(&b_var)?.enforce_congruent(&ab_var)?; + neg_a_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&ab_var)?; + a_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&neg_ab_var)?; + neg_a_var + .mul_unaligned(&b_var)? + .enforce_congruent(&neg_ab_var)?; + + a_var.mul_unaligned(&ab_var)?.enforce_congruent(&aab_var)?; + neg_a_var + .mul_unaligned(&neg_ab_var)? + .enforce_congruent(&aab_var)?; + a_var + .mul_unaligned(&neg_ab_var)? + .enforce_congruent(&neg_aab_var)?; + neg_a_var + .mul_unaligned(&ab_var)? + .enforce_congruent(&neg_aab_var)?; + + ab_var.mul_unaligned(&b_var)?.enforce_congruent(&abb_var)?; + neg_ab_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&abb_var)?; + ab_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&neg_abb_var)?; + neg_ab_var + .mul_unaligned(&b_var)? + .enforce_congruent(&neg_abb_var)?; + + assert_eq!(a_var.mul_unaligned(&b_var)?.modulo()?.value()?, ab); + assert_eq!(neg_a_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, ab); + assert_eq!(a_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, -ab); + assert_eq!(neg_a_var.mul_unaligned(&b_var)?.modulo()?.value()?, -ab); + + assert_eq!(a_var.mul_unaligned(&ab_var)?.modulo()?.value()?, aab); + assert_eq!( + neg_a_var.mul_unaligned(&neg_ab_var)?.modulo()?.value()?, + aab + ); + assert_eq!(a_var.mul_unaligned(&neg_ab_var)?.modulo()?.value()?, -aab); + assert_eq!(neg_a_var.mul_unaligned(&ab_var)?.modulo()?.value()?, -aab); + + assert_eq!(ab_var.mul_unaligned(&b_var)?.modulo()?.value()?, abb); + assert_eq!( + neg_ab_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, + abb + ); + assert_eq!(ab_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, -abb); + assert_eq!(neg_ab_var.mul_unaligned(&b_var)?.modulo()?.value()?, -abb); + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_pow() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut thread_rng(); + + let a = Fq::rand(rng); + + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; + + let mut r_var = a_var.clone(); + for _ in 0..16 { + r_var = r_var.mul_unaligned(&r_var)?.modulo()?; + } + r_var = r_var.mul_unaligned(&a_var)?.modulo()?; + assert_eq!(a.pow([65537u64]), r_var.value()?); + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_vec_vec_mul() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let len = 1000; + + let rng = &mut thread_rng(); + let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); + let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); + + let a_var = Vec::>::new_witness(cs.clone(), || Ok(&a[..]))?; + let b_var = Vec::>::new_witness(cs.clone(), || Ok(&b[..]))?; + + let mut c = Fq::zero(); + let mut r_var: LimbedVar = + EmulatedFieldVar::constant(BigUint::zero().into()).into(); + for i in 0..len { + c += a[i] * b[i]; + r_var = r_var.add_unaligned(&a_var[i].mul_unaligned(&b_var[i])?)?; + } + let c_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(c))?; + r_var.enforce_congruent(&c_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } +} diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs new file mode 100644 index 000000000..cb148e60a --- /dev/null +++ b/crates/primitives/src/algebra/field/mod.rs @@ -0,0 +1,212 @@ +//! This module defines extension traits for field elements and their in-circuit +//! counterparts, along with some common implementations. + +use ark_ff::{BigInteger, Field, Fp, Fp2, Fp2Config, FpConfig, PrimeField}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{ + any::TypeId, + mem::transmute_copy, + ops::{Add, Mul, Sub}, +}; + +#[cfg(feature = "evm")] +use crate::utils::evm::serialize::EVMSerialize; +use crate::{ + algebra::{Val, field::emulated::EmulatedFieldVar}, + circuits::{WitnessToPublic, inputize::Inputize}, + transcripts::{Absorbable, AbsorbableVar}, +}; + +pub mod emulated; + +/// [`SonobeField`] trait is a wrapper around [`PrimeField`] that also includes +/// necessary bounds for the field to be used conveniently in folding schemes. +pub trait SonobeField: + PrimeField + + Absorbable + + Val< + Var: FieldVar + WitnessToPublic + Inputize, + EmulatedVar = EmulatedFieldVar, + > +{ + /// [`SonobeField::BITS_PER_LIMB`] defines the bit length of each limb when + /// representing field elements as limbs in an emulated field variable. + // TODO: either make it configurable, or compute an optimal value based on + // the modulus size. + const BITS_PER_LIMB: usize; +} + +impl, const N: usize> SonobeField for Fp { + const BITS_PER_LIMB: usize = 32; +} + +impl, const N: usize> Val for Fp { + type PreferredConstraintField = Self; + type Var = FpVar; + + type EmulatedVar = EmulatedFieldVar; +} + +impl, const N: usize> Absorbable for Fp { + fn absorb_into(&self, dest: &mut Vec) { + if TypeId::of::() == TypeId::of::() { + // Safe because `F` and `Self` have the same type + // TODO (@winderica): specialization when??? + dest.push(unsafe { transmute_copy::(self) }); + } else { + let bits_per_limb = F::MODULUS_BIT_SIZE - 1; + let num_limbs = Self::MODULUS_BIT_SIZE.div_ceil(bits_per_limb); + + let mut limbs = self + .into_bigint() + .to_bits_le() + .chunks(bits_per_limb as usize) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) + .collect::>(); + limbs.resize(num_limbs as usize, F::zero()); + + dest.extend(&limbs) + } + } +} + +impl AbsorbableVar for FpVar { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + dest.push(self.clone()); + Ok(()) + } +} + +impl Inputize for FpVar { + fn inputize(value: &Self::Value) -> Vec { + vec![*value] + } +} + +impl Inputize for EmulatedFieldVar { + fn inputize(value: &Self::Value) -> Vec { + // TODO: pack bits + value + .into_bigint() + .to_bits_le() + .chunks(Base::BITS_PER_LIMB) + .map(Base::BigInt::from_bits_le) + .map(Base::from) + .collect() + } +} + +impl WitnessToPublic for FpVar { + fn mark_as_public(&self) -> Result<(), SynthesisError> { + // This line "converts" `x` from a witness to a public input. + // Instead of directly modifying the constraint system, we allocate a + // public input variable explicitly and enforce that its value is indeed + // `x`. + // While seemingly redundant, comparing `x` with itself is necessary + // because: + // - `.value()` allows an honest prover to extract public inputs without + // computing them outside the circuit. + // - `.enforce_equal()` prevents a malicious prover from claiming public + // inputs that are not the honest `x` computed in-circuit. + self.enforce_equal(&Self::new_input(self.cs(), || { + Ok(self.value().unwrap_or_default()) + })?) + } +} + +impl WitnessToPublic for EmulatedFieldVar { + fn mark_as_public(&self) -> Result<(), SynthesisError> { + self.enforce_equal(&Self::new_input(self.cs(), || { + Ok(self.value().unwrap_or_default()) + })?) + } +} + +#[cfg(feature = "evm")] +impl, const N: usize> EVMSerialize for Fp { + fn to_calldata(&self) -> Vec { + self.into_bigint().to_bytes_be() + } +} + +#[cfg(feature = "evm")] +impl> EVMSerialize for Fp2

{ + fn to_calldata(&self) -> Vec { + [self.c1.to_calldata(), self.c0.to_calldata()].concat() + } +} + +/// [`TwoStageFieldVar`] abstracts over field variables that support a +/// two-stage arithmetic model. +/// +/// In this model, we consider two stages of in-circuit variables for field +/// elements when performing field operations: +/// 1. Before the operations, we have the standard field variable type, i.e., +/// the implementor of this trait. +/// 2. During the operations, we use [`TwoStageFieldVar::Intermediate`] to hold +/// the intermediate results. +/// Therefore, the field operations on two field variables yield a new +/// intermediate variable. +pub trait TwoStageFieldVar: + Clone + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Sub + + for<'a> Sub<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate> + + GR1CSVar + + AllocVar +{ + // TODO: seems that using GR1CSVar's Value breaks the compiler... + type ValueField: Field; + type ConstraintField: Field; + + /// The intermediate variable type used during field operations. + /// + /// We require this type to support conversions from and to the original + /// field variable type. + /// + /// In addition, to allow chaining operations without excessive conversions, + /// we require this type to support field operations with both itself and + /// the original field variable type. + type Intermediate: Clone + + From + + TryInto + + Add + + for<'a> Add<&'a Self::Intermediate, Output = Self::Intermediate> + + Sub + + for<'a> Sub<&'a Self::Intermediate, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self::Intermediate, Output = Self::Intermediate> + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Sub + + for<'a> Sub<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate>; + + fn additive_identity() -> Self; + fn multiplicative_identity() -> Self; +} + +// Operations over the canonical variable `FpVar` always yield another `FpVar`. +impl TwoStageFieldVar for FpVar { + type ValueField = F; + type ConstraintField = F; + type Intermediate = Self; + + fn additive_identity() -> Self { + Self::zero() + } + + fn multiplicative_identity() -> Self { + Self::one() + } +} diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs new file mode 100644 index 000000000..0b18cccd4 --- /dev/null +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -0,0 +1,208 @@ +//! This module provides implementation of in-circuit variables for emulated +//! elliptic curve points. +//! +//! This is useful when we want to express points whose coordinates lie in a +//! different field than the circuit's constraint field. +//! +//! Note that currently this module only provides the representation of such +//! points, without any arithmetic operations. + +use ark_ec::{AffineRepr, short_weierstrass::SWFlags}; +use ark_ff::Zero; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + eq::EqGadget, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; +use ark_std::borrow::Borrow; + +use crate::{ + algebra::{ + field::{SonobeField, emulated::EmulatedFieldVar}, + group::SonobeCurve, + }, + transcripts::AbsorbableVar, +}; + +/// [`EmulatedAffineVar`] defines an in-circuit elliptic curve point with its +/// affine representation, where the coordinates are in the curve's base field +/// `Target::BaseField` and are emulated over the constraint field `Base` in the +/// circuit. +#[derive(Debug, Clone)] +pub struct EmulatedAffineVar { + /// [`EmulatedAffineVar::x`] is the x-coordinate of the point's affine + /// representation. + pub x: EmulatedFieldVar, + /// [`EmulatedAffineVar::y`] is the y-coordinate of the point's affine + /// representation. + pub y: EmulatedFieldVar, +} + +impl AllocVar + for EmulatedAffineVar +{ + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let affine = val.borrow().into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + + let x = EmulatedFieldVar::new_variable(cs.clone(), || Ok(x), mode)?; + let y = EmulatedFieldVar::new_variable(cs.clone(), || Ok(y), mode)?; + + Ok(Self { x, y }) + }) + } +} + +impl GR1CSVar for EmulatedAffineVar { + type Value = Target; + + fn cs(&self) -> ConstraintSystemRef { + self.x.cs().or(self.y.cs()) + } + + fn value(&self) -> Result { + let x = self.x.value()?; + let y = self.y.value()?; + // Below is a workaround to convert the `x` and `y` coordinates to a + // point. This is because the `SonobeCurve` trait does not provide a + // method to construct a point from `BaseField` elements. + let mut bytes = vec![]; + // `unwrap` below is safe because serialization of a `PrimeField` value + // only fails if the serialization flag has more than 8 bits, but here + // we call `serialize_uncompressed` which uses an empty flag. + x.serialize_uncompressed(&mut bytes).unwrap(); + // `unwrap` below is also safe, because the bit size of `SWFlags` is 2. + y.serialize_with_flags( + &mut bytes, + if x.is_zero() && y.is_zero() { + SWFlags::PointAtInfinity + } else if y <= -y { + SWFlags::YIsPositive + } else { + SWFlags::YIsNegative + }, + ) + .unwrap(); + // `unwrap` below is safe because `bytes` is constructed from the `x` + // and `y` coordinates of a valid point, and these coordinates are + // serialized in the same way as the `SonobeCurve` implementation. + Ok(Target::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) + } +} + +impl EqGadget for EmulatedAffineVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + Ok(self.x.is_eq(&other.x)? & self.y.is_eq(&other.y)?) + } + + fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { + self.x.enforce_equal(&other.x)?; + self.y.enforce_equal(&other.y)?; + Ok(()) + } +} + +impl EmulatedAffineVar { + /// [`EmulatedAffineVar::zero`] allocates the zero point (point at infinity) + /// of the curve as a constant. + pub fn zero() -> Self { + // `unwrap` below is safe because we are allocating a constant value, + // which is guaranteed to succeed. + Self::new_constant(ConstraintSystemRef::None, Target::zero()).unwrap() + } +} + +impl AbsorbableVar + for EmulatedAffineVar +{ + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + (&self.x, &self.y).absorb_into(dest) + } +} + +impl CondSelectGadget + for EmulatedAffineVar +{ + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + Ok(Self { + x: cond.select(&true_value.x, &false_value.x)?, + y: cond.select(&true_value.y, &false_value.y)?, + }) + } +} + +#[cfg(test)] +mod tests { + use ark_pallas::{Fq, Fr, PallasConfig, Projective}; + use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{UniformRand, error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::{circuits::inputize::Inputize, transcripts::Absorbable}; + + #[test] + fn test_alloc_zero() { + let cs = ConstraintSystem::::new_ref(); + + // dealing with the 'zero' point should not panic when doing the unwrap + let p = Projective::zero(); + assert!(EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p)).is_ok()); + } + + #[test] + fn test_to_hash_preimage() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let mut rng = thread_rng(); + let p = Projective::rand(&mut rng); + let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; + + let mut v = vec![]; + let mut v_var = vec![]; + p.absorb_into(&mut v); + p_var.absorb_into(&mut v_var)?; + + assert_eq!(v_var.value()?, v); + Ok(()) + } + + #[test] + fn test_inputize() -> Result<(), Box> { + let mut rng = thread_rng(); + let p = Projective::rand(&mut rng); + + let cs = ConstraintSystem::::new_ref(); + let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; + assert_eq!( + [p_var.x.limbs.value()?, p_var.y.limbs.value()?].concat(), + EmulatedAffineVar::inputize(&p) + ); + + let cs = ConstraintSystem::::new_ref(); + let p_var = ProjectiveVar::>::new_witness(cs.clone(), || Ok(p))?; + assert_eq!( + vec![p_var.x.value()?, p_var.y.value()?, p_var.z.value()?], + ProjectiveVar::inputize(&p) + ); + Ok(()) + } +} diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs new file mode 100644 index 000000000..2f17e8ee2 --- /dev/null +++ b/crates/primitives/src/algebra/group/mod.rs @@ -0,0 +1,148 @@ +//! This module defines extension traits for elliptic curve points and their +//! in-circuit counterparts, along with some common implementations. + +use ark_ec::{ + AffineRepr, CurveGroup, PrimeGroup, + short_weierstrass::{Affine, Projective, SWCurveConfig}, +}; +use ark_ff::{Field, One, PrimeField, Zero}; +use ark_r1cs_std::{ + convert::ToConstraintFieldGadget, + fields::fp::FpVar, + groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, +}; +use ark_relations::gr1cs::SynthesisError; + +#[cfg(feature = "evm")] +use crate::utils::evm::serialize::EVMSerialize; +use crate::{ + algebra::{ + Val, + field::{SonobeField, emulated::EmulatedFieldVar}, + group::emulated::EmulatedAffineVar, + }, + circuits::{WitnessToPublic, inputize::Inputize}, + transcripts::{Absorbable, AbsorbableVar}, + utils::dummy::Dummy, +}; + +pub mod emulated; + +/// [`CF1`] is a type alias for the scalar field of a curve `C`. +pub type CF1 = ::ScalarField; +/// [`CF2`] is a type alias for the base field of a curve `C`. +pub type CF2 = <::BaseField as Field>::BasePrimeField; + +/// [`SonobeCurve`] trait is a wrapper around [`CurveGroup`] that also includes +/// necessary bounds for the curve to be used conveniently in folding schemes. +pub trait SonobeCurve: + CurveGroup + + Absorbable + + Val< + Var: CurveVar + + AbsorbableVar + + WitnessToPublic + + Inputize, + EmulatedVar = EmulatedAffineVar, + > +{ +} + +impl> SonobeCurve + for Projective

+{ +} + +impl> Val for Projective

{ + type PreferredConstraintField = P::BaseField; + type Var = ProjectiveVar>; + + type EmulatedVar = EmulatedAffineVar; +} + +impl Dummy for C { + fn dummy(_: T) -> Self { + Default::default() + } +} + +impl> Absorbable for Projective

{ + fn absorb_into(&self, dest: &mut Vec) { + let affine = self.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + [x, y].absorb_into(dest); + } +} + +impl> AbsorbableVar + for ProjectiveVar> +{ + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + let mut vec = self.to_constraint_field()?; + // The last element in the vector tells whether the point is infinity, + // but we can in fact avoid absorbing it without loss of soundness. + // This is because the `to_constraint_field` method internally invokes + // [`ProjectiveVar::to_afine`](https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/groups/curves/short_weierstrass/mod.rs#L160-L195), + // which guarantees that an infinity point is represented as `(0, 0)`, + // but the y-coordinate of a non-infinity point is never 0 (for why, see + // https://crypto.stackexchange.com/a/108242 ). + vec.pop(); + dest.extend(vec); + Ok(()) + } +} + +impl> Inputize + for ProjectiveVar> +{ + fn inputize(value: &Self::Value) -> Vec { + let affine = value.into_affine(); + match affine.xy() { + Some((x, y)) => vec![x, y, One::one()], + None => vec![Zero::zero(), One::one(), Zero::zero()], + } + } +} + +impl Inputize for EmulatedAffineVar { + fn inputize(value: &Self::Value) -> Vec { + let affine = value.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + <[EmulatedFieldVar]>::inputize(&vec![x, y]) + } +} + +impl> WitnessToPublic + for ProjectiveVar> +{ + fn mark_as_public(&self) -> Result<(), SynthesisError> { + // We only need the x and y coordinates of the point, but the `infinity` + // flag is not necessary. + self.to_constraint_field()?[..2].mark_as_public() + } +} + +impl WitnessToPublic for EmulatedAffineVar { + fn mark_as_public(&self) -> Result<(), SynthesisError> { + self.x.mark_as_public()?; + self.y.mark_as_public()?; + Ok(()) + } +} + +#[cfg(feature = "evm")] +impl> EVMSerialize for Affine

{ + fn to_calldata(&self) -> Vec { + // the encoding of the additive identity is [0, 0] on the EVM + let (x, y) = self.xy().unwrap_or_default(); + + [x.to_calldata(), y.to_calldata()].concat() + } +} + +#[cfg(feature = "evm")] +impl> EVMSerialize for Projective

{ + fn to_calldata(&self) -> Vec { + self.into_affine().to_calldata() + } +} diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs new file mode 100644 index 000000000..975c3f0c7 --- /dev/null +++ b/crates/primitives/src/algebra/mod.rs @@ -0,0 +1,33 @@ +//! This module provides algebraic abstractions used across Sonobe, including +//! field and group type enhancements, in-circuit (both canonical and emulated) +//! variables, and common algebraic operations. + +use ark_ff::PrimeField; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; + +use crate::algebra::field::SonobeField; + +pub mod field; +pub mod group; +pub mod ops; + +/// [`Val`] associates a type with its in-circuit variables. +pub trait Val { + /// [`Val::PreferredConstraintField`] is the preferred constraint field for + /// expressing `Self` in-circuit. + type PreferredConstraintField: PrimeField; + + /// [`Val::Var`] is the *canonical* in-circuit variable. + /// + /// In this case, the circuit is defined over the preferred constraint field + /// and can represent `Self` directly (i.e., without emulation). + type Var: AllocVar + + GR1CSVar; + + /// [`Val::EmulatedVar`] is the *emulated* in-circuit variable. + /// + /// In this case, the circuit is defined over an arbitrary field `F` which + /// may differ from the preferred constraint field, and `Self` is + /// represented in-circuit via emulation. + type EmulatedVar: AllocVar + GR1CSVar; +} diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs new file mode 100644 index 000000000..0ecf35bee --- /dev/null +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -0,0 +1,63 @@ +//! This module defines traits for conversion between bit representations and +//! algebraic types inside and outside circuits. + +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; + +/// [`FromBits`] reconstructs a value from bits. +pub trait FromBits { + /// [`FromBits::from_bits_le`] computes a value from its little-endian bits. + fn from_bits_le(bits: &[bool]) -> Self; +} + +impl FromBits for F { + fn from_bits_le(bits: &[bool]) -> Self { + F::from(F::BigInt::from_bits_le(bits)) + } +} + +/// [`FromBitsGadget`] is the in-circuit counterpart of [`FromBits`], which +/// reconstructs an in-circuit variable from boolean variables. +pub trait FromBitsGadget: Sized { + /// [`FromBitsGadget::from_bits_le`] computes a variable from its + /// little-endian bits, inferring bounds from the length of `bits`. + fn from_bits_le(bits: &[Boolean]) -> Result; +} + +/// [`ToBitsGadgetExt`] extends the standard [`ark_r1cs_std::convert::ToBitsGadget`] +/// with more functionality. +pub trait ToBitsGadgetExt: Sized { + /// [`ToBitsGadgetExt::to_n_bits_le`] decomposes `self` into `n` + /// little-endian bits. + /// + /// An error is returned if `self` cannot be represented in `n` bits. + fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError>; + + /// [`ToBitsGadgetExt::enforce_bit_length`] enforces that `self` can be + /// represented in at most `n` bits. + /// + /// This is useful for checking that a field element is within the range of + /// `[0, 2^n - 1]` + fn enforce_bit_length(&self, n: usize) -> Result<(), SynthesisError> { + self.to_n_bits_le(n)?; + Ok(()) + } +} +impl FromBitsGadget for FpVar { + fn from_bits_le(bits: &[Boolean]) -> Result { + Boolean::le_bits_to_fp(bits) + } +} + +impl ToBitsGadgetExt for FpVar { + fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError> { + let mut bits = self.value().unwrap_or_default().into_bigint().to_bits_le(); + bits.resize(n, false); + let bits = Vec::new_variable_with_inferred_mode(self.cs(), || Ok(bits))?; + + Boolean::le_bits_to_fp(&bits)?.enforce_equal(self)?; + + Ok(bits) + } +} diff --git a/crates/primitives/src/algebra/ops/eq.rs b/crates/primitives/src/algebra/ops/eq.rs new file mode 100644 index 000000000..364a3b425 --- /dev/null +++ b/crates/primitives/src/algebra/ops/eq.rs @@ -0,0 +1,36 @@ +//! This module defines traits for enforcing custom, user-defined equivalence +//! relation between in-circuit variables, enabling flexible checks for equality +//! and congruence. + +use ark_ff::PrimeField; +use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; + +/// [`EquivalenceGadget`] enforces two in-circuit variables are "equivalent". +/// +/// This does not only allow us to ensure the equality of two variables of the +/// same type, but can also be used for guaranteeing variables of different +/// types represent the "same" (depending on the context) value. +pub trait EquivalenceGadget { + /// [`EquivalenceGadget::enforce_equivalent`] enforces that `self` and + /// `other` are equivalent. + fn enforce_equivalent(&self, other: &Other) -> Result<(), SynthesisError>; +} + +impl EquivalenceGadget> for FpVar { + fn enforce_equivalent(&self, other: &FpVar) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} + +impl, T> EquivalenceGadget<[T]> for [S] { + fn enforce_equivalent(&self, other: &[T]) -> Result<(), SynthesisError> { + if self.len() != other.len() { + return Err(SynthesisError::Unsatisfiable); + } + + self.iter() + .zip(other) + .try_for_each(|(a, b)| a.enforce_equivalent(b)) + } +} diff --git a/crates/primitives/src/algebra/ops/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs new file mode 100644 index 000000000..ed84edf6e --- /dev/null +++ b/crates/primitives/src/algebra/ops/matrix.rs @@ -0,0 +1,60 @@ +//! This module defines in-circuit sparse matrix types and implements operations +//! over them. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, +}; +use ark_relations::gr1cs::{Matrix, Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, ops::Index}; + +use crate::algebra::ops::vector::VectorMulGadget; + +/// [`MatrixGadget`] defines operations on in-circuit matrix variables. +pub trait MatrixGadget { + /// [`MatrixGadget::mul_vector`] computes the product of `self` and a column + /// vector `v`. + fn mul_vector(&self, v: &impl Index) -> Result, SynthesisError>; +} + +/// [`SparseMatrixVar`] is a sparse matrix represented as a vector of rows, +/// where each row is a vector of `(value, column_index)` pairs. +/// +/// This follows the same format as [`ark_relations::gr1cs::Matrix`]. +#[derive(Debug, Clone)] +pub struct SparseMatrixVar(pub Vec>); + +impl> AllocVar, CF> for SparseMatrixVar { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let mut coeffs: Vec> = Vec::new(); + for row in val.borrow().iter() { + coeffs.push( + row.iter() + .map(|&(value, col)| { + Ok((FV::new_variable(cs.clone(), || Ok(value), mode)?, col)) + }) + .collect::, _>>()?, + ); + } + + Ok(Self(coeffs)) + }) + } +} + +impl MatrixGadget> for SparseMatrixVar> { + fn mul_vector( + &self, + v: &impl Index>, + ) -> Result>, SynthesisError> { + self.0.iter().map(|row| row.mul(v)).collect() + } +} diff --git a/crates/primitives/src/algebra/ops/mod.rs b/crates/primitives/src/algebra/ops/mod.rs new file mode 100644 index 000000000..50c0595c8 --- /dev/null +++ b/crates/primitives/src/algebra/ops/mod.rs @@ -0,0 +1,18 @@ +//! This module collects common algebraic operation traits and their in-circuit +//! gadgets, including: +//! +//! * [`bits`]: conversions between bit representations and algebraic variables. +//! * [`eq`]: generalization of equality checks. +//! * [`matrix`]: sparse matrix representation and operations. +//! * [`poly`]: helpers for polynomial operations. +//! * [`pow`]: computation of powers. +//! * [`rlc`]: random linear combinations. +//! * [`vector`]: vector operations. + +pub mod bits; +pub mod eq; +pub mod matrix; +pub mod poly; +pub mod pow; +pub mod rlc; +pub mod vector; diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs new file mode 100644 index 000000000..10a5c9cea --- /dev/null +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -0,0 +1,75 @@ +//! This module provides helpers for working with polynomials inside circuits. + +use ark_ff::{Field, PrimeField, Zero}; +use ark_poly::{DenseMultilinearExtension, EvaluationDomain, GeneralEvaluationDomain}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::log2; + +use super::pow::Pow; + +/// [`MLEHelper`] provides functionality for multilinear extensions. +pub trait MLEHelper { + /// [`MLEHelper::from_evaluations`] builds a multilinear extension from a + /// (possibly non-power-of-two) vector of evaluations, padding with zeros + /// up to the next power of two. + fn from_evaluations(evaluations: &[F]) -> Self; +} + +impl MLEHelper for DenseMultilinearExtension { + fn from_evaluations(evaluations: &[F]) -> Self { + let l = evaluations.len(); + let pad = vec![Zero::zero(); l.next_power_of_two() - l]; + Self::from_evaluations_vec(log2(l) as usize, [evaluations, &pad].concat()) + } +} + +/// [`EvaluationDomainGadget`] provides a subset of evaluation domain operations +/// in [`EvaluationDomain`] for in-circuit field variables. +pub trait EvaluationDomainGadget { + /// [`EvaluationDomainGadget::evaluate_all_lagrange_coefficients_var`] + /// computes all Lagrange basis polynomials evaluated at `tau`. + /// + /// It is the in-circuit counterpart of [`EvaluationDomain::evaluate_all_lagrange_coefficients`]. + fn evaluate_all_lagrange_coefficients_var( + &self, + tau: &FpVar, + ) -> Result>, SynthesisError>; + + /// [`EvaluationDomainGadget::evaluate_vanishing_polynomial_var`] evaluates + /// the vanishing polynomial of the domain at `tau`. + /// + /// It is the in-circuit counterpart of [`EvaluationDomain::evaluate_vanishing_polynomial`]. + fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) + -> Result, SynthesisError>; +} + +impl EvaluationDomainGadget for GeneralEvaluationDomain { + fn evaluate_all_lagrange_coefficients_var( + &self, + tau: &FpVar, + ) -> Result>, SynthesisError> { + let size = self.size() as u64; + let size_inv = self.size_inv(); + let offset = self.coset_offset(); + let offset_inv = self.coset_offset_inv(); + let group_gen = self.group_gen(); + + // We assume that the evaluation of vanishing polynomial at tau is non-0 + + let l_i = (tau.pow_by_constant([size])? * offset_inv.pow([size - 1]) - offset) * size_inv; + + group_gen + .powers(size as usize) + .into_iter() + .map(|g| (&l_i * g).mul_by_inverse(&(tau - offset * g))) + .collect() + } + + fn evaluate_vanishing_polynomial_var( + &self, + tau: &FpVar, + ) -> Result, SynthesisError> { + Ok(tau.pow_by_constant([self.size() as u64])? - self.coset_offset_pow_size()) + } +} diff --git a/crates/primitives/src/algebra/ops/pow.rs b/crates/primitives/src/algebra/ops/pow.rs new file mode 100644 index 000000000..3d1808522 --- /dev/null +++ b/crates/primitives/src/algebra/ops/pow.rs @@ -0,0 +1,100 @@ +//! This module defines and implements powering utilities in and out of circuit. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; + +/// [`Pow`] provides powering operations for field elements. +pub trait Pow: Sized { + /// [`Pow::powers`] computes: + /// $self^0, self^1, ..., self^{n-1}$. + fn powers(&self, n: usize) -> Vec; + + /// [`Pow::repeated_squares`] computes: + /// $self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}$. + fn repeated_squares(&self, n: usize) -> Vec; + + /// [`Pow::powers_from_repeated_squares`] expands a vector of repeated + /// squares $x^{2^0}, x^{2^1}, ..., x^{2^{n-1}}$ into all powers: + /// $x^0, x^1, ..., x^{2^n - 1}$. + fn powers_from_repeated_squares(squares: &[Self]) -> Vec; +} + +impl Pow for F { + fn powers(&self, n: usize) -> Vec { + let mut res = vec![F::one(); n]; + for i in 1..n { + res[i] = res[i - 1] * self; + } + res + } + + fn repeated_squares(&self, n: usize) -> Vec { + if n == 0 { + return vec![]; + } + let mut res = vec![F::zero(); n]; + res[0] = *self; + for i in 1..n { + res[i] = res[i - 1].square(); + } + res + } + + fn powers_from_repeated_squares(squares: &[Self]) -> Vec { + let mut pows = vec![F::one()]; + for square in squares.iter().rev() { + pows = pows.into_iter().flat_map(|e| [e, e * square]).collect(); + } + pows + } +} + +/// [`PowGadget`] is the in-circuit counterpart of [`Pow`], providing powering +/// operations for field element variables. +pub trait PowGadget: Sized { + /// [`PowGadget::powers`] computes: + /// $self^0, self^1, ..., self^{n-1}$. + fn powers(&self, n: usize) -> Vec; + + /// [`PowGadget::repeated_squares`] computes: + /// $self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}$. + fn repeated_squares(&self, n: usize) -> Vec; + + /// [`PowGadget::powers_from_repeated_squares`] expands a vector of repeated + /// squares $x^{2^0}, x^{2^1}, ..., x^{2^{n-1}}$ into all powers: + /// $x^0, x^1, ..., x^{2^n - 1}$. + fn powers_from_repeated_squares(squares: &[Self]) -> Vec; +} + +impl PowGadget for FpVar { + fn powers(&self, n: usize) -> Vec { + let mut res = vec![FpVar::one(); n]; + for i in 1..n { + res[i] = &res[i - 1] * self; + } + res + } + + fn repeated_squares(&self, n: usize) -> Vec { + if n == 0 { + return vec![]; + } + let mut res = vec![FpVar::zero(); n]; + res[0] = self.clone(); + for i in 1..n { + res[i] = &res[i - 1] * &res[i - 1]; + } + res + } + + fn powers_from_repeated_squares(squares: &[Self]) -> Vec { + let mut pows = vec![FpVar::one()]; + for square in squares.iter().rev() { + pows = pows + .into_iter() + .flat_map(|e| [e.clone(), e * square]) + .collect(); + } + pows + } +} diff --git a/crates/primitives/src/algebra/ops/rlc.rs b/crates/primitives/src/algebra/ops/rlc.rs new file mode 100644 index 000000000..301aa2696 --- /dev/null +++ b/crates/primitives/src/algebra/ops/rlc.rs @@ -0,0 +1,65 @@ +//! This module defines and implements the computation of random linear +//! combination (RLC). +//! +//! An RLC computes $\sum v_i \cdot c_i$ where $v_i$ are values (scalars or +//! vectors) and $c_i$ are the randomness (challenge coefficients), which is +//! used extensively in folding schemes. + +use ark_std::{ + iter::Sum, + ops::{Add, Mul}, +}; +use itertools::Itertools; + +/// [`ScalarRLC`] computes the random linear combination for a sequence of +/// scalars (i.e., each $v_i$ is a scalar). +pub trait ScalarRLC { + /// [`ScalarRLC::Value`] is the result type of the RLC computation. + type Value; + + /// [`ScalarRLC::scalar_rlc`] evaluates the RLC with the given coefficients + /// `coeffs`. + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value; +} + +impl ScalarRLC for I +where + I::Item: Add + Sum + for<'a> Mul<&'a Coeff, Output = I::Item>, +{ + type Value = I::Item; + + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value { + self.zip_eq(coeffs).map(|(v, c)| v * c).sum::() + } +} + +/// [`SliceRLC`] computes the random linear combination for a sequence of +/// vectors (i.e., each $v_i$ is a vector), by computing the RLC element-wise. +// TODO (@winderica): can we unify `ScalarRLC` and `SliceRLC` into one trait? +pub trait SliceRLC { + /// [`SliceRLC::Value`] is the result type of the RLC computation. + type Value; + + /// [`SliceRLC::slice_rlc`] evaluates the RLC with the given coefficients + /// `coeffs`. + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; +} + +impl<'a, T, I: Iterator, Coeff> SliceRLC for I +where + T: 'a + Add + Clone, + for<'x> T: Mul<&'x Coeff, Output = T>, +{ + type Value = T; + + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec { + let mut iter = self + .zip_eq(coeffs) + .map(|(v, c)| v.iter().map(|x| x.clone() * c)); + let first = iter.next().unwrap(); + + iter.fold(first.collect(), |acc, v| { + acc.into_iter().zip_eq(v).map(|(a, b)| a + b).collect() + }) + } +} diff --git a/crates/primitives/src/algebra/ops/vector.rs b/crates/primitives/src/algebra/ops/vector.rs new file mode 100644 index 000000000..e6bfa9fd5 --- /dev/null +++ b/crates/primitives/src/algebra/ops/vector.rs @@ -0,0 +1,40 @@ +//! This module provides definitions and implementations of in-circuit vector +//! operations. + +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; +use ark_std::ops::Index; + +/// [`VectorMulGadget`] defines the multiplication (dot product) operation on +/// in-circuit vector variables. +pub trait VectorMulGadget { + type Output; + + fn mul(&self, other: &Other) -> Result; +} + +impl VectorMulGadget<[FpVar]> for [FpVar] { + type Output = FpVar; + + fn mul(&self, other: &[FpVar]) -> Result { + if self.len() != other.len() { + return Err(SynthesisError::Unsatisfiable); + } + + Ok(self.iter().zip(other).map(|(a, b)| a * b).sum()) + } +} + +impl>> VectorMulGadget + for [(FpVar, usize)] +{ + type Output = FpVar; + + fn mul(&self, other: &Other) -> Result { + Ok(self + .iter() + .map(|(value, index)| value * &other[*index]) + .sum()) + } +} diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs new file mode 100644 index 000000000..183a50263 --- /dev/null +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -0,0 +1,179 @@ +//! This module implements the Customizable Constraint System (CCS) and its +//! relation checks against plain witnesses and instances. +//! +//! Proposed in the CCS [paper], it is a generalization of R1CS as well as many +//! other constraint systems. +//! A CCS structure is defined by the following components: +//! - The number of constraints `m`, the number of variables `n`, and the number +//! of public inputs `l`. +//! - The degree `d`. +//! - A sequence of `t` matrices `M`. +//! - A sequence of `q` multisets `S`, where each multiset `S_i` has at most `d` +//! elements and each element is an index in `[0, t - 1]` pointing to a matrix +//! `M_j`. +//! - A sequence of `q` coefficients `c`. +//! +//! A vector of assignments `z` satisfies the CCS if its evaluation +//! `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j · z))` is zero, where `〇` denotes +//! the Hadamard product among all `M_j · z`. +//! +//! [paper]: https://eprint.iacr.org/2023/552.pdf + +use ark_ff::Field; +use ark_poly::DenseMultilinearExtension; +use ark_relations::gr1cs::{ConstraintSystem, Matrix, SynthesisError}; +use ark_std::{cfg_into_iter, cfg_iter, ops::Index}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +use super::{Arith, Error}; +use crate::{ + algebra::{ + field::TwoStageFieldVar, + ops::{matrix::SparseMatrixVar, poly::MLEHelper, vector::VectorMulGadget}, + }, + circuits::Assignments, +}; + +/// [`CCS`] is an abstract trait that defines the behavior of all CCS variants, +/// including but not limited to R1CS. +pub trait CCS: + Arith + for<'a> From<&'a ConstraintSystem> + From> +{ + /// [`CCS::Field`] specifies the underlying field of a CCS instance + type Field: Field; + + /// [`CCS::matrices`] returns the matrices contained in a concrete CCS + /// instance `self`. + fn matrices(&self) -> &[Matrix]; + + /// [`CCS::evaluate_ccs`] evaluates the CCS relation at a given vector of + /// assignments, multisets, and coefficients. + fn evaluate_ccs( + &self, + z: Assignments + Sync>, + multisets: [Vec; Q], + coefficients: [Self::Field; Q], + ) -> Result, Error> { + let cfg = self.config(); + let matrices = self.matrices(); + + let public_len = z.public.as_ref().len(); + let private_len = z.private.as_ref().len(); + if public_len != cfg.n_public_inputs { + return Err(Error::MalformedAssignments(format!( + "The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", + cfg.n_public_inputs, public_len + ))); + } + if private_len != cfg.n_witnesses { + return Err(Error::MalformedAssignments(format!( + "The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", + cfg.n_witnesses, private_len + ))); + } + + // Recall that the evaluation of CCS at z is defined as: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j · z))`, + // where $\prod$ denotes the Hadamard product. + // + // Below, we manually expand the vector and matrix operations for less + // allocations and better efficiency. + // Specifically, we independently compute each entry of the resulting + // vector, and collect them at the end. + // We parallelize the outer loop over rows (when the `parallel` feature + // is enabled), since the number of constraints in the CCS is typically + // large in practice. + Ok(cfg_into_iter!(0..cfg.n_constraints) + .map(|row| { + // The `row`-th entry of the resulting vector is: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j[row] · z))` + multisets + .iter() + .zip(coefficients) + .map(|(s, c)| { + // Each term in the sum is: + // `c_i · 〇_{j ∈ S_i} (M_j[row] · z)` + c * s + .iter() + .map(|&i| { + // Each factor in the product is `M_j[row] · z`, + // i.e., the dot product of `M_j[row]` and `z`. + matrices[i][row] + .iter() + .map(|(val, col)| z[*col] * val) + .sum::() + }) + .product::() + }) + .sum() + }) + .collect()) + } + + /// [`CCS::mles`] returns the multilinear extensions of all CCS matrices + /// `M_i` evaluated over the assignments `z`. + fn mles( + &self, + z: Assignments + Sync>, + ) -> Vec> { + self.matrices() + .iter() + .map(|matrix| { + DenseMultilinearExtension::from_evaluations( + &cfg_iter!(matrix) + .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) + .collect::>(), + ) + }) + .collect() + } +} + +pub trait CCSGadget { + type FieldVar: TwoStageFieldVar; + + /// [`CCS::matrices`] returns the matrices contained in a concrete CCS + /// instance `self`. + fn matrices(&self) -> &[SparseMatrixVar]; + + /// [`CCS::evaluate_ccs`] evaluates the CCS relation at a given vector of + /// assignments, multisets, and coefficients. + fn evaluate_ccs, const Q: usize>( + &self, + z: A, + multisets: [Vec; Q], + coefficients: [impl Clone + Into<::Intermediate>; Q], + ) -> Result::Intermediate>, SynthesisError> + where + [(Self::FieldVar, usize)]: + VectorMulGadget::Intermediate>, + { + let matrices = self.matrices(); + + // Recall that the evaluation of CCS at z is defined as: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j · z))`, + // where $\prod$ denotes the Hadamard product. + (0..matrices[0].0.len()) + .map(|row| { + // The `row`-th entry of the resulting vector is: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j[row] · z))` + let mut sum = None; + + for (s, c) in multisets.iter().zip(&coefficients) { + // Each term in the sum is: + // `c_i · 〇_{j ∈ S_i} (M_j[row] · z)` + let mut prod = c.clone().into(); + for i in s { + prod = prod * matrices[*i].0[row].mul(&z)?; + } + sum = match sum { + Some(sum) => Some(sum + prod), + None => Some(prod), + }; + } + Ok(sum.unwrap()) + }) + .collect() + } +} diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs new file mode 100644 index 000000000..257b3661f --- /dev/null +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -0,0 +1,204 @@ +//! This module defines and implements traits for arithmetizations, also known +//! as constraint systems. +//! +//! In Sonobe, we currently support two constraint systems: the Rank-1 +//! Constraint System (R1CS) and the Customizable Constraint System (CCS). +//! However, user circuits are always synthesized into R1CS currently, since +//! R1CS is the only supported constraint system by ark-relations. + +use ark_ff::Field; +use ark_r1cs_std::alloc::AllocVar; +use ark_relations::gr1cs::SynthesisError; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::{fmt::Debug, log2}; +use thiserror::Error; + +use crate::relations::{Relation, RelationGadget}; + +pub mod ccs; +pub mod r1cs; + +/// [`Error`] enumerates possible errors during arithmetization operations. +#[derive(Error, Debug)] +pub enum Error { + /// [`Error::MalformedAssignments`] indicates that the provided assignments + /// have incorrect shape. + #[error("The provided assignments have incorrect shape: {0}")] + MalformedAssignments(String), + /// [`Error::UnsatisfiedAssignments`] indicates that the provided + /// assignments do not satisfy the constraint system. + #[error("The provided assignments do not satisfy the constraint system: {0}")] + UnsatisfiedAssignments(String), + /// [`Error::InvalidNumberOfConstraints`] indicates that the constraint + /// system's number of constraints does not match the provided config. + #[error( + "The number of constraints in the constraint system configuration is invalid. Provided: {0}, expected: {1}" + )] + InvalidNumberOfConstraints(usize, usize), + /// [`Error::InvalidNumberOfVariables`] indicates that the constraint + /// system's number of variables does not match the provided config. + #[error( + "The number of variables in the constraint system configuration is invalid. Provided: {0}, expected: {1}" + )] + InvalidNumberOfVariables(usize, usize), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. + #[error(transparent)] + SynthesisError(#[from] SynthesisError), +} + +/// [`ArithConfig`] describes the configuration of a constraint system. +#[derive(Clone, Debug, Default, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +pub struct ArithConfig { + /// [`ArithConfig::degree`] specifies the degree of the constraint system. + pub degree: usize, + + /// [`ArithConfig::n_constraints`] specifies the number of constraints in + /// the constraint system. + pub n_constraints: usize, + + /// [`ArithConfig::n_variables`] specifies the number of variables in the + /// constraint system. + pub n_variables: usize, + + /// [`ArithConfig::n_public_inputs`] specifies the number of public inputs + /// in the constraint system. + pub n_public_inputs: usize, + + /// [`ArithConfig::n_witnesses`] specifies the number of witnesses in the + /// constraint system. + pub n_witnesses: usize, +} + +impl ArithConfig { + /// [`ArithConfig::log_constraints`] returns the base-2 logarithm of the + /// number of constraints in the constraint system. + pub fn log_constraints(&self) -> usize { + log2(self.n_constraints) as usize + } +} + +/// [`Arith`] is a trait for constraint systems (R1CS, CCS, etc.), where we +/// define methods to get and set configuration about the constraint system. +/// In addition to the configuration, the implementor of this trait may also +/// store the actual constraints and other information. +pub trait Arith: Clone + Default + Send + Sync + CanonicalSerialize + CanonicalDeserialize { + /// [`Arith::config`] returns the configuration of the constraint system. + fn config(&self) -> ArithConfig; +} + +pub trait ArithGadget: AllocVar { + type ConstraintField: Field; + + type Widget: Arith; +} + +/// [`ArithRelation`] treats a constraint system as a relation between a witness +/// of type `W` and an instance of type `U`, and in this trait, we separate the +/// relation check into two steps: evaluating the constraint system and checking +/// the evaluation result. +/// +/// Note that `W` and `U` are part of the trait parameters instead of associated +/// types, because the same constraint system may support different types of `W` +/// and `U`, and the satisfiability check may vary. +/// This "same constraint system, different witness-instance pair" abstraction +/// turns out to be very flexible, as one constraint system struct now can have +/// many different relation checks depending on the context. +/// +/// For example, some folding schemes consider a variant of R1CS known as +/// relaxed R1CS, which is also represented by the `A`, `B`, and `C` matrices +/// but has a different relation check compared to plain R1CS. +/// We handle their similarities and differences in the following way: +/// - Since the structure of relaxed R1CS is exactly the same as plain R1CS, we +/// use a single R1CS struct to represent both of them. +/// - To distinguish their relation checks, we instead use distinct types of `W` +/// and `U`. +/// - For plain R1CS, we use plain witness `W = w` and instance `U = x` that +/// are simply vectors of field elements. +/// The implementation of `ArithRelation` for such `W` and `U` then checks +/// if `Az ∘ Bz = Cz`, where `z = [1, x, w]`. +/// - For relaxed R1CS, we use relaxed witness `W` and relaxed instance `U` +/// that contain extra data such as the error or slack terms, e.g., +/// - In Nova, `W = (w, e, ...)`, `U = (u, x, ...)`. +/// The implementation of `ArithRelation` for such `W` and `U` checks +/// if `Az ∘ Bz = uCz + e`, where `z = [u, x, w]`. +/// - In ProtoGalaxy, `W = (w, ...)`, `U = (x, e, β, ...)`. +/// The implementation of `ArithRelation` for such `W` and `U` checks +/// if `e = Σ pow_i(β) v_i`, where `v = Az ∘ Bz - Cz`,`z = [1, x, w]`. +/// +/// This is also the case for CCS, where `W` and `U` may be vectors of field +/// elements or running / incoming witness-instance pairs of different folding +/// schemes such as HyperNova. +pub trait ArithRelation: Arith { + /// [`ArithRelation::Evaluation`] defines the type of the evaluation result + /// returned by [`ArithRelation::eval_relation`], and consumed by + /// [`ArithRelation::check_evaluation`]. + /// + /// The evaluation result is usually a vector of field elements. + /// However, we use an associated type to represent the evaluation result + /// for future extensions. + type Evaluation; + + /// [`ArithRelation::eval_relation`] evaluates the constraint system at + /// witness `w` and instance `u`. It returns the evaluation result. + /// + /// For instance: + /// - Evaluating the plain R1CS at `W = w` and `U = x` returns + /// `Az ∘ Bz - Cz`, where `z = [1, x, w]`. + /// - Evaluating the relaxed R1CS in Nova at `W = (w, e, ...)` and + /// `U = (u, x, ...)` returns `Az ∘ Bz - uCz`, where `z = [u, x, w]`. + /// - Evaluating the relaxed R1CS in ProtoGalaxy at `W = (w, ...)` and + /// `U = (x, e, β, ...)` returns `Az ∘ Bz - Cz`, where `z = [1, x, w]`. + fn eval_relation(&self, w: &W, u: &U) -> Result; + + /// [`ArithRelation::check_evaluation`] checks if the evaluation result is + /// valid. The witness `w` and instance `u` are also parameters, because the + /// validity check may need information contained in `w` and/or `u`. + /// + /// For instance: + /// - The evaluation `v` of plain R1CS at satisfying `W` and `U` should be + /// an all-zero vector. + /// - The evaluation `v` of relaxed R1CS in Nova at satisfying `W` and `U` + /// should be equal to the error term `e` in `W`. + /// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W` + /// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error + /// term in `U`. + fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; +} + +impl> Relation for A { + type Error = Error; + + fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error> { + // `check_relation` is implemented by combining `eval_relation` and + // `check_evaluation`. + let e = self.eval_relation(w, u)?; + Self::check_evaluation(w, u, e) + } +} + +/// [`ArithRelationGadget`] defines the in-circuit gadget for constraint system +/// operations in the same way as [`ArithRelation`]. +pub trait ArithRelationGadget: ArithGadget { + /// [`ArithRelationGadget::Evaluation`] defines the type of the evaluation + /// result returned by [`ArithRelationGadget::eval_relation`], and consumed + /// by [`ArithRelationGadget::check_evaluation`]. + type Evaluation; + + /// [`ArithRelationGadget::eval_relation`] evaluates the constraint system + /// at witness `w` and instance `u`. It returns the evaluation result. + fn eval_relation(&self, w: &WVar, u: &UVar) -> Result; + + /// [`ArithRelationGadget::check_evaluation`] checks if the evaluation + /// result is valid under the help of the witness `w` and instance `u`. + fn check_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>; +} + +impl> RelationGadget for A { + fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> { + // `check_relation` is implemented by combining `eval_relation` and + // `check_evaluation`. + let e = self.eval_relation(w, u)?; + Self::check_evaluation(w, u, e) + } +} diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs new file mode 100644 index 000000000..869fbf9f8 --- /dev/null +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -0,0 +1,195 @@ +//! This module implements in-circuit R1CS variables and relation check gadgets. + +use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, ops::Index}; + +use super::{R1CS, RelaxedInstance, RelaxedWitness}; +use crate::{ + algebra::{ + field::TwoStageFieldVar, + ops::{eq::EquivalenceGadget, matrix::SparseMatrixVar, vector::VectorMulGadget}, + }, + arithmetizations::{ArithGadget, ArithRelationGadget, ccs::CCSGadget}, + circuits::Assignments, +}; + +/// [`R1CSVar`] is the in-circuit variable of a given R1CS structure. +/// +/// Only the matrices are represented, while the remaining R1CS parameters are +/// constants to the circuit. +#[allow(non_snake_case)] +#[derive(Debug, Clone)] +pub struct R1CSVar { + matrices: [SparseMatrixVar; 3], +} + +impl ArithGadget for R1CSVar { + type ConstraintField = FVar::ConstraintField; + + type Widget = R1CS; +} + +impl CCSGadget for R1CSVar { + type FieldVar = FVar; + + fn matrices(&self) -> &[SparseMatrixVar] { + &self.matrices[..] + } +} + +impl AllocVar, FVar::ConstraintField> for R1CSVar { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let val = val.borrow(); + + Ok(Self { + matrices: [ + AllocVar::new_variable(cs.clone(), || Ok(&val.matrices[0]), mode)?, + AllocVar::new_variable(cs.clone(), || Ok(&val.matrices[1]), mode)?, + AllocVar::new_variable(cs.clone(), || Ok(&val.matrices[2]), mode)?, + ], + }) + }) + } +} + +impl R1CSVar { + pub fn evaluate_r1cs>( + &self, + z: A, + ) -> Result, SynthesisError> + where + [(FVar, usize)]: VectorMulGadget, + { + let neg_u = FVar::additive_identity() - &z[0]; + self.evaluate_ccs( + z, + [vec![0, 1], vec![2]], + [FVar::multiplicative_identity().into(), neg_u], + ) + } +} + +impl, UVar: AsRef<[FVar]>> + ArithRelationGadget for R1CSVar +where + [FVar::Intermediate]: EquivalenceGadget<[FVar]>, + [(FVar, usize)]: + for<'a> VectorMulGadget, Output = FVar::Intermediate>, +{ + type Evaluation = Vec; + + fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { + self.evaluate_r1cs(Assignments::from(( + FVar::multiplicative_identity(), + u.as_ref(), + w.as_ref(), + ))) + } + + fn check_evaluation(_w: &WVar, _u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError> { + e.enforce_equivalent(&vec![FVar::additive_identity(); e.len()]) + } +} + +impl ArithRelationGadget, RelaxedInstance<&[FVar]>> + for R1CSVar +where + [FVar::Intermediate]: EquivalenceGadget<[FVar]>, + [(FVar, usize)]: + for<'a> VectorMulGadget, Output = FVar::Intermediate>, +{ + type Evaluation = Vec; + + fn eval_relation( + &self, + w: &RelaxedWitness<&[FVar]>, + u: &RelaxedInstance<&[FVar]>, + ) -> Result { + self.evaluate_r1cs(Assignments::from((u.u.clone(), u.x, w.w))) + } + + fn check_evaluation( + w: &RelaxedWitness<&[FVar]>, + _u: &RelaxedInstance<&[FVar]>, + e: Self::Evaluation, + ) -> Result<(), SynthesisError> { + e.enforce_equivalent(&w.e) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::{One, UniformRand, Zero}; + use ark_std::{error::Error, rand::thread_rng}; + + use super::*; + use crate::{ + circuits::test_utils::{constraints_for_test, satisfying_assignments_for_test}, + relations::Relation, + }; + + #[test] + fn test_eval() -> Result<(), Box> { + let mut rng = thread_rng(); + let r1cs = constraints_for_test::(); + + assert!( + r1cs.evaluate_r1cs(satisfying_assignments_for_test(Fr::rand(&mut rng)))? + .into_iter() + .all(|e| e.is_zero()) + ); + assert!( + !r1cs + .evaluate_r1cs(Assignments::from(( + Fr::one(), + vec![Fr::rand(&mut rng)], + vec![ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + )))? + .into_iter() + .all(|e| e.is_zero()) + ); + + Ok(()) + } + + #[test] + fn test_check() -> Result<(), Box> { + let mut rng = thread_rng(); + let r1cs = constraints_for_test::(); + + let assignments = satisfying_assignments_for_test(Fr::rand(&mut rng)); + + assert!( + r1cs.check_relation(&assignments.private, &assignments.public) + .is_ok() + ); + assert!( + r1cs.check_relation( + &[ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + &[Fr::rand(&mut rng)] + ) + .is_err() + ); + + Ok(()) + } +} diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs new file mode 100644 index 000000000..0c87aba7c --- /dev/null +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -0,0 +1,305 @@ +//! This module implements the Rank-1 Constraint System (R1CS) and its relation +//! checks against plain and relaxed witnesses and instances. + +use ark_ff::Field; +use ark_relations::gr1cs::{ConstraintSystem, Matrix, R1CS_PREDICATE_LABEL}; +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, +}; +use ark_std::{cfg_into_iter, cfg_iter}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +use super::{Arith, ArithConfig, ArithRelation, Error, ccs::CCS}; +use crate::circuits::Assignments; + +pub mod circuits; + +/// [`R1CS`] holds the three sparse matrices `A`, `B`, `C` together with the +/// configuration. +#[derive(Debug, Clone, Default, PartialEq, CanonicalSerialize)] +pub struct R1CS { + m: usize, // number of constraints + n: usize, // number of variables + l: usize, // io len + matrices: [Matrix; 3], +} + +impl Arith for R1CS { + #[inline] + fn config(&self) -> ArithConfig { + ArithConfig { + degree: 2, + n_constraints: self.m, + n_variables: self.n, + n_public_inputs: self.l, + n_witnesses: self.n - self.l - 1, + } + } +} + +impl CCS for R1CS { + type Field = F; + + fn matrices(&self) -> &[Matrix] { + &self.matrices[..] + } +} + +impl R1CS { + /// [`R1CS::new`] creates a new R1CS structure from the given configuration + /// and matrices. + pub fn new( + n_constraints: usize, + n_variables: usize, + n_public_inputs: usize, + matrices: [Matrix; 3], + ) -> Result { + let r1cs = + Self::new_without_validity_check(n_constraints, n_variables, n_public_inputs, matrices); + r1cs.validate()?; + Ok(r1cs) + } + + /// [`R1CS::validate`] checks that the structural invariant of the R1CS + /// holds, i.w., every matrix has exactly `m` rows (one per constraint), and + /// no column index reaches beyond the `n` variables. + pub fn validate(&self) -> Result<(), Error> { + for matrix in &self.matrices { + if matrix.len() != self.m { + return Err(Error::InvalidNumberOfConstraints(self.m, matrix.len())); + } + for row in matrix { + if let Some(max) = row.iter().map(|(_, i)| *i).max() + && max >= self.n + { + return Err(Error::InvalidNumberOfVariables(self.n, max + 1)); + } + } + } + Ok(()) + } + + /// [`R1CS::new_without_validity_check`] creates a new R1CS structure from + /// the given configuration and matrices without checking their validity. + pub fn new_without_validity_check( + n_constraints: usize, + n_variables: usize, + n_public_inputs: usize, + matrices: [Matrix; 3], + ) -> Self { + Self { + m: n_constraints, + l: n_public_inputs, + n: n_variables, + matrices, + } + } + + /// [`R1CS::evaluate_r1cs`] evaluates the R1CS relation at a given vector of + /// assignments `z`. + /// + /// This method is simply a wrapper of [`CCS::evaluate_ccs`] with fixed + /// coefficients and multisets. + pub fn evaluate_r1cs( + &self, + z: Assignments + Sync>, + ) -> Result, Error> { + let u = z[0]; + self.evaluate_ccs(z, [vec![0, 1], vec![2]], [F::one(), -u]) + } +} + +impl Valid for R1CS { + fn check(&self) -> Result<(), SerializationError> { + self.matrices.check()?; + self.validate().map_err(|_| SerializationError::InvalidData) + } +} + +impl CanonicalDeserialize for R1CS { + fn deserialize_with_mode( + mut reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let m = usize::deserialize_with_mode(&mut reader, compress, Validate::No)?; + let n = usize::deserialize_with_mode(&mut reader, compress, Validate::No)?; + let l = usize::deserialize_with_mode(&mut reader, compress, Validate::No)?; + let matrices = + <[Matrix; 3]>::deserialize_with_mode(&mut reader, compress, Validate::No)?; + + let r1cs = Self::new_without_validity_check(m, n, l, matrices); + if validate == Validate::Yes { + r1cs.check()?; + } + Ok(r1cs) + } +} + +impl From<&ConstraintSystem> for R1CS { + fn from(cs: &ConstraintSystem) -> Self { + // Get the R1CS predicate matrices + let r1cs_predicate = &cs.predicate_constraint_systems[R1CS_PREDICATE_LABEL]; + let matrices = r1cs_predicate.to_matrices(cs); + + // matrices are extracted from a circuit, which we assume is trusted + R1CS::new_without_validity_check( + cs.num_constraints(), + cs.num_instance_variables + cs.num_witness_variables, + cs.num_instance_variables - 1, // -1 to subtract the first '1' + matrices.try_into().unwrap(), // safe as R1CS always has 3 matrices + ) + } +} + +impl From> for R1CS { + fn from(cs: ConstraintSystem) -> Self { + Self::from(&cs) + } +} + +impl, U: AsRef<[F]>> ArithRelation for R1CS { + type Evaluation = Vec; + + fn eval_relation(&self, w: &W, x: &U) -> Result { + self.evaluate_r1cs((F::one(), x.as_ref(), w.as_ref()).into()) + } + + fn check_evaluation(_w: &W, _x: &U, e: Self::Evaluation) -> Result<(), Error> { + cfg_into_iter!(e) + .all(|i| i.is_zero()) + .then_some(()) + .ok_or(Error::UnsatisfiedAssignments( + "Evaluation contains non-zero values".into(), + )) + } +} + +/// [`RelaxedWitness`] defines a relaxed version of R1CS witness. +/// +/// It is the basis of witnesses in many folding schemes that support R1CS. +pub struct RelaxedWitness { + /// [`RelaxedWitness::w`] is the witness vector + pub w: V, + /// [`RelaxedWitness::e`] is the error term + pub e: V, +} + +/// [`RelaxedInstance`] defines a relaxed version of R1CS instance. +/// +/// It is the basis of instances in many folding schemes that support R1CS. +pub struct RelaxedInstance { + /// [`RelaxedInstance::x`] is the public input vector + pub x: V, + /// [`RelaxedInstance::u`] is the constant term + pub u: V::Item, +} + +impl ArithRelation, RelaxedInstance<&[F]>> for R1CS { + type Evaluation = Vec; + + fn eval_relation( + &self, + w: &RelaxedWitness<&[F]>, + u: &RelaxedInstance<&[F]>, + ) -> Result { + self.evaluate_r1cs((*u.u, u.x, w.w).into()) + } + + fn check_evaluation( + w: &RelaxedWitness<&[F]>, + _u: &RelaxedInstance<&[F]>, + v: Self::Evaluation, + ) -> Result<(), Error> { + if w.e.len() != v.len() { + return Err(Error::MalformedAssignments(format!( + "The number of constraints in R1CS ({}) does not match the length of the provided relaxed witness's error term ({}).", + v.len(), + w.e.len() + ))); + } + + cfg_iter!(w.e) + .zip(&v) + .all(|(e, v)| e == v) + .then_some(()) + .ok_or(Error::UnsatisfiedAssignments( + "Evaluation does not match error term".into(), + )) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_std::{error::Error, rand::thread_rng}; + + use super::*; + use crate::{ + circuits::test_utils::{constraints_for_test, satisfying_assignments_for_test}, + relations::Relation, + }; + + #[test] + fn test_check() -> Result<(), Box> { + let mut rng = thread_rng(); + let r1cs = constraints_for_test::(); + + let assignments = satisfying_assignments_for_test(Fr::rand(&mut rng)); + + assert!( + r1cs.check_relation(&assignments.private, &assignments.public) + .is_ok() + ); + assert!( + r1cs.check_relation( + &[ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + &[Fr::rand(&mut rng)] + ) + .is_err() + ); + + Ok(()) + } + + #[test] + fn test_deserialize_rejects_malformed() -> Result<(), Box> { + let valid = R1CS::::new(1, 1, 0, [vec![vec![]], vec![vec![]], vec![vec![]]]).unwrap(); + let mut bytes = vec![]; + valid.serialize_compressed(&mut bytes)?; + assert_eq!(valid, R1CS::::deserialize_compressed(&bytes[..])?); + + let mismatched_constraints = R1CS::::new_without_validity_check( + 2, + 1, + 0, + [vec![vec![]], vec![vec![]], vec![vec![]]], + ); + let mut bytes = vec![]; + mismatched_constraints + .serialize_compressed(&mut bytes) + .unwrap(); + assert!(R1CS::::deserialize_compressed_unchecked(&bytes[..]).is_ok()); + assert!(R1CS::::deserialize_compressed(&bytes[..]).is_err()); + + let out_of_range_variable = R1CS::::new_without_validity_check( + 1, + 1, + 0, + [vec![vec![(Fr::from(1u64), 5)]], vec![vec![]], vec![vec![]]], + ); + let mut bytes = vec![]; + out_of_range_variable.serialize_compressed(&mut bytes)?; + assert!(R1CS::::deserialize_compressed_unchecked(&bytes[..]).is_ok()); + assert!(R1CS::::deserialize_compressed(&bytes[..]).is_err()); + + Ok(()) + } +} diff --git a/crates/primitives/src/circuits/cache.rs b/crates/primitives/src/circuits/cache.rs new file mode 100644 index 000000000..d5986e064 --- /dev/null +++ b/crates/primitives/src/circuits/cache.rs @@ -0,0 +1,27 @@ +use ark_std::hash::{BuildHasherDefault, Hasher}; +use hashbrown::HashSet; + +#[derive(Default)] +pub struct IdentityHasher { + value: u64, +} + +impl Hasher for IdentityHasher { + fn finish(&self) -> u64 { + self.value + } + + fn write(&mut self, _: &[u8]) { + panic!("IdentityHasher only supports usize"); + } + + fn write_usize(&mut self, value: usize) { + self.value = value as u64; + } +} + +pub type UsizeSet = HashSet>; + +pub struct CommittedCache; +pub struct CommitmentKeyCache; +pub struct RandomnessCache; diff --git a/crates/primitives/src/circuits/inputize.rs b/crates/primitives/src/circuits/inputize.rs new file mode 100644 index 000000000..1ec1a0455 --- /dev/null +++ b/crates/primitives/src/circuits/inputize.rs @@ -0,0 +1,25 @@ +use ark_ff::Field; +use ark_r1cs_std::GR1CSVar; + +/// [`Inputize`] converts a value into a vector of field elements, ordered in +/// the same way as how the value's corresponding in-circuit variable would be +/// represented in the circuit when allocated as public input. +/// +/// This is useful for the verifier to compute the public inputs. +pub trait Inputize: GR1CSVar { + /// [`Inputize::inputize`] outputs the underlying field elements of `self` + /// as if it is allocated in the canonical way in-circuit. + fn inputize(value: &Self::Value) -> Vec; +} + +impl> Inputize for [T] { + fn inputize(value: &Self::Value) -> Vec { + value.iter().flat_map(T::inputize).collect() + } +} + +impl, const N: usize> Inputize for [T; N] { + fn inputize(value: &Self::Value) -> Vec { + value.iter().flat_map(T::inputize).collect() + } +} diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs new file mode 100644 index 000000000..ad94466fd --- /dev/null +++ b/crates/primitives/src/circuits/mod.rs @@ -0,0 +1,374 @@ +//! This module defines circuits and helpers used by Sonobe. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, OptimizationGoal, SynthesisError, + SynthesisMode, +}; +use ark_std::{ + fmt::Debug, + mem::take, + ops::{Deref, Index, IndexMut}, +}; + +use crate::{ + circuits::inputize::Inputize, + transcripts::{Absorbable, AbsorbableVar}, +}; + +pub mod cache; +pub mod inputize; +pub mod test_utils; + +/// [`FCircuit`] defines the trait of step circuits being proven by IVC schemes. +/// +/// In IVC, a step circuit is repeatedly invoked to update some state persisted +/// throughout the execution. +/// For flexibility, we further allow each step to take some external inputs +/// and produce some external outputs that are not part of the state, which may +/// or may not be constrained inside the step circuit. +/// +/// Such a design has several advantages: +/// 1. It allows the implementation to keep the state minimal, only including +/// the parts that need to be preserved and constrained across steps, while +/// step-specific inputs that might be large are not part of the state. +/// +/// For example, in a Merkle tree update circuit, the state may only contain +/// the root of the tree, while the leaf value and authentication path which +/// are large can be provided as external inputs at each step. +/// +/// 2. The caller of the step circuit can peek into the circuit execution at +/// each step via the external outputs by having the circuit return +/// `var.value()` for desired variables. +/// +/// For example, in a Merkle tree update circuit, the circuit can return the +/// intermediate hashes computed at each step as external outputs, allowing +/// the caller to test if the hash computation is correct. +/// +/// 3. The implementation can mix out-of-circuit and in-circuit logic in this +/// structure, where the out-of-circuit logic may consume external inputs and +/// produce external outputs for the next step. +/// This is why the implementation may choose to constrain or not constrain +/// the external inputs/outputs inside the step circuit. +/// Such a mixed design can be helpful if the out-of-circuit logic and the +/// in-circuit logic are highly interdependent. +/// +/// For example, in a Merkle tree update circuit, one may write both the +/// Merkle proof generation (out-of-circuit) and verification (in-circuit) +/// logic in a single [`FCircuit::synthesize_step`]. +/// In this case, the external inputs contain the leaf value to be added, as +/// well as all the existing tree nodes. +/// The latter will be used by the out-of-circuit logic to compute the path, +/// but will not be constrained inside the circuit. +/// The external outputs contain the new tree nodes after the update, which +/// will be used as the inputs to the next step. +/// +/// To summarize, the step circuit takes as input the current state and some +/// external inputs, and returns the next state and some external outputs. +pub trait FCircuit { + /// [`FCircuit::Field`] is the field over which the circuit is defined. + type Field: PrimeField; + /// [`FCircuit::State`] is the type of the state. + /// + /// It is usually an array of field elements, but we make our design quite + /// flexible so that the implementation is free to choose any structure for + /// it. + type State: Clone + PartialEq + Absorbable; + /// [`FCircuit::StateVar`] is the in-circuit variable type for the state. + /// + /// If the implementation chooses custom structures for the state, it should + /// implement the required traits for the corresponding variable type. + type StateVar: GR1CSVar + + AllocVar + + AbsorbableVar + + EqGadget + + Inputize; + /// [`FCircuit::ExternalInputs`] is the type of external inputs provided to + /// each step of the circuit. + type ExternalInputs; + /// [`FCircuit::ExternalOutputs`] is the type of external outputs produced + /// by each step of the circuit. + type ExternalOutputs; + + /// [`FCircuit::same_state_shape`] returns whether two states `a` and `b` + /// have the same shape/structure. + /// + /// This allows the verifier to check whether the prover's claimed states + /// have the desired shape. + /// + /// The implementation should perform the checks carefully to ensure that + /// all fields/members of the provided states are consistent. Specifically, + /// if all fields/members of [`FCircuit::State`] have fixed size, then the + /// shape consistency is trivially `true`. However, if [`FCircuit::State`] + /// contains variable-length fields/members (e.g., `Vec`, `Vec>`), + /// then the implementation must examine all of such fields/members and + /// return `false` when any of them are differently sized in `a` and `b`. + fn same_state_shape(a: &Self::State, b: &Self::State) -> bool; + + /// [`FCircuit::dummy_state`] returns a dummy state for the circuit. + /// + /// The dummy state should have the same shape as states in real IVC + /// executions. + fn dummy_state(&self) -> Self::State; + + /// [`FCircuit::dummy_external_inputs`] returns dummy external inputs for + /// the circuit. + fn dummy_external_inputs(&self) -> Self::ExternalInputs; + + /// [`FCircuit::synthesize_step`] generates the constraints for + /// the `i`-th step of invocation of the step circuit with the current state + /// `state` and external inputs `external_inputs`, producing the next state + /// and external outputs. + /// + /// ### Tips + /// + /// - Since this method uses `self`, the implementation can store some fixed + /// info that is shared across all steps inside `self`. + /// - Variables in the implementation should be allocated as witnesses (not + /// public inputs) in the implementation. + /// - If needed, the constraint system `cs` can be accessed via `i.cs()` or + /// `state.cs()` using arkworks' [`GR1CSVar::cs`] method. + fn synthesize_step( + &self, + i: FpVar, + state: Self::StateVar, + external_inputs: Self::ExternalInputs, + ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError>; +} + +/// [`Assignments`] represents a full assignment vector `z = (u, x, w)` for a +/// constraint system. +#[derive(Clone, Debug, PartialEq)] +pub struct Assignments { + /// [`Assignments::constant`] is the "constant" part (leading scalar) of the + /// assignment, which is usually 1 but might be relaxed in some cases. + pub constant: F, + /// [`Assignments::public`] contains the public inputs. + pub public: V, + /// [`Assignments::private`] contains the witnesses. + pub private: V, +} + +/// [`AssignmentsOwned`] is a convenience alias for owned assignment vectors. +pub type AssignmentsOwned = Assignments>; + +impl From<(F, V, V)> for Assignments { + fn from((u, x, w): (F, V, V)) -> Self { + Self { + constant: u, + public: x, + private: w, + } + } +} + +impl> Index for Assignments { + type Output = F; + + fn index(&self, index: usize) -> &Self::Output { + let public = self.public.as_ref(); + let private = self.private.as_ref(); + if index == 0 { + &self.constant + } else if index <= public.len() { + &public[index - 1] + } else { + &private[index - 1 - public.len()] + } + } +} + +impl + AsMut<[F]>> IndexMut for Assignments { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + let public = self.public.as_mut(); + let private = self.private.as_mut(); + if index == 0 { + &mut self.constant + } else if index <= public.len() { + &mut public[index - 1] + } else { + &mut private[index - 1 - public.len()] + } + } +} + +/// [`ConstraintSystemExt`] wraps a `ConstraintSystemRef` with compile-time +/// flags that control whether constraint matrices (`ARITH_ENABLED`) and / or +/// assignment vectors (`ASSIGNMENTS_ENABLED`) are collected during synthesis. +pub struct ConstraintSystemExt( + ConstraintSystem, +); + +impl Deref + for ConstraintSystemExt +{ + type Target = ConstraintSystem; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl + ConstraintSystemExt +{ + /// [`ConstraintSystemExt::new`] creates a new constraint system wrapper + /// with the specified flags. + pub fn new() -> Self { + let mut cs = ConstraintSystem::::new(); + cs.set_optimization_goal(OptimizationGoal::Constraints); + let mode = if ASSIGNMENTS_ENABLED { + SynthesisMode::Prove { + construct_matrices: ARITH_ENABLED, + generate_lc_assignments: ARITH_ENABLED, + } + } else { + SynthesisMode::Setup + }; + cs.set_mode(mode); + Self(cs) + } + + /// [`ConstraintSystemExt::execute_synthesizer`] executes a circuit inside + /// the constraint system, where the circuit should implement the + /// [`ConstraintSynthesizer`] trait. + pub fn execute_synthesizer( + &mut self, + circuit: impl ConstraintSynthesizer, + ) -> Result<(), SynthesisError> { + self.execute_fn(|cs| circuit.generate_constraints(cs)) + } + + /// [`ConstraintSystemExt::execute_fn`] executes a circuit inside the + /// constraint system, where the circuit should be defined as a closure that + /// takes as input a `ConstraintSystemRef` and returns a result of type `R`. + /// The return value of the closure will be returned by this method. + pub fn execute_fn( + &mut self, + circuit: impl FnOnce(ConstraintSystemRef) -> Result, + ) -> Result { + let cs = take(&mut self.0); + let result = { + let cs_ref = ConstraintSystemRef::new(cs); + let result = circuit(cs_ref.clone())?; + self.0 = cs_ref.into_inner().unwrap(); + result + }; + if ARITH_ENABLED { + self.0.finalize(); + } + Ok(result) + } +} + +impl Default + for ConstraintSystemExt +{ + fn default() -> Self { + Self::new() + } +} + +/// [`ArithExtractor`] collects only the constraint matrices (no assignments) +/// from a synthesized circuit. +pub type ArithExtractor = ConstraintSystemExt; +/// [`AssignmentsExtractor`] collects only the assignments (no constraint +/// matrices) from a synthesized circuit. +pub type AssignmentsExtractor = ConstraintSystemExt; + +impl ArithExtractor { + /// [`ArithExtractor::arith`] extracts the constraint matrices from the + /// circuit and returns them as an arithmetization / constraint system + /// structure of type `A`. + pub fn arith>>(self) -> Result { + Ok(self.0.into()) + } +} + +impl AssignmentsExtractor { + /// [`AssignmentsExtractor::assignments`] extracts the assignments from the + /// circuit and returns them as `Assignments`. + pub fn assignments(self) -> Result>, SynthesisError> { + let cs = self.0; + + let witness = cs.assignments.witness_assignment; + let mut instance = cs.assignments.instance_assignment; + // skip the first element which is '1' + instance.remove(0); + + Ok((F::one(), instance, witness).into()) + } +} + +/// [`WitnessToPublic`] defines a helper trait for marking witness variables as +/// public inputs in the constraint system. +pub trait WitnessToPublic { + /// [`WitnessToPublic::mark_as_public`] marks a witness variable as public. + fn mark_as_public(&self) -> Result<(), SynthesisError>; +} + +impl WitnessToPublic for &T { + fn mark_as_public(&self) -> Result<(), SynthesisError> { + (*self).mark_as_public() + } +} + +impl WitnessToPublic for [T] { + fn mark_as_public(&self) -> Result<(), SynthesisError> { + self.iter().try_for_each(|x| x.mark_as_public()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_relations::gr1cs::ConstraintSynthesizer; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::{ + test_utils::{CircuitForTest, constraints_for_test, satisfying_assignments_for_test}, + *, + }; + use crate::arithmetizations::r1cs::R1CS; + + #[test] + fn test_satisfiability() -> Result<(), Box> { + let mut rng = thread_rng(); + let circuit = CircuitForTest:: { + x: Fr::rand(&mut rng), + }; + let cs = ConstraintSystem::new_ref(); + circuit.generate_constraints(cs.clone())?; + assert!(cs.is_satisfied()?); + + Ok(()) + } + + #[test] + fn test_constraint_extraction() -> Result<(), Box> { + let mut rng = thread_rng(); + let circuit = CircuitForTest:: { + x: Fr::rand(&mut rng), + }; + let mut cs = ArithExtractor::new(); + cs.execute_synthesizer(circuit)?; + assert_eq!(cs.arith::>()?, constraints_for_test()); + Ok(()) + } + + #[test] + fn test_witness_extraction() -> Result<(), Box> { + let mut rng = thread_rng(); + let x = Fr::rand(&mut rng); + let circuit = CircuitForTest:: { x }; + + let mut cs = AssignmentsExtractor::new(); + cs.execute_synthesizer(circuit)?; + assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x)); + Ok(()) + } +} diff --git a/crates/primitives/src/circuits/test_utils.rs b/crates/primitives/src/circuits/test_utils.rs new file mode 100644 index 000000000..137d465d3 --- /dev/null +++ b/crates/primitives/src/circuits/test_utils.rs @@ -0,0 +1,159 @@ +//! This module provides utility circuits. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + fields::fp::{AllocatedFp, FpVar}, +}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError, Variable}; + +use super::Assignments; +use crate::{algebra::field::SonobeField, arithmetizations::r1cs::R1CS, circuits::FCircuit}; + +/// [`CircuitForTest`] implements a simple test circuit computing +/// `y = x^3 + x + 5` with 4 R1CS constraints. +/// +/// It is used in unit tests to verify constraint extraction and witness +/// generation. +pub struct CircuitForTest { + /// [`CircuitForTest::x`] is the input variable `x` of the circuit. + pub x: F, +} + +impl ConstraintSynthesizer for CircuitForTest { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + // Variable 0 (implicitly added by arkworks as 1) + // Variable 1 + let x = AllocatedFp::new_input(cs.clone(), || Ok(self.x))?; + // Variable 2 + let y = AllocatedFp::new_witness(cs.clone(), || Ok(self.x.pow([3]) + self.x + F::from(5)))?; + + // Variable 3, Constraint 0 + let x_square = x.square()?; + // Variable 4, Constraint 1 + let x_cube = x_square.mul(&x); + // Variable 5 + let t = AllocatedFp::new_witness(cs.clone(), || Ok(self.x.pow([3]) + self.x))?; + let x_cube_plus_x = x.add(&x_cube); + // Constraint 2 + cs.enforce_r1cs_constraint( + || x_cube_plus_x.variable.into(), + || Variable::one().into(), + || t.variable.into(), + )?; + let x_cube_plus_x_plus_5 = t.add_constant(F::from(5)); + // Constraint 3 + cs.enforce_r1cs_constraint( + || x_cube_plus_x_plus_5.variable.into(), + || Variable::one().into(), + || y.variable.into(), + )?; + Ok(()) + } +} + +impl FCircuit for CircuitForTest { + type Field = F; + type State = [F; 1]; + type StateVar = [FpVar; 1]; + + type ExternalInputs = (); + type ExternalOutputs = (); + + fn dummy_state(&self) -> Self::State { + [F::zero(); 1] + } + + fn same_state_shape(_a: &Self::State, _b: &Self::State) -> bool { + // `[F; 1]` is fixed-size, so all states share the same shape. + true + } + + fn dummy_external_inputs(&self) -> Self::ExternalInputs {} + + fn synthesize_step( + &self, + _i: FpVar, + z_i: Self::StateVar, + _external_inputs: Self::ExternalInputs, + ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError> { + let cs = z_i.cs(); + + // Variable 0 (implicitly added by arkworks as 1) + // Variable 1 + let x = if let FpVar::Var(x) = z_i[0].clone() { + x + } else { + unreachable!() + }; + // Variable 2 + let y = AllocatedFp::new_witness(cs.clone(), || { + Ok(x.value()?.pow([3]) + x.value()? + F::from(5)) + })?; + + // Variable 3, Constraint 0 + let x_square = x.square()?; + // Variable 4, Constraint 1 + let x_cube = x_square.mul(&x); + // Variable 5 + let t = AllocatedFp::new_witness(cs.clone(), || Ok(x.value()?.pow([3]) + x.value()?))?; + let x_cube_plus_x = x.add(&x_cube); + // Constraint 2 + cs.enforce_r1cs_constraint( + || x_cube_plus_x.variable.into(), + || Variable::one().into(), + || t.variable.into(), + )?; + let x_cube_plus_x_plus_5 = t.add_constant(F::from(5)); + // Constraint 3 + cs.enforce_r1cs_constraint( + || x_cube_plus_x_plus_5.variable.into(), + || Variable::one().into(), + || y.variable.into(), + )?; + Ok(([FpVar::Var(x_cube_plus_x_plus_5)], ())) + } +} + +/// [`constraints_for_test`] returns the R1CS constraints for the test circuit. +#[allow(non_snake_case)] +pub fn constraints_for_test() -> R1CS { + // R1CS for: x^3 + x + 5 = y (example from article + // https://vitalik.eth.limo/general/2016/12/10/qap.html) + let A = vec![ + vec![(F::one(), 1)], + vec![(F::one(), 3)], + vec![(F::one(), 1), (F::one(), 4)], + vec![(F::from(5), 0), (F::one(), 5)], + ]; + let B = vec![ + vec![(F::one(), 1)], + vec![(F::one(), 1)], + vec![(F::one(), 0)], + vec![(F::one(), 0)], + ]; + let C = vec![ + vec![(F::one(), 3)], + vec![(F::one(), 4)], + vec![(F::one(), 5)], + vec![(F::one(), 2)], + ]; + + R1CS::::new_without_validity_check(4, 6, 1, [A, B, C]) +} + +/// [`satisfying_assignments_for_test`] returns a satisfying assignment for the +/// test circuit given an input `x`. +pub fn satisfying_assignments_for_test(x: F) -> Assignments> { + Assignments::from(( + F::one(), + vec![x], + vec![ + x * x * x + x + F::from(5), // x^3 + x + 5 + x * x, // x^2 + x * x * x, // x^2 * x + x * x * x + x, // x^3 + x + ], + )) +} diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs new file mode 100644 index 000000000..79709017b --- /dev/null +++ b/crates/primitives/src/commitments/mod.rs @@ -0,0 +1,251 @@ +//! Abstract traits and implementations for commitment schemes. + +use ark_ff::UniformRand; +use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, select::CondSelectGadget, +}; +use ark_relations::gr1cs::SynthesisError; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::{ + fmt::Debug, + iter::Sum, + ops::{Add, Mul}, + rand::RngCore, +}; +use thiserror::Error; + +use crate::{ + algebra::{ + Val, + field::{SonobeField, TwoStageFieldVar, emulated::EmulatedFieldVar}, + group::{CF1, CF2, SonobeCurve, emulated::EmulatedAffineVar}, + ops::bits::FromBitsGadget, + }, + circuits::inputize::Inputize, + transcripts::{Absorbable, AbsorbableVar}, +}; + +pub mod pedersen; +// TODO: add back other commitment schemes + +/// [`Error`] enumerates possible errors during commitment operations. +#[derive(Debug, Error)] +pub enum Error { + /// [`Error::MessageTooLong`] indicates that the message being committed to + /// is longer than the maximum supported length. + #[error( + "The message being committed to has length {1}, exceeding the maximum supported length ({0})" + )] + MessageTooLong(usize, usize), + /// [`Error::CommitmentVerificationFail`] indicates that the provided + /// opening does not verify against the commitment. + #[error("Commitment verification failed")] + CommitmentVerificationFail, +} + +/// [`CommitmentKey`] represents a commitment key (e.g., a vector of group +/// generators for many group-based commitment schemes). +pub trait CommitmentKey: Clone + Send + Sync + CanonicalSerialize + CanonicalDeserialize { + /// [`CommitmentKey::max_scalars_len`] returns the maximum number of scalars + /// that can be committed to with this key. + fn max_scalars_len(&self) -> usize; +} + +/// [`CommitmentDef`] provides the core type definitions of a commitment scheme, +/// defining the types of relevant cryptographic objects such as the commitment +/// key, scalars, commitments, and randomness. +pub trait CommitmentDef: 'static + Clone + Debug + PartialEq + Eq { + /// [`CommitmentDef::IS_HIDING`] indicates whether the commitment scheme has + /// the hiding property. + const IS_HIDING: bool; + + /// [`CommitmentDef::Key`] is the type of the commitment key. + type Key: CommitmentKey; + /// [`CommitmentDef::Scalar`] is the type of the scalars being committed to. + /// + /// For generality, we do not restrict this to field elements and instead + /// only bound it by necessary traits. + type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable + UniformRand; + /// [`CommitmentDef::Commitment`] is the type of the commitment. + /// + /// In the future we may introduce other commitment schemes such as those + /// based on hash functions or lattices, so we do not restrict this to be + /// group elements. + type Commitment: Clone + Default + Debug + PartialEq + Eq + Sync + Absorbable; + /// [`CommitmentDef::Randomness`] is the type of the randomness used in + /// the commitment. + /// + /// Hiding commitment schemes and non-hiding schemes may have different + /// randomness types, e.g., the former holds real data, while the latter + /// is just a placeholder type. + /// + /// In this way, we can leverage the compiler to reject misuse, e.g., using + /// randomness where it is not needed, or vice versa, with a unified API. + type Randomness: Clone + + Copy + + Default + + Debug + + PartialEq + + Eq + + Sync + + Add + + Mul + + for<'a> Add<&'a Self::Scalar, Output = Self::Randomness> + + for<'a> Mul<&'a Self::Scalar, Output = Self::Randomness> + + Add + + Mul + + Sum; +} + +/// [`CommitmentOps`] defines algorithms for commitment schemes. +pub trait CommitmentOps: CommitmentDef { + /// [`CommitmentOps::generate_key`] defines the key generation algorithm, + /// which is a randomized algorithm that takes as input the maximum length + /// `len` of supported messages, and a randomness source `rng`, and outputs + /// the commitment key. + fn generate_key(len: usize, rng: impl RngCore) -> Result; + + /// [`CommitmentOps::commit`] defines the commitment generation algorithm, + /// which is a (probably) randomized algorithm that takes as input + /// commitment key `ck`, a vector of scalars `v` to be committed to, and a + /// randomness source `rng`, and outputs the commitment and the randomness. + fn commit( + ck: &Self::Key, + v: &[Self::Scalar], + rng: impl RngCore, + ) -> Result<(Self::Commitment, Self::Randomness), Error>; + + /// [`CommitmentOps::open`] defines the commitment opening algorithm, which + /// is a deterministic algorithm that takes as input commitment key `ck`, + /// a vector of scalars `v`, the randomness `r`, and a commitment `cm`, and + /// outputs `Ok(())` if the opening verifies, or an error otherwise. + fn open( + ck: &Self::Key, + v: &[Self::Scalar], + r: &Self::Randomness, + cm: &Self::Commitment, + ) -> Result<(), Error>; +} + +/// [`CommitmentDefGadget`] specifies the in-circuit associated types for a +/// commitment scheme gadget. +pub trait CommitmentDefGadget: Clone { + /// [`CommitmentDefGadget::ConstraintField`] is the field over which the + /// circuit running the commitment scheme is defined. + type ConstraintField: SonobeField; + + /// [`CommitmentDefGadget::KeyVar`] is the in-circuit variable type for the + /// commitment key. + type KeyVar: AllocVar<::Key, Self::ConstraintField>; + /// [`CommitmentDefGadget::ScalarVar`] is the in-circuit variable type for + /// the scalars being committed to. + type ScalarVar: AbsorbableVar + + CondSelectGadget + + EqGadget + + FromBitsGadget + + TwoStageFieldVar< + ConstraintField = Self::ConstraintField, + ValueField = ::Scalar, + >; + /// [`CommitmentDefGadget::CommitmentVar`] is the in-circuit variable type + /// for the commitment. + type CommitmentVar: Clone + + AbsorbableVar + + CondSelectGadget + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment> + + Inputize; + /// [`CommitmentDefGadget::RandomnessVar`] is the in-circuit variable type + /// for the randomness used in the commitment. + type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> + + GR1CSVar::Randomness>; + + /// [`CommitmentDefGadget::Widget`] points to the out-of-circuit commitment + /// scheme widget. + type Widget: CommitmentDef; +} + +/// [`CommitmentOpsGadget`] defines algorithms (majorly the opening algorithm) +/// for commitment schemes in-circuit. +pub trait CommitmentOpsGadget: CommitmentDefGadget { + /// [`CommitmentOpsGadget::open`] defines the commitment opening gadget + /// that matches its out-of-circuit widget [`CommitmentOps::open`]. + fn open( + ck: &Self::KeyVar, + v: &[Self::ScalarVar], + r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError>; +} + +/// [`GroupBasedCommitment`] is a variant of commitment schemes built on groups +/// (elliptic curves). +pub trait GroupBasedCommitment: + CommitmentDef::Commitment>> + + CommitmentOps +{ + /// [`GroupBasedCommitment::Gadget1`] points to the in-circuit gadget for + /// the group-based commitment scheme over the curve's base field. + type Gadget1: CommitmentOpsGadget< + ConstraintField = CF2, + ScalarVar = EmulatedFieldVar, Self::Scalar>, + CommitmentVar = ::Var, + Widget = Self, + >; + /// [`GroupBasedCommitment::Gadget2`] points to the in-circuit gadget for + /// the group-based commitment scheme over the curve's scalar field. + type Gadget2: CommitmentOpsGadget< + ConstraintField = Self::Scalar, + ScalarVar = FpVar, + CommitmentVar = EmulatedAffineVar, + Widget = Self, + >; +} + +#[cfg(test)] +mod tests { + use ark_ff::UniformRand; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::error::Error; + + use super::*; + + pub fn test_commitment_correctness( + mut rng: impl RngCore, + len: usize, + ) -> Result<(), Box> { + let v = (0..len) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + + let ck = CM::generate_key(len, &mut rng)?; + let (cm, r) = CM::commit(&ck, &v, &mut rng)?; + CM::open(&ck, &v, &r, &cm)?; + Ok(()) + } + + pub fn test_commitment_gadget_correctness( + mut rng: impl RngCore, + len: usize, + ) -> Result<(), Box> { + let v = (0..len) + .map(|_| UniformRand::rand(&mut rng)) + .collect::>(); + + let ck = CM::Widget::generate_key(len, &mut rng)?; + let (cm, r) = CM::Widget::commit(&ck, &v, &mut rng)?; + + let cs = ConstraintSystem::new_ref(); + + let v_var = Vec::new_witness(cs.clone(), || Ok(v))?; + let r_var = AllocVar::new_witness(cs.clone(), || Ok(r))?; + let ck_var = AllocVar::new_constant(cs.clone(), ck)?; + let cm_var = AllocVar::new_witness(cs.clone(), || Ok(cm))?; + + CM::open(&ck_var, &v_var, &r_var, &cm_var)?; + + assert!(cs.is_satisfied()?); + + Ok(()) + } +} diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs new file mode 100644 index 000000000..67a240bb9 --- /dev/null +++ b/crates/primitives/src/commitments/pedersen.rs @@ -0,0 +1,500 @@ +//! Implementation of the Pedersen commitment scheme, including out-of-circuit +//! widgets and in-circuit gadgets. +//! +//! The Pedersen commitment to a vector `v` is computed as ` + h · r`, +//! where `g` and `h` are generators, `r` is a random scalar, and `` is +//! the multi-scalar multiplication of `g` and `v`. + +use ark_ec::AffineRepr; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + convert::ToBitsGadget, + eq::EqGadget, + fields::fp::FpVar, + groups::CurveVar, +}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::{ + UniformRand, any::TypeId, borrow::Borrow, iter::repeat_with, marker::PhantomData, rand::RngCore, +}; + +use super::{CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, Error}; +use crate::{ + algebra::{ + field::emulated::EmulatedFieldVar, + group::{CF1, CF2, SonobeCurve, emulated::EmulatedAffineVar}, + }, + circuits::{ + WitnessToPublic, + cache::{CommitmentKeyCache, CommittedCache, RandomnessCache, UsizeSet}, + }, + commitments::{CommitmentOpsGadget, GroupBasedCommitment}, + utils::null::Null, +}; + +/// [`PedersenKey`] stores the public parameters for the Pedersen commitment +/// scheme, where `H` controls whether the scheme is hiding or not. +#[derive(Clone, CanonicalSerialize, CanonicalDeserialize)] +pub struct PedersenKey { + pub g: Vec, + pub h: C::Affine, +} + +impl CommitmentKey for PedersenKey { + fn max_scalars_len(&self) -> usize { + self.g.len() + } +} + +impl PedersenKey { + fn new(len: usize, mut rng: impl RngCore) -> Self { + let generators = repeat_with(|| C::rand(&mut rng)) + .take(len.next_power_of_two()) + .collect::>(); + Self { + g: C::normalize_batch(&generators), + h: if H { + C::Affine::rand(&mut rng) + } else { + C::Affine::zero() + }, + } + } +} + +impl PedersenKey { + fn commit(&self, v: &[C::ScalarField], r: &C::ScalarField) -> Result { + if self.g.len() < v.len() { + return Err(Error::MessageTooLong(self.g.len(), v.len())); + } + // + h * r + // use msm_unchecked because we already ensured at the if that generators are long enough + Ok(C::msm_unchecked(&self.g, v) + self.h * r) + } +} + +impl PedersenKey { + fn commit(&self, v: &[C::ScalarField]) -> Result { + if self.g.len() < v.len() { + return Err(Error::MessageTooLong(self.g.len(), v.len())); + } + // + // use msm_unchecked because we already ensured at the if that generators are long enough + Ok(C::msm_unchecked(&self.g, v)) + } +} + +/// [`PedersenKeyVar`] is the in-circuit variable for [`PedersenKey`], whose +/// generators are encoded in the canonical form. +pub struct PedersenKeyVar { + g: Vec, + h: C::Var, +} + +/// [`PedersenEmulatedKeyVar`] is the in-circuit variable for [`PedersenKey`], +/// whose generators are encoded in the emulated form. +pub struct PedersenEmulatedKeyVar { + #[allow(dead_code)] + g: Vec, C>>, + #[allow(dead_code)] + h: EmulatedAffineVar, C>, +} + +pub struct PedersenFakeKeyVar(PedersenKey); + +impl AllocVar, C::BaseField> + for PedersenKeyVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let PedersenKey { g, h } = v.borrow(); + + Ok(Self { + g: AllocVar::new_variable(cs.clone(), || Ok(&g[..]), mode)?, + h: AllocVar::new_variable(cs.clone(), || Ok(*h), mode)?, + }) + } +} + +impl AllocVar, CF1> + for PedersenEmulatedKeyVar +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let PedersenKey { g, h } = v.borrow(); + + Ok(Self { + g: AllocVar::new_variable( + cs.clone(), + || Ok(g.iter().map(|i| i.into_group()).collect::>()), + mode, + )?, + h: AllocVar::new_variable(cs.clone(), || Ok(h.into_group()), mode)?, + }) + } +} + +impl AllocVar, CF1> + for PedersenFakeKeyVar +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Ok(PedersenFakeKeyVar(v.borrow().clone())) + } +} + +/// [`Pedersen`] defines the out-of-circuit Pedersen widget, where `H` controls +/// whether the scheme is hiding or not. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Pedersen { + _c: PhantomData, +} + +impl CommitmentDef for Pedersen { + const IS_HIDING: bool = false; + + type Key = PedersenKey; + type Scalar = C::ScalarField; + type Commitment = C; + type Randomness = Null; +} + +impl CommitmentDef for Pedersen { + const IS_HIDING: bool = true; + + type Key = PedersenKey; + type Scalar = C::ScalarField; + type Commitment = C; + type Randomness = C::ScalarField; +} + +impl GroupBasedCommitment for Pedersen { + type Gadget1 = PedersenGadget; + type Gadget2 = PedersenCommitAndProveGadget; +} + +impl GroupBasedCommitment for Pedersen { + type Gadget1 = PedersenGadget; + type Gadget2 = PedersenCommitAndProveGadget; +} + +impl CommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { + Ok(PedersenKey::new(len, rng)) + } + + fn commit( + ck: &PedersenKey, + v: &[CF1], + _rng: impl RngCore, + ) -> Result<(C, Null), Error> { + Ok((ck.commit(v)?, Null)) + } + + fn open(ck: &PedersenKey, v: &[CF1], _r: &Null, cm: &C) -> Result<(), Error> { + (&ck.commit(v)? == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) + } +} + +impl CommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { + Ok(PedersenKey::new(len, rng)) + } + + fn commit( + ck: &PedersenKey, + v: &[CF1], + mut rng: impl RngCore, + ) -> Result<(C, CF1), Error> { + let r = C::ScalarField::rand(&mut rng); + Ok((ck.commit(v, &r)?, r)) + } + + fn open(ck: &PedersenKey, v: &[CF1], r: &CF1, cm: &C) -> Result<(), Error> { + (&(ck.commit(v, r)?) == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) + } +} + +/// [`PedersenGadget`] defines the in-circuit Pedersen gadget that operates over +/// the base field of the curve and supports canonical elliptic curve point +/// variables as commitments, where `H` controls whether the scheme is hiding or +/// not. +#[derive(Clone)] +pub struct PedersenGadget { + _c: PhantomData, +} + +impl PedersenGadget { + /// [`PedersenGadget::msm`] performs multi-scalar multiplication in-circuit + /// with the given generators `g` and scalar bits `v`. + fn msm(g: &[C::Var], v: &[Vec>>]) -> Result { + let mut res = C::Var::zero(); + for (g_i, v_i) in g.iter().zip(v) { + res += g_i.scalar_mul_le(v_i.to_bits_le()?.iter())?; + } + Ok(res) + } +} + +impl CommitmentOpsGadget for PedersenGadget { + fn open( + ck: &PedersenKeyVar, + v: &[EmulatedFieldVar, CF1>], + _r: &Null, + cm: &C::Var, + ) -> Result<(), SynthesisError> { + Self::msm( + &ck.g, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )? + .enforce_equal(cm) + } +} + +impl CommitmentOpsGadget for PedersenGadget { + fn open( + ck: &PedersenKeyVar, + v: &[EmulatedFieldVar, CF1>], + r: &EmulatedFieldVar, CF1>, + cm: &C::Var, + ) -> Result<(), SynthesisError> { + let gv = Self::msm( + &ck.g, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )?; + let hr = ck.h.scalar_mul_le(r.to_bits_le()?.iter())?; + (gv + hr).enforce_equal(cm) + } +} + +/// [`PedersenEmulatedGadget`] defines the in-circuit Pedersen gadget that +/// operates over the scalar field of the curve and supports emulated elliptic +/// curve point variables as commitments, where `H` controls whether the scheme +/// is hiding or not. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PedersenEmulatedGadget { + _c: PhantomData, +} + +impl CommitmentDefGadget for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = PedersenKeyVar; + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = PedersenKeyVar; + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = EmulatedFieldVar, CF1>; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = PedersenEmulatedKeyVar; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = PedersenEmulatedKeyVar; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + type Widget = Pedersen; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PedersenCommitAndProveGadget { + _c: PhantomData, +} + +impl CommitmentDefGadget for PedersenCommitAndProveGadget { + type ConstraintField = CF1; + + type KeyVar = PedersenFakeKeyVar; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenCommitAndProveGadget { + type ConstraintField = CF1; + + type KeyVar = PedersenFakeKeyVar; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + type Widget = Pedersen; +} + +impl CommitmentOpsGadget for PedersenCommitAndProveGadget { + fn open( + ck: &PedersenFakeKeyVar, + v: &[FpVar>], + _r: &Null, + cm: &EmulatedAffineVar, C>, + ) -> Result<(), SynthesisError> { + todo!() + } +} + +impl CommitmentOpsGadget for PedersenCommitAndProveGadget { + fn open( + ck: &PedersenFakeKeyVar, + v: &[FpVar>], + r: &FpVar>, + cm: &EmulatedAffineVar, C>, + ) -> Result<(), SynthesisError> { + cm.mark_as_public()?; + + let cs = v.cs().or(r.cs()).or(cm.cs()); + + let cs = cs.borrow_mut().ok_or(SynthesisError::MissingCS)?; + let mut cache = cs.cache_map.borrow_mut(); + if !cs.is_in_setup_mode() { + cache + .get_mut(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast_mut::>>() + .ok_or(SynthesisError::AssignmentMissing)? + .push(r.value()?); + } else { + cache + .get_mut(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast_mut::>>() + .ok_or(SynthesisError::AssignmentMissing)? + .push(PedersenKey { + g: ck.0.g[..v.len()].to_vec(), + h: ck.0.h, + }); + } + // TODO: independent on alloc order + let indices = v + .iter() + .map(|i| match i { + FpVar::Constant(_) => Err(SynthesisError::AssignmentMissing), + FpVar::Var(fp) => { + if !fp.variable.is_witness() { + Err(SynthesisError::AssignmentMissing) + } else { + Ok(fp.variable.index().unwrap()) + } + } + }) + .collect::, _>>()?; + cache + .get_mut(&TypeId::of::()) + .ok_or(SynthesisError::AssignmentMissing)? + .downcast_mut::() + .ok_or(SynthesisError::AssignmentMissing)? + .extend(indices); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::G1Projective; + use ark_std::{ + error::Error, + rand::{Rng, thread_rng}, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::commitments::tests::{ + test_commitment_correctness, test_commitment_gadget_correctness, + }; + + #[test] + fn test_pedersen_commitment() -> Result<(), Box> { + let mut rng = thread_rng(); + for i in 0..10 { + let len = rng.gen_range((1 << i)..(1 << (i + 1))); + test_commitment_correctness::>(&mut rng, len)?; + test_commitment_correctness::>(&mut rng, len)?; + } + Ok(()) + } + + #[test] + fn test_pedersen_commitment_circuit() -> Result<(), Box> { + let mut rng = thread_rng(); + for i in 0..5 { + let len = rng.gen_range((1 << i)..(1 << (i + 1))); + test_commitment_gadget_correctness::>( + &mut rng, len, + )?; + test_commitment_gadget_correctness::>( + &mut rng, len, + )?; + } + Ok(()) + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs new file mode 100644 index 000000000..1529d3a6e --- /dev/null +++ b/crates/primitives/src/lib.rs @@ -0,0 +1,17 @@ +#![warn(missing_docs)] + +//! This crate provides the foundational primitives used throughout Sonobe's +//! folding scheme and IVC implementations. +//! +//! It includes algebraic abstractions (fields, groups, and their in-circuit +//! emulated counterparts), constraint system arithmetizations (R1CS, CCS), +//! commitment schemes, transcript/sponge constructions, sum-check protocols, +//! and various utility types. + +pub mod algebra; +pub mod arithmetizations; +pub mod circuits; +pub mod commitments; +pub mod relations; +pub mod transcripts; +pub mod utils; diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs new file mode 100644 index 000000000..7c055bdbf --- /dev/null +++ b/crates/primitives/src/relations/mod.rs @@ -0,0 +1,42 @@ +//! This module defines the core relation traits for generic witness-instance +//! satisfaction checks and satisfying pair generation. +//! +//! These traits are intentionally generic so that different arithmetizations +//! (R1CS, CCS) and different forms (plain, relaxed) can all implement them. + +use ark_relations::gr1cs::SynthesisError; +use ark_std::{error::Error, rand::RngCore}; + +/// [`Relation`] checks whether a witness `W` and an instance `U` satisfy the +/// specified relation. +pub trait Relation { + /// [`Relation::Error`] defines the error type that may occur when checking + /// the relation. + type Error: Error; + + /// [`Relation::check_relation`] returns `Ok(())` when `w` and `u` satisfy + /// `self`, or an error otherwise. + fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; +} + +/// [`RelationGadget`] is the in-circuit counterpart of [`Relation`]. +pub trait RelationGadget { + /// [`RelationGadget::check_relation`] generates constraints enforcing that + /// `w` and `u` satisfy the relation. + fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError>; +} + +/// [`WitnessInstanceSampler`] allows sampling a random witness-instance pair +/// that satisfies the relation. +pub trait WitnessInstanceSampler { + /// [`WitnessInstanceSampler::Source`] defines the type of the source from + /// which a satisfying pair is sampled. + type Source; + + /// [`WitnessInstanceSampler::Error`] defines the error type that may occur + /// when sampling a satisfying pair. + type Error: Error; + + /// [`WitnessInstanceSampler::sample`] draws a random satisfying pair. + fn sample(&self, source: Self::Source, rng: impl RngCore) -> Result<(W, U), Self::Error>; +} diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs new file mode 100644 index 000000000..0c2f2f7de --- /dev/null +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -0,0 +1,136 @@ +//! This module defines traits for converting values into a form absorbable by a +//! sponge or transcript. +//! +//! Implementations are provided for some primitive types as well as composite +//! types (references, tuples, slices, etc.). + +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; + +// TODO (@winderica): +// +// Ideally this trait should be defined as follows, so that we can use it for +// absorbing values into bits/bytes/etc., in addition to field elements. +// (Although Arkworks' `Absorb` trait covers both bytes and field elements, it +// requires downstream types to support absorbing into both as well, even if the +// downstream type doesn't support/is unrelated to one absorbing target.) +// +// ```rs +// pub trait Absorbable { +// fn absorb_into(&self, dest: &mut Vec); + +// fn to_absorbable(&self) -> Vec { +// let mut result = Vec::new(); +// self.absorb_into(&mut result); +// result +// } +// } +// ``` +// +// But my attempt was unsuccessful. In our use case, `SonobeField` needs to be +// absorbed into prime fields that are unknown when making the definition. Due +// to the `F` type parameter in `Absorbable`, I have three options: +// 1. Define `SonobeField` as `SonobeField: Absorbable`. This means that +// I need to add `F` to everywhere `SonobeField` is used, making the codebase +// much more verbose. +// 2. Remove the `Absorbable` bound from `SonobeField`, but instead manually add +// `Absorbable` to `T: SonobeField`'s bounds whenever we need `T` to be +// absorbable. This also increases the verbosity a lot. +// 3. Wait for https://github.com/rust-lang/rust/issues/108185 to be resolved, +// so I can define `SonobeField: for Absorbable`. +// Personally I think the best option is 3. File an issue or submit a PR if you +// have better solution :) +/// [`Absorbable`] is a trait for objects that can be absorbed into a sponge or +/// transcript. +pub trait Absorbable { + /// [`Absorbable::absorb_into`] absorbs `self` into the given destination + /// vector of field elements. + /// + /// The implementation should append the field elements representing `self` + /// to `dest`. + fn absorb_into(&self, dest: &mut Vec); +} + +impl Absorbable for usize { + fn absorb_into(&self, dest: &mut Vec) { + dest.push(F::from(*self as u64)); + } +} + +impl Absorbable for &T { + fn absorb_into(&self, dest: &mut Vec) { + (*self).absorb_into(dest); + } +} + +impl Absorbable for (T, T) { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest); + self.1.absorb_into(dest); + } +} + +impl Absorbable for [T] { + fn absorb_into(&self, dest: &mut Vec) { + for t in self.iter() { + t.absorb_into(dest); + } + } +} + +impl Absorbable for [T; N] { + fn absorb_into(&self, dest: &mut Vec) { + self.as_ref().absorb_into(dest); + } +} + +impl Absorbable for Vec { + fn absorb_into(&self, dest: &mut Vec) { + self.as_slice().absorb_into(dest); + } +} + +/// [`AbsorbableVar`] is a trait for in-circuit variables that can be absorbed +/// into a sponge or transcript defined over constraint field `F`. +/// +/// Matches [`Absorbable`]. +pub trait AbsorbableVar { + /// [`AbsorbableVar::absorb_into`] absorbs `self` into the given + /// destination vector of field element variables. + /// + /// The implementation should append the field element variables + /// representing `self` to `dest`. + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError>; +} + +impl> AbsorbableVar for &T { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + (*self).absorb_into(dest) + } +} + +impl> AbsorbableVar for (T, T) { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest)?; + self.1.absorb_into(dest) + } +} + +impl> AbsorbableVar for [T] { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.iter().try_for_each(|t| t.absorb_into(dest)) + } +} + +impl, const N: usize> AbsorbableVar for [T; N] { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.as_ref().absorb_into(dest) + } +} + +impl> AbsorbableVar for Vec { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.as_slice().absorb_into(dest) + } +} diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs new file mode 100644 index 000000000..a97fd7f77 --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -0,0 +1,624 @@ +//! Implementation of the Griffin circuit-friendly hash function and its +//! parameter generation, as well as out-of-circuit widgets and in-circuit +//! gadgets for permutation, hashing, sponges, and transcripts. +//! +//! According to the Griffin [paper], it is very efficient in terms of the +//! number of constraints, but later an [attack] on Griffin and similar hash +//! functions was discovered. +//! Therefore, it is recommended to avoid using Griffin in production. +//! +//! The code is forked from the [implementation] in the Hash Functions for +//! Zero-Knowledge Applications Zoo but uses arkworks instead of bellman as the +//! underlying cryptographic library. +//! +//! [paper]: https://eprint.iacr.org/2022/403.pdf +//! [attack]: https://eprint.iacr.org/2024/347.pdf +//! [implementation]: https://extgit.isec.tugraz.at/krypto/zkfriendlyhashzoo + +// Below we attach Hash functions for Zero-Knowledge applications Zoo's original +// license notice. +// +// Copyright (c) 2021 Graz University of Technology +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use ark_ff::{LegendreSymbol, PrimeField, field_hashers::hash_to_field}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::SynthesisError; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use itertools::Itertools; +use num_bigint::BigUint; +use sha3::{ + Shake128, Shake128Reader, + digest::{ExtendableOutput, Update, XofReader}, +}; + +pub mod sponge; + +/// [`GriffinParams`] stores the full parameterisation of the Griffin +/// permutation for a given prime field: state width `t`, S-box degree `d`, +/// number of rounds, round constants, alpha/beta constants, and the MDS-like +/// matrix. +#[derive(Clone, Debug, CanonicalSerialize, CanonicalDeserialize)] +pub struct GriffinParams { + round_constants: Vec>, + t: usize, + d: usize, + d_inv: Vec, + rounds: usize, + alpha_beta: Vec<[F; 2]>, + mat: Vec>, + rate: usize, + capacity: usize, +} + +impl GriffinParams { + const INIT_SHAKE: &'static str = "Griffin"; + + /// [`GriffinParams::new`] constructs new Griffin parameters with the given + /// state width `t`, S-box degree `d`, and number of rounds `rounds`. + pub fn new(t: usize, d: usize, rounds: usize) -> Self { + // Equivalent to `assert!(t == 3 || t % 4 == 0);`, but bypass clippy's + // warning about `is_multiple_of`. + assert!(t == 3 || t & 3 == 0); + assert!(d == 3 || d == 5); + assert!(rounds >= 1); + + let mut shake = Self::init_shake(); + + let d_inv = BigUint::from(d) + .modinv(&(-F::one()).into()) + .unwrap() + .to_radix_be(2) + .into_iter() + .map(|i| i != 0) + .skip_while(|i| !i) + .collect(); + let round_constants = Self::instantiate_rc(t, rounds, &mut shake); + let alpha_beta = Self::instantiate_alpha_beta(t, &mut shake); + + let mat = Self::instantiate_matrix(t); + + GriffinParams { + round_constants, + t, + d, + d_inv, + rounds, + alpha_beta, + mat, + rate: t - 1, + capacity: 1, + } + } + + fn init_shake() -> Shake128Reader { + let mut shake = Shake128::default(); + shake.update(Self::INIT_SHAKE.as_bytes()); + for i in F::characteristic() { + shake.update(&i.to_le_bytes()); + } + shake.finalize_xof() + } + + fn instantiate_rc(t: usize, rounds: usize, shake: &mut Shake128Reader) -> Vec> { + (0..rounds - 1) + .map(|_| (0..t).map(|_| hash_to_field::<_, _, 128>(shake)).collect()) + .collect() + } + + fn instantiate_alpha_beta(t: usize, shake: &mut Shake128Reader) -> Vec<[F; 2]> { + fn hash_to_non_zero_field(reader: &mut impl XofReader) -> F { + loop { + let element = hash_to_field::(reader); + if !element.is_zero() { + return element; + } + } + } + + let mut alpha_beta = Vec::with_capacity(t - 2); + + // random alpha/beta + loop { + let alpha = hash_to_non_zero_field::(shake); + let mut beta = hash_to_non_zero_field::(shake); + // distinct + while alpha == beta { + beta = hash_to_non_zero_field::(shake); + } + let mut symbol = alpha; + symbol.square_in_place(); + let mut tmp = beta; + tmp.double_in_place(); + tmp.double_in_place(); + symbol.sub_assign(&tmp); + if symbol.legendre() == LegendreSymbol::QuadraticNonResidue { + alpha_beta.push([alpha, beta]); + break; + } + } + + // other alphas/betas + for i in 2..t - 1 { + let mut alpha = alpha_beta[0][0]; + let mut beta = alpha_beta[0][1]; + alpha.mul_assign(&F::from(i as u64)); + beta.mul_assign(&F::from((i * i) as u64)); + // distinct + while alpha == beta { + beta = hash_to_non_zero_field::(shake); + } + + #[cfg(debug_assertions)] + { + // check if really ok + let mut symbol = alpha; + symbol.square_in_place(); + let mut tmp = beta; + tmp.double_in_place(); + tmp.double_in_place(); + symbol.sub_assign(&tmp); + assert_eq!(symbol.legendre(), LegendreSymbol::QuadraticNonResidue); + } + + alpha_beta.push([alpha, beta]); + } + + alpha_beta + } + + fn instantiate_matrix(t: usize) -> Vec> { + if t == 3 { + let row = vec![F::from(2), F::from(1), F::from(1)]; + let t = row.len(); + let mut mat: Vec> = Vec::with_capacity(t); + let mut rot = row.to_owned(); + mat.push(rot.clone()); + for _ in 1..t { + rot.rotate_right(1); + mat.push(rot.clone()); + } + mat + } else { + let row1 = vec![F::from(5), F::from(7), F::from(1), F::from(3)]; + let row2 = vec![F::from(4), F::from(6), F::from(1), F::from(1)]; + let row3 = vec![F::from(1), F::from(3), F::from(5), F::from(7)]; + let row4 = vec![F::from(1), F::from(1), F::from(4), F::from(6)]; + let c_mat = vec![row1, row2, row3, row4]; + if t == 4 { + c_mat + } else { + assert_eq!(t % 4, 0); + let mut mat: Vec> = vec![vec![F::zero(); t]; t]; + for (row, matrow) in mat.iter_mut().enumerate().take(t) { + for (col, matitem) in matrow.iter_mut().enumerate().take(t) { + let row_mod = row % 4; + let col_mod = col % 4; + *matitem = c_mat[row_mod][col_mod]; + if row / 4 == col / 4 { + matitem.add_assign(&c_mat[row_mod][col_mod]); + } + } + } + mat + } + } + } +} + +/// [`Griffin`] implements the Griffin permutation and Griffin hash. +pub struct Griffin; + +impl Griffin { + fn affine_3(params: &GriffinParams, input: &mut [F], round: usize) { + // multiplication by circ(2 1 1) is equal to state + sum(state) + let mut sum = input[0]; + input.iter().skip(1).for_each(|el| sum.add_assign(el)); + + if round < params.rounds - 1 { + for (el, rc) in input + .iter_mut() + .zip_eq(params.round_constants[round].iter()) + { + el.add_assign(&sum); + el.add_assign(rc); // add round constant + } + } else { + // no round constant + for el in input.iter_mut() { + el.add_assign(&sum); + } + } + } + + fn affine_4(params: &GriffinParams, input: &mut [F], round: usize) { + let mut t_0 = input[0]; + t_0.add_assign(&input[1]); + let mut t_1 = input[2]; + t_1.add_assign(&input[3]); + let mut t_2 = input[1]; + t_2.double_in_place(); + t_2.add_assign(&t_1); + let mut t_3 = input[3]; + t_3.double_in_place(); + t_3.add_assign(&t_0); + let mut t_4 = t_1; + t_4.double_in_place(); + t_4.double_in_place(); + t_4.add_assign(&t_3); + let mut t_5 = t_0; + t_5.double_in_place(); + t_5.double_in_place(); + t_5.add_assign(&t_2); + let mut t_6 = t_3; + t_6.add_assign(&t_5); + let mut t_7 = t_2; + t_7.add_assign(&t_4); + input[0] = t_6; + input[1] = t_5; + input[2] = t_7; + input[3] = t_4; + + if round < params.rounds - 1 { + for (i, rc) in input + .iter_mut() + .zip_eq(params.round_constants[round].iter()) + { + i.add_assign(rc); + } + } + } + + fn affine(params: &GriffinParams, input: &mut [F], round: usize) { + if params.t == 3 { + Griffin::affine_3(params, input, round); + return; + } + if params.t == 4 { + Griffin::affine_4(params, input, round); + return; + } + + // first matrix + let t4 = params.t / 4; + for i in 0..t4 { + let start_index = i * 4; + let mut t_0 = input[start_index]; + t_0.add_assign(&input[start_index + 1]); + let mut t_1 = input[start_index + 2]; + t_1.add_assign(&input[start_index + 3]); + let mut t_2 = input[start_index + 1]; + t_2.double_in_place(); + t_2.add_assign(&t_1); + let mut t_3 = input[start_index + 3]; + t_3.double_in_place(); + t_3.add_assign(&t_0); + let mut t_4: F = t_1; + t_4.double_in_place(); + t_4.double_in_place(); + t_4.add_assign(&t_3); + let mut t_5 = t_0; + t_5.double_in_place(); + t_5.double_in_place(); + t_5.add_assign(&t_2); + input[start_index] = t_3 + t_5; + input[start_index + 1] = t_5; + input[start_index + 2] = t_2 + t_4; + input[start_index + 3] = t_4; + } + + // second matrix + let mut stored = [F::zero(); 4]; + for l in 0..4 { + stored[l] = input[l]; + for j in 1..t4 { + stored[l].add_assign(&input[4 * j + l]); + } + } + + for i in 0..input.len() { + input[i].add_assign(&stored[i % 4]); + if round < params.rounds - 1 { + input[i].add_assign(¶ms.round_constants[round][i]); // add round constant + } + } + } + + fn non_linear(params: &GriffinParams, input: &mut [F]) { + // first two state words + input[0] = { + let mut res = F::one(); + for &i in ¶ms.d_inv { + res.square_in_place(); + if i { + res *= input[0]; + } + } + res + }; + + let mut state = input[1]; + + input[1].square_in_place(); + match params.d { + 3 => {} + 5 => { + input[1].square_in_place(); + } + _ => panic!(), + } + input[1].mul_assign(&state); + + let mut y01_i = input[1]; + // rest of the state + for i in 2..input.len() { + y01_i += input[0]; + let l = if i == 2 { y01_i } else { y01_i + state }; + let ab = ¶ms.alpha_beta[i - 2]; + state = input[i]; + input[i] *= l.square() + l * ab[0] + ab[1]; + } + } + + /// [`Griffin::permute`] applies the Griffin permutation to the given input + /// state `input` in place under parameters `params`. + pub fn permute(params: &GriffinParams, input: &mut [F]) { + Griffin::affine(params, input, params.rounds); // no RC + + for r in 0..params.rounds { + Griffin::non_linear(params, input); + Griffin::affine(params, input, r); + } + } + + /// [`Griffin::hash`] implements the Griffin hash function based on the + /// sponge construction, which produces a single field element as the digest + /// of the given message `message` under parameters `params`. + pub fn hash(params: &GriffinParams, message: &[F]) -> F { + let mut state = vec![F::zero(); params.t]; + for chunk in message.chunks(params.rate) { + for i in 0..chunk.len() { + state[i] += &chunk[i]; + } + Griffin::permute(params, &mut state) + } + state[0] + } +} + +/// [`GriffinGadget`] implements the gadgets for Griffin permutation and Griffin +/// hash. +pub struct GriffinGadget; + +impl GriffinGadget { + fn non_linear( + params: &GriffinParams, + state: &[FpVar], + ) -> Result>, SynthesisError> { + let cs = state.cs(); + let mut result = state.to_owned(); + // x0 + result[0] = FpVar::new_variable_with_inferred_mode(cs, || { + Ok({ + { + let v = result[0].value().unwrap_or_default(); + let mut res = F::one(); + for &i in ¶ms.d_inv { + res.square_in_place(); + if i { + res *= v; + } + } + res + } + }) + })?; + + let mut sq = result[0].square()?; + if params.d == 5 { + sq = sq.square()?; + } + result[0].mul_equals(&sq, &state[0])?; + + // x1 + let mut sq = result[1].square()?; + if params.d == 5 { + sq = sq.square()?; + } + result[1] *= sq; + + let mut y01_i = result[1].clone(); + + // rest of the state + for i in 2..result.len() { + y01_i += &result[0]; + let l = if i == 2 { + y01_i.clone() + } else { + &y01_i + &state[i - 1] + }; + let ab = ¶ms.alpha_beta[i - 2]; + result[i] *= l.square()? + l * ab[0] + ab[1]; + } + + Ok(result) + } + + /// [`GriffinGadget::permute`] applies the Griffin permutation to the given + /// input state variables `input` in place under parameters `params`. + pub fn permute( + params: &GriffinParams, + state: &[FpVar], + ) -> Result>, SynthesisError> { + let mut current_state = state.to_owned(); + current_state = params + .mat + .iter() + .map(|row| current_state.iter().zip_eq(row).map(|(a, b)| a * *b).sum()) + .collect(); + + for r in 0..params.rounds { + current_state = GriffinGadget::non_linear(params, ¤t_state)?; + current_state = params + .mat + .iter() + .map(|row| current_state.iter().zip_eq(row).map(|(a, b)| a * *b).sum()) + .collect(); + if r < params.rounds - 1 { + current_state = current_state + .iter() + .zip_eq(¶ms.round_constants[r]) + .map(|(c, rc)| c + *rc) + .collect(); + } + } + Ok(current_state) + } + + /// [`GriffinGadget::hash`] implements the gadget for Griffin hash based on + /// the sponge construction, which produces a single field element variable + /// as the digest of the given message `message` under parameters `params`. + pub fn hash( + params: &GriffinParams, + message: &[FpVar], + ) -> Result, SynthesisError> { + let mut state = vec![FpVar::zero(); params.t]; + for chunk in message.chunks(params.rate) { + for i in 0..chunk.len() { + state[i] += &chunk[i]; + } + state = GriffinGadget::permute(params, &state)?; + } + Ok(state[0].clone()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + #[test] + fn test() -> Result<(), Box> { + let rng = &mut thread_rng(); + let params = GriffinParams::new(24, 5, 9); + let t = params.t; + let x: Vec = (0..t).map(|_| Fr::rand(rng)).collect(); + + let y = Griffin::hash(¶ms, &x); + + let cs = ConstraintSystem::new_ref(); + let x_var = Vec::new_witness(cs.clone(), || Ok(x.clone()))?; + let y_var = GriffinGadget::hash(¶ms, &x_var)?; + assert_eq!(y, y_var.value()?); + println!("{}", cs.num_constraints()); + assert!(cs.is_satisfied()?); + + Ok(()) + } + + #[test] + fn test_consistent_perm() { + let rng = &mut thread_rng(); + let params = GriffinParams::new(3, 5, 12); + let t = params.t; + for _ in 0..5 { + let input1: Vec<_> = (0..t).map(|_| Fr::rand(rng)).collect(); + + let mut input2: Vec<_>; + loop { + input2 = (0..t).map(|_| Fr::rand(rng)).collect(); + if input1 != input2 { + break; + } + } + + let mut perm1 = input1.clone(); + let mut perm2 = input1.clone(); + let mut perm3 = input2.clone(); + Griffin::permute(¶ms, &mut perm1); + Griffin::permute(¶ms, &mut perm2); + Griffin::permute(¶ms, &mut perm3); + assert_eq!(perm1, perm2); + assert_ne!(perm1, perm3); + } + } + + fn matmul(input: &[F], mat: &[Vec]) -> Vec { + let t = mat.len(); + debug_assert!(t == input.len()); + let mut out = vec![F::zero(); t]; + for row in 0..t { + for (col, inp) in input.iter().enumerate() { + let mut tmp = mat[row][col]; + tmp *= inp; + out[row] += &tmp; + } + } + out + } + + fn test_affine_opt(t: usize) { + let rng = &mut thread_rng(); + let params = GriffinParams::::new(t, 5, 1); + + let mat = ¶ms.mat; + + for _ in 0..5 { + let input: Vec = (0..t).map(|_| F::rand(rng)).collect(); + + // affine 1 + let output1 = matmul(&input, mat); + let mut output2 = input.to_owned(); + Griffin::affine(¶ms, &mut output2, 1); + assert_eq!(output1, output2); + } + } + + #[test] + fn test_affine_3() { + test_affine_opt::(3); + } + + #[test] + fn test_affine_4() { + test_affine_opt::(4); + } + + #[test] + fn test_affine_8() { + test_affine_opt::(8); + } + + #[test] + fn test_affine_60() { + test_affine_opt::(60); + } +} diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs new file mode 100644 index 000000000..e644f6cd2 --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -0,0 +1,428 @@ +//! Implementation of transcript traits for Griffin sponge. + +use ark_crypto_primitives::sponge::DuplexSpongeMode; +use ark_ff::PrimeField; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::sync::Arc; + +use crate::transcripts::{ + AbsorbableVar, Transcript, TranscriptGadget, + griffin::{Griffin, GriffinGadget, GriffinParams}, +}; + +/// [`GriffinSponge`] is a duplex sponge built on the Griffin permutation. +/// +/// The implementation mirrors arkworks' [`ark_crypto_primitives::sponge::poseidon::PoseidonSponge`]. +#[derive(Clone)] +pub struct GriffinSponge { + params: Arc>, + state: Vec, + mode: DuplexSpongeMode, +} + +impl GriffinSponge { + fn permute(&mut self) { + Griffin::permute(&self.params, &mut self.state); + } + + // Absorbs everything in elements, this does not end in an absorption. + fn absorb_internal(&mut self, mut rate_start_index: usize, elements: &[F]) { + let mut remaining_elements = elements; + + loop { + // if we can finish in this call + if rate_start_index + remaining_elements.len() <= self.params.rate { + for (i, element) in remaining_elements.iter().enumerate() { + self.state[self.params.capacity + i + rate_start_index] += element; + } + self.mode = DuplexSpongeMode::Absorbing { + next_absorb_index: rate_start_index + remaining_elements.len(), + }; + + return; + } + // otherwise absorb (rate - rate_start_index) elements + let num_elements_absorbed = self.params.rate - rate_start_index; + for (i, element) in remaining_elements + .iter() + .enumerate() + .take(num_elements_absorbed) + { + self.state[self.params.capacity + i + rate_start_index] += element; + } + self.permute(); + // the input elements got truncated by num elements absorbed + remaining_elements = &remaining_elements[num_elements_absorbed..]; + rate_start_index = 0; + } + } + + // Squeeze |output| many elements. This does not end in a squeeze + fn squeeze_internal(&mut self, mut rate_start_index: usize, output: &mut [F]) { + let mut output_remaining = output; + loop { + // if we can finish in this call + if rate_start_index + output_remaining.len() <= self.params.rate { + output_remaining.clone_from_slice( + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + output_remaining.len() + rate_start_index)], + ); + self.mode = DuplexSpongeMode::Squeezing { + next_squeeze_index: rate_start_index + output_remaining.len(), + }; + return; + } + // otherwise squeeze (rate - rate_start_index) elements + let num_elements_squeezed = self.params.rate - rate_start_index; + output_remaining[..num_elements_squeezed].clone_from_slice( + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + num_elements_squeezed + rate_start_index)], + ); + + // Repeat with updated output slices + output_remaining = &mut output_remaining[num_elements_squeezed..]; + // Unless we are done with squeezing in this call, permute. + if !output_remaining.is_empty() { + self.permute(); + } + + rate_start_index = 0; + } + } +} + +/// [`GriffinSpongeVar`] is the in-circuit variable of [`GriffinSponge`]. +/// +/// The implementation mirrors arkworks' [`ark_crypto_primitives::sponge::poseidon::constraints::PoseidonSpongeVar`]. +#[derive(Clone)] +pub struct GriffinSpongeVar { + params: Arc>, + state: Vec>, + mode: DuplexSpongeMode, +} + +impl GriffinSpongeVar { + fn permute(&mut self) -> Result<(), SynthesisError> { + self.state = GriffinGadget::permute(&self.params, &self.state)?; + Ok(()) + } + + fn absorb_internal( + &mut self, + mut rate_start_index: usize, + elements: &[FpVar], + ) -> Result<(), SynthesisError> { + let mut remaining_elements = elements; + loop { + // if we can finish in this call + if rate_start_index + remaining_elements.len() <= self.params.rate { + for (i, element) in remaining_elements.iter().enumerate() { + self.state[self.params.capacity + i + rate_start_index] += element; + } + self.mode = DuplexSpongeMode::Absorbing { + next_absorb_index: rate_start_index + remaining_elements.len(), + }; + + return Ok(()); + } + // otherwise absorb (rate - rate_start_index) elements + let num_elements_absorbed = self.params.rate - rate_start_index; + for (i, element) in remaining_elements + .iter() + .enumerate() + .take(num_elements_absorbed) + { + self.state[self.params.capacity + i + rate_start_index] += element; + } + self.permute()?; + // the input elements got truncated by num elements absorbed + remaining_elements = &remaining_elements[num_elements_absorbed..]; + rate_start_index = 0; + } + } + + // Squeeze |output| many elements. This does not end in a squeeze + fn squeeze_internal( + &mut self, + mut rate_start_index: usize, + output: &mut [FpVar], + ) -> Result<(), SynthesisError> { + let mut remaining_output = output; + loop { + // if we can finish in this call + if rate_start_index + remaining_output.len() <= self.params.rate { + remaining_output.clone_from_slice( + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + remaining_output.len() + rate_start_index)], + ); + self.mode = DuplexSpongeMode::Squeezing { + next_squeeze_index: rate_start_index + remaining_output.len(), + }; + return Ok(()); + } + // otherwise squeeze (rate - rate_start_index) elements + let num_elements_squeezed = self.params.rate - rate_start_index; + remaining_output[..num_elements_squeezed].clone_from_slice( + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + num_elements_squeezed + rate_start_index)], + ); + + // Repeat with updated output slices and rate start index + remaining_output = &mut remaining_output[num_elements_squeezed..]; + + // Unless we are done with squeezing in this call, permute. + if !remaining_output.is_empty() { + self.permute()?; + } + rate_start_index = 0; + } + } +} + +impl Transcript for GriffinSponge { + type Config = Arc>; + type Gadget = GriffinSpongeVar; + + fn new(parameters: Arc>) -> Self { + let state = vec![F::zero(); parameters.rate + parameters.capacity]; + let mode = DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }; + + Self { + params: parameters.clone(), + state, + mode, + } + } + + fn add_field_elements(&mut self, elems: &[F]) -> &mut Self { + if elems.is_empty() { + return self; + } + + match self.mode { + DuplexSpongeMode::Absorbing { next_absorb_index } => { + let mut absorb_index = next_absorb_index; + if absorb_index == self.params.rate { + self.permute(); + absorb_index = 0; + } + self.absorb_internal(absorb_index, elems); + } + DuplexSpongeMode::Squeezing { + next_squeeze_index: _, + } => { + self.absorb_internal(0, elems); + } + }; + self + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + let mut squeezed_elems = vec![F::zero(); num_elements]; + match self.mode { + DuplexSpongeMode::Absorbing { + next_absorb_index: _, + } => { + self.permute(); + self.squeeze_internal(0, &mut squeezed_elems); + } + DuplexSpongeMode::Squeezing { next_squeeze_index } => { + let mut squeeze_index = next_squeeze_index; + if squeeze_index == self.params.rate { + self.permute(); + squeeze_index = 0; + } + self.squeeze_internal(squeeze_index, &mut squeezed_elems); + } + }; + + squeezed_elems + } +} + +impl TranscriptGadget for GriffinSpongeVar { + type Config = Arc>; + type Widget = GriffinSponge; + + fn new(parameters: Arc>) -> Self + where + Self: Sized, + { + let zero = FpVar::::zero(); + let state = vec![zero; parameters.rate + parameters.capacity]; + let mode = DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }; + + Self { + params: parameters.clone(), + state, + mode, + } + } + + fn add>(&mut self, input: &A) -> Result<&mut Self, SynthesisError> { + let input = { + let mut result = Vec::new(); + input.absorb_into(&mut result)?; + result + }; + + if input.is_empty() { + return Ok(self); + } + + match self.mode { + DuplexSpongeMode::Absorbing { next_absorb_index } => { + let mut absorb_index = next_absorb_index; + if absorb_index == self.params.rate { + self.permute()?; + absorb_index = 0; + } + self.absorb_internal(absorb_index, input.as_slice())?; + } + DuplexSpongeMode::Squeezing { + next_squeeze_index: _, + } => { + self.absorb_internal(0, input.as_slice())?; + } + }; + + Ok(self) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + let zero = FpVar::zero(); + let mut squeezed_elems = vec![zero; num_elements]; + match self.mode { + DuplexSpongeMode::Absorbing { + next_absorb_index: _, + } => { + self.permute()?; + self.squeeze_internal(0, &mut squeezed_elems)?; + } + DuplexSpongeMode::Squeezing { next_squeeze_index } => { + let mut squeeze_index = next_squeeze_index; + if squeeze_index == self.params.rate { + self.permute()?; + squeeze_index = 0; + } + self.squeeze_internal(squeeze_index, &mut squeezed_elems)?; + } + }; + + Ok(squeezed_elems) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fq, Fr, G1Projective as G1, g1::Config}; + use ark_ff::UniformRand; + use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, + groups::curves::short_weierstrass::ProjectiveVar, + }; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::algebra::group::emulated::EmulatedAffineVar; + + #[test] + fn test_challenge_field_element() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(config.clone()); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_challenge_bits() -> Result<(), Box> { + let nbits = 128; + + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(config.clone()); + tr.add(&Fq::from(42_u32)); + let c = tr.challenge_bits(nbits); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_bits(nbits)?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_absorb_canonical_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(config.clone()); + let rng = &mut thread_rng(); + + let p = G1::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(config); + let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_absorb_emulated_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(config.clone()); + let rng = &mut thread_rng(); + + let p = G1::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(config); + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } +} diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs new file mode 100644 index 000000000..a20426248 --- /dev/null +++ b/crates/primitives/src/transcripts/mod.rs @@ -0,0 +1,277 @@ +//! Abstractions of sponges and Fiat-Shamir transcripts. +//! +//! This module defines the traits that unify hash functions (Poseidon, Griffin, +//! etc.) behind a common absorb / squeeze interface suitable for building +//! non-interactive proofs. +//! +//! Concrete implementations live in the [`poseidon`] and [`griffin`] +//! sub-modules. + +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{boolean::Boolean, convert::ToBitsGadget, fields::fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; + +pub use self::absorbable::{Absorbable, AbsorbableVar}; + +pub mod absorbable; +pub mod griffin; +pub mod poseidon; +pub mod recording; +pub mod replay; + +/// [`Transcript`] is the out-of-circuit widget for transcripts and sponges. +/// +/// Provers and verifiers can use this trait to absorb messages and squeeze +/// challenges in a way that is agnostic to the underlying hash function. +pub trait Transcript: Clone { + /// [`Transcript::Config`] is the configuration for the underlying hash + /// function of the transcript. + type Config: Clone; + + /// [`Transcript::Gadget`] is the in-circuit gadget corresponding to this + /// widget. + type Gadget: TranscriptGadget; + + /// [`Transcript::new`] creates a new transcript / sponge under the given + /// configuration `config`. + fn new(config: Self::Config) -> Self; + + /// [`Transcript::new_with_pp_hash`] is a convenience method for creating a + /// new transcript / sponge under the given configuration `config` and + /// additionally absorbing a hash of the public parameters `pp_hash`. + fn new_with_pp_hash(config: Self::Config, pp_hash: F) -> Self { + let mut sponge = Self::new(config); + sponge.add_field_elements(&[pp_hash]); + sponge + } + + /// [`Transcript::add`] absorbs a message `input` that can be any type + /// implementing the [`Absorbable`] trait into the transcript / sponge. + fn add(&mut self, input: &A) -> &mut Self { + let mut elems = Vec::new(); + input.absorb_into(&mut elems); + + self.add_field_elements(&elems) + } + + /// [`Transcript::add_field_elements`] absorbs a message `input` that is + /// represented as field elements into the transcript / sponge. + fn add_field_elements(&mut self, input: &[F]) -> &mut Self; + + /// [`Transcript::get_bits`] squeezes `num_bits` bits from the transcript / + /// sponge. + fn get_bits(&mut self, num_bits: usize) -> Vec { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.get_field_elements(num_elements); + + let mut bits: Vec = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + let elem_bits = elem.into_bigint().to_bits_le(); + bits.extend_from_slice(&elem_bits[..usable_bits]); + } + + bits.truncate(num_bits); + bits + } + + /// [`Transcript::get_field_element`] squeezes a single field element from + /// the transcript / sponge. + fn get_field_element(&mut self) -> F { + self.get_field_elements(1)[0] + } + + /// [`Transcript::get_field_elements`] squeezes `num_elements` field + /// elements from the transcript / sponge. + fn get_field_elements(&mut self, num_elements: usize) -> Vec; + + /// [`Transcript::separate_domain`] creates a new transcript / sponge by + /// applying domain separation using the provided `domain` byte sequence. + fn separate_domain(&self, domain: &[u8]) -> Self { + let mut new_sponge = self.clone(); + + // Encode the domain length with a fixed-width `u64` so the derived + // challenges are identical across targets. + let mut input = (domain.len() as u64).to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + // Chunk into `(MODULUS_BIT_SIZE - 1) / 8` bytes so a full chunk is + // always `< 2^(MODULUS_BIT_SIZE - 1) <= MODULUS` + let limbs = input + .chunks((F::MODULUS_BIT_SIZE as usize - 1) / 8) + .map(|chunk| F::from_le_bytes_mod_order(chunk)) + .collect::>(); + + new_sponge.add_field_elements(&limbs); + + new_sponge + } + + /// [`Transcript::challenge_field_element`] squeezes a challenge from the + /// transcript as a field element. + /// + /// Internally, it first squeezes a field element and then absorbs it back + /// into the transcript to ensure security. + fn challenge_field_element(&mut self) -> F { + let c = self.get_field_elements(1); + self.add_field_elements(&c); + c[0] + } + + /// [`Transcript::challenge_bits`] squeezes a challenge from the transcript + /// as a bit vector. + /// + /// Internally, it squeezes several field elements, absorbs them back to the + /// transcript (for strong Fiat-Shamir), and decomposes them into bits. + fn challenge_bits(&mut self, num_bits: usize) -> Vec { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.challenge_field_elements(num_elements); + + let mut bits: Vec = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + let elem_bits = elem.into_bigint().to_bits_le(); + bits.extend_from_slice(&elem_bits[..usable_bits]); + } + + bits.truncate(num_bits); + bits + } + + /// [`Transcript::challenge_field_elements`] squeezes `n` challenges from + /// the transcript as field elements. + /// + /// Internally, it first squeezes the field elements and then absorbs them + /// back into the transcript to ensure security. + fn challenge_field_elements(&mut self, n: usize) -> Vec { + let c = self.get_field_elements(n); + self.add_field_elements(&c); + c + } +} + +/// [`TranscriptGadget`] is the in-circuit gadget for transcripts and sponges. +pub trait TranscriptGadget: Clone { + /// [`TranscriptGadget::Config`] is the configuration for the underlying + /// hash function of the transcript gadget. + type Config: Clone; + + /// [`TranscriptGadget::Widget`] points to the out-of-circuit widget for + /// this transcript gadget. + type Widget: Transcript; + + /// [`TranscriptGadget::new`] creates a new transcript / sponge variable + /// under the given configuration `config`. + fn new(config: Self::Config) -> Self; + + /// [`TranscriptGadget::new_with_pp_hash`] is a convenience method for + /// creating a new transcript / sponge variable under the given + /// configuration `config` and additionally absorbing a hash of the public + /// parameters `pp_hash`. + fn new_with_pp_hash(config: Self::Config, pp_hash: &FpVar) -> Result { + let mut sponge = Self::new(config); + sponge.add(&pp_hash)?; + Ok(sponge) + } + + /// [`TranscriptGadget::add`] absorbs a message `input` that can be any type + /// implementing the [`AbsorbableGadget`] trait into the transcript / sponge + /// variable. + fn add>(&mut self, input: &A) -> Result<&mut Self, SynthesisError>; + + /// [`TranscriptGadget::get_bits`] squeezes `num_bits` bit variables from + /// the transcript / sponge variable. + fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.get_field_elements(num_elements)?; + + let mut bits: Vec> = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + bits.extend_from_slice(&elem.to_bits_le()?[..usable_bits]); + } + + bits.truncate(num_bits); + Ok(bits) + } + + /// [`TranscriptGadget::get_field_element`] squeezes a single field element + /// variable from the transcript / sponge variable. + fn get_field_element(&mut self) -> Result, SynthesisError> { + Ok(self.get_field_elements(1)?.swap_remove(0)) + } + + /// [`TranscriptGadget::get_field_elements`] squeezes `num_elements` field + /// element variables from the transcript / sponge variable. + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError>; + + /// [`TranscriptGadget::separate_domain`] creates a new transcript / sponge + /// variable by applying domain separation using the provided `domain` byte + /// sequence. + fn separate_domain(&self, domain: &[u8]) -> Result { + let mut new_sponge = self.clone(); + + // Encode the domain length with a fixed-width `u64` so the derived + // challenges are identical across targets. + let mut input = (domain.len() as u64).to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + // Chunk into `(MODULUS_BIT_SIZE - 1) / 8` bytes so a full chunk is + // always `< 2^(MODULUS_BIT_SIZE - 1) <= MODULUS` + let limbs = input + .chunks((F::MODULUS_BIT_SIZE as usize - 1) / 8) + .map(|chunk| FpVar::Constant(F::from_le_bytes_mod_order(chunk))) + .collect::>(); + + new_sponge.add(&limbs)?; + + Ok(new_sponge) + } + + /// [`TranscriptGadget::challenge_field_element`] squeezes a challenge from + /// the transcript variable as a field element variable. + /// + /// Internally, it first squeezes a field element variable and then absorbs + /// it back into the transcript variable to ensure security. + fn challenge_field_element(&mut self) -> Result, SynthesisError> { + let mut c = self.get_field_elements(1)?; + self.add(&c[0])?; + Ok(c.swap_remove(0)) + } + + /// [`TranscriptGadget::challenge_bits`] squeezes a challenge from the + /// transcript variable as a vector of bit variables. + /// + /// Internally, it squeezes several field element variables, absorbs them + /// back to the transcript variable (for strong Fiat-Shamir), and decomposes + /// them into bit variables. + fn challenge_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.challenge_field_elements(num_elements)?; + + let mut bits: Vec> = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + bits.extend_from_slice(&elem.to_bits_le()?[..usable_bits]); + } + + bits.truncate(num_bits); + Ok(bits) + } + + /// [`TranscriptGadget::challenge_field_elements`] squeezes `n` challenges + /// from the transcript variable as field element variables. + /// + /// Internally, it first squeezes the field element variables and then + /// absorbs them back into the transcript variable to ensure + /// security. + fn challenge_field_elements(&mut self, n: usize) -> Result>, SynthesisError> { + let c = self.get_field_elements(n)?; + self.add(&c)?; + Ok(c) + } +} diff --git a/crates/primitives/src/transcripts/poseidon/mod.rs b/crates/primitives/src/transcripts/poseidon/mod.rs new file mode 100644 index 000000000..ca9b881aa --- /dev/null +++ b/crates/primitives/src/transcripts/poseidon/mod.rs @@ -0,0 +1,158 @@ +//! Poseidon-based transcript configurations and implementations. + +use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, find_poseidon_ark_and_mds}; +use ark_ff::{One, PrimeField}; +use num_bigint::BigUint; +use num_integer::Integer; + +pub mod sponge; + +fn log2_order() -> f64 { + let x = F::MODULUS.into(); + let bits = x.bits(); // bit length + if bits <= 53 { + // Fits in f64 mantissa exactly + let val: u64 = x.try_into().unwrap(); + return (val as f64).log2(); + } + // Shift right so only top ~53 bits remain + let shift = bits - 53; + let top = x >> shift; + let top_u64: u64 = top.try_into().unwrap(); + (top_u64 as f64).log2() + shift as f64 +} + +fn sat_inequiv_alpha(t: usize, r_f: u64, r_p: u64, alpha: u64, m: usize) -> bool { + let log2_p = log2_order::(); + let n = log2_p.ceil() as usize; + let m_f = m as f64; + let n_f = n as f64; + let t_f = t as f64; + let r_p_f = r_p as f64; + let r_f_f = r_f as f64; + let alpha_f = alpha as f64; + let log2_alpha = 2.0f64.ln() / alpha_f.ln(); + + let r_f_1: f64 = if m_f <= (log2_p - (alpha_f - 1.0) / 2.0).floor() * (t_f + 1.0) { + 6.0 + } else { + 10.0 + }; + + let r_f_2 = 1.0 + log2_alpha * m_f.min(n_f) + (t_f.ln() / alpha_f.ln()).ceil() - r_p_f; + + let r_f_3 = 1.0 + log2_alpha * (m_f / 3.0).min(log2_p / 2.0) - r_p_f; + + let r_f_4 = t_f - 1.0 + (log2_alpha * m_f / (t_f + 1.0)).min(log2_alpha * log2_p / 2.0) - r_p_f; + + let r_f_max = r_f_1 + .ceil() + .max(r_f_2.ceil()) + .max(r_f_3.ceil()) + .max(r_f_4.ceil()); + + r_f_f >= r_f_max +} + +fn get_sbox_cost(r_f: u64, r_p: u64, _n: usize, t: usize) -> usize { + t * r_f as usize + r_p as usize +} + +fn find_fd_round_numbers( + t: usize, + alpha: u64, + m: usize, + cost_function: fn(u64, u64, usize, usize) -> usize, + security_margin: bool, +) -> (u64, u64) { + let n = log2_order::().ceil() as usize; + let n_total = n * t; + + let mut r_p: u64 = 0; + let mut r_f: u64 = 0; + let mut min_cost = usize::MAX; + let mut max_cost_rf: u64 = 0; + + for r_p_t in 1u64..500 { + for r_f_t in (4u64..100).step_by(2) { + if !sat_inequiv_alpha::(t, r_f_t, r_p_t, alpha, m) { + continue; + } + + let (r_f_eff, r_p_eff) = if security_margin { + (r_f_t + 2, (r_p_t as f64 * 1.075).ceil() as u64) + } else { + (r_f_t, r_p_t) + }; + + let cost = cost_function(r_f_eff, r_p_eff, n_total, t); + if cost < min_cost || (cost == min_cost && r_f_eff < max_cost_rf) { + r_p = r_p_eff; + r_f = r_f_eff; + min_cost = cost; + max_cost_rf = r_f; + } + } + } + + assert_ne!(min_cost, usize::MAX); + + (r_f, r_p) +} + +/// [`poseidon_paper_config`] produces a Poseidon configuration which agrees +/// with the paper's reference implementation. +/// +/// TODO(@winderica): alpha = -1 is not supported yet. +pub fn poseidon_paper_config( + alpha: u64, + rate: usize, +) -> PoseidonConfig { + assert_ne!(alpha, 1); + assert_eq!( + BigUint::from(alpha).gcd(&(-F::one()).into()), + BigUint::one() + ); + let (full_rounds, partial_rounds) = + find_fd_round_numbers::(rate + 1, alpha, SECURITY_BITS, get_sbox_cost, true); + let (ark, mds) = find_poseidon_ark_and_mds( + F::MODULUS_BIT_SIZE as u64, + rate, + full_rounds, + partial_rounds, + 0, + ); + + PoseidonConfig::new( + full_rounds as usize, + partial_rounds as usize, + alpha, + mds, + ark, + rate, + 1, + ) +} + +/// [`poseidon_circom_config`] produces a Poseidon configuration for BN254's +/// scalar field that agrees with Circom's Poseidon(4). +pub fn poseidon_circom_config() -> PoseidonConfig { + // 120 bit security target as in + // https://eprint.iacr.org/2019/458.pdf + // t = rate + 1 + + let full_rounds = 8; + let partial_rounds = 60; + let alpha = 5; + let rate = 4; + + let (ark, mds) = find_poseidon_ark_and_mds( + ark_bn254::Fr::MODULUS_BIT_SIZE as u64, + rate, + full_rounds as u64, + partial_rounds as u64, + 0, + ); + + PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, rate, 1) +} diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs new file mode 100644 index 000000000..daf8bc537 --- /dev/null +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -0,0 +1,217 @@ +//! Implementation of transcript traits for arkworks' Poseidon sponge. + +use ark_crypto_primitives::sponge::{ + Absorb, CryptographicSponge, DuplexSpongeMode, FieldBasedCryptographicSponge, + constraints::CryptographicSpongeVar, + poseidon::{PoseidonConfig, PoseidonSponge, constraints::PoseidonSpongeVar}, +}; +use ark_ff::PrimeField; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::mem::transmute_copy; + +use crate::transcripts::{AbsorbableVar, Transcript, TranscriptGadget}; + +impl Transcript for PoseidonSponge { + type Config = PoseidonConfig; + type Gadget = PoseidonSpongeVar; + + fn new(config: Self::Config) -> Self { + Self { + state: vec![F::zero(); config.rate + config.capacity], + parameters: config, + mode: DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }, + } + } + + fn add_field_elements(&mut self, input: &[F]) -> &mut Self { + struct Hack(I); + impl Absorb for Hack<&[F]> { + fn to_sponge_bytes(&self, _: &mut Vec) { + // Unreachable because `PoseidonSponge::absorb` only calls + // `to_sponge_field_elements_as_vec::` + unreachable!() + } + + fn to_sponge_field_elements(&self, dest: &mut Vec) { + // Safe because `F` in `to_sponge_field_elements_as_vec::`, + // which is called by `PoseidonSponge::absorb`, is the same as + // `T` here. + dest.extend(unsafe { transmute_copy::<&[F], &[T]>(&self.0) }); + } + } + CryptographicSponge::absorb(self, &Hack(input)); + self + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + self.squeeze_native_field_elements(num_elements) + } +} + +impl TranscriptGadget for PoseidonSpongeVar { + type Config = PoseidonConfig; + type Widget = PoseidonSponge; + + fn new(config: PoseidonConfig) -> Self + where + Self: Sized, + { + Self { + cs: ConstraintSystemRef::None, + state: vec![FpVar::::zero(); config.rate + config.capacity], + parameters: config, + mode: DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }, + } + } + + fn add>(&mut self, input: &A) -> Result<&mut Self, SynthesisError> { + let mut result = Vec::new(); + input.absorb_into(&mut result)?; + + self.absorb(&result)?; + Ok(self) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + self.squeeze_field_elements(num_elements) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective as G1}; + use ark_crypto_primitives::sponge::poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as G2; + use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, + groups::curves::short_weierstrass::ProjectiveVar, + }; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{error::Error, rand::thread_rng, str::FromStr}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use crate::{ + algebra::group::emulated::EmulatedAffineVar, + transcripts::{Transcript, TranscriptGadget, poseidon::poseidon_circom_config}, + }; + + // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 + #[test] + fn check_against_circom_poseidon() -> Result<(), Box> { + let config = poseidon_circom_config(); + let mut poseidon_sponge = PoseidonSponge::new(config); + let v = vec![1, 2, 3, 4] + .into_iter() + .map(Fr::from) + .collect::>(); + poseidon_sponge.add(&v); + poseidon_sponge.get_field_elements(1); + assert_eq!( + poseidon_sponge.state[0], + Fr::from_str( + "18821383157269793795438455681495246036402687001665670618754263018637548127333" + ) + .unwrap() + ); + Ok(()) + } + + #[test] + fn test_challenge_field_element() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = poseidon_circom_config(); + let mut tr = PoseidonSponge::new(config.clone()); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::new_ref(); + let mut tr_var = PoseidonSpongeVar::new(config); + let v = FpVar::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_challenge_bits() -> Result<(), Box> { + let nbits = 128; + + // Create a transcript outside of the circuit + let config = poseidon_circom_config(); + let mut tr = PoseidonSponge::new(config.clone()); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_bits(nbits); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::new_ref(); + let mut tr_var = PoseidonSpongeVar::new(config); + let v = FpVar::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_bits(nbits)?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_absorb_canonical_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = poseidon_circom_config(); + let mut tr = PoseidonSponge::new(config.clone()); + let rng = &mut thread_rng(); + + let p = G2::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::new_ref(); + let mut tr_var = PoseidonSpongeVar::new(config); + let p_var = ProjectiveVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_absorb_emulated_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = poseidon_circom_config(); + let mut tr = PoseidonSponge::new(config.clone()); + let rng = &mut thread_rng(); + + let p = G1::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::new_ref(); + let mut tr_var = PoseidonSpongeVar::new(config); + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } +} diff --git a/crates/primitives/src/transcripts/recording/mod.rs b/crates/primitives/src/transcripts/recording/mod.rs new file mode 100644 index 000000000..57266ffe9 --- /dev/null +++ b/crates/primitives/src/transcripts/recording/mod.rs @@ -0,0 +1,73 @@ +//! Implementation of transcripts that can automatically record generated +//! challenges, eliminating the need to pass challenges throughout protocols. + +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; + +use super::{AbsorbableVar, Transcript, TranscriptGadget}; + +/// [`RecordingTranscript`] wraps a regular transcript to record all challenges +/// it produces. +#[derive(Clone)] +pub struct RecordingTranscript> { + inner: T, + /// [`RecordingTranscript::cached_challenges`] contains the challenge field + /// elements recorded so far. + pub cached_challenges: Vec, +} + +impl> Transcript for RecordingTranscript { + type Config = T; + type Gadget = RecordingTranscriptVar; + + fn new(inner: Self::Config) -> Self { + Self { + inner, + cached_challenges: vec![], + } + } + + fn add_field_elements(&mut self, input: &[F]) -> &mut Self { + self.inner.add_field_elements(input); + self + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + let v = self.inner.get_field_elements(num_elements); + self.cached_challenges.extend_from_slice(&v); + v + } +} + +/// [`RecordingTranscriptVar`] is the in-circuit variable of [`RecordingTranscript`]. +#[derive(Clone)] +pub struct RecordingTranscriptVar> { + inner: T, + /// [`RecordingTranscriptVar::cached_challenges`] contains the challenge + /// field element variables recorded so far. + pub cached_challenges: Vec>, +} + +impl> TranscriptGadget for RecordingTranscriptVar { + type Config = T; + type Widget = RecordingTranscript; + + fn new(inner: Self::Config) -> Self { + Self { + inner, + cached_challenges: vec![], + } + } + + fn add>(&mut self, input: &A) -> Result<&mut Self, SynthesisError> { + self.inner.add(input)?; + Ok(self) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + let v = self.inner.get_field_elements(num_elements)?; + self.cached_challenges.extend_from_slice(&v); + Ok(v) + } +} diff --git a/crates/primitives/src/transcripts/replay/mod.rs b/crates/primitives/src/transcripts/replay/mod.rs new file mode 100644 index 000000000..5ba177913 --- /dev/null +++ b/crates/primitives/src/transcripts/replay/mod.rs @@ -0,0 +1,83 @@ +//! Implementation of transcripts that always produces designated challenge +//! values. + +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; + +use super::{AbsorbableVar, Transcript, TranscriptGadget}; +use crate::transcripts::recording::{RecordingTranscript, RecordingTranscriptVar}; + +/// [`ReplayTranscript`] is a convenience struct that generates specific values +/// as challenges without running the actual hash function. +/// +/// WARNING: This struct itself is insecure. The caller is responsible for +/// checking the validity of the designated challenge values. +#[derive(Clone)] +pub struct ReplayTranscript { + cached_challenges: Vec, +} + +impl> From> for ReplayTranscript { + fn from(value: RecordingTranscript) -> Self { + Self::new(value.cached_challenges) + } +} + +impl Transcript for ReplayTranscript { + type Config = Vec; + type Gadget = ReplayTranscriptVar; + + fn new(mut cached_challenges: Self::Config) -> Self { + cached_challenges.reverse(); + Self { cached_challenges } + } + + fn add_field_elements(&mut self, _: &[F]) -> &mut Self { + self + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + let mut result = vec![]; + for _ in 0..num_elements { + result.push(self.cached_challenges.pop().unwrap()) + } + result + } +} + +/// [`ReplayTranscriptVar`] is the in-circuit variable of [`ReplayTranscript`]. +#[derive(Clone)] +pub struct ReplayTranscriptVar { + cached_challenges: Vec>, +} + +impl> From> + for ReplayTranscriptVar +{ + fn from(value: RecordingTranscriptVar) -> Self { + Self::new(value.cached_challenges) + } +} + +impl TranscriptGadget for ReplayTranscriptVar { + type Config = Vec>; + type Widget = ReplayTranscript; + + fn new(mut cached_challenges: Vec>) -> Self { + cached_challenges.reverse(); + Self { cached_challenges } + } + + fn add + ?Sized>(&mut self, _: &A) -> Result<&mut Self, SynthesisError> { + Ok(self) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + let mut result = vec![]; + for _ in 0..num_elements { + result.push(self.cached_challenges.pop().unwrap()) + } + Ok(result) + } +} diff --git a/crates/primitives/src/utils/dummy.rs b/crates/primitives/src/utils/dummy.rs new file mode 100644 index 000000000..408cd4f95 --- /dev/null +++ b/crates/primitives/src/utils/dummy.rs @@ -0,0 +1,29 @@ +/// [`Dummy`] provides a way to construct a placeholder ("dummy") value of a +/// given type, parameterized by some configuration `Cfg`. +/// +/// This is useful when initializing data structures that require a value of a +/// certain shape before the real data is available, e.g., when setting up the +/// initial state of a folding scheme. +pub trait Dummy { + /// [`Dummy::dummy`] constructs a dummy value of `Self` based on the given + /// configuration `cfg`. + fn dummy(cfg: Cfg) -> Self; +} + +impl Dummy for Vec { + fn dummy(cfg: usize) -> Self { + vec![Default::default(); cfg] + } +} + +impl + Copy, const N: usize> Dummy for [T; N] { + fn dummy(cfg: Cfg) -> Self { + [T::dummy(cfg); N] + } +} + +impl, B: Dummy> Dummy for (A, B) { + fn dummy(cfg: Cfg) -> Self { + (A::dummy(cfg), B::dummy(cfg)) + } +} diff --git a/crates/primitives/src/utils/evm/compiler.rs b/crates/primitives/src/utils/evm/compiler.rs new file mode 100644 index 000000000..cff618394 --- /dev/null +++ b/crates/primitives/src/utils/evm/compiler.rs @@ -0,0 +1,167 @@ +use ark_std::{ + io::{Error as IOError, Write}, + process::{Command, Stdio}, +}; +use hashbrown::HashMap; +use revm::primitives::{ + Bytes, + hex::{FromHexError, decode}, +}; +use serde::Deserialize; +use serde_json::{Error as JSONError, from_slice, json, to_vec}; +use thiserror::Error; + +/// [`Error`] enumerates possible errors during solidity compilation. +#[derive(Debug, Error)] +pub enum Error { + /// Failed to run the `solc` process or read/write its pipes. + #[error("solc i/o error: {0}")] + Io(#[from] IOError), + /// Failed to (de)serialize the `solc` Standard JSON. + #[error("solc json error: {0}")] + Json(#[from] JSONError), + /// Failed to decode the `solc` bytecode hex. + #[error("invalid bytecode hex: {0}")] + Hex(#[from] FromHexError), + /// `solc` reported a compilation failure, or produced no bytecode. + #[error("solc compilation failed: {0}")] + Solc(String), + /// The compiled contract has no function with the requested name. + #[error("no function named `{0}` in the compiled contract")] + UnknownFunction(String), +} + +#[derive(Deserialize)] +struct Output { + #[serde(default)] + errors: Vec, + #[serde(default)] + contracts: HashMap>, +} + +#[derive(Deserialize)] +struct OutputError { + severity: String, + #[serde(rename = "formattedMessage")] + formatted_message: String, +} + +#[derive(Deserialize)] +struct OutputContract { + evm: OutputEvm, +} + +#[derive(Deserialize)] +struct OutputEvm { + bytecode: OutputBytecode, + #[serde(rename = "methodIdentifiers")] + method_identifiers: HashMap, +} + +#[derive(Deserialize)] +struct OutputBytecode { + object: String, +} + +/// [`SolidityCompiler`] contains a handle to a Solidity compiler for compiling +/// rendered verifier contracts in tests. +pub struct SolidityCompiler { + solc: String, +} + +impl Default for SolidityCompiler { + fn default() -> Self { + Self::new("solc") + } +} + +impl SolidityCompiler { + /// [`SolidityCompiler::new`] creates a compiler that uses a specific `solc` + /// binary (a path, or a command on `PATH`). + pub fn new(solc: impl Into) -> Self { + Self { solc: solc.into() } + } + + /// [`SolidityCompiler::available`] tries to invoke the compiler and returns + /// whether the invocation is successful. + pub fn available(&self) -> bool { + Command::new(&self.solc) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } + + /// [`SolidityCompiler::compile`] compiles `sources`, which is a list of + /// file name and solidity code pairs, retrieves the compilation results for + /// `contract`, and returns its bytecode and function selectors. + pub fn compile( + &self, + sources: Vec<(impl Into, impl Into)>, + contract: impl Into, + ) -> Result<(Bytes, HashMap), Error> { + let contract = &contract.into(); + + let input = json!({ + "language": "Solidity", + "sources": sources + .into_iter() + .map(|(name, src)| (name.into(), json!({ "content": src.into() }))) + .collect::>(), + "settings": { + "optimizer": { "enabled": true, "runs": 200 }, + "outputSelection": { + "*": { "*": ["evm.bytecode.object", "evm.methodIdentifiers"] }, + }, + }, + }); + + let mut child = Command::new(&self.solc) + .arg("--standard-json") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + child + .stdin + .take() + .ok_or_else(|| Error::Solc("failed to open solc stdin".into()))? + .write_all(&to_vec(&input)?)?; + let out = child.wait_with_output()?; + if !out.status.success() { + return Err(Error::Solc( + String::from_utf8_lossy(&out.stderr).into_owned(), + )); + } + + let output: Output = from_slice(&out.stdout)?; + if let Some(e) = output.errors.iter().find(|e| e.severity == "error") { + return Err(Error::Solc(e.formatted_message.clone())); + } + + let evm = output + .contracts + .into_values() + .find_map(|mut cs| cs.remove(contract)) + .map(|c| c.evm) + .ok_or_else(|| Error::Solc(format!("no output for `{contract}`")))?; + + Ok(( + decode(evm.bytecode.object)?.into(), + evm.method_identifiers + .into_iter() + .map(|(sig, sel)| { + // `sig` ("name(types)") -> name + let name = sig.split('(').next().unwrap_or(&sig).to_string(); + // `sel` (hex string) -> bytes + let bytes: [u8; 4] = decode(&sel)? + .try_into() + .map_err(|_| Error::Solc(format!("bad selector for `{sig}`")))?; + Ok((name, bytes)) + }) + .collect::>()?, + )) + } +} diff --git a/crates/primitives/src/utils/evm/harness.rs b/crates/primitives/src/utils/evm/harness.rs new file mode 100644 index 000000000..a393a0ad7 --- /dev/null +++ b/crates/primitives/src/utils/evm/harness.rs @@ -0,0 +1,86 @@ +use ark_std::convert::Infallible; +use revm::{ + ExecuteCommitEvm, ExecuteEvm, MainBuilder, MainContext, MainnetEvm, + context::{Context, TxEnv, result::EVMError}, + context_interface::result::ExecutionResult, + database::{CacheDB, EmptyDB}, + handler::MainnetContext, + primitives::{Address, Bytes}, +}; + +use super::serialize::EVMSerialize; + +/// [`TestEVM`] is a minimal in-memory `revm` harness for deploying and invoking +/// rendered verifier contracts in tests. +pub struct TestEVM { + evm: MainnetEvm>>, +} + +impl Default for TestEVM { + fn default() -> Self { + Self { + evm: Context::mainnet() + .with_db(CacheDB::default()) + .modify_cfg_chained(|cfg| cfg.disable_nonce_check = true) + .build_mainnet(), + } + } +} + +impl TestEVM { + /// [`TestEVM::deploy`] deploys a smart contract's `bytecode`, calls its + /// constructor with `args`, and returns the deployed contract's address. + pub fn deploy( + &mut self, + bytecode: &Bytes, + args: impl EVMSerialize, + ) -> Result, EVMError> { + self.evm + .transact_commit( + TxEnv::builder() + .create() + .data([&bytecode[..], &args.to_calldata()].concat().into()) + .build_fill(), + ) + .map(|i| i.created_address()) + } + + /// [`TestEVM::send`] sends to the contract deployed at `to` a transaction, + /// which calls the function specified by `selector` with `args`. + /// + /// This method mutates the onchain state and returns the execution result. + pub fn send( + &mut self, + to: Address, + selector: [u8; 4], + args: impl EVMSerialize, + ) -> Result> { + self.evm.transact_commit( + TxEnv::builder() + .call(to) + .data([&selector[..], &args.to_calldata()].concat().into()) + .build_fill(), + ) + } + + /// [`TestEVM::view`] simulates the execution of the contract deployed at + /// `to` on a call to the function specified by `selector` with `args`. + /// + /// This method keeps the onchain state unchanged and returns the execution + /// result. + pub fn view( + &mut self, + to: Address, + selector: [u8; 4], + args: impl EVMSerialize, + ) -> Result> { + self.evm + .transact( + TxEnv::builder() + .call(to) + .data([&selector[..], &args.to_calldata()].concat().into()) + .build_fill(), + ) + .map(|i| i.result) + } +} diff --git a/crates/primitives/src/utils/evm/mod.rs b/crates/primitives/src/utils/evm/mod.rs new file mode 100644 index 000000000..76c1a63e5 --- /dev/null +++ b/crates/primitives/src/utils/evm/mod.rs @@ -0,0 +1,5 @@ +//! EVM serialization and harness utilities. + +pub mod compiler; +pub mod serialize; +pub mod harness; diff --git a/crates/primitives/src/utils/evm/serialize.rs b/crates/primitives/src/utils/evm/serialize.rs new file mode 100644 index 000000000..1df129e2a --- /dev/null +++ b/crates/primitives/src/utils/evm/serialize.rs @@ -0,0 +1,68 @@ +/// [`EVMSerialize`] encodes a value as EVM calldata (the ABI byte encoding the +/// on-chain verifier expects). +pub trait EVMSerialize { + fn to_calldata(&self) -> Vec; +} + +impl EVMSerialize for &T { + fn to_calldata(&self) -> Vec { + T::to_calldata(self) + } +} + +impl EVMSerialize for [T] { + fn to_calldata(&self) -> Vec { + self.iter().flat_map(EVMSerialize::to_calldata).collect() + } +} + +impl EVMSerialize for [T; N] { + fn to_calldata(&self) -> Vec { + self[..].to_calldata() + } +} + +impl EVMSerialize for Vec { + fn to_calldata(&self) -> Vec { + self[..].to_calldata() + } +} + +impl EVMSerialize for u8 { + fn to_calldata(&self) -> Vec { + vec![*self] + } +} + +impl EVMSerialize for () { + fn to_calldata(&self) -> Vec { + Vec::new() + } +} + +macro_rules! impl_evm_serialize_tuple { + ($($T:ident),+) => { + impl<$($T: EVMSerialize),+> EVMSerialize for ($($T,)+) { + fn to_calldata(&self) -> Vec { + #[allow(non_snake_case)] + let ($($T,)+) = self; + let mut out = Vec::new(); + $(out.extend($T.to_calldata());)+ + out + } + } + }; +} + +impl_evm_serialize_tuple!(A); +impl_evm_serialize_tuple!(A, B); +impl_evm_serialize_tuple!(A, B, C); +impl_evm_serialize_tuple!(A, B, C, D); +impl_evm_serialize_tuple!(A, B, C, D, E); +impl_evm_serialize_tuple!(A, B, C, D, E, F); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G, H); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G, H, I); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G, H, I, J); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G, H, I, J, K); +impl_evm_serialize_tuple!(A, B, C, D, E, F, G, H, I, J, K, L); diff --git a/crates/primitives/src/utils/mod.rs b/crates/primitives/src/utils/mod.rs new file mode 100644 index 000000000..9c393840e --- /dev/null +++ b/crates/primitives/src/utils/mod.rs @@ -0,0 +1,6 @@ +//! Miscellaneous utilities shared across the primitives crate. + +pub mod dummy; +#[cfg(feature = "evm")] +pub mod evm; +pub mod null; diff --git a/crates/primitives/src/utils/null.rs b/crates/primitives/src/utils/null.rs new file mode 100644 index 000000000..da179385b --- /dev/null +++ b/crates/primitives/src/utils/null.rs @@ -0,0 +1,83 @@ +//! This module defines a zero-cost placeholder type that have well-defined +//! arithmetic operations. + +use ark_ff::Field; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + fmt::Debug, + iter::Sum, + ops::{Add, Mul}, +}; + +/// [`Null`] is a zero-sized type that absorbs any arithmetic and always returns +/// itself. +/// +/// It also has itself as its in-circuit representation, which does not allocate +/// any variables or require any constraints. +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct Null; + +impl Add for Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Add for &Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Mul for Null { + type Output = Self; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Mul for &Null { + type Output = Null; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Sum for Null { + fn sum>(_: I) -> Self { + Null + } +} + +impl AllocVar for Null { + fn new_variable>( + _cs: impl Into>, + _f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + Ok(Self) + } +} + +impl GR1CSVar for Null { + type Value = Null; + + fn cs(&self) -> ConstraintSystemRef { + ConstraintSystemRef::None + } + + fn value(&self) -> Result { + Ok(Null) + } +} diff --git a/crates/snarks/Cargo.toml b/crates/snarks/Cargo.toml new file mode 100644 index 000000000..f84e13746 --- /dev/null +++ b/crates/snarks/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "sonobe-snarks" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"], optional = true } +ark-ec = { workspace = true } +ark-ff = { workspace = true, features = ["asm"] } +ark-groth16 = { workspace = true } +ark-poly = { workspace = true } +ark-relations = { workspace = true } +ark-serialize = { workspace = true } +ark-snark = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +askama = { workspace = true, optional = true } +hashbrown = { workspace = true } +itertools = { workspace = true } +thiserror = { workspace = true } +rayon = { workspace = true } + +sonobe-primitives = { workspace = true } + +[dev-dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-grumpkin = { workspace = true, features = ["r1cs"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[features] +default = [] +evm = ["dep:askama", "sonobe-primitives/evm", "dep:ark-bn254"] +parallel = [ + "sonobe-primitives/parallel", + "ark-groth16/parallel", +] diff --git a/crates/snarks/src/cp/legogroth16/evm_verifier.rs b/crates/snarks/src/cp/legogroth16/evm_verifier.rs new file mode 100644 index 000000000..c46d29783 --- /dev/null +++ b/crates/snarks/src/cp/legogroth16/evm_verifier.rs @@ -0,0 +1,49 @@ +use ark_bn254::Bn254; +use ark_ec::AffineRepr; +use askama::Template; + +use crate::cp::legogroth16::VerifierKey; + +#[derive(Template)] +#[template(path = "legogroth16.sol.askama")] +pub struct LegoGroth16VerifierTemplate<'a> { + pub vk: &'a VerifierKey, +} + +#[cfg(test)] +mod tests { + use ark_std::{error::Error, rand::thread_rng}; + use sonobe_primitives::utils::evm::{compiler::SolidityCompiler, harness::TestEVM}; + + use super::*; + use crate::cp::legogroth16::tests::{toy_keygen, toy_prove}; + + #[test] + fn test_evm_verifier() -> Result<(), Box> { + let solc = SolidityCompiler::default(); + assert!(solc.available()); + + let mut rng = thread_rng(); + let ck_sizes = [1, 1, 1]; + let (pk, vk, generators) = toy_keygen::(&ck_sizes, &mut rng); + let (g, cm, proof) = toy_prove(&pk, &generators, &ck_sizes, &mut rng); + + let source = LegoGroth16VerifierTemplate { vk: &vk }.render()?; + let (bytecode, selectors) = solc.compile( + vec![("LegoGroth16Verifier.sol", source)], + "LegoGroth16Verifier", + )?; + let mut evm = TestEVM::default(); + let addr = evm.deploy(&bytecode, ())?.unwrap(); + + assert!( + evm.view( + addr, + *selectors.get("verifyProof").unwrap(), + ([g], cm, proof) + )? + .is_success() + ); + Ok(()) + } +} diff --git a/crates/snarks/src/cp/legogroth16/mod.rs b/crates/snarks/src/cp/legogroth16/mod.rs new file mode 100644 index 000000000..698bf627e --- /dev/null +++ b/crates/snarks/src/cp/legogroth16/mod.rs @@ -0,0 +1,694 @@ +use ark_ec::{ + AffineRepr, CurveGroup, VariableBaseMSM, + pairing::{Pairing, PairingOutput}, + scalar_mul::BatchMulPreprocessing, +}; +use ark_ff::{Field, One, PrimeField, UniformRand, Zero}; +use ark_groth16::{ + Proof as Groth16Proof, + r1cs_to_qap::{LibsnarkReduction, R1CSToQAP}, +}; +use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{ + borrow::Borrow, cfg_into_iter, cfg_iter, cfg_iter_mut, end_timer, hash::BuildHasherDefault, + marker::PhantomData, rand::RngCore, start_timer, +}; +use hashbrown::HashSet; +#[cfg(not(feature = "parallel"))] +use itertools::{Either, Itertools}; +#[cfg(feature = "parallel")] +use rayon::{iter::Either, prelude::*}; +#[cfg(feature = "evm")] +use sonobe_primitives::utils::evm::serialize::EVMSerialize; +use sonobe_primitives::{ + algebra::{field::SonobeField, group::SonobeCurve}, + arithmetizations::{Arith, ccs::CCS, r1cs::R1CS}, + circuits::cache::{IdentityHasher, UsizeSet}, + commitments::pedersen::PedersenKey, +}; +use thiserror::Error; + +use crate::{ + cp::CPSNARK, + linear_subspace::{ + LinearSubspaceSNARK, ProverKey as LinearSubspacePK, VerifierKey as LinearSubspaceVK, + }, +}; + +#[cfg(feature = "evm")] +pub mod evm_verifier; + +pub struct CCGroth16ProverKey { + /// The element `beta * G` in `E::G1`. + pub alpha_g1: E::G1Affine, + pub beta_g1: E::G1Affine, + pub beta_g2: E::G2Affine, + /// The element `delta * G` in `E::G1`. + pub delta_g1: E::G1Affine, + pub delta_g2: E::G2Affine, + /// The element `eta*delta^-1 * G` in `E::G1`. + pub eta_delta_inv_g1: E::G1Affine, + /// The element `eta*gamma^-1 * G` in `E::G1`. + pub eta_gamma_inv_g1: E::G1Affine, + /// The elements `a_i * G` in `E::G1`. + pub a_query: Vec, + /// The elements `b_i * G` in `E::G1`. + pub b_g1_query: Vec, + /// The elements `b_i * H` in `E::G2`. + pub b_g2_query: Vec, + /// The elements `h_i * G` in `E::G1`. + pub h_query: Vec, + /// The elements `l_i * G` in `E::G1`. + pub l_query: Vec, + /// The `gamma^{-1} * (beta * a_i + alpha * b_i + c_i) * H`, where `H` is + /// the generator of `E::G1`. + pub gamma_abc_g1_cm: Vec, +} + +pub struct CCGroth16VerifierKey { + #[cfg(feature = "evm")] + pub alpha_g1: E::G1Affine, + #[cfg(feature = "evm")] + pub beta_g2_neg: E::G2Affine, + #[cfg(feature = "evm")] + pub gamma_g2_neg: E::G2Affine, + #[cfg(feature = "evm")] + pub delta_g2_neg: E::G2Affine, + + /// The element `e(alpha * G, beta * H)` in `E::GT`. + pub alpha_g1_beta_g2: PairingOutput, + /// The element `- gamma * H` in `E::G2`, prepared for use in pairings. + pub gamma_g2_neg_pc: E::G2Prepared, + /// The element `- delta * H` in `E::G2`, prepared for use in pairings. + pub delta_g2_neg_pc: E::G2Prepared, + /// The `gamma^{-1} * (beta * a_i + alpha * b_i + c_i) * H`, where `H` is + /// the generator of `E::G1`. + pub gamma_abc_g1_pub: Vec, +} + +pub struct ProverKey { + pub r1cs: R1CS, + pub committed_variable_indices: HashSet>, + pub cc_pk: CCGroth16ProverKey, + pub link_ek: LinearSubspacePK, +} + +pub struct VerifierKey { + pub cc_vk: CCGroth16VerifierKey, + pub link_vk: LinearSubspaceVK, +} + +pub struct Proof { + pub groth16_proof: Groth16Proof, + /// The `D` element in `G1`. Commits to a subset of private inputs of the + /// circuit + pub d: E::G1Affine, + /// proof of commitment opening equality between `cp_{link}` and `d` + pub link_pi: E::G1Affine, +} + +#[cfg(feature = "evm")] +impl> EVMSerialize for Proof { + fn to_calldata(&self) -> Vec { + [ + self.groth16_proof.a.to_calldata(), + self.groth16_proof.b.to_calldata(), + self.groth16_proof.c.to_calldata(), + self.d.to_calldata(), + self.link_pi.to_calldata(), + ] + .concat() + } +} + +/// [`Error`] enumerates possible errors during LegoGroth16 operations. +#[derive(Debug, Error)] +pub enum Error { + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. + #[error(transparent)] + SynthesisError(#[from] SynthesisError), + /// [`Error::VerificationFail`] indicates that the verification has + /// failed. + #[error("Verification failed")] + VerificationFail, +} + +pub struct LegoGroth16< + E: Pairing, + QAP: R1CSToQAP = LibsnarkReduction, +> { + _p: PhantomData<(E, QAP)>, +} + +impl, QAP: R1CSToQAP> + CPSNARK for LegoGroth16 +{ + type Field = E::ScalarField; + + type Relation = (R1CS, UsizeSet); + + type Commitment = E::G1Affine; + type CommitmentKey = PedersenKey; + type CommitmentOpening = E::ScalarField; + + type ProverKey = ProverKey; + type VerifierKey = VerifierKey; + type Error = SynthesisError; + type Proof = Proof; + + fn generate_keys( + (r1cs, committed_variable_indices): Self::Relation, + commitment_key: &[impl Borrow + Sync], + mut rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Self::Error> { + let alpha = E::ScalarField::rand(&mut rng); + let beta = E::ScalarField::rand(&mut rng); + let gamma = E::ScalarField::rand(&mut rng); + let delta = E::ScalarField::rand(&mut rng); + let eta = E::ScalarField::rand(&mut rng); + + let gamma_inverse = gamma.inverse().ok_or(SynthesisError::DivisionByZero)?; + let delta_inverse = delta.inverse().ok_or(SynthesisError::DivisionByZero)?; + + let g1_generator = E::G1::rand(&mut rng); + let g2_generator = E::G2::rand(&mut rng); + + let setup_time = start_timer!(|| "Groth16::Generator"); + + let r1cs_config = r1cs.config(); + let n_instance_variables = r1cs_config.n_public_inputs + 1; + let n_constraints = r1cs_config.n_constraints; + let n_variables = r1cs_config.n_variables; + let matrices = r1cs.matrices(); + + let n_committed_variables = committed_variable_indices.len(); + + // Following is the mapping of symbols from the Groth16 paper to this + // implementation l -> num_instance_variables + // m -> qap_num_variables + // x -> t + // t(x) - zt + // u_i(x) -> a + // v_i(x) -> b + // w_i(x) -> c + + /////////////////////////////////////////////////////////////////////////// + let domain_time = start_timer!(|| "Constructing evaluation domain"); + + let domain = GeneralEvaluationDomain::new(n_constraints + n_instance_variables) + .ok_or(SynthesisError::PolynomialDegreeTooLarge)?; + let domain_size = domain.size(); + + let t = domain.sample_element_outside_domain(&mut rng); + let zt_over_delta = domain.evaluate_vanishing_polynomial(t) * delta_inverse; + + end_timer!(domain_time); + /////////////////////////////////////////////////////////////////////////// + + let reduction_time = start_timer!(|| "R1CS to QAP Instance Map with Evaluation"); + + // Evaluate all Lagrange polynomials + let coefficients_time = start_timer!(|| "Evaluate Lagrange coefficients"); + let u = domain.evaluate_all_lagrange_coefficients(t); + end_timer!(coefficients_time); + + let mut a = vec![E::ScalarField::zero(); n_variables]; + let mut b = vec![E::ScalarField::zero(); n_variables]; + let mut c = vec![E::ScalarField::zero(); n_variables]; + + a[0..n_instance_variables] + .copy_from_slice(&u[n_constraints..(n_instance_variables + n_constraints)]); + + for (i, u_i) in u.into_iter().enumerate().take(n_constraints) { + for &(ref coeff, index) in &matrices[0][i] { + a[index] += &(u_i * coeff); + } + for &(ref coeff, index) in &matrices[1][i] { + b[index] += &(u_i * coeff); + } + for &(ref coeff, index) in &matrices[2][i] { + c[index] += &(u_i * coeff); + } + } + + end_timer!(reduction_time); + + // Compute query densities + let non_zero_a = cfg_into_iter!(0..n_variables) + .map(|i| usize::from(!a[i].is_zero())) + .sum::(); + + let non_zero_b = cfg_into_iter!(0..n_variables) + .map(|i| usize::from(!b[i].is_zero())) + .sum::(); + + let (gamma_abc, l): (Vec<_>, Vec<_>) = cfg_iter!(a) + .zip(&b) + .zip(&c) + .enumerate() + .partition_map(|(i, ((a, b), c))| { + if i < n_instance_variables + || committed_variable_indices.contains(&(i - n_instance_variables)) + { + Either::Left((beta * a + alpha * b + c) * gamma_inverse) + } else { + Either::Right((beta * a + alpha * b + c) * delta_inverse) + } + }); + + drop(c); + + // Compute B window table + let g2_time = start_timer!(|| "Compute G2 table"); + let g2_table = BatchMulPreprocessing::new(g2_generator, non_zero_b); + end_timer!(g2_time); + + // Compute the B-query in G2 + let b_g2_time = start_timer!(|| format!("Calculate B G2 of size {}", b.len())); + let b_g2_query = g2_table.batch_mul(&b); + drop(g2_table); + end_timer!(b_g2_time); + + // Compute G window table + let g1_window_time = start_timer!(|| "Compute G1 window table"); + let num_scalars = non_zero_a + non_zero_b + n_variables + domain_size - 1; + let g1_table = BatchMulPreprocessing::new(g1_generator, num_scalars); + end_timer!(g1_window_time); + + // Generate the R1CS proving key + let proving_key_time = start_timer!(|| "Generate the R1CS proving key"); + + // Compute the A-query + let a_time = start_timer!(|| "Calculate A"); + let a_query = g1_table.batch_mul(&a); + drop(a); + end_timer!(a_time); + + // Compute the B-query in G1 + let b_g1_time = start_timer!(|| "Calculate B G1"); + let b_g1_query = g1_table.batch_mul(&b); + drop(b); + end_timer!(b_g1_time); + + // Compute the H-query + let h_time = start_timer!(|| "Calculate H"); + let h_scalars = cfg_into_iter!(0..domain_size - 1) + .map(|i| zt_over_delta * t.pow([i as u64])) + .collect::>(); + let h_query = g1_table.batch_mul(&h_scalars); + end_timer!(h_time); + + // Compute the L-query + let l_time = start_timer!(|| "Calculate L"); + let l_query = g1_table.batch_mul(&l); + drop(l); + end_timer!(l_time); + + end_timer!(proving_key_time); + + // Generate R1CS verification key + let verifying_key_time = start_timer!(|| "Generate the R1CS verification key"); + let mut gamma_abc_g1 = g1_table.batch_mul(&gamma_abc); + let gamma_abc_g1_part_2 = gamma_abc_g1.split_off(n_instance_variables); + drop(g1_table); + + end_timer!(verifying_key_time); + + let eta_gamma_inv_g1 = (g1_generator * (eta * gamma_inverse)).into_affine(); + + let eta_delta_inv_g1 = (g1_generator * (eta * delta_inverse)).into_affine(); + + // Setup public params for the Subspace Snark + let (link_ek, link_vk) = LinearSubspaceSNARK::generate_keys( + commitment_key.len() + 1, + |k| { + let mut p = cfg_iter!(commitment_key) + .zip(&k) + .flat_map(|(ck, u)| cfg_iter!(ck.borrow().g).map(move |i| *i * u)) + .chain( + cfg_iter!(commitment_key) + .zip(&k) + .map(|(ck, u)| ck.borrow().h * u), + ) + .collect::>(); + assert_eq!(p.len(), n_committed_variables + commitment_key.len()); + + cfg_iter_mut!(p) + .zip(&gamma_abc_g1_part_2) + .for_each(|(v, i)| { + *v += *i * k[commitment_key.len()]; + }); + p.push(eta_gamma_inv_g1 * k[commitment_key.len()]); + + p + }, + &mut rng, + ); + + end_timer!(setup_time); + + let alpha_g1 = (g1_generator * alpha).into_affine(); + let beta_g1 = (g1_generator * beta).into_affine(); + let beta_g2 = (g2_generator * beta).into_affine(); + let gamma_g2 = (g2_generator * gamma).into_affine(); + let delta_g1 = (g1_generator * delta).into_affine(); + let delta_g2 = (g2_generator * delta).into_affine(); + + Ok(( + ProverKey { + r1cs, + committed_variable_indices, + cc_pk: CCGroth16ProverKey { + alpha_g1, + beta_g1, + beta_g2, + delta_g1, + delta_g2, + eta_delta_inv_g1, + eta_gamma_inv_g1, + a_query, + b_g1_query, + b_g2_query, + h_query, + l_query, + gamma_abc_g1_cm: gamma_abc_g1_part_2, + }, + link_ek, + }, + VerifierKey { + cc_vk: CCGroth16VerifierKey { + #[cfg(feature = "evm")] + alpha_g1, + #[cfg(feature = "evm")] + beta_g2_neg: -beta_g2, + #[cfg(feature = "evm")] + gamma_g2_neg: -gamma_g2, + #[cfg(feature = "evm")] + delta_g2_neg: -delta_g2, + alpha_g1_beta_g2: E::pairing(alpha_g1, beta_g2), + gamma_g2_neg_pc: (-gamma_g2).into(), + delta_g2_neg_pc: (-delta_g2).into(), + gamma_abc_g1_pub: gamma_abc_g1, + }, + link_vk, + }, + )) + } + + fn prove( + pk: &Self::ProverKey, + x: &[Self::Field], + w: &[Self::Field], + o: &[Self::CommitmentOpening], + mut rng: impl RngCore, + ) -> Result { + let r = E::ScalarField::rand(&mut rng); + let s = E::ScalarField::rand(&mut rng); + let v = E::ScalarField::rand(&mut rng); + + let r1cs_config = pk.r1cs.config(); + let num_inputs = r1cs_config.n_public_inputs + 1; + let num_constraints = r1cs_config.n_constraints; + + let prover_time = start_timer!(|| "Groth16::Prover"); + + let assignment = [&[E::ScalarField::one()][..], x, w].concat(); + + let witness_map_time = start_timer!(|| "R1CS to QAP witness map"); + let h = QAP::witness_map_from_matrices::<_, GeneralEvaluationDomain<_>>( + pk.r1cs.matrices(), + num_inputs, + num_constraints, + &assignment, + )?; + + end_timer!(witness_map_time); + + let assignment_bigint = cfg_into_iter!(assignment) + .map(|s| s.into_bigint()) + .collect::>(); + + // Compute A + let a_acc_time = start_timer!(|| "Compute A"); + let g_a = pk.cc_pk.delta_g1 * r + + E::G1::msm_bigint(&pk.cc_pk.a_query, &assignment_bigint) + + pk.cc_pk.alpha_g1; + end_timer!(a_acc_time); + + // Compute B in G1 if needed + let b_g1_acc_time = start_timer!(|| "Compute B in G1"); + let g1_b = if !r.is_zero() { + pk.cc_pk.delta_g1 * s + + E::G1::msm_bigint(&pk.cc_pk.b_g1_query, &assignment_bigint) + + pk.cc_pk.beta_g1 + } else { + E::G1::zero() + }; + end_timer!(b_g1_acc_time); + + // Compute B in G2 + let b_g2_acc_time = start_timer!(|| "Compute B in G2"); + let g2_b = pk.cc_pk.delta_g2 * s + + E::G2::msm_bigint(&pk.cc_pk.b_g2_query, &assignment_bigint) + + pk.cc_pk.beta_g2; + + end_timer!(b_g2_acc_time); + + // Compute C + let c_time = start_timer!(|| "Compute C"); + let mut g_c = g_a * s; + g_c += g1_b * r; + g_c -= pk.cc_pk.delta_g1 * (r * s); + + let (witness_bigint, committed_bigint) = cfg_into_iter!(assignment_bigint) + .skip(num_inputs) + .enumerate() + .partition_map::, Vec<_>, _, _, _>(|(i, v)| { + if pk.committed_variable_indices.contains(&i) { + Either::Right(v) + } else { + Either::Left(v) + } + }); + g_c += E::G1::msm_bigint(&pk.cc_pk.l_query, &witness_bigint); + drop(witness_bigint); + + let h_bigint = cfg_into_iter!(h) + .map(|s| s.into_bigint()) + .collect::>(); + g_c += E::G1::msm_bigint(&pk.cc_pk.h_query, &h_bigint); + drop(h_bigint); + + g_c -= pk.cc_pk.eta_delta_inv_g1 * v; + end_timer!(c_time); + + // Compute D + let d_acc_time = start_timer!(|| "Compute D"); + let g_d = E::G1::msm_bigint(&pk.cc_pk.gamma_abc_g1_cm, &committed_bigint) + + pk.cc_pk.eta_gamma_inv_g1 * v; + end_timer!(d_acc_time); + + let link_time = start_timer!(|| "Compute CP_{link}"); + let mut ss_snark_witness = committed_bigint; + ss_snark_witness.extend(o.iter().chain([&v]).map(|i| i.into_bigint())); + let link_pi = LinearSubspaceSNARK::prove(&pk.link_ek, &ss_snark_witness); + end_timer!(link_time); + + end_timer!(prover_time); + + Ok(Proof { + groth16_proof: Groth16Proof { + a: g_a.into_affine(), + b: g2_b.into_affine(), + c: g_c.into_affine(), + }, + + d: g_d.into_affine(), + + link_pi, + }) + } + + fn verify( + vk: &Self::VerifierKey, + x: &[Self::Field], + c: &[Self::Commitment], + proof: &Self::Proof, + ) -> Result<(), Self::Error> { + let mut g_ic = vk.cc_vk.gamma_abc_g1_pub[0].into_group(); + for (i, b) in x.iter().zip(vk.cc_vk.gamma_abc_g1_pub.iter().skip(1)) { + g_ic += *b * i; + } + + if E::multi_pairing( + [ + E::G1Prepared::from(proof.groth16_proof.a), + (proof.d + g_ic).into_affine().into(), + proof.groth16_proof.c.into(), + ], + [ + proof.groth16_proof.b.into(), + vk.cc_vk.gamma_g2_neg_pc.clone(), + vk.cc_vk.delta_g2_neg_pc.clone(), + ], + ) != vk.cc_vk.alpha_g1_beta_g2 + { + return Err(SynthesisError::Unsatisfiable); + } + + if !LinearSubspaceSNARK::verify(&vk.link_vk, &[c, &[proof.d][..]].concat(), &proof.link_pi) + { + return Err(SynthesisError::Unsatisfiable); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Bn254; + use ark_relations::{ + gr1cs::{ConstraintSynthesizer, ConstraintSystemRef}, + lc, + }; + use ark_std::rand::thread_rng; + use sonobe_primitives::{ + algebra::group::SonobeCurve, + circuits::{ArithExtractor, AssignmentsExtractor}, + }; + + use super::*; + + /// A toy circuit enforcing `(a + b + c) * (d + e + f) = g`. + pub(crate) struct ToyCircuit { + pub a: F, + pub b: F, + pub c: F, + pub d: F, + pub e: F, + pub f: F, + } + + impl ConstraintSynthesizer for ToyCircuit { + fn generate_constraints( + self, + cs: ConstraintSystemRef, + ) -> Result<(), SynthesisError> { + let d = cs.new_witness_variable(|| Ok(self.d))?; + let a = cs.new_witness_variable(|| Ok(self.a))?; + let b = cs.new_witness_variable(|| Ok(self.b))?; + let e = cs.new_witness_variable(|| Ok(self.e))?; + let c = cs.new_witness_variable(|| Ok(self.c))?; + let g = cs.new_input_variable(|| { + Ok((self.a + self.b + self.c) * (self.d + self.e + self.f)) + })?; + let f = cs.new_witness_variable(|| Ok(self.f))?; + + cs.enforce_r1cs_constraint(|| lc![a, b, c], || lc![d, e, f], || lc![g])?; + + Ok(()) + } + } + + pub(crate) fn toy_keygen< + E: Pairing, + >( + ck_sizes: &[usize], + mut rng: impl RngCore, + ) -> (ProverKey, VerifierKey, Vec>) { + let cks: Vec<_> = ck_sizes + .iter() + .map(|&n| { + let g: Vec<_> = (0..n).map(|_| E::G1Affine::rand(&mut rng)).collect(); + PedersenKey { + g, + h: E::G1Affine::rand(&mut rng), + } + }) + .collect(); + let generators = cks.iter().map(|ck| [&ck.g[..], &[ck.h]].concat()).collect(); + + let mut cs = ArithExtractor::new(); + cs.execute_synthesizer(ToyCircuit:: { + a: Default::default(), + b: Default::default(), + c: Default::default(), + d: Default::default(), + e: Default::default(), + f: Default::default(), + }) + .unwrap(); + let (pk, vk) = LegoGroth16::::generate_keys( + (cs.arith().unwrap(), UsizeSet::from_iter(vec![0, 3, 5])), + &cks, + rng, + ) + .unwrap(); + (pk, vk, generators) + } + + pub(crate) fn toy_prove< + E: Pairing, + >( + pk: &ProverKey, + generators: &[Vec], + ck_sizes: &[usize], + mut rng: impl RngCore, + ) -> (E::ScalarField, Vec, Proof) { + let [a, b, c, d, e, f] = [(); 6].map(|_| E::ScalarField::rand(&mut rng)); + + let mut cs = AssignmentsExtractor::new(); + cs.execute_synthesizer(ToyCircuit { a, b, c, d, e, f }) + .unwrap(); + let assignments = cs.assignments().unwrap(); + + let committed = [d, e, f]; + let mut next = 0; + let mut o = vec![]; + let commitments = ck_sizes + .iter() + .zip(generators) + .map(|(&n, gens)| { + let opening = E::ScalarField::rand(&mut rng); + o.push(opening); + let scalars: Vec<_> = committed[next..next + n] + .iter() + .copied() + .chain([opening]) + .collect(); + next += n; + E::G1::msm_unchecked(gens, &scalars).into_affine() + }) + .collect(); + + let proof = LegoGroth16::::prove(pk, &assignments.public, &assignments.private, &o, rng) + .unwrap(); + + ((a + b + c) * (d + e + f), commitments, proof) + } + + fn test_legogroth16_opt< + E: Pairing, + >( + ck_sizes: &[usize], + mut rng: impl RngCore, + ) { + let (pk, vk, generators) = toy_keygen::(ck_sizes, &mut rng); + + let (g, cm, proof) = toy_prove::(&pk, &generators, ck_sizes, &mut rng); + assert!(LegoGroth16::::verify(&vk, &[g], &cm, &proof).is_ok()); + assert!(LegoGroth16::::verify(&vk, &[g + E::ScalarField::ONE], &cm, &proof).is_err()); + } + + #[test] + fn test_legogroth16() { + let mut rng = thread_rng(); + // Three commitment layouts for the committed witnesses `d, e, f`. + test_legogroth16_opt::(&[1, 1, 1], &mut rng); + test_legogroth16_opt::(&[2, 1], &mut rng); + test_legogroth16_opt::(&[3], &mut rng); + } +} diff --git a/crates/snarks/src/cp/mod.rs b/crates/snarks/src/cp/mod.rs new file mode 100644 index 000000000..1cd406cef --- /dev/null +++ b/crates/snarks/src/cp/mod.rs @@ -0,0 +1,42 @@ +use ark_ff::Field; +use ark_std::{borrow::Borrow, rand::RngCore}; + +pub mod legogroth16; + +pub trait CPSNARK { + type Field: Field; + + type Relation; + + type CommitmentKey; + type CommitmentOpening; + type Commitment; + + type ProverKey; + type VerifierKey; + + type Proof; + + type Error; + + fn generate_keys( + relation: Self::Relation, + commitment_key: &[impl Borrow + Sync], + rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Self::Error>; + + fn prove( + pk: &Self::ProverKey, + x: &[Self::Field], + w: &[Self::Field], + o: &[Self::CommitmentOpening], + rng: impl RngCore, + ) -> Result; + + fn verify( + vk: &Self::VerifierKey, + x: &[Self::Field], + c: &[Self::Commitment], + proof: &Self::Proof, + ) -> Result<(), Self::Error>; +} diff --git a/crates/snarks/src/lib.rs b/crates/snarks/src/lib.rs new file mode 100644 index 000000000..bf50b3cc7 --- /dev/null +++ b/crates/snarks/src/lib.rs @@ -0,0 +1,4 @@ +#![warn(missing_docs)] + +pub mod cp; +pub mod linear_subspace; diff --git a/crates/snarks/src/linear_subspace/mod.rs b/crates/snarks/src/linear_subspace/mod.rs new file mode 100644 index 000000000..c5e223896 --- /dev/null +++ b/crates/snarks/src/linear_subspace/mod.rs @@ -0,0 +1,204 @@ +use ark_ec::{CurveGroup, ScalarMul, VariableBaseMSM, pairing::Pairing}; +use ark_ff::{PrimeField, UniformRand, Zero}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::rand::Rng; + +#[derive(Clone, Default, PartialEq, Debug, CanonicalSerialize, CanonicalDeserialize)] +pub struct ProverKey { + pub p: Vec, +} + +#[derive(Clone, Default, PartialEq, Debug, CanonicalSerialize, CanonicalDeserialize)] +pub struct VerifierKey { + pub c: Vec, + pub a_neg: E::G2Affine, +} + +pub struct LinearSubspaceSNARK {} + +impl LinearSubspaceSNARK { + pub fn generate_keys( + n_rows: usize, + mvm: impl Fn(Vec) -> Vec, + rng: &mut impl Rng, + ) -> (ProverKey, VerifierKey) { + let k = (0..n_rows) + .map(|_| E::ScalarField::rand(rng)) + .collect::>(); + let a = E::G2::rand(rng); + let c = a.batch_mul(&k); + + ( + ProverKey { + p: E::G1::normalize_batch(&mvm(k)), + }, + VerifierKey { + c, + a_neg: -a.into_affine(), + }, + ) + } + + pub fn prove( + ek: &ProverKey, + w: &[::BigInt], + ) -> E::G1Affine { + assert_eq!(ek.p.len(), w.len()); + E::G1::msm_bigint(&ek.p, w).into_affine() + } + + pub fn verify(vk: &VerifierKey, x: &[E::G1Affine], pi: &E::G1Affine) -> bool { + // `E::multi_pairing` internally uses `zip_eq` + E::multi_pairing([x, &[*pi][..]].concat(), [&vk.c, &[vk.a_neg][..]].concat()).is_zero() + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Bn254, Fr, G1Affine}; + use ark_ec::AffineRepr; + use ark_ff::One; + use ark_std::rand::thread_rng; + + use super::*; + + fn test_mvm(m: &[Vec]) -> impl Fn(Vec) -> Vec { + move |k| { + (0..m[0].len()) + .map(|i| k.iter().zip(m).map(|(u, v)| v[i] * u).sum::()) + .collect::>() + } + } + + #[test] + fn test_basic() { + // Prove knowledge of all `x_i` in `y = \sum_i g_i * x_i` + let mut rng = thread_rng(); + let g1 = G1Affine::rand(&mut rng); + + let m = vec![vec![g1, g1]]; + + let x = vec![Fr::one().into_bigint(), Fr::zero().into_bigint()]; + + let x_bad = vec![Fr::one().into_bigint(), Fr::one().into_bigint()]; + + let y: Vec = vec![g1]; + + let (ek, vk) = LinearSubspaceSNARK::generate_keys::(m.len(), test_mvm(&m), &mut rng); + + let pi = LinearSubspaceSNARK::prove(&ek, &x); + let pi_bad = LinearSubspaceSNARK::prove(&ek, &x_bad); + + assert!(LinearSubspaceSNARK::verify(&vk, &y, &pi)); + assert!(!LinearSubspaceSNARK::verify(&vk, &y, &pi_bad)); + } + + #[test] + fn test_basic_1() { + // Prove knowledge of all `w_i` in `y = \sum_i h_i * w_i` + let mut rng = thread_rng(); + + let h1 = G1Affine::rand(&mut rng); + let h2 = G1Affine::rand(&mut rng); + let m = vec![vec![h1, h2]]; + + let two = Fr::one() + Fr::one(); + let three = Fr::one() + two; + + // Correct witness + let w = vec![two.into_bigint(), three.into_bigint()]; + // Incorrect witness + let w_bad = vec![Fr::one().into_bigint(), Fr::one().into_bigint()]; + + // y is a Pedersen-like commitment to `two` and `three` and bases `h1` and `h2`, + // i.e `y = h1 * two + h2 * three` + let y: Vec = vec![ + (h1.mul_bigint(two.into_bigint()) + h2.mul_bigint(three.into_bigint())).into_affine(), + ]; + + let (ek, vk) = LinearSubspaceSNARK::generate_keys::(m.len(), test_mvm(&m), &mut rng); + + let pi = LinearSubspaceSNARK::prove(&ek, &w); + let pi_bad = LinearSubspaceSNARK::prove(&ek, &w_bad); + + assert!(LinearSubspaceSNARK::verify(&vk, &y, &pi)); + assert!(!LinearSubspaceSNARK::verify(&vk, &y, &pi_bad)); + } + + #[test] + fn test_same_value_different_bases() { + // Given `bases1 = [h1, h2]` and `bases2 = [h3, h4]`, prove knowledge of `x1, x2 + // x3` in `y0 = h1 * x0 + h2 * x2` and `y1 = h3 * x1 + h4 * x2` + + let mut rng = thread_rng(); + + let bases1 = [G1Affine::rand(&mut rng), G1Affine::rand(&mut rng)]; + let bases2 = [G1Affine::rand(&mut rng), G1Affine::rand(&mut rng)]; + let m = vec![ + vec![bases1[0], G1Affine::zero(), bases1[1]], + vec![G1Affine::zero(), bases2[0], bases2[1]], + ]; + + let w = vec![ + Fr::rand(&mut rng).into_bigint(), + Fr::rand(&mut rng).into_bigint(), + Fr::rand(&mut rng).into_bigint(), + ]; + + let x: Vec = vec![ + (bases1[0].mul_bigint(w[0]) + bases1[1].mul_bigint(w[2])).into_affine(), + (bases2[0].mul_bigint(w[1]) + bases2[1].mul_bigint(w[2])).into_affine(), + ]; + + let (ek, vk) = LinearSubspaceSNARK::generate_keys::(m.len(), test_mvm(&m), &mut rng); + + let pi = LinearSubspaceSNARK::prove(&ek, &w); + + assert!(LinearSubspaceSNARK::verify(&vk, &x, &pi)); + } + + #[test] + fn test_some_vals_equal() { + // Given `bases1 = [h1, h2, h3]` and `bases2 = [h4, h5, h6]`, prove knowledge of + // `x1, x2 x3, x4` in `y0 = h1 * x0 + h2 * x2 + h3 * x3` and `y1 = h4 * x1 + h5 + // * x2 + h6 * x4` + + let mut rng = thread_rng(); + + let bases1 = [ + G1Affine::rand(&mut rng), + G1Affine::rand(&mut rng), + G1Affine::rand(&mut rng), + ]; + let bases2 = [ + G1Affine::rand(&mut rng), + G1Affine::rand(&mut rng), + G1Affine::rand(&mut rng), + ]; + + let m = vec![ + vec![bases1[0], bases1[1], bases1[2], G1Affine::zero()], + vec![bases2[0], bases2[1], G1Affine::zero(), bases2[2]], + ]; + + let w = vec![ + Fr::rand(&mut rng).into_bigint(), + Fr::rand(&mut rng).into_bigint(), + Fr::rand(&mut rng).into_bigint(), + Fr::rand(&mut rng).into_bigint(), + ]; + + let x: Vec = vec![ + (bases1[0].mul_bigint(w[0]) + bases1[1].mul_bigint(w[1]) + bases1[2].mul_bigint(w[2])) + .into_affine(), + (bases2[0].mul_bigint(w[0]) + bases2[1].mul_bigint(w[1]) + bases2[2].mul_bigint(w[3])) + .into_affine(), + ]; + + let (ek, vk) = LinearSubspaceSNARK::generate_keys::(m.len(), test_mvm(&m), &mut rng); + + let pi = LinearSubspaceSNARK::prove(&ek, &w); + + assert!(LinearSubspaceSNARK::verify(&vk, &x, &pi)); + } +} diff --git a/crates/snarks/templates/legogroth16.sol.askama b/crates/snarks/templates/legogroth16.sol.askama new file mode 100644 index 000000000..33be31f23 --- /dev/null +++ b/crates/snarks/templates/legogroth16.sol.askama @@ -0,0 +1,139 @@ +{%- let numCommitments = vk.link_vk.c.len() - 1 -%} +{%- let numPublicInputs = vk.cc_vk.gamma_abc_g1_pub.len() - 1 %} +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.35; + +contract LegoGroth16Verifier { + error ProofInvalid(); + error NonCanonicalInput(); + + // BN254 scalar field order + uint256 constant R = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + uint256 constant ALPHA_X = {{ vk.cc_vk.alpha_g1.x().unwrap() }}; + uint256 constant ALPHA_Y = {{ vk.cc_vk.alpha_g1.y().unwrap() }}; + + uint256 constant BETA_NEG_X_0 = {{ vk.cc_vk.beta_g2_neg.x().unwrap().c0 }}; + uint256 constant BETA_NEG_X_1 = {{ vk.cc_vk.beta_g2_neg.x().unwrap().c1 }}; + uint256 constant BETA_NEG_Y_0 = {{ vk.cc_vk.beta_g2_neg.y().unwrap().c0 }}; + uint256 constant BETA_NEG_Y_1 = {{ vk.cc_vk.beta_g2_neg.y().unwrap().c1 }}; + + uint256 constant GAMMA_NEG_X_0 = {{ vk.cc_vk.gamma_g2_neg.x().unwrap().c0 }}; + uint256 constant GAMMA_NEG_X_1 = {{ vk.cc_vk.gamma_g2_neg.x().unwrap().c1 }}; + uint256 constant GAMMA_NEG_Y_0 = {{ vk.cc_vk.gamma_g2_neg.y().unwrap().c0 }}; + uint256 constant GAMMA_NEG_Y_1 = {{ vk.cc_vk.gamma_g2_neg.y().unwrap().c1 }}; + + uint256 constant DELTA_NEG_X_0 = {{ vk.cc_vk.delta_g2_neg.x().unwrap().c0 }}; + uint256 constant DELTA_NEG_X_1 = {{ vk.cc_vk.delta_g2_neg.x().unwrap().c1 }}; + uint256 constant DELTA_NEG_Y_0 = {{ vk.cc_vk.delta_g2_neg.y().unwrap().c0 }}; + uint256 constant DELTA_NEG_Y_1 = {{ vk.cc_vk.delta_g2_neg.y().unwrap().c1 }}; + + {% for i in 0..vk.cc_vk.gamma_abc_g1_pub.len() %} + uint256 constant GAMMA_ABC_{{ i }}_X = {{ vk.cc_vk.gamma_abc_g1_pub[i].x().unwrap() }}; + uint256 constant GAMMA_ABC_{{ i }}_Y = {{ vk.cc_vk.gamma_abc_g1_pub[i].y().unwrap() }}; + {%- endfor %} + + {% for i in 0..vk.link_vk.c.len() %} + uint256 constant LINK_C_{{ i }}_X_0 = {{ vk.link_vk.c[i].x().unwrap().c0 }}; + uint256 constant LINK_C_{{ i }}_X_1 = {{ vk.link_vk.c[i].x().unwrap().c1 }}; + uint256 constant LINK_C_{{ i }}_Y_0 = {{ vk.link_vk.c[i].y().unwrap().c0 }}; + uint256 constant LINK_C_{{ i }}_Y_1 = {{ vk.link_vk.c[i].y().unwrap().c1 }}; + {%- endfor %} + + uint256 constant LINK_A_NEG_X_0 = {{ vk.link_vk.a_neg.x().unwrap().c0 }}; + uint256 constant LINK_A_NEG_X_1 = {{ vk.link_vk.a_neg.x().unwrap().c1 }}; + uint256 constant LINK_A_NEG_Y_0 = {{ vk.link_vk.a_neg.y().unwrap().c0 }}; + uint256 constant LINK_A_NEG_Y_1 = {{ vk.link_vk.a_neg.y().unwrap().c1 }}; + + function verifyProof( + uint256[{{ numPublicInputs }}] calldata x, + uint256[{{ numCommitments * 2 }}] calldata c, + uint256[12] calldata proof + ) public view { + for (uint256 k = 0; k < {{ numPublicInputs }}; k++) { + if (x[k] >= R) { + revert NonCanonicalInput(); + } + } + + bool ok = true; + uint256 pub_x; + uint256 pub_y; + + assembly ("memory-safe") { + let f := mload(0x40) + + // Public input MSM + mstore(f, GAMMA_ABC_0_X) + mstore(add(f, 0x20), GAMMA_ABC_0_Y) + {% for i in 1..vk.cc_vk.gamma_abc_g1_pub.len() %} + mstore(add(f, 0x40), GAMMA_ABC_{{ i }}_X) + mstore(add(f, 0x60), GAMMA_ABC_{{ i }}_Y) + mstore(add(f, 0x80), calldataload(add(x, {{ (i - 1) * 0x20 }}))) + ok := and(ok, staticcall(gas(), 0x07, add(f, 0x40), 0x60, add(f, 0x40), 0x40)) + ok := and(ok, staticcall(gas(), 0x06, f, 0x80, f, 0x40)) + {%- endfor %} + + pub_x := mload(f) + pub_y := mload(add(f, 0x20)) + + // A, B + calldatacopy(f, proof, 0xc0) + + // C, -δ + calldatacopy(add(f, 0xc0), add(proof, 0xc0), 0x40) + mstore(add(f, 0x100), DELTA_NEG_X_1) + mstore(add(f, 0x120), DELTA_NEG_X_0) + mstore(add(f, 0x140), DELTA_NEG_Y_1) + mstore(add(f, 0x160), DELTA_NEG_Y_0) + + // α, -β + mstore(add(f, 0x180), ALPHA_X) + mstore(add(f, 0x1a0), ALPHA_Y) + mstore(add(f, 0x1c0), BETA_NEG_X_1) + mstore(add(f, 0x1e0), BETA_NEG_X_0) + mstore(add(f, 0x200), BETA_NEG_Y_1) + mstore(add(f, 0x220), BETA_NEG_Y_0) + + // pub + D, -γ + mstore(add(f, 0x240), pub_x) + mstore(add(f, 0x260), pub_y) + calldatacopy(add(f, 0x280), add(proof, 0x100), 0x40) + ok := and(ok, staticcall(gas(), 0x06, add(f, 0x240), 0x80, add(f, 0x240), 0x40)) + mstore(add(f, 0x280), GAMMA_NEG_X_1) + mstore(add(f, 0x2a0), GAMMA_NEG_X_0) + mstore(add(f, 0x2c0), GAMMA_NEG_Y_1) + mstore(add(f, 0x2e0), GAMMA_NEG_Y_0) + + ok := and(ok, staticcall(gas(), 0x08, f, 0x300, f, 0x20)) + ok := and(ok, mload(f)) + + // c || D, Link_C + {% for i in 0..numCommitments %} + calldatacopy(add(f, {{ i * 6 * 0x20 }}), add(c, {{ i * 2 * 0x20 }}), 0x40) + mstore(add(f, {{ i * 6 * 0x20 + 0x40 }}), LINK_C_{{ i }}_X_1) + mstore(add(f, {{ i * 6 * 0x20 + 0x60 }}), LINK_C_{{ i }}_X_0) + mstore(add(f, {{ i * 6 * 0x20 + 0x80 }}), LINK_C_{{ i }}_Y_1) + mstore(add(f, {{ i * 6 * 0x20 + 0xa0 }}), LINK_C_{{ i }}_Y_0) + {%- endfor %} + calldatacopy(add(f, {{ numCommitments * 6 * 0x20 }}), add(proof, 0x100), 0x40) + mstore(add(f, {{ numCommitments * 6 * 0x20 + 0x40 }}), LINK_C_{{ numCommitments }}_X_1) + mstore(add(f, {{ numCommitments * 6 * 0x20 + 0x60 }}), LINK_C_{{ numCommitments }}_X_0) + mstore(add(f, {{ numCommitments * 6 * 0x20 + 0x80 }}), LINK_C_{{ numCommitments }}_Y_1) + mstore(add(f, {{ numCommitments * 6 * 0x20 + 0xa0 }}), LINK_C_{{ numCommitments }}_Y_0) + // Link_pi, -Link_A + calldatacopy(add(f, {{ (numCommitments + 1) * 6 * 0x20 }}), add(proof, 0x140), 0x40) + mstore(add(f, {{ (numCommitments + 1) * 6 * 0x20 + 0x40 }}), LINK_A_NEG_X_1) + mstore(add(f, {{ (numCommitments + 1) * 6 * 0x20 + 0x60 }}), LINK_A_NEG_X_0) + mstore(add(f, {{ (numCommitments + 1) * 6 * 0x20 + 0x80 }}), LINK_A_NEG_Y_1) + mstore(add(f, {{ (numCommitments + 1) * 6 * 0x20 + 0xa0 }}), LINK_A_NEG_Y_0) + + ok := and(ok, staticcall(gas(), 0x08, f, {{ (numCommitments + 2) * 6 * 0x20 }}, f, 0x20)) + ok := and(ok, mload(f)) + } + if (!ok) { + revert ProofInvalid(); + } + } +} \ No newline at end of file diff --git a/docs/Terminology.md b/docs/Terminology.md new file mode 100644 index 000000000..b6e736017 --- /dev/null +++ b/docs/Terminology.md @@ -0,0 +1,50 @@ +## Disambiguation of "Native" + +In cryptographic proof systems, the term "native" can have multiple interpretations depending on the specific context of discussion. + +### Context 1: Native vs Emulated + +When referring to "native field / curve" and "emulated (non-native) field / curve," "native" denotes that the field or curve can be directly represented within the arithmetic circuit of the proof system. +More specifically, "native" in native field means that the field is the same as the circuit's constraint field (i.e., the field over which the circuit is defined). +Similarly, a "native" curve is one whose base field (i.e., the field over which the curve is defined, which is also the field that a point's coordinates belong to) matches the circuit's constraint field. + +In contrast, "emulated" or "non-native" fields and curves are those that cannot be directly represented in the circuit's constraint field and thus require special handling (a.k.a. emulation) within the circuit. + +> [!TIP] +> A side note irrelevant to the main discussion is that the boundary between "native" and "emulated" is not very clear-cut. +> +> For instance, as long as the foundamental element of the circuit is not a curve point (which is the case for all current constraint systems), even a native curve is "emulated" in some sense because curve points need to be encoded as multiple elements in the constraint field and curve operations need to be broken down into field operations. +> One can further argue that, if we regard such an "emulation" as a native representation of the curve, then why not also consider emulated fields as native as well, since they are also encoded as multiple elements in the constraint field. +> +> In Sonobe, we distinguish "native" and "emulated" based on whether the in-circuit representation is the preferred or most efficient form for the given field or curve. For a field element, the preferred representation is a single element in the constraint field, while for a curve point, it is a tuple of elements in the constraint field representing the coordinates, but each coordinate itself is not further decomposed. Consequently, if the circuit is able to achieve these preferred representations, we classify the field or curve as "native"; otherwise, it is deemed "emulated." + +### Context 2: Native vs In-Circuit + +Another common usage of "native" is to distinguish between values and operations built in the host programming language (e.g., Rust) and those defined in the arithmetic circuit of the proof system. In the former case, we refer to them as "native"/"out-of-circuit", while in the latter case, we call them "in-circuit". + +### Proposed New Terminology + +It is unlikely for experienced practitioners to confuse the two contexts above when "native" is used, as the context usually makes it clear which meaning is intended. +However, to ensure everyone is on the same page and to avoid any potential mental overhead in interpreting "native" correctly, we propose adopting more specific terminology for each context. + +- For Context 1, prefer _Canonical_ vs _Emulated_. + + **Justification**: _Canonical_ is not a standard term in the literature and is coined for use in Sonobe. However, it intuitively conveys the idea of being the standard or preferred representation within the circuit. +- For Context 2: + - When referring to something that holds data: + - Prefer _Value_ vs _Variable_. Further qualify them as _Out-of-Circuit Value_ and _In-Circuit Variable_ if necessary. + - Neutral terms such as _Data_, _Element_, _Key_, _Instance_, _Witness_, etc., are also acceptable when solely focusing on the in-circuit or out-of-circuit context. + + **Justification**: The use of _Value_ and _Variable_ aligns with existing conventions, as these terms are widely used in the arkworks codebase. + - When referring to something that performs computation: + - Prefer _Widget_ vs _Gadget_. Further qualify them as _Out-of-Circuit Widget_ and _In-Circuit Gadget_ if necessary. + - Neutral terms such as _Algorithm_, _Procedure_, _Function_, _Method_, etc., are also acceptable when solely focusing on the in-circuit or out-of-circuit context. + + **Justification**: _Gadget_ is already a standard term for in-circuit computation modules or utilities. + + _Widget_ is invented by us to suggest a computational component that operates outside the circuit while maintaining a consistent and visually / phonetically appealing naming scheme. + + Furthermore, searching for "widget vs gadget" yields results that align with our intended meanings. For instance, [this article](https://www.thoughtco.com/widget-vs-gadget-3486689) suggests that in web development, "widgets work on multiple platforms, but gadgets are usually limited to specific devices or systems." This distinction resonates with our usage, where widgets operate in the general-purpose host environment, while gadgets are specialized for the circuit environment. + +The proposed terminology is used throughout the Sonobe documentation and codebase. +For contributions, we recommend doing so as well to enhance clarity and reduce ambiguity. However, in casual discussions / issue reports, it is fine to use "native" for both contexts. \ No newline at end of file diff --git a/examples/circom_full_flow.rs b/examples/circom_full_flow.rs deleted file mode 100644 index c54c6a964..000000000 --- a/examples/circom_full_flow.rs +++ /dev/null @@ -1,169 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] -/// -/// This example performs the full flow: -/// - define the circuit to be folded -/// - fold the circuit with Nova+CycleFold's IVC -/// - generate a DeciderEthCircuit final proof -/// - generate the Solidity contract that verifies the proof -/// - verify the proof in the EVM -/// -use ark_bn254::{Bn254, Fr, G1Projective as G1}; - -use ark_groth16::Groth16; -use ark_grumpkin::Projective as G2; - -use std::path::PathBuf; -use std::time::Instant; - -use experimental_frontends::{circom::CircomFCircuit, utils::VecF}; -use folding_schemes::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - folding::{ - nova::{decider_eth::Decider as DeciderEth, Nova, PreprocessorParam}, - traits::CommittedInstanceOps, - }, - frontend::FCircuit, - transcript::poseidon::poseidon_canonical_config, - Decider, Error, FoldingScheme, -}; -use solidity_verifiers::calldata::{ - prepare_calldata_for_nova_cyclefold_verifier, NovaVerificationMode, -}; -use solidity_verifiers::{ - evm::{compile_solidity, Evm}, - verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, - NovaCycleFoldVerifierKey, -}; - -fn main() -> Result<(), Error> { - // set the initial state - let z_0 = vec![Fr::from(3_u32)]; - - // set the external inputs to be used at each step of the IVC, it has length of 10 since this - // is the number of steps that we will do - let external_inputs = vec![ - vec![Fr::from(6u32), Fr::from(7u32)], - vec![Fr::from(8u32), Fr::from(9u32)], - vec![Fr::from(10u32), Fr::from(11u32)], - vec![Fr::from(12u32), Fr::from(13u32)], - vec![Fr::from(14u32), Fr::from(15u32)], - vec![Fr::from(6u32), Fr::from(7u32)], - vec![Fr::from(8u32), Fr::from(9u32)], - vec![Fr::from(10u32), Fr::from(11u32)], - vec![Fr::from(12u32), Fr::from(13u32)], - vec![Fr::from(14u32), Fr::from(15u32)], - ]; - - // initialize the Circom circuit - let r1cs_path = - PathBuf::from("./experimental-frontends/src/circom/test_folder/with_external_inputs.r1cs"); - let wasm_path = PathBuf::from( - "./experimental-frontends/src/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm", - ); - - let f_circuit_params = (r1cs_path.into(), wasm_path.into()); - - const STATE_LEN: usize = 1; // state len = 1, external - const EXT_INP_LEN: usize = 2; // external inputs len = 2 - let f_circuit = CircomFCircuit::::new(f_circuit_params)?; - - pub type N = Nova< - G1, - G2, - CircomFCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - pub type D = DeciderEth< - G1, - G2, - CircomFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, - N, - >; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = ark_std::rand::rngs::OsRng; - - // prepare the Nova prover & verifier params - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params.clone(), f_circuit.state_len()))?; - - // initialize the folding scheme engine, in our case we use Nova - let mut nova = N::init(&nova_params, f_circuit.clone(), z_0)?; - - // run n steps of the folding iteration - for (i, external_inputs_at_step) in external_inputs.iter().enumerate() { - let start = Instant::now(); - nova.prove_step(rng, VecF(external_inputs_at_step.clone()), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - - // verify the last IVC proof - let ivc_proof = nova.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("generated Decider proof: {:?}", start.elapsed()); - - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider proof verification: {}", verified); - - // Now, let's generate the Solidity code that verifies this Decider final proof - let calldata: Vec = prepare_calldata_for_nova_cyclefold_verifier( - NovaVerificationMode::Explicit, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i, - &nova.u_i, - &proof, - )?; - - // prepare the setup params for the solidity verifier - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, f_circuit.state_len())); - - // generate the solidity code - let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk); - - // verify the proof against the solidity code in the EVM - let nova_cyclefold_verifier_bytecode = compile_solidity(&decider_solidity_code, "NovaDecider"); - let mut evm = Evm::default(); - let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // save smart contract and the calldata - println!("storing nova-verifier.sol and the calldata into files"); - use std::fs; - fs::write( - "./examples/nova-verifier.sol", - decider_solidity_code.clone(), - )?; - fs::write("./examples/solidity-calldata.calldata", calldata.clone())?; - let s = solidity_verifiers::calldata::get_formatted_calldata(calldata.clone()); - fs::write("./examples/solidity-calldata.inputs", s.join(",\n")).expect(""); - Ok(()) -} diff --git a/examples/external_inputs.rs b/examples/external_inputs.rs deleted file mode 100644 index f2f7dbb87..000000000 --- a/examples/external_inputs.rs +++ /dev/null @@ -1,209 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] - -use ark_bn254::{Bn254, Fr, G1Projective as Projective}; -use ark_crypto_primitives::{ - crh::{ - poseidon::constraints::{CRHGadget, CRHParametersVar}, - CRHSchemeGadget, - }, - sponge::{poseidon::PoseidonConfig, Absorb}, -}; -use ark_ff::PrimeField; -use ark_grumpkin::Projective as Projective2; -use ark_r1cs_std::alloc::AllocVar; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use core::marker::PhantomData; -use std::time::Instant; - -use folding_schemes::commitment::{kzg::KZG, pedersen::Pedersen}; -use folding_schemes::folding::nova::{Nova, PreprocessorParam}; -use folding_schemes::frontend::FCircuit; -use folding_schemes::transcript::poseidon::poseidon_canonical_config; -use folding_schemes::{Error, FoldingScheme}; - -/// This is the circuit that we want to fold, it implements the FCircuit trait. The parameter z_i -/// denotes the current state which contains 1 element, and z_{i+1} denotes the next state which we -/// get by applying the step. -/// -/// In this example we set the state to be the previous state together with an external input, and -/// the new state is an array which contains the new state. -/// -/// This is useful for example if we want to fold multiple verifications of signatures, where the -/// circuit F checks the signature and is folded for each of the signatures and public keys. To -/// keep things simpler, the following example does not verify signatures but does a similar -/// approach with a chain of hashes, where each iteration hashes the previous step output (z_i) -/// together with an external input (w_i). -/// -/// w_1 w_2 w_3 w_4 -/// │ │ │ │ -/// ▼ ▼ ▼ ▼ -/// ┌─┐ ┌─┐ ┌─┐ ┌─┐ -/// ─────►│F├────►│F├────►│F├────►│F├────► -/// z_1 └─┘ z_2 └─┘ z_3 └─┘ z_4 └─┘ z_5 -/// -/// -/// where each F is: -/// w_i -/// │ ┌────────────────────┐ -/// │ │FCircuit │ -/// │ │ │ -/// └────►│ h =Hash(z_i[0],w_i)│ -/// ────────►│ │ ├───────► -/// z_i │ └──►z_{i+1}=[h] │ z_{i+1} -/// │ │ -/// └────────────────────┘ -/// -/// where each w_i value is set at the external_inputs array. -/// -/// The last state z_i is used together with the external input w_i as inputs to compute the new -/// state z_{i+1}. -#[derive(Clone, Debug)] -pub struct ExternalInputsCircuit -where - F: Absorb, -{ - _f: PhantomData, - poseidon_config: PoseidonConfig, -} -impl FCircuit for ExternalInputsCircuit -where - F: Absorb, -{ - type Params = PoseidonConfig; - type ExternalInputs = [F; 1]; - type ExternalInputsVar = [FpVar; 1]; - - fn new(params: Self::Params) -> Result { - Ok(Self { - _f: PhantomData, - poseidon_config: params, - }) - } - fn state_len(&self) -> usize { - 1 - } - /// generates the constraints and returns the next state value for the step of F for the given - /// z_i and external_inputs - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let crh_params = - CRHParametersVar::::new_constant(cs.clone(), self.poseidon_config.clone())?; - let hash_input: [FpVar; 2] = [z_i[0].clone(), external_inputs[0].clone()]; - let h = CRHGadget::::evaluate(&crh_params, &hash_input)?; - Ok(vec![h]) - } -} - -/// cargo test --example external_inputs -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::crh::{poseidon::CRH, CRHScheme}; - use ark_r1cs_std::GR1CSVar; - use ark_relations::gr1cs::ConstraintSystem; - - fn external_inputs_step_native( - z_i: Vec, - external_inputs: Vec, - poseidon_config: &PoseidonConfig, - ) -> Vec { - let hash_input: [F; 2] = [z_i[0], external_inputs[0]]; - let h = CRH::::evaluate(poseidon_config, hash_input).unwrap(); - vec![h] - } - - // test to check that the ExternalInputsCircuit computes the same values inside and outside the circuit - #[test] - fn test_f_circuit() -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - - let cs = ConstraintSystem::::new_ref(); - - let circuit = ExternalInputsCircuit::::new(poseidon_config.clone())?; - let z_i = vec![Fr::from(1_u32)]; - let external_inputs = vec![Fr::from(3_u32)]; - - let z_i1 = - external_inputs_step_native(z_i.clone(), external_inputs.clone(), &poseidon_config); - - let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i))?; - let external_inputsVar: [FpVar; 1] = - Vec::>::new_witness(cs.clone(), || Ok(external_inputs))? - .try_into() - .unwrap(); - - let computed_z_i1Var = - circuit.generate_step_constraints(cs.clone(), 0, z_iVar, external_inputsVar)?; - assert_eq!(computed_z_i1Var.value()?, z_i1); - Ok(()) - } -} - -/// cargo run --release --example external_inputs -fn main() -> Result<(), Error> { - let num_steps = 5; - let initial_state = vec![Fr::from(1_u32)]; - - // prepare the external inputs to be used at each folding step - let external_inputs = vec![ - [Fr::from(3_u32)], - [Fr::from(33_u32)], - [Fr::from(73_u32)], - [Fr::from(103_u32)], - [Fr::from(125_u32)], - ]; - assert_eq!(external_inputs.len(), num_steps); - - let poseidon_config = poseidon_canonical_config::(); - let F_circuit = ExternalInputsCircuit::::new(poseidon_config.clone())?; - - /// The idea here is that eventually we could replace the next line chunk that defines the - /// `type N = Nova<...>` by using another folding scheme that fulfills the `FoldingScheme` - /// trait, and the rest of our code would be working without needing to be updated. - type N = Nova< - Projective, - Projective2, - ExternalInputsCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - - let mut rng = rand::rngs::OsRng; - - println!("Prepare Nova's ProverParams & VerifierParams"); - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, F_circuit.clone()); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - println!("Initialize FoldingScheme"); - let mut folding_scheme = N::init(&nova_params, F_circuit, initial_state.clone())?; - - // compute a step of the IVC - for (i, external_inputs_at_step) in external_inputs.iter().enumerate() { - let start = Instant::now(); - folding_scheme.prove_step(rng, external_inputs_at_step.clone(), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - println!( - "state at last step (after {} iterations): {:?}", - num_steps, - folding_scheme.state() - ); - - println!("Run the Nova's IVC verifier"); - let ivc_proof = folding_scheme.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - Ok(()) -} diff --git a/examples/full_flow.rs b/examples/full_flow.rs deleted file mode 100644 index 93399b86b..000000000 --- a/examples/full_flow.rs +++ /dev/null @@ -1,154 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] -/// -/// This example performs the full flow: -/// - define the circuit to be folded -/// - fold the circuit with Nova+CycleFold's IVC -/// - generate a DeciderEthCircuit final proof -/// - generate the Solidity contract that verifies the proof -/// - verify the proof in the EVM -/// -use ark_bn254::{Bn254, Fr, G1Projective as G1}; -use ark_ff::PrimeField; -use ark_groth16::Groth16; -use ark_grumpkin::Projective as G2; -use ark_r1cs_std::alloc::AllocVar; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use std::marker::PhantomData; -use std::time::Instant; - -use folding_schemes::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - folding::{ - nova::{decider_eth::Decider as DeciderEth, Nova, PreprocessorParam}, - traits::CommittedInstanceOps, - }, - frontend::FCircuit, - transcript::poseidon::poseidon_canonical_config, - Decider, Error, FoldingScheme, -}; -use solidity_verifiers::calldata::{ - prepare_calldata_for_nova_cyclefold_verifier, NovaVerificationMode, -}; -use solidity_verifiers::{ - evm::{compile_solidity, Evm}, - verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, - NovaCycleFoldVerifierKey, -}; - -/// Test circuit to be folded -#[derive(Clone, Copy, Debug)] -pub struct CubicFCircuit { - _f: PhantomData, -} -impl FCircuit for CubicFCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 1 - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - let z_i = z_i[0].clone(); - - Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) - } -} - -fn main() -> Result<(), Error> { - let n_steps = 5; - // set the initial state - let z_0 = vec![Fr::from(3_u32)]; - - let f_circuit = CubicFCircuit::::new(())?; - - pub type N = Nova, KZG<'static, Bn254>, Pedersen, false>; - pub type D = - DeciderEth, KZG<'static, Bn254>, Pedersen, Groth16, N>; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = ark_std::rand::rngs::OsRng; - - // prepare the Nova prover & verifier params - let nova_preprocess_params = PreprocessorParam::new(poseidon_config.clone(), f_circuit); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params.clone(), f_circuit.state_len()))?; - - // initialize the folding scheme engine, in our case we use Nova - let mut nova = N::init(&nova_params, f_circuit, z_0)?; - - // run n steps of the folding iteration - for i in 0..n_steps { - let start = Instant::now(); - nova.prove_step(rng, (), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("generated Decider proof: {:?}", start.elapsed()); - - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider proof verification: {}", verified); - - // Now, let's generate the Solidity code that verifies this Decider final proof - let calldata: Vec = prepare_calldata_for_nova_cyclefold_verifier( - NovaVerificationMode::Explicit, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i, - &nova.u_i, - &proof, - )?; - - // prepare the setup params for the solidity verifier - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, f_circuit.state_len())); - - // generate the solidity code - let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk); - - // verify the proof against the solidity code in the EVM - let nova_cyclefold_verifier_bytecode = compile_solidity(&decider_solidity_code, "NovaDecider"); - let mut evm = Evm::default(); - let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // save smart contract and the calldata - println!("storing nova-verifier.sol and the calldata into files"); - use std::fs; - fs::write( - "./examples/nova-verifier.sol", - decider_solidity_code.clone(), - )?; - fs::write("./examples/solidity-calldata.calldata", calldata.clone())?; - let s = solidity_verifiers::calldata::get_formatted_calldata(calldata.clone()); - fs::write("./examples/solidity-calldata.inputs", s.join(",\n")).expect(""); - Ok(()) -} diff --git a/examples/multi_inputs.rs b/examples/multi_inputs.rs deleted file mode 100644 index 9463d31ac..000000000 --- a/examples/multi_inputs.rs +++ /dev/null @@ -1,153 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] - -use ark_ff::PrimeField; -use ark_r1cs_std::alloc::AllocVar; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use core::marker::PhantomData; -use std::time::Instant; - -use ark_bn254::{Bn254, Fr, G1Projective as Projective}; -use ark_grumpkin::Projective as Projective2; - -use folding_schemes::commitment::{kzg::KZG, pedersen::Pedersen}; -use folding_schemes::folding::nova::{Nova, PreprocessorParam}; -use folding_schemes::frontend::FCircuit; -use folding_schemes::transcript::poseidon::poseidon_canonical_config; -use folding_schemes::{Error, FoldingScheme}; - -/// This is the circuit that we want to fold, it implements the FCircuit trait. The parameter z_i -/// denotes the current state which contains 5 elements, and z_{i+1} denotes the next state which -/// we get by applying the step. -/// In this example we set z_i and z_{i+1} to have five elements, and at each step we do different -/// operations on each of them. -#[derive(Clone, Copy, Debug)] -pub struct MultiInputsFCircuit { - _f: PhantomData, -} -impl FCircuit for MultiInputsFCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 5 - } - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let four = FpVar::::new_constant(cs.clone(), F::from(4u32))?; - let forty = FpVar::::new_constant(cs.clone(), F::from(40u32))?; - let onehundred = FpVar::::new_constant(cs.clone(), F::from(100u32))?; - let a = z_i[0].clone() + four.clone(); - let b = z_i[1].clone() + forty.clone(); - let c = z_i[2].clone() * four; - let d = z_i[3].clone() * forty; - let e = z_i[4].clone() + onehundred; - - Ok(vec![a, b, c, d, e]) - } -} - -/// cargo test --example multi_inputs -#[cfg(test)] -pub mod tests { - use super::*; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - - fn multi_inputs_step_native(z_i: Vec) -> Vec { - let a = z_i[0] + F::from(4_u32); - let b = z_i[1] + F::from(40_u32); - let c = z_i[2] * F::from(4_u32); - let d = z_i[3] * F::from(40_u32); - let e = z_i[4] + F::from(100_u32); - - vec![a, b, c, d, e] - } - - // test to check that the MultiInputsFCircuit computes the same values inside and outside the circuit - #[test] - fn test_f_circuit() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let circuit = MultiInputsFCircuit::::new(())?; - let z_i = vec![ - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - ]; - - let z_i1 = multi_inputs_step_native(z_i.clone()); - - let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i))?; - let computed_z_i1Var = - circuit.generate_step_constraints(cs.clone(), 0, z_iVar.clone(), ())?; - assert_eq!(computed_z_i1Var.value()?, z_i1); - Ok(()) - } -} - -/// cargo run --release --example multi_inputs -fn main() -> Result<(), Error> { - let num_steps = 10; - let initial_state = vec![ - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - Fr::from(1_u32), - ]; - - let F_circuit = MultiInputsFCircuit::::new(())?; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = rand::rngs::OsRng; - - /// The idea here is that eventually we could replace the next line chunk that defines the - /// `type N = Nova<...>` by using another folding scheme that fulfills the `FoldingScheme` - /// trait, and the rest of our code would be working without needing to be updated. - type N = Nova< - Projective, - Projective2, - MultiInputsFCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - - println!("Prepare Nova ProverParams & VerifierParams"); - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - println!("Initialize FoldingScheme"); - let mut folding_scheme = N::init(&nova_params, F_circuit, initial_state.clone())?; - - // compute a step of the IVC - for i in 0..num_steps { - let start = Instant::now(); - folding_scheme.prove_step(rng, (), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - - println!("Run the Nova's IVC verifier"); - let ivc_proof = folding_scheme.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - Ok(()) -} diff --git a/examples/noir_full_flow.rs b/examples/noir_full_flow.rs deleted file mode 100644 index 0886e32de..000000000 --- a/examples/noir_full_flow.rs +++ /dev/null @@ -1,141 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] -/// -/// This example performs the full flow: -/// - define the circuit to be folded -/// - fold the circuit with Nova+CycleFold's IVC -/// - generate a DeciderEthCircuit final proof -/// - generate the Solidity contract that verifies the proof -/// - verify the proof in the EVM -/// -use ark_bn254::{Bn254, Fr, G1Projective as G1}; - -use ark_groth16::Groth16; -use ark_grumpkin::Projective as G2; - -use experimental_frontends::{noir::NoirFCircuit, utils::VecF}; -use folding_schemes::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - folding::{ - nova::{decider_eth::Decider as DeciderEth, Nova, PreprocessorParam}, - traits::CommittedInstanceOps, - }, - frontend::FCircuit, - transcript::poseidon::poseidon_canonical_config, - Decider, Error, FoldingScheme, -}; -use std::{path::Path, time::Instant}; - -use solidity_verifiers::calldata::{ - prepare_calldata_for_nova_cyclefold_verifier, NovaVerificationMode, -}; -use solidity_verifiers::{ - evm::{compile_solidity, Evm}, - verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, - NovaCycleFoldVerifierKey, -}; - -fn main() -> Result<(), Error> { - // set the initial state - let z_0 = vec![Fr::from(1)]; - - // initialize the noir fcircuit - const EXT_INP_LEN: usize = 0; - const STATE_LEN: usize = 1; - let f_circuit = NoirFCircuit::::new( - Path::new("./experimental-frontends/src/noir/test_folder/test_mimc/target/test_mimc.json") - .into(), - )?; - - pub type N = - Nova, KZG<'static, Bn254>, Pedersen>; - pub type D = DeciderEth< - G1, - G2, - NoirFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, - N, - >; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = ark_std::rand::rngs::OsRng; - - // prepare the Nova prover & verifier params - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params.clone(), f_circuit.state_len()))?; - - // initialize the folding scheme engine, in our case we use Nova - let mut nova = N::init(&nova_params, f_circuit.clone(), z_0)?; - - // run n steps of the folding iteration - for i in 0..5 { - let start = Instant::now(); - nova.prove_step(rng, VecF(vec![]), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - // verify the last IVC proof - let ivc_proof = nova.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("generated Decider proof: {:?}", start.elapsed()); - - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider proof verification: {}", verified); - - // Now, let's generate the Solidity code that verifies this Decider final proof - let calldata: Vec = prepare_calldata_for_nova_cyclefold_verifier( - NovaVerificationMode::Explicit, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i, - &nova.u_i, - &proof, - )?; - - // prepare the setup params for the solidity verifier - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, f_circuit.state_len())); - - // generate the solidity code - let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk); - - // verify the proof against the solidity code in the EVM - let nova_cyclefold_verifier_bytecode = compile_solidity(&decider_solidity_code, "NovaDecider"); - let mut evm = Evm::default(); - let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // save smart contract and the calldata - println!("storing nova-verifier.sol and the calldata into files"); - use std::fs; - fs::write( - "./examples/nova-verifier.sol", - decider_solidity_code.clone(), - )?; - fs::write("./examples/solidity-calldata.calldata", calldata.clone())?; - let s = solidity_verifiers::calldata::get_formatted_calldata(calldata.clone()); - fs::write("./examples/solidity-calldata.inputs", s.join(",\n")).expect(""); - Ok(()) -} diff --git a/examples/noname_full_flow.rs b/examples/noname_full_flow.rs deleted file mode 100644 index fad8846ab..000000000 --- a/examples/noname_full_flow.rs +++ /dev/null @@ -1,162 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] -/// -/// This example performs the full flow: -/// - define the circuit to be folded -/// - fold the circuit with Nova+CycleFold's IVC -/// - generate a DeciderEthCircuit final proof -/// - generate the Solidity contract that verifies the proof -/// - verify the proof in the EVM -/// -use ark_bn254::{Bn254, Fr, G1Projective as G1}; -use noname::backends::r1cs::R1csBn254Field; - -use ark_groth16::Groth16; -use ark_grumpkin::Projective as G2; - -use experimental_frontends::{noname::NonameFCircuit, utils::VecF}; -use folding_schemes::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - folding::{ - nova::{decider_eth::Decider as DeciderEth, Nova, PreprocessorParam}, - traits::CommittedInstanceOps, - }, - frontend::FCircuit, - transcript::poseidon::poseidon_canonical_config, - Decider, Error, FoldingScheme, -}; -use std::time::Instant; - -use solidity_verifiers::calldata::{ - prepare_calldata_for_nova_cyclefold_verifier, NovaVerificationMode, -}; -use solidity_verifiers::{ - evm::{compile_solidity, Evm}, - verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, - NovaCycleFoldVerifierKey, -}; - -fn main() -> Result<(), Error> { - const NONAME_CIRCUIT_EXTERNAL_INPUTS: &str = - "fn main(pub ivc_inputs: [Field; 2], external_inputs: [Field; 2]) -> [Field; 2] { - let xx = external_inputs[0] + ivc_inputs[0]; - let yy = external_inputs[1] * ivc_inputs[1]; - assert_eq(yy, xx); - return [xx, yy]; -}"; - - // set the initial state - let z_0 = vec![Fr::from(2), Fr::from(5)]; - - // set the external inputs to be used at each step of the IVC, it has length of 10 since this - // is the number of steps that we will do - let external_inputs = vec![ - vec![Fr::from(8u32), Fr::from(2u32)], - vec![Fr::from(40), Fr::from(5)], - ]; - - // initialize the noname circuit - let f_circuit_params = NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(); - const STATE_LEN: usize = 2; - const EXT_INP_LEN: usize = 2; - let f_circuit = - NonameFCircuit::::new(f_circuit_params)?; - - pub type N = Nova< - G1, - G2, - NonameFCircuit, - KZG<'static, Bn254>, - Pedersen, - >; - pub type D = DeciderEth< - G1, - G2, - NonameFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, - N, - >; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = ark_std::rand::rngs::OsRng; - - // prepare the Nova prover & verifier params - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params.clone(), f_circuit.state_len()))?; - - // initialize the folding scheme engine, in our case we use Nova - let mut nova = N::init(&nova_params, f_circuit.clone(), z_0)?; - - // run n steps of the folding iteration - for (i, external_inputs_at_step) in external_inputs.iter().enumerate() { - let start = Instant::now(); - nova.prove_step(rng, VecF(external_inputs_at_step.clone()), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - - // verify the last IVC proof - let ivc_proof = nova.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("generated Decider proof: {:?}", start.elapsed()); - - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider proof verification: {}", verified); - - // Now, let's generate the Solidity code that verifies this Decider final proof - let calldata: Vec = prepare_calldata_for_nova_cyclefold_verifier( - NovaVerificationMode::Explicit, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i, - &nova.u_i, - &proof, - )?; - - // prepare the setup params for the solidity verifier - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, f_circuit.state_len())); - - // generate the solidity code - let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk); - - // verify the proof against the solidity code in the EVM - let nova_cyclefold_verifier_bytecode = compile_solidity(&decider_solidity_code, "NovaDecider"); - let mut evm = Evm::default(); - let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // save smart contract and the calldata - println!("storing nova-verifier.sol and the calldata into files"); - use std::fs; - fs::write( - "./examples/nova-verifier.sol", - decider_solidity_code.clone(), - )?; - fs::write("./examples/solidity-calldata.calldata", calldata.clone())?; - let s = solidity_verifiers::calldata::get_formatted_calldata(calldata.clone()); - fs::write("./examples/solidity-calldata.inputs", s.join(",\n")).expect(""); - Ok(()) -} diff --git a/examples/sha256.rs b/examples/sha256.rs deleted file mode 100644 index 1dd8a7a04..000000000 --- a/examples/sha256.rs +++ /dev/null @@ -1,139 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] - -use ark_crypto_primitives::crh::{ - sha256::constraints::{Sha256Gadget, UnitVar}, - CRHSchemeGadget, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - convert::{ToBytesGadget, ToConstraintFieldGadget}, - fields::fp::FpVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use core::marker::PhantomData; -use std::time::Instant; - -use ark_bn254::{Bn254, Fr, G1Projective as Projective}; -use ark_grumpkin::Projective as Projective2; - -use folding_schemes::commitment::{kzg::KZG, pedersen::Pedersen}; -use folding_schemes::folding::nova::{Nova, PreprocessorParam}; -use folding_schemes::frontend::FCircuit; -use folding_schemes::transcript::poseidon::poseidon_canonical_config; -use folding_schemes::{Error, FoldingScheme}; - -/// This is the circuit that we want to fold, it implements the FCircuit trait. -/// The parameter z_i denotes the current state, and z_{i+1} denotes the next state which we get by -/// applying the step. -/// In this example we set z_i and z_{i+1} to be a single value, but the trait is made to support -/// arrays, so our state could be an array with different values. -#[derive(Clone, Copy, Debug)] -pub struct Sha256FCircuit { - _f: PhantomData, -} -impl FCircuit for Sha256FCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 1 - } - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - &self, - _cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let unit_var = UnitVar::default(); - let out_bytes = Sha256Gadget::evaluate(&unit_var, &z_i[0].to_bytes_le()?)?; - let out = out_bytes.0.to_constraint_field()?; - Ok(vec![out[0].clone()]) - } -} - -/// cargo test --example sha256 -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::crh::{sha256::Sha256, CRHScheme}; - use ark_ff::{BigInteger, ToConstraintField}; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - - fn sha256_step_native(z_i: Vec) -> Vec { - let out_bytes = Sha256::evaluate(&(), z_i[0].into_bigint().to_bytes_le()).unwrap(); - let out: Vec = out_bytes.to_field_elements().unwrap(); - - vec![out[0]] - } - - // test to check that the Sha256FCircuit computes the same values inside and outside the circuit - #[test] - fn test_f_circuit() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let circuit = Sha256FCircuit::::new(())?; - let z_i = vec![Fr::from(1_u32)]; - - let z_i1 = sha256_step_native(z_i.clone()); - - let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i))?; - let computed_z_i1Var = - circuit.generate_step_constraints(cs.clone(), 0, z_iVar.clone(), ())?; - assert_eq!(computed_z_i1Var.value()?, z_i1); - Ok(()) - } -} - -/// cargo run --release --example sha256 -fn main() -> Result<(), Error> { - let num_steps = 10; - let initial_state = vec![Fr::from(1_u32)]; - - let F_circuit = Sha256FCircuit::::new(())?; - - /// The idea here is that eventually we could replace the next line chunk that defines the - /// `type N = Nova<...>` by using another folding scheme that fulfills the `FoldingScheme` - /// trait, and the rest of our code would be working without needing to be updated. - type N = Nova< - Projective, - Projective2, - Sha256FCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - - let poseidon_config = poseidon_canonical_config::(); - let mut rng = rand::rngs::OsRng; - - println!("Prepare Nova ProverParams & VerifierParams"); - let nova_preprocess_params = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &nova_preprocess_params)?; - - println!("Initialize FoldingScheme"); - let mut folding_scheme = N::init(&nova_params, F_circuit, initial_state.clone())?; - // compute a step of the IVC - for i in 0..num_steps { - let start = Instant::now(); - folding_scheme.prove_step(rng, (), None)?; - println!("Nova::prove_step {}: {:?}", i, start.elapsed()); - } - - println!("Run the Nova's IVC verifier"); - let ivc_proof = folding_scheme.ivc_proof(); - N::verify( - nova_params.1, // Nova's verifier params - ivc_proof, - )?; - Ok(()) -} diff --git a/experimental-frontends/Cargo.toml b/experimental-frontends/Cargo.toml deleted file mode 100644 index 9fba6d4bb..000000000 --- a/experimental-frontends/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "experimental-frontends" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-ff = { workspace = true, features = ["parallel", "asm"] } -ark-std = { workspace = true, features = ["parallel"] } -ark-relations = { workspace = true } -ark-r1cs-std = { workspace = true, features = ["parallel"] } -ark-serialize = { workspace = true } -ark-circom = { workspace = true } -num-bigint = { workspace = true } -noname = { workspace = true } -acvm = { workspace = true } -folding-schemes = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -wasmer = { workspace = true } - -[dev-dependencies] -ark-bn254 = { workspace = true, features = ["r1cs"] } - -# This allows the crate to be built when targeting WASM. -# See more at: https://docs.rs/getrandom/#webassembly-support -[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] -getrandom = { workspace = true, features = ["js"] } - -[features] -default = ["ark-circom/default", "parallel"] -parallel = [] -wasm = ["ark-circom/wasm"] diff --git a/experimental-frontends/README.md b/experimental-frontends/README.md deleted file mode 100644 index dedc9e77e..000000000 --- a/experimental-frontends/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# experimental-frontends - -This crate contains *experimental frontends* for Sonobe. -The recommended frontend is to directly use [arkworks](https://github.com/arkworks-rs) to define the FCircuit, just following the [`FCircuit` trait](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs). - -> Warning: the following frontends are experimental and some computational and time overhead is expected when using them compared to directly using the [arkworks frontend](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs). - -Available experimental frontends: -- [Circom](https://github.com/iden3/circom), iden3, 0Kims Association. Supported version`<=v2.1.9`. -- [Noir](https://github.com/noir-lang/noir), Aztec. -- [Noname](https://github.com/zksecurity/noname), zkSecurity. Partially supported. - - -Documentation about frontend interface and experimental frontends: https://privacy-scaling-explorations.github.io/sonobe-docs/usage/frontend.html - -## Implementing new frontends -Support for new frontends can be added (even from outside this repo) by implementing the [`FCircuit` trait](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs). diff --git a/experimental-frontends/src/circom/mod.rs b/experimental-frontends/src/circom/mod.rs deleted file mode 100644 index 76b8385b0..000000000 --- a/experimental-frontends/src/circom/mod.rs +++ /dev/null @@ -1,347 +0,0 @@ -use ark_circom::circom::R1CS as CircomR1CS; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - fields::fp::{AllocatedFp, FpVar}, - GR1CSVar, -}; -use ark_relations::{ - gr1cs::{ConstraintSystemRef, SynthesisError, Variable}, - lc, -}; -use ark_std::fmt::Debug; -use folding_schemes::{frontend::FCircuit, utils::PathOrBin, Error}; -use num_bigint::{BigInt, BigUint}; - -pub mod utils; -use crate::utils::{VecF, VecFpVar}; -use utils::CircomWrapper; - -/// Define CircomFCircuit. The parameter `SL` indicates the length of the state vector. -/// The parameter `EIL` indicates the length of the ExternalInputs vector of field elements. -#[derive(Clone, Debug)] -pub struct CircomFCircuit { - circom_wrapper: CircomWrapper, - r1cs: CircomR1CS, -} - -impl FCircuit for CircomFCircuit { - /// (r1cs_path, wasm_path) - type Params = (PathOrBin, PathOrBin); - type ExternalInputs = VecF; - type ExternalInputsVar = VecFpVar; - - fn new(params: Self::Params) -> Result { - let (r1cs_path, wasm_path) = params; - let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path)?; - - let r1cs = circom_wrapper.extract_r1cs()?; - Ok(Self { - circom_wrapper, - r1cs, - }) - } - - fn state_len(&self) -> usize { - SL - } - - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - #[cfg(test)] - assert_eq!(z_i.len(), SL); - #[cfg(test)] - assert_eq!(external_inputs.0.len(), EIL); - - let input_values = Self::fpvars_to_bigints(&z_i); - let mut inputs_map = vec![("ivc_input".to_string(), input_values)]; - - if EIL > 0 { - let external_inputs_bi = Self::fpvars_to_bigints(&external_inputs.0); - inputs_map.push(("external_inputs".to_string(), external_inputs_bi)); - } - - // The layout of `witness` is as follows: - // [ - // 1, // The constant 1 is implicitly allocated by Arkworks - // ...z_{i + 1}, // The next state marked as `signal output` in the circom circuit - // ...z_i, // The current state marked as `signal input` in the circom circuit - // ...external_inputs, // The optional external inputs marked as `external input` in the circom circuit - // ...aux, // The intermediate witnesses - // ] - // Here, 1, z_i, and external_inputs have already been allocated in the - // constraint system, while z_{i + 1} and aux are yet to be allocated. - let witness = self - .circom_wrapper - .extract_witness(inputs_map) - .map_err(|_| SynthesisError::AssignmentMissing)?; - - // In order to convert the indexes of variables in the circom circuit to - // those in the arkworks circuit, we adopt the tricks from - // https://github.com/arnaucube/circom-compat/pull/1 - - // Since our cs might already have allocated constraints, - // We store a mapping between circom's defined indexes and the newly obtained cs indexes - let mut circom_index_to_cs_index = vec![]; - - // Constant 1 at idx 0 is already allocated by arkworks - circom_index_to_cs_index.push(Variable::One); - - // Allocate the next state (1..1 + SL) as witness, and at the same time, - // record the allocated variable's index in `circom_index_to_cs_index`. - // Cf. https://github.com/arnaucube/circom-compat/blob/22c8f5/src/circom/circuit.rs#L56-L86 - let mut z_i1 = vec![]; - for &w in witness.iter().skip(1).take(SL) { - let v = cs.new_witness_variable(|| Ok(w))?; - circom_index_to_cs_index.push(v); - z_i1.push(FpVar::Var(AllocatedFp::new(Some(w), v, cs.clone()))); - } - - // `z_i` and `external_inputs` have already been allocated as witness, - // so we just record their indexes in `circom_index_to_cs_index`. - // Cf. https://github.com/arnaucube/circom-compat/blob/22c8f5/src/circom/circuit.rs#L89-L95 - for v in z_i.iter().chain(&external_inputs.0) { - match v { - FpVar::Var(v) => circom_index_to_cs_index.push(v.variable), - // safe because `z_i` and `external_inputs` are allocated as - // witness (not constant) - _ => unreachable!(), - }; - } - - // Allocate the remaining aux variables as witness. - // Also, record their indexes in `circom_index_to_cs_index`. - // Cf. https://github.com/arnaucube/circom-compat/blob/22c8f5/src/circom/circuit.rs#L106-L121 - for w in witness.into_iter().skip(circom_index_to_cs_index.len()) { - circom_index_to_cs_index.push(cs.new_witness_variable(|| Ok(w))?); - } - - let fold_lc = |lc, &(i, coeff)| lc + (coeff, circom_index_to_cs_index[i]); - - // Generates the constraints for the circom_circuit. - for (a, b, c) in &self.r1cs.constraints { - cs.enforce_r1cs_constraint( - || a.iter().fold(lc!(), fold_lc), - || b.iter().fold(lc!(), fold_lc), - || c.iter().fold(lc!(), fold_lc), - )?; - } - - #[cfg(test)] - if !cs.is_in_setup_mode() && !cs.is_satisfied()? { - return Err(SynthesisError::Unsatisfiable); - } - - Ok(z_i1) - } -} - -impl CircomFCircuit { - fn fpvars_to_bigints(fpvars: &[FpVar]) -> Vec { - fpvars - .value() - .unwrap_or(vec![F::zero(); fpvars.len()]) - .into_iter() - .map(Into::::into) - .map(BigInt::from) - .collect() - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_bn254::Fr; - use ark_r1cs_std::alloc::AllocVar; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - use std::path::PathBuf; - - /// Native implementation of `src/circom/test_folder/cubic_circuit.r1cs` - fn cubic_step_native(z_i: Vec) -> Vec { - let z = z_i[0]; - vec![z * z * z + z + F::from(5)] - } - - /// Native implementation of `src/circom/test_folder/with_external_inputs.r1cs` - fn external_inputs_step_native(z_i: Vec, external_inputs: Vec) -> Vec { - let temp1 = z_i[0] * z_i[0]; - let temp2 = z_i[0] * external_inputs[0]; - vec![temp1 * z_i[0] + temp2 + external_inputs[1]] - } - - /// Native implementation of `src/circom/test_folder/no_external_inputs.r1cs` - fn no_external_inputs_step_native(z_i: Vec) -> Vec { - let temp1 = z_i[0] * z_i[1]; - let temp2 = temp1 * z_i[2]; - vec![ - temp1 * z_i[0], - temp1 * z_i[1] + temp1, - temp1 * z_i[2] + temp2, - ] - } - - // Tests the step_native function of CircomFCircuit. - #[test] - fn test_circom_step_native() -> Result<(), Error> { - let z_i = vec![Fr::from(3u32)]; - let z_i1 = cubic_step_native(z_i); - assert_eq!(z_i1, vec![Fr::from(35u32)]); - Ok(()) - } - - // Tests the generate_step_constraints function of CircomFCircuit. - #[test] - fn test_circom_step_constraints() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs"); - let wasm_path = - PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - - let circom_fcircuit = - CircomFCircuit::::new((r1cs_path.into(), wasm_path.into()))?; // state_len:1, external_inputs_len:0 - - let cs = ConstraintSystem::::new_ref(); - - let z_i = vec![Fr::from(3u32)]; - - let z_i_var = Vec::>::new_witness(cs.clone(), || Ok(z_i))?; - let z_i1_var = - circom_fcircuit.generate_step_constraints(cs.clone(), 1, z_i_var, VecFpVar(vec![]))?; - assert_eq!(z_i1_var.value()?, vec![Fr::from(35u32)]); - Ok(()) - } - - // Tests the WrapperCircuit with CircomFCircuit. - #[test] - fn test_wrapper_circomtofcircuit() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs"); - let wasm_path = - PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - - let circom_fcircuit = - CircomFCircuit::::new((r1cs_path.into(), wasm_path.into()))?; // state_len:1, external_inputs_len:0 - - // Allocates z_i1 by using step_native function. - let z_i = vec![Fr::from(3_u32)]; - let wrapper_circuit = folding_schemes::frontend::utils::WrapperCircuit { - FC: circom_fcircuit.clone(), - z_i: Some(z_i.clone()), - z_i1: Some(cubic_step_native(z_i)), - }; - - let cs = ConstraintSystem::::new_ref(); - - wrapper_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?, "Constraint system is not satisfied"); - Ok(()) - } - - #[test] - fn test_circom_external_inputs() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/with_external_inputs.r1cs"); - let wasm_path = PathBuf::from( - "./src/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm", - ); - let circom_fcircuit = - CircomFCircuit::::new((r1cs_path.into(), wasm_path.into()))?; // state_len:1, external_inputs_len:2 - let cs = ConstraintSystem::::new_ref(); - let z_i = vec![Fr::from(3u32)]; - let external_inputs = vec![Fr::from(6u32), Fr::from(7u32)]; - - // run native step - let z_i1_native = external_inputs_step_native(z_i.clone(), external_inputs.clone()); - - // run gadget step - let z_i_var = Vec::>::new_witness(cs.clone(), || Ok(z_i))?; - let external_inputs_var = - Vec::>::new_witness(cs.clone(), || Ok(external_inputs.clone()))?; - let z_i1_var = circom_fcircuit.generate_step_constraints( - cs.clone(), - 1, - z_i_var, - VecFpVar(external_inputs_var), - )?; - - assert_eq!(z_i1_var.value()?, z_i1_native); - - // re-init cs and run gadget step with wrong ivc inputs (first ivc should not be zero) - let cs = ConstraintSystem::::new_ref(); - let wrong_z_i = vec![Fr::from(0)]; - let wrong_z_i_var = Vec::>::new_witness(cs.clone(), || Ok(wrong_z_i))?; - let external_inputs_var = - Vec::>::new_witness(cs.clone(), || Ok(external_inputs))?; - let _z_i1_var = circom_fcircuit.generate_step_constraints( - cs.clone(), - 1, - wrong_z_i_var, - VecFpVar(external_inputs_var), - ); - // TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104 - // Disable check for now - // assert!(z_i1_var.is_err()); - Ok(()) - } - - #[test] - fn test_circom_no_external_inputs() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/no_external_inputs.r1cs"); - let wasm_path = - PathBuf::from("./src/circom/test_folder/no_external_inputs_js/no_external_inputs.wasm"); - let circom_fcircuit = - CircomFCircuit::::new((r1cs_path.into(), wasm_path.into()))?; - let cs = ConstraintSystem::::new_ref(); - let z_i = vec![Fr::from(3u32), Fr::from(4u32), Fr::from(5u32)]; - let z_i_var = Vec::>::new_witness(cs.clone(), || Ok(z_i.clone()))?; - - // run native step - let z_i1_native = no_external_inputs_step_native(z_i.clone()); - - // run gadget step - let z_i1_var = - circom_fcircuit.generate_step_constraints(cs.clone(), 1, z_i_var, VecFpVar(vec![]))?; - - assert_eq!(z_i1_var.value()?, z_i1_native); - - // re-init cs and run gadget step with wrong ivc inputs (first ivc input should not be zero) - let cs = ConstraintSystem::::new_ref(); - let wrong_z_i = vec![Fr::from(0u32), Fr::from(4u32), Fr::from(5u32)]; - let wrong_z_i_var = Vec::>::new_witness(cs.clone(), || Ok(wrong_z_i))?; - let _z_i1_var = circom_fcircuit.generate_step_constraints( - cs.clone(), - 1, - wrong_z_i_var, - VecFpVar(vec![]), - ); - // TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104 - // Disable check for now - // assert!(z_i1_var.is_err()) - Ok(()) - } - - #[test] - fn test_custom_code() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs"); - let wasm_path = - PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - - let circom_fcircuit = - CircomFCircuit::::new((r1cs_path.into(), wasm_path.into()))?; // state_len:1, external_inputs_len:0 - - // Allocates z_i1 by using step_native function. - let z_i = vec![Fr::from(3_u32)]; - let wrapper_circuit = folding_schemes::frontend::utils::WrapperCircuit { - FC: circom_fcircuit.clone(), - z_i: Some(z_i.clone()), - z_i1: Some(cubic_step_native(z_i)), - }; - - let cs = ConstraintSystem::::new_ref(); - - wrapper_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?, "Constraint system is not satisfied"); - Ok(()) - } -} diff --git a/experimental-frontends/src/circom/test_folder/circuits/is_zero.circom b/experimental-frontends/src/circom/test_folder/circuits/is_zero.circom deleted file mode 100644 index 8ec62a9eb..000000000 --- a/experimental-frontends/src/circom/test_folder/circuits/is_zero.circom +++ /dev/null @@ -1,14 +0,0 @@ -pragma circom 2.0.0; -// From: https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom - -template IsZero() { - signal input in; - signal output out; - - signal inv; - - inv <-- in!=0 ? 1/in : 0; - - out <== -in*inv +1; - in*out === 0; -} \ No newline at end of file diff --git a/experimental-frontends/src/circom/test_folder/compile.sh b/experimental-frontends/src/circom/test_folder/compile.sh deleted file mode 100755 index 1993e3ce8..000000000 --- a/experimental-frontends/src/circom/test_folder/compile.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -circom ./experimental-frontends/src/circom/test_folder/cubic_circuit.circom --r1cs --sym --wasm --prime bn128 --output ./experimental-frontends/src/circom/test_folder/ -circom ./experimental-frontends/src/circom/test_folder/with_external_inputs.circom --r1cs --sym --wasm --prime bn128 --output ./experimental-frontends/src/circom/test_folder/ -circom ./experimental-frontends/src/circom/test_folder/no_external_inputs.circom --r1cs --sym --wasm --prime bn128 --output ./experimental-frontends/src/circom/test_folder/ diff --git a/experimental-frontends/src/circom/test_folder/cubic_circuit.circom b/experimental-frontends/src/circom/test_folder/cubic_circuit.circom deleted file mode 100644 index 28e206793..000000000 --- a/experimental-frontends/src/circom/test_folder/cubic_circuit.circom +++ /dev/null @@ -1,12 +0,0 @@ -pragma circom 2.0.3; - -template Example () { - signal input ivc_input[1]; - signal output ivc_output[1]; - signal temp; - - temp <== ivc_input[0] * ivc_input[0]; - ivc_output[0] <== temp * ivc_input[0] + ivc_input[0] + 5; -} - -component main {public [ivc_input]} = Example(); diff --git a/experimental-frontends/src/circom/test_folder/no_external_inputs.circom b/experimental-frontends/src/circom/test_folder/no_external_inputs.circom deleted file mode 100644 index 258121fb6..000000000 --- a/experimental-frontends/src/circom/test_folder/no_external_inputs.circom +++ /dev/null @@ -1,23 +0,0 @@ -pragma circom 2.0.3; - -include "./circuits/is_zero.circom"; - -template NoExternalInputs () { - signal input ivc_input[3]; - signal output ivc_output[3]; - - component check_input = IsZero(); - check_input.in <== ivc_input[0]; - check_input.out === 0; - - signal temp1; - signal temp2; - - temp1 <== ivc_input[0] * ivc_input[1]; - temp2 <== temp1 * ivc_input[2]; - ivc_output[0] <== temp1 * ivc_input[0]; - ivc_output[1] <== temp1 * ivc_input[1] + temp1; - ivc_output[2] <== temp1 * ivc_input[2] + temp2; -} - -component main {public [ivc_input]} = NoExternalInputs(); diff --git a/experimental-frontends/src/circom/test_folder/with_external_inputs.circom b/experimental-frontends/src/circom/test_folder/with_external_inputs.circom deleted file mode 100644 index 8614de0d2..000000000 --- a/experimental-frontends/src/circom/test_folder/with_external_inputs.circom +++ /dev/null @@ -1,22 +0,0 @@ -pragma circom 2.0.3; - -include "./circuits/is_zero.circom"; - -template WithExternalInputs () { - signal input ivc_input[1]; - signal input external_inputs[2]; - signal output ivc_output[1]; - - component check_input = IsZero(); - check_input.in <== ivc_input[0]; - check_input.out === 0; - - signal temp1; - signal temp2; - - temp1 <== ivc_input[0] * ivc_input[0]; - temp2 <== ivc_input[0] * external_inputs[0]; - ivc_output[0] <== temp1 * ivc_input[0] + temp2 + external_inputs[1]; -} - -component main {public [ivc_input]} = WithExternalInputs(); \ No newline at end of file diff --git a/experimental-frontends/src/circom/utils.rs b/experimental-frontends/src/circom/utils.rs deleted file mode 100644 index 6ea48149e..000000000 --- a/experimental-frontends/src/circom/utils.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::{fs::File, io::Cursor, path::PathBuf}; - -use ark_circom::{ - circom::{r1cs_reader, R1CS}, - WitnessCalculator, -}; -use ark_ff::PrimeField; -use ark_serialize::Read; -use num_bigint::BigInt; -use wasmer::{Module, Store}; - -use folding_schemes::{utils::PathOrBin, Error}; - -// A struct that wraps Circom functionalities, allowing for extraction of R1CS and witnesses -// based on file paths to Circom's .r1cs and .wasm. -#[derive(Clone, Debug)] -pub struct CircomWrapper { - r1csfile_bytes: Vec, - wasmfile_bytes: Vec, -} - -impl CircomWrapper { - // Creates a new instance of the CircomWrapper with the file paths. - pub fn new(r1cs: PathOrBin, wasm: PathOrBin) -> Result { - match (r1cs, wasm) { - (PathOrBin::Path(r1cs_path), PathOrBin::Path(wasm_path)) => { - Self::new_from_path(r1cs_path, wasm_path) - } - (PathOrBin::Bin(r1cs_bin), PathOrBin::Bin(wasm_bin)) => Ok(Self { - r1csfile_bytes: r1cs_bin, - wasmfile_bytes: wasm_bin, - }), - _ => unreachable!("You should pass the same enum branch for both inputs"), - } - } - - // Creates a new instance of the CircomWrapper with the file paths. - fn new_from_path(r1cs_file_path: PathBuf, wasm_file_path: PathBuf) -> Result { - let mut file = File::open(r1cs_file_path)?; - let metadata = File::metadata(&file)?; - let mut r1csfile_bytes = vec![0; metadata.len() as usize]; - file.read_exact(&mut r1csfile_bytes)?; - - let mut file = File::open(wasm_file_path)?; - let metadata = File::metadata(&file)?; - let mut wasmfile_bytes = vec![0; metadata.len() as usize]; - file.read_exact(&mut wasmfile_bytes)?; - - Ok(CircomWrapper { - r1csfile_bytes, - wasmfile_bytes, - }) - } - - // Aggregated function to obtain R1CS and witness from Circom. - pub fn extract_r1cs_and_witness( - &self, - inputs: Vec<(String, Vec)>, - ) -> Result<(R1CS, Option>), Error> { - // Extracts the R1CS - let r1cs_file = r1cs_reader::R1CSFile::new(Cursor::new(&self.r1csfile_bytes))?; - let r1cs = r1cs_reader::R1CS::from(r1cs_file); - - // Extracts the witness vector - let witness_vec = self.extract_witness(inputs)?; - - Ok((r1cs, Some(witness_vec))) - } - - pub fn extract_r1cs(&self) -> Result, Error> { - let r1cs_file = r1cs_reader::R1CSFile::new(Cursor::new(&self.r1csfile_bytes))?; - let mut r1cs = r1cs_reader::R1CS::from(r1cs_file); - r1cs.wire_mapping = None; - Ok(r1cs) - } - - // Extracts the witness vector as a vector of PrimeField elements. - pub fn extract_witness( - &self, - inputs: Vec<(String, Vec)>, - ) -> Result, Error> { - let witness_bigint = self.calculate_witness(inputs)?; - - witness_bigint - .into_iter() - .map(|big_int| { - big_int.to_biguint().map(F::from).ok_or_else(|| { - Error::ConversionError( - "BigInt".into(), - "BigUint".into(), - "BigInt is negative".into(), - ) - }) - }) - .collect() - } - - // Calculates the witness given the Wasm filepath and inputs. - pub fn calculate_witness( - &self, - inputs: Vec<(String, Vec)>, - ) -> Result, Error> { - let mut store = Store::default(); - let module = Module::new(&store, &self.wasmfile_bytes).map_err(|e| { - Error::WitnessCalculationError(format!("Failed to create Wasm module: {e}")) - })?; - let mut calculator = WitnessCalculator::from_module(&mut store, module).map_err(|e| { - Error::WitnessCalculationError(format!("Failed to create WitnessCalculator: {e}")) - })?; - calculator - .calculate_witness(&mut store, inputs, true) - .map_err(|e| { - Error::WitnessCalculationError(format!("Failed to calculate witness: {e}")) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_bn254::Fr; - use ark_circom::circom::{CircomBuilder, CircomConfig}; - use ark_circom::CircomCircuit; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - - //To generate .r1cs and .wasm files, run the below command in the terminal. - //bash ./frontends/src/circom/test_folder/compile.sh - - // Test the satisfication by using the CircomBuilder of circom-compat - #[test] - fn test_circombuilder_satisfied() -> Result<(), Error> { - let cfg = CircomConfig::::new( - "./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm", - "./src/circom/test_folder/cubic_circuit.r1cs", - ) - .unwrap(); - let mut builder = CircomBuilder::new(cfg); - builder.push_input("ivc_input", 3); - - let circom = builder.build().unwrap(); - let cs = ConstraintSystem::::new_ref(); - circom.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - Ok(()) - } - - // Test the satisfication by using the CircomWrapper - #[test] - fn test_extract_r1cs_and_witness() -> Result<(), Error> { - let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs"); - let wasm_path = - PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - - let inputs = vec![("ivc_input".to_string(), vec![BigInt::from(3)])]; - let wrapper = CircomWrapper::new(r1cs_path.into(), wasm_path.into())?; - - let (r1cs, witness) = wrapper.extract_r1cs_and_witness(inputs)?; - - let cs = ConstraintSystem::::new_ref(); - - let circom_circuit = CircomCircuit { r1cs, witness }; - - circom_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/experimental-frontends/src/lib.rs b/experimental-frontends/src/lib.rs deleted file mode 100644 index da1a1786f..000000000 --- a/experimental-frontends/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod circom; -pub mod noir; -pub mod noname; -pub mod utils; diff --git a/experimental-frontends/src/noir/bridge.rs b/experimental-frontends/src/noir/bridge.rs deleted file mode 100644 index e46bf0f5f..000000000 --- a/experimental-frontends/src/noir/bridge.rs +++ /dev/null @@ -1,154 +0,0 @@ -// From https://github.com/dmpierre/arkworks_backend/tree/feat/sonobe-integration -use std::collections::{BTreeMap, HashMap}; -use std::convert::TryInto; - -use acvm::acir::{ - acir_field::GenericFieldElement, - circuit::{Circuit, Opcode, PublicInputs}, - native_types::{Expression, Witness, WitnessMap}, -}; -use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::alloc::AllocVar; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::{ - gr1cs::{ - ConstraintSynthesizer, ConstraintSystemRef, LinearCombination, SynthesisError, Variable, - }, - lc, -}; - -// AcirCircuit and AcirArithGate are structs that arkworks can synthesise. -// -// The difference between these structures and the ACIR structure that the compiler uses is the following: -// - The compilers ACIR struct is currently fixed to bn254 -// - These structures only support arithmetic gates, while the compiler has other -// gate types. These can be added later once the backend knows how to deal with things like XOR -// or once ACIR is taught how to do convert these black box functions to Arithmetic gates. -// -// XXX: Ideally we want to implement `ConstraintSynthesizer` on ACIR however -// this does not seem possible since ACIR is juts a description of the constraint system and the API Asks for prover values also. -// -// Perfect API would look like: -// - index(srs, circ) -// - prove(index_pk, prover_values, rng) -// - verify(index_vk, verifier, rng) -#[derive(Clone)] -pub struct AcirCircuitSonobe<'a, F: Field + PrimeField> { - pub(crate) gates: Vec>>, - pub(crate) public_inputs: PublicInputs, - pub(crate) values: BTreeMap, - pub already_assigned_witnesses: HashMap>, -} - -impl<'a, ConstraintF: Field + PrimeField> ConstraintSynthesizer - for AcirCircuitSonobe<'a, ConstraintF> -{ - fn generate_constraints( - self, - cs: ConstraintSystemRef, - ) -> Result<(), SynthesisError> { - let mut variables = Vec::with_capacity(self.values.len()); - - // First create all of the witness indices by adding the values into the constraint system - for (i, val) in self.values.iter() { - let var = if self.already_assigned_witnesses.contains_key(i) { - let var = self.already_assigned_witnesses.get(i).unwrap(); - if let FpVar::Var(allocated) = var { - allocated.variable - } else { - return Err(SynthesisError::Unsatisfiable); - } - } else if self.public_inputs.contains(i.0.try_into().unwrap()) { - cs.new_witness_variable(|| Ok(*val))? - } else { - cs.new_witness_variable(|| Ok(*val))? - }; - variables.push(var); - } - - // Now iterate each gate and add it to the constraint system - for gate in self.gates { - let mut arith_gate = LinearCombination::::new(); - - // Process mul terms - for mul_term in gate.mul_terms { - let coeff = mul_term.0; - let left_val = self.values[&mul_term.1]; - let right_val = self.values[&mul_term.2]; - - let out_val = left_val * right_val; - let out_var = FpVar::::new_witness(cs.clone(), || Ok(out_val))?; - // out var can't be a type different from FpVar::Var - if let FpVar::Var(allocated) = out_var { - arith_gate += (coeff.into_repr(), allocated.variable); - } - } - - // Process Add terms - for add_term in gate.linear_combinations { - let coeff = add_term.0; - let add_var = &variables[add_term.1.as_usize()]; - arith_gate += (coeff.into_repr(), *add_var); - } - - // Process constant term - arith_gate += (gate.q_c.into_repr(), Variable::One); - - cs.enforce_r1cs_constraint(|| lc!() + Variable::One, || arith_gate, || lc!())?; - } - - Ok(()) - } -} - -impl<'a, F: PrimeField> - From<( - &Circuit>, - WitnessMap>, - )> for AcirCircuitSonobe<'a, F> -{ - fn from( - circ_val: ( - &Circuit>, - WitnessMap>, - ), - ) -> AcirCircuitSonobe<'a, F> { - // Currently non-arithmetic gates are not supported - // so we extract all of the arithmetic gates only - let (circuit, witness_map) = circ_val; - - let public_inputs = circuit.public_inputs(); - let arith_gates: Vec<_> = circuit - .opcodes - .iter() - .filter_map(|opcode| { - if let Opcode::AssertZero(code) = opcode { - Some(code.clone()) - } else { - None - } - }) - .collect(); - - let num_variables: usize = circuit.num_vars().try_into().unwrap(); - - let values: BTreeMap = (0..num_variables) - .map(|witness_index| { - // Get the value if it exists. If i does not, then we fill it with the zero value - let witness = Witness(witness_index as u32); - let value = witness_map - .get(&witness) - .map_or(F::zero(), |field| field.into_repr()); - - (witness, value) - }) - .collect(); - - AcirCircuitSonobe { - gates: arith_gates, - values, - public_inputs, - already_assigned_witnesses: HashMap::new(), - } - } -} diff --git a/experimental-frontends/src/noir/mod.rs b/experimental-frontends/src/noir/mod.rs deleted file mode 100644 index d590758c0..000000000 --- a/experimental-frontends/src/noir/mod.rs +++ /dev/null @@ -1,215 +0,0 @@ -use acvm::{ - acir::{ - acir_field::GenericFieldElement, - circuit::{Circuit, Program}, - native_types::{Witness as AcvmWitness, WitnessMap}, - }, - blackbox_solver::StubbedBlackBoxSolver, - pwg::ACVM, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use serde::{self, Deserialize, Serialize}; -use std::collections::HashMap; - -use self::bridge::AcirCircuitSonobe; -use crate::utils::{VecF, VecFpVar}; -use folding_schemes::{frontend::FCircuit, utils::PathOrBin, Error}; - -mod bridge; - -#[derive(Clone, Debug)] -pub struct NoirFCircuit { - pub circuit: Circuit>, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct ProgramArtifactGeneric { - #[serde( - serialize_with = "Program::serialize_program_base64", - deserialize_with = "Program::deserialize_program_base64" - )] - pub bytecode: Program>, -} - -impl FCircuit for NoirFCircuit { - type Params = PathOrBin; - type ExternalInputs = VecF; - type ExternalInputsVar = VecFpVar; - - fn new(source: Self::Params) -> Result { - let input_string = match source { - PathOrBin::Path(path) => { - let file_path = path.with_extension("json"); - std::fs::read(&file_path).map_err(|_| Error::Other(format!("{} is not a valid path\nRun either `nargo compile` to generate missing build artifacts or `nargo prove` to construct a proof", file_path.display())))? - } - PathOrBin::Bin(bin) => bin, - }; - let program: ProgramArtifactGeneric = serde_json::from_slice(&input_string) - .map_err(|err| Error::JSONSerdeError(err.to_string()))?; - let circuit: Circuit> = program.bytecode.functions[0].clone(); - let ivc_input_length = circuit.public_parameters.0.len(); - let ivc_return_length = circuit.return_values.0.len(); - - if ivc_input_length != ivc_return_length { - return Err(Error::NotSameLength( - "IVC input: ".to_string(), - ivc_input_length, - "IVC output: ".to_string(), - ivc_return_length, - )); - } - - Ok(NoirFCircuit { circuit }) - } - - fn state_len(&self) -> usize { - SL - } - - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputsVar, // inputs that are not part of the state - ) -> Result>, SynthesisError> { - let mut acvm = ACVM::new( - &StubbedBlackBoxSolver, - &self.circuit.opcodes, - WitnessMap::new(), - &[], - &[], - ); - - let mut already_assigned_witness_values = HashMap::new(); - - self.circuit.public_parameters.0.iter().for_each(|witness| { - let idx: usize = witness.as_usize(); - let witness = AcvmWitness(witness.witness_index()); - already_assigned_witness_values.insert(witness, &z_i[idx]); - - let val = z_i[idx].value().unwrap_or_default(); - - let f = GenericFieldElement::::from_repr(val); - acvm.overwrite_witness(witness, f); - }); - - // write witness values for external_inputs - self.circuit.private_parameters.iter().for_each(|witness| { - let idx = witness.as_usize() - z_i.len(); - let witness = AcvmWitness(witness.witness_index()); - already_assigned_witness_values.insert(witness, &external_inputs.0[idx]); - - let val = external_inputs.0[idx].value().unwrap_or_default(); - - let f = GenericFieldElement::::from_repr(val); - acvm.overwrite_witness(witness, f); - }); - - // computes the witness - let _ = acvm.solve(); - let witness_map = acvm.finalize(); - - // get the z_{i+1} output state - let assigned_z_i1 = self - .circuit - .return_values - .0 - .iter() - .map(|witness| { - let noir_field_element = witness_map - .get(witness) - .ok_or(SynthesisError::AssignmentMissing)?; - FpVar::::new_witness(cs.clone(), || Ok(noir_field_element.into_repr())) - }) - .collect::>, SynthesisError>>()?; - - // initialize circuit and set already assigned values - let mut acir_circuit = AcirCircuitSonobe::from((&self.circuit, witness_map)); - acir_circuit.already_assigned_witnesses = already_assigned_witness_values; - - acir_circuit.generate_constraints(cs.clone())?; - - Ok(assigned_z_i1) - } -} - -#[cfg(test)] -mod tests { - use ark_bn254::Fr; - use ark_ff::PrimeField; - use ark_r1cs_std::GR1CSVar; - use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; - use ark_relations::gr1cs::ConstraintSystem; - use folding_schemes::{frontend::FCircuit, Error}; - use std::env; - - use crate::noir::NoirFCircuit; - use crate::utils::VecFpVar; - - /// Native implementation of `src/noir/test_folder/test_circuit` - fn external_inputs_step_native(z_i: Vec, external_inputs: Vec) -> Vec { - let xx = external_inputs[0] * z_i[0]; - let yy = external_inputs[1] * z_i[1]; - vec![xx, yy] - } - - #[test] - fn test_step_native() -> Result<(), Error> { - let inputs = vec![Fr::from(2), Fr::from(5)]; - let res = external_inputs_step_native(inputs.clone(), inputs); - assert_eq!(res, vec![Fr::from(4), Fr::from(25)]); - Ok(()) - } - - #[test] - fn test_step_constraints() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let cur_path = env::current_dir()?; - // external inputs length: 2, state length: 2 - let noirfcircuit = NoirFCircuit::::new( - cur_path - .join("src/noir/test_folder/test_circuit/target/test_circuit.json") - .into(), - )?; - let inputs = vec![Fr::from(2), Fr::from(5)]; - let z_i = Vec::>::new_witness(cs.clone(), || Ok(inputs.clone()))?; - let external_inputs = Vec::>::new_witness(cs.clone(), || Ok(inputs))?; - let output = noirfcircuit.generate_step_constraints( - cs.clone(), - 0, - z_i, - VecFpVar(external_inputs), - )?; - assert_eq!(output[0].value()?, Fr::from(4)); - assert_eq!(output[1].value()?, Fr::from(25)); - Ok(()) - } - - #[test] - fn test_step_constraints_no_external_inputs() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let cur_path = env::current_dir()?; - // external inputs length: 0, state length: 2 - let noirfcircuit = NoirFCircuit::::new( - cur_path - .join("src/noir/test_folder/test_no_external_inputs/target/test_no_external_inputs.json") - .into() -) - ?; - let inputs = vec![Fr::from(2), Fr::from(5)]; - let z_i = Vec::>::new_witness(cs.clone(), || Ok(inputs.clone()))?; - let external_inputs = vec![]; - let output = noirfcircuit.generate_step_constraints( - cs.clone(), - 0, - z_i, - VecFpVar(external_inputs), - )?; - assert_eq!(output[0].value()?, Fr::from(4)); - assert_eq!(output[1].value()?, Fr::from(25)); - Ok(()) - } -} diff --git a/experimental-frontends/src/noir/test_folder/compile.sh b/experimental-frontends/src/noir/test_folder/compile.sh deleted file mode 100755 index 598a7087a..000000000 --- a/experimental-frontends/src/noir/test_folder/compile.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -CUR_DIR=$(pwd) -TEST_PATH="${CUR_DIR}/experimental-frontends/src/noir/test_folder/" -for test_path in test_circuit test_mimc test_no_external_inputs; do - FOLDER="${TEST_PATH}${test_path}/" - cd ${FOLDER} && nargo compile && cd ${TEST_PATH} -done diff --git a/experimental-frontends/src/noir/test_folder/test_circuit/Nargo.toml b/experimental-frontends/src/noir/test_folder/test_circuit/Nargo.toml deleted file mode 100644 index 69429a856..000000000 --- a/experimental-frontends/src/noir/test_folder/test_circuit/Nargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "test_circuit" -type = "bin" -authors = [""] -compiler_version = ">=0.30.0" - -[dependencies] - diff --git a/experimental-frontends/src/noir/test_folder/test_circuit/src/main.nr b/experimental-frontends/src/noir/test_folder/test_circuit/src/main.nr deleted file mode 100644 index 4e0c90a51..000000000 --- a/experimental-frontends/src/noir/test_folder/test_circuit/src/main.nr +++ /dev/null @@ -1,11 +0,0 @@ -fn main(public_inputs: pub [Field; 2], private_inputs: [Field; 2]) -> pub [Field; 2]{ - let a_pub = public_inputs[0]; - let b_pub = public_inputs[1]; - let c_private = private_inputs[0]; - let d_private = private_inputs[1]; - - let out_1 = a_pub * c_private; - let out_2 = b_pub * d_private; - - [out_1, out_2] -} diff --git a/experimental-frontends/src/noir/test_folder/test_mimc/Nargo.toml b/experimental-frontends/src/noir/test_folder/test_mimc/Nargo.toml deleted file mode 100644 index 2c1990941..000000000 --- a/experimental-frontends/src/noir/test_folder/test_mimc/Nargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "test_mimc" -type = "bin" -authors = [""] -compiler_version = ">=0.30.0" - -[dependencies] -mimc = { tag = "v0.1.0", git = "https://github.com/noir-lang/mimc" } diff --git a/experimental-frontends/src/noir/test_folder/test_mimc/src/main.nr b/experimental-frontends/src/noir/test_folder/test_mimc/src/main.nr deleted file mode 100644 index 9956da7b0..000000000 --- a/experimental-frontends/src/noir/test_folder/test_mimc/src/main.nr +++ /dev/null @@ -1,4 +0,0 @@ -pub fn main(x: pub [Field; 1]) -> pub Field { - let hash = mimc::mimc_bn254(x); - hash -} diff --git a/experimental-frontends/src/noir/test_folder/test_no_external_inputs/Nargo.toml b/experimental-frontends/src/noir/test_folder/test_no_external_inputs/Nargo.toml deleted file mode 100644 index 22373d315..000000000 --- a/experimental-frontends/src/noir/test_folder/test_no_external_inputs/Nargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "test_no_external_inputs" -type = "bin" -authors = [""] -compiler_version = ">=0.30.0" - -[dependencies] - diff --git a/experimental-frontends/src/noir/test_folder/test_no_external_inputs/src/main.nr b/experimental-frontends/src/noir/test_folder/test_no_external_inputs/src/main.nr deleted file mode 100644 index 26f6cd7f3..000000000 --- a/experimental-frontends/src/noir/test_folder/test_no_external_inputs/src/main.nr +++ /dev/null @@ -1,9 +0,0 @@ -fn main(public_inputs: pub [Field; 2]) -> pub [Field; 2]{ - let a_pub = public_inputs[0]; - let b_pub = public_inputs[1]; - let out_1 = a_pub * a_pub; - let out_2 = b_pub * b_pub; - - [out_1, out_2] -} - diff --git a/experimental-frontends/src/noname/bridge.rs b/experimental-frontends/src/noname/bridge.rs deleted file mode 100644 index 8c53a1fa8..000000000 --- a/experimental-frontends/src/noname/bridge.rs +++ /dev/null @@ -1,123 +0,0 @@ -// From https://github.com/dmpierre/ark-noname/tree/feat/sonobe-integration -use std::collections::HashMap; - -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystemRef, LinearCombination, SynthesisError, Variable, -}; -use noname::backends::{ - r1cs::{GeneratedWitness, LinearCombination as NoNameLinearCombination, R1CS}, - BackendField, -}; -use noname::witness::CompiledCircuit; -use num_bigint::BigUint; - -pub struct NonameSonobeCircuit<'a, 'b, 'c, F: PrimeField, BF: BackendField> { - pub compiled_circuit: CompiledCircuit>, - pub witness: GeneratedWitness, - pub assigned_z_i: &'a Vec>, - pub assigned_external_inputs: &'b Vec>, - pub assigned_z_i1: &'c Vec>, -} - -impl<'a, 'b, 'c, F: PrimeField, BF: BackendField> ConstraintSynthesizer - for NonameSonobeCircuit<'a, 'b, 'c, F, BF> -{ - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let public_io_length = self.assigned_z_i.len() * 2; - let external_inputs_len = self.assigned_external_inputs.len(); - - // we need to map noname r1cs indexes with sonobe - let mut idx_to_var = HashMap::new(); - - // for both the z_i, z_i1 vectors, we assume that they have been assigned in the order - // with which it will appear in the witness - let mut z_i_pointer = 0; - let mut z_i1_pointer = 0; - let mut external_inputs_pointer = 0; - - // arkworks assigns by default the 1 constant - // assumes witness is: [1, public_outputs, public_inputs, private_inputs, aux] - let witness_size = self.witness.witness.len(); - for idx in 1..witness_size { - if idx <= public_io_length { - if idx <= self.assigned_z_i.len() { - // in noname public outputs come first - // we are in the case of public outputs (z_i1 vector) - // those have already been assigned at specific indexes by sonobe - let var = match &self.assigned_z_i1[z_i1_pointer] { - FpVar::Var(allocated_fp) => allocated_fp.variable, - _ => return Err(SynthesisError::Unsatisfiable), - }; - idx_to_var.insert(idx, var); - z_i1_pointer += 1; - } else { - // we are in the case of public inputs (z_i values) - // those have already been assigned at specific indexes by sonobe - let var = match &self.assigned_z_i[z_i_pointer] { - FpVar::Var(allocated_fp) => allocated_fp.variable, - _ => return Err(SynthesisError::Unsatisfiable), - }; - idx_to_var.insert(idx, var); - z_i_pointer += 1; - } - } else if idx <= public_io_length + external_inputs_len { - // we are in the case of external inputs - // those have already been assigned at specific indexes - let var = match &self.assigned_external_inputs[external_inputs_pointer] { - FpVar::Var(allocated_fp) => allocated_fp.variable, - _ => return Err(SynthesisError::Unsatisfiable), - }; - idx_to_var.insert(idx, var); - external_inputs_pointer += 1; - } else { - // we are in the case of auxiliary private inputs - // we need to assign those - let value: BigUint = Into::into(self.witness.witness[idx]); - let field_element = F::from(value); - let var = cs.new_witness_variable(|| Ok(field_element))?; - idx_to_var.insert(idx, var); - } - } - - if (z_i_pointer != self.assigned_z_i.len()) - || (external_inputs_pointer != self.assigned_external_inputs.len()) - { - return Err(SynthesisError::AssignmentMissing); - } - let make_index = |index: usize| match index == 0 { - true => Ok(Variable::One), - false => { - let var = idx_to_var - .get(&index) - .ok_or(SynthesisError::AssignmentMissing)?; - Ok(var.to_owned()) - } - }; - - let make_lc = |lc_data: NoNameLinearCombination| { - let mut lc = LinearCombination::::zero(); - for (cellvar, coeff) in lc_data.terms.into_iter() { - let idx = make_index(cellvar.index)?; - let coeff = F::from(Into::::into(coeff)); - - lc += (coeff, idx) - } - - // add constant - let constant = F::from(Into::::into(lc_data.constant)); - lc += (constant, make_index(0)?); - Ok(lc) - }; - - for constraint in self.compiled_circuit.circuit.backend.constraints { - let lc_a = make_lc(constraint.a)?; - let lc_b = make_lc(constraint.b)?; - let lc_c = make_lc(constraint.c)?; - cs.enforce_r1cs_constraint(|| lc_a, || lc_b, || lc_c)?; - } - - Ok(()) - } -} diff --git a/experimental-frontends/src/noname/mod.rs b/experimental-frontends/src/noname/mod.rs deleted file mode 100644 index 248734e1a..000000000 --- a/experimental-frontends/src/noname/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::alloc::AllocVar; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use noname::backends::{r1cs::R1CS as R1CSNoname, BackendField}; -use noname::witness::CompiledCircuit; -use num_bigint::BigUint; -use std::marker::PhantomData; - -use folding_schemes::{frontend::FCircuit, Error}; - -pub mod bridge; -pub mod utils; -use crate::utils::{VecF, VecFpVar}; - -use self::bridge::NonameSonobeCircuit; -use self::utils::{compile_source_code, NonameInputs}; - -// `L` indicates the length of the ExternalInputs vector of field elements. -#[derive(Debug, Clone)] -pub struct NonameFCircuit { - pub circuit: CompiledCircuit>, - _f: PhantomData, -} - -impl FCircuit - for NonameFCircuit -{ - type Params = String; - type ExternalInputs = VecF; - type ExternalInputsVar = VecFpVar; - - fn new(code: Self::Params) -> Result { - let compiled_circuit = compile_source_code::(&code).map_err(|_| { - Error::Other("Encountered an error while compiling a noname circuit".to_owned()) - })?; - Ok(NonameFCircuit { - circuit: compiled_circuit, - _f: PhantomData, - }) - } - - fn state_len(&self) -> usize { - SL - } - - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let wtns_external_inputs = - NonameInputs::from_fpvars((&external_inputs.0, "external_inputs".to_string())); - let wtns_ivc_inputs = NonameInputs::from_fpvars((&z_i, "ivc_inputs".to_string())); - let noname_witness = self - .circuit - .generate_witness(wtns_ivc_inputs.0, wtns_external_inputs.0) - .map_err(|_| SynthesisError::Unsatisfiable)?; - let z_i1_end_index = z_i.len() + 1; - let assigned_z_i1: Vec> = (1..z_i1_end_index) - .map(|idx| -> Result, SynthesisError> { - // the assigned zi1 is of the same size than the initial zi and is located in the - // output of the witness vector - // we prefer to assign z_i1 here since (1) we have to return it, (2) we can't return - // anything with the `generate_constraints` method used below - let value: BigUint = Into::into(noname_witness.witness[idx]); - let field_element = F::from(value); - FpVar::::new_witness(cs.clone(), || Ok(field_element)) - }) - .collect::>, SynthesisError>>()?; - - let noname_circuit = NonameSonobeCircuit { - compiled_circuit: self.circuit.clone(), - witness: noname_witness, - assigned_z_i: &z_i, - assigned_external_inputs: &external_inputs.0, - assigned_z_i1: &assigned_z_i1, - }; - noname_circuit.generate_constraints(cs.clone())?; - - Ok(assigned_z_i1) - } -} - -#[cfg(test)] -mod tests { - use ark_bn254::Fr; - use ark_ff::PrimeField; - use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use noname::backends::r1cs::R1csBn254Field; - - use folding_schemes::{frontend::FCircuit, Error}; - - use super::NonameFCircuit; - use crate::utils::VecFpVar; - - /// Native implementation of `NONAME_CIRCUIT_EXTERNAL_INPUTS` - fn external_inputs_step_native(z_i: Vec, external_inputs: Vec) -> Vec { - let xx = external_inputs[0] + z_i[0]; - let yy = external_inputs[1] * z_i[1]; - assert_eq!(yy, xx); - vec![xx, yy] - } - - const NONAME_CIRCUIT_EXTERNAL_INPUTS: &str = - "fn main(pub ivc_inputs: [Field; 2], external_inputs: [Field; 2]) -> [Field; 2] { - let xx = external_inputs[0] + ivc_inputs[0]; - let yy = external_inputs[1] * ivc_inputs[1]; - assert_eq(yy, xx); - return [xx, yy]; -}"; - - const NONAME_CIRCUIT_NO_EXTERNAL_INPUTS: &str = - "fn main(pub ivc_inputs: [Field; 2]) -> [Field; 2] { - let out = ivc_inputs[0] * ivc_inputs[1]; - return [out, ivc_inputs[1]]; -}"; - - #[test] - fn test_step_native() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let params = NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(); - // state length = 2, external inputs length = 2 - let circuit = NonameFCircuit::::new(params)?; - let inputs_public = vec![Fr::from(2), Fr::from(5)]; - let inputs_private = vec![Fr::from(8), Fr::from(2)]; - - let ivc_inputs_var = - Vec::>::new_witness(cs.clone(), || Ok(inputs_public.clone()))?; - let external_inputs_var = - Vec::>::new_witness(cs.clone(), || Ok(inputs_private.clone()))?; - - let z_i1 = circuit.generate_step_constraints( - cs.clone(), - 0, - ivc_inputs_var, - VecFpVar(external_inputs_var), - )?; - let z_i1_native = external_inputs_step_native(inputs_public, inputs_private); - - assert_eq!(z_i1[0].value()?, z_i1_native[0]); - assert_eq!(z_i1[1].value()?, z_i1_native[1]); - Ok(()) - } - - #[test] - fn test_step_constraints() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let params = NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(); - // state length = 2, external inputs length = 2 - let circuit = NonameFCircuit::::new(params)?; - let inputs_public = vec![Fr::from(2), Fr::from(5)]; - let inputs_private = vec![Fr::from(8), Fr::from(2)]; - - let ivc_inputs_var = Vec::>::new_witness(cs.clone(), || Ok(inputs_public))?; - let external_inputs_var = Vec::>::new_witness(cs.clone(), || Ok(inputs_private))?; - - let z_i1 = circuit.generate_step_constraints( - cs.clone(), - 0, - ivc_inputs_var, - VecFpVar(external_inputs_var), - )?; - assert!(cs.is_satisfied()?); - assert_eq!(z_i1[0].value()?, Fr::from(10_u8)); - assert_eq!(z_i1[1].value()?, Fr::from(10_u8)); - Ok(()) - } - - #[test] - fn test_generate_constraints_no_external_inputs() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let params = NONAME_CIRCUIT_NO_EXTERNAL_INPUTS.to_owned(); - let inputs_public = vec![Fr::from(2), Fr::from(5)]; - - let ivc_inputs_var = Vec::>::new_witness(cs.clone(), || Ok(inputs_public))?; - - // state length = 2, external inputs length = 0 - let f_circuit = NonameFCircuit::::new(params)?; - f_circuit.generate_step_constraints(cs.clone(), 0, ivc_inputs_var, VecFpVar(vec![]))?; - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/experimental-frontends/src/noname/utils.rs b/experimental-frontends/src/noname/utils.rs deleted file mode 100644 index b31180138..000000000 --- a/experimental-frontends/src/noname/utils.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::collections::HashMap; - -use ark_ff::PrimeField; -use ark_r1cs_std::{fields::fp::FpVar, GR1CSVar}; -use folding_schemes::Error; -use noname::{ - backends::{r1cs::R1CS, BackendField}, - circuit_writer::CircuitWriter, - compiler::{typecheck_next_file, Sources}, - inputs::JsonInputs, - type_checker::TypeChecker, - witness::CompiledCircuit, -}; -use serde_json::json; - -pub struct NonameInputs(pub JsonInputs); - -impl From<(&Vec, String)> for NonameInputs { - fn from(value: (&Vec, String)) -> Self { - let (values, key) = value; - let mut inputs = HashMap::new(); - if values.is_empty() { - NonameInputs(JsonInputs(inputs)) - } else { - let field_elements: Vec = values - .iter() - .map(|value| { - if value.is_zero() { - "0".to_string() - } else { - value.to_string() - } - }) - .collect(); - inputs.insert(key, json!(field_elements)); - NonameInputs(JsonInputs(inputs)) - } - } -} - -impl NonameInputs { - pub fn from_fpvars(value: (&Vec>, String)) -> Self { - let (values, key) = value; - let mut inputs = HashMap::new(); - if !values.is_empty() { - let field_elements: Vec = values - .iter() - .map(|var| var.value().unwrap_or_default().to_string()) - .collect::>(); - inputs.insert(key, json!(field_elements)); - } - NonameInputs(JsonInputs(inputs)) - } -} - -// from: https://github.com/zksecurity/noname/blob/main/src/tests/modules.rs -// TODO: this will not work in the case where we are using libraries -pub fn compile_source_code( - code: &str, -) -> Result>, Error> { - let mut sources = Sources::new(); - - // parse the transitive dependency - let mut checker = TypeChecker::>::new(); - let _ = typecheck_next_file( - &mut checker, - None, - &mut sources, - "main.no".to_string(), - code.to_string(), - 0, - ) - .unwrap(); - let r1cs = R1CS::::new(); - // compile - CircuitWriter::generate_circuit(checker, r1cs).map_err(|_| { - Error::Other("Encountered an error while compiling a noname circuit".to_owned()) - }) -} diff --git a/experimental-frontends/src/utils.rs b/experimental-frontends/src/utils.rs deleted file mode 100644 index ae6d52eca..000000000 --- a/experimental-frontends/src/utils.rs +++ /dev/null @@ -1,38 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::fmt::Debug; -use core::borrow::Borrow; - -#[derive(Clone, Debug)] -pub struct VecF(pub Vec); -impl Default for VecF { - fn default() -> Self { - VecF(vec![F::zero(); L]) - } -} -#[derive(Clone, Debug)] -pub struct VecFpVar(pub Vec>); -impl AllocVar, F> for VecFpVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let v = Vec::>::new_variable(cs.clone(), || Ok(val.borrow().0.clone()), mode)?; - - Ok(VecFpVar(v)) - }) - } -} -impl Default for VecFpVar { - fn default() -> Self { - VecFpVar(vec![FpVar::::Constant(F::zero()); L]) - } -} diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml deleted file mode 100644 index 9dc674a5a..000000000 --- a/folding-schemes/Cargo.toml +++ /dev/null @@ -1,81 +0,0 @@ -[package] -name = "folding-schemes" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-ec = { workspace = true, features = ["parallel"] } -ark-ff = { workspace = true, features = ["parallel", "asm"] } -ark-poly = { workspace = true, features = ["parallel"] } -ark-std = { workspace = true, features = ["parallel"] } -ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh", "parallel"] } -ark-poly-commit = { workspace = true, features = ["parallel"] } -ark-relations = { workspace = true } -ark-r1cs-std = { workspace = true, features = ["parallel"] } -ark-snark = { workspace = true } -ark-serialize = { workspace = true } -ark-groth16 = { workspace = true, features = ["parallel"] } -ark-bn254 = { workspace = true } -ark-grumpkin = { workspace = true } -thiserror = { workspace = true } -rayon = { workspace = true } -num-bigint = { workspace = true } -num-integer = { workspace = true } -sha3 = { workspace = true } -log = { workspace = true } - -[dev-dependencies] -ark-pallas = { workspace = true, features = ["r1cs"] } -ark-vesta = { workspace = true, features = ["r1cs"] } -ark-bn254 = { workspace = true, features = ["r1cs"] } -ark-grumpkin = { workspace = true, features = ["r1cs"] } -# Note: do not use the MNTx_298 curves in practice due security reasons, here -# we only use them in the tests. -ark-mnt4-298 = { workspace = true, features = ["r1cs"] } -ark-mnt6-298 = { workspace = true, features = ["r1cs"] } -rand = { workspace = true } -num-bigint = { workspace = true, features = ["rand"] } - -# for benchmarks -criterion = { workspace = true } -pprof = { workspace = true, features = ["criterion", "flamegraph"] } - -# This allows the crate to be built when targeting WASM. -# See more at: https://docs.rs/getrandom/#webassembly-support -[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] -getrandom = { workspace = true, features = ["js"] } - -[features] -default = ["parallel"] -parallel = [] -light-test = [] - - -[[bench]] -name = "nova" -path = "../benches/nova.rs" -harness = false - -[[bench]] -name = "hypernova" -path = "../benches/hypernova.rs" -harness = false - -[[bench]] -name = "protogalaxy" -path = "../benches/protogalaxy.rs" -harness = false - -[[example]] -name = "sha256" -path = "../examples/sha256.rs" - -[[example]] -name = "multi_inputs" -path = "../examples/multi_inputs.rs" - -[[example]] -name = "external_inputs" -path = "../examples/external_inputs.rs" diff --git a/folding-schemes/src/arith/ccs/circuits.rs b/folding-schemes/src/arith/ccs/circuits.rs deleted file mode 100644 index fe1fdf1f3..000000000 --- a/folding-schemes/src/arith/ccs/circuits.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::CCS; -use crate::utils::gadgets::SparseMatrixVar; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::borrow::Borrow; - -/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. -#[derive(Debug, Clone)] -pub struct CCSMatricesVar { - // we only need native representation, so the constraint field==F - pub M: Vec>>, -} - -impl AllocVar, F> for CCSMatricesVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - _mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - let M: Vec>> = val - .borrow() - .M - .iter() - .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) - .collect::>()?; - Ok(Self { M }) - }) - } -} diff --git a/folding-schemes/src/arith/ccs/mod.rs b/folding-schemes/src/arith/ccs/mod.rs deleted file mode 100644 index c1549d01e..000000000 --- a/folding-schemes/src/arith/ccs/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -use ark_ff::PrimeField; -use ark_std::log2; - -use crate::utils::vec::{ - hadamard, is_zero_vec, mat_vec_mul, vec_add, vec_scalar_mul, SparseMatrix, -}; -use crate::Error; - -use super::{r1cs::R1CS, ArithRelation}; -use super::{Arith, ArithSerializer}; - -pub mod circuits; - -/// CCS represents the Customizable Constraint Systems structure defined in -/// the [CCS paper](https://eprint.iacr.org/2023/552) -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CCS { - /// m: number of rows in M_i (such that M_i \in F^{m, n}) - m: usize, - /// n = |z|, number of cols in M_i - n: usize, - /// l = |io|, size of public input/output - l: usize, - /// t = |M|, number of matrices - pub t: usize, - /// q = |c| = |S|, number of multisets - q: usize, - /// d: max degree in each variable - d: usize, - /// s = log(m), dimension of x - pub s: usize, - - /// vector of matrices - pub M: Vec>, - /// vector of multisets - pub S: Vec>, - /// vector of coefficients - pub c: Vec, -} - -impl CCS { - /// Evaluates the CCS relation at a given vector of assignments `z` - pub fn eval_at_z(&self, z: &[F]) -> Result, Error> { - let mut result = vec![F::zero(); self.m]; - - for i in 0..self.q { - // extract the needed M_j matrices out of S_i - let vec_M_j: Vec<&SparseMatrix> = self.S[i].iter().map(|j| &self.M[*j]).collect(); - - // complete the hadamard chain - let mut hadamard_result = vec![F::one(); self.m]; - for M_j in vec_M_j.into_iter() { - hadamard_result = hadamard(&hadamard_result, &mat_vec_mul(M_j, z)?)?; - } - - // multiply by the coefficient of this step - let c_M_j_z = vec_scalar_mul(&hadamard_result, &self.c[i]); - - // add it to the final vector - result = vec_add(&result, &c_M_j_z)?; - } - - Ok(result) - } -} - -impl Arith for CCS { - #[inline] - fn degree(&self) -> usize { - self.d - } - - #[inline] - fn n_constraints(&self) -> usize { - self.m - } - - #[inline] - fn n_variables(&self) -> usize { - self.n - } - - #[inline] - fn n_public_inputs(&self) -> usize { - self.l - } - - #[inline] - fn n_witnesses(&self) -> usize { - self.n_variables() - self.n_public_inputs() - 1 - } - - fn split_z(&self, z: &[P]) -> (Vec

, Vec

) { - (z[self.l + 1..].to_vec(), z[1..self.l + 1].to_vec()) - } -} - -impl, U: AsRef<[F]>> ArithRelation for CCS { - type Evaluation = Vec; - - fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_at_z(&[&[F::one()], u.as_ref(), w.as_ref()].concat()) - } - - fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { - is_zero_vec(&e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -impl ArithSerializer for CCS { - fn params_to_le_bytes(&self) -> Vec { - [ - (self.l as u64).to_le_bytes(), - (self.m as u64).to_le_bytes(), - (self.n as u64).to_le_bytes(), - (self.t as u64).to_le_bytes(), - (self.q as u64).to_le_bytes(), - (self.d as u64).to_le_bytes(), - ] - .concat() - } -} - -impl From> for CCS { - fn from(r1cs: R1CS) -> Self { - let m = r1cs.n_constraints(); - let n = r1cs.n_variables(); - CCS { - m, - n, - l: r1cs.n_public_inputs(), - s: log2(m) as usize, - t: 3, - q: 2, - d: r1cs.degree(), - - S: vec![vec![0, 1], vec![2]], - c: vec![F::one(), F::one().neg()], - M: vec![r1cs.A, r1cs.B, r1cs.C], - } - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::{ - arith::r1cs::tests::{get_test_r1cs, get_test_z as r1cs_get_test_z, get_test_z_split}, - utils::vec::is_zero_vec, - }; - use ark_pallas::Fr; - - pub fn get_test_ccs() -> CCS { - get_test_r1cs::().into() - } - pub fn get_test_z(input: usize) -> Vec { - r1cs_get_test_z(input) - } - - #[test] - fn test_eval_ccs_relation() -> Result<(), Error> { - let ccs = get_test_ccs::(); - let (_, x, mut w) = get_test_z_split(3); - - let f_w = ccs.eval_relation(&w, &x)?; - assert!(is_zero_vec(&f_w)); - - w[1] = Fr::from(111); - let f_w = ccs.eval_relation(&w, &x)?; - assert!(!is_zero_vec(&f_w)); - Ok(()) - } - - /// Test that a basic CCS relation can be satisfied - #[test] - fn test_check_ccs_relation() -> Result<(), Error> { - let ccs = get_test_ccs::(); - let (_, x, w) = get_test_z_split(3); - - ccs.check_relation(&w, &x)?; - Ok(()) - } -} diff --git a/folding-schemes/src/arith/mod.rs b/folding-schemes/src/arith/mod.rs deleted file mode 100644 index fbbff81fe..000000000 --- a/folding-schemes/src/arith/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -use ark_ff::PrimeField; -use ark_relations::gr1cs::SynthesisError; -use ark_std::rand::RngCore; - -use crate::{commitment::CommitmentScheme, folding::traits::Dummy, Curve, Error}; - -pub mod ccs; -pub mod r1cs; - -/// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we -/// define methods for getting information about the constraint system. -pub trait Arith: Clone { - /// Returns the degree of the constraint system - fn degree(&self) -> usize; - - /// Returns the number of constraints in the constraint system - fn n_constraints(&self) -> usize; - - /// Returns the number of variables in the constraint system - fn n_variables(&self) -> usize; - - /// Returns the number of public inputs / public IO / instances / statements - /// in the constraint system - fn n_public_inputs(&self) -> usize; - - /// Returns the number of witnesses / secret inputs in the constraint system - fn n_witnesses(&self) -> usize; - - /// Returns a tuple containing (w, x) (witness and public inputs respectively) - fn split_z(&self, z: &[F]) -> (Vec, Vec); -} - -/// `ArithRelation` *treats a constraint system as a relation* between a witness -/// of type `W` and a statement / public input / public IO / instance of type -/// `U`, and in this trait, we define the necessary operations on the relation. -/// -/// Note that the same constraint system may support different types of `W` and -/// `U`, and the satisfiability check may vary. -/// -/// For example, both plain R1CS and relaxed R1CS are represented by 3 matrices, -/// but the types of `W` and `U` are different: -/// - The plain R1CS has `W` and `U` as vectors of field elements. -/// -/// `W = w` and `U = x` satisfy R1CS if `Az ∘ Bz = Cz`, where `z = [1, x, w]`. -/// -/// - In Nova, Relaxed R1CS has `W` as [`crate::folding::nova::Witness`], -/// and `U` as [`crate::folding::nova::CommittedInstance`]. -/// -/// `W = (w, e, ...)` and `U = (u, x, ...)` satisfy Relaxed R1CS if -/// `Az ∘ Bz = uCz + e`, where `z = [u, x, w]`. -/// (commitments in `U` are not checked here) -/// -/// Also, `W` and `U` have non-native field elements as their components when -/// used as CycleFold witness and instance. -/// -/// - In ProtoGalaxy, Relaxed R1CS has `W` as [`crate::folding::protogalaxy::Witness`], -/// and `U` as [`crate::folding::protogalaxy::CommittedInstance`]. -/// -/// `W = (w, ...)` and `U = (x, e, β, ...)` satisfy Relaxed R1CS if -/// `e = Σ pow_i(β) v_i`, where `v = Az ∘ Bz - Cz`, `z = [1, x, w]`. -/// (commitments in `U` are not checked here) -/// -/// This is also the case of CCS, where `W` and `U` may be vectors of field -/// elements, [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::lcccs::LCCCS`], -/// or [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::cccs::CCCS`]. -pub trait ArithRelation: Arith { - type Evaluation; - - /// Returns a dummy witness and instance - fn dummy_witness_instance<'a>(&'a self) -> (W, U) - where - W: Dummy<&'a Self>, - U: Dummy<&'a Self>, - { - (W::dummy(self), U::dummy(self)) - } - - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. - /// - /// The evaluation result is usually a vector of field elements. - /// For instance: - /// - Evaluating the plain R1CS at `W = w` and `U = x` returns - /// `Az ∘ Bz - Cz`, where `z = [1, x, w]`. - /// - /// - Evaluating the relaxed R1CS in Nova at `W = (w, e, ...)` and - /// `U = (u, x, ...)` returns `Az ∘ Bz - uCz`, where `z = [u, x, w]`. - /// - /// - Evaluating the relaxed R1CS in ProtoGalaxy at `W = (w, ...)` and - /// `U = (x, e, β, ...)` returns `Az ∘ Bz - Cz`, where `z = [1, x, w]`. - /// - /// However, we use `Self::Evaluation` to represent the evaluation result - /// for future extensibility. - fn eval_relation(&self, w: &W, u: &U) -> Result; - - /// Checks if the evaluation result is valid. The witness `w` and instance - /// `u` are also parameters, because the validity check may need information - /// contained in `w` and/or `u`. - /// - /// For instance: - /// - The evaluation `v` of plain R1CS at satisfying `W` and `U` should be - /// an all-zero vector. - /// - /// - The evaluation `v` of relaxed R1CS in Nova at satisfying `W` and `U` - /// should be equal to the error term `e` in the witness. - /// - /// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W` - /// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error - /// term in the committed instance. - fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; - - /// Checks if witness `w` and instance `u` satisfy the constraint system - /// `self` by first computing the evaluation result and then checking the - /// validity of the evaluation result. - /// - /// Used only for testing. - fn check_relation(&self, w: &W, u: &U) -> Result<(), Error> { - let e = self.eval_relation(w, u)?; - Self::check_evaluation(w, u, e) - } -} - -/// `ArithSerializer` is for serializing constraint systems. -/// -/// Currently we only support converting parameters to bytes, but in the future -/// we may consider implementing methods for serializing the actual data (e.g., -/// R1CS matrices). -pub trait ArithSerializer { - /// Returns the bytes that represent the parameters, that is, the matrices sizes, the amount of - /// public inputs, etc, without the matrices/polynomials values. - fn params_to_le_bytes(&self) -> Vec; -} - -/// `ArithSampler` allows sampling random pairs of witness and instance that -/// satisfy the constraint system `self`. -/// -/// This is useful for constructing a zero-knowledge layer for a folding-based -/// IVC. -/// An example of such a layer can be found in Appendix D of the [HyperNova] -/// paper. -/// -/// Note that we use a separate trait for sampling, because this operation may -/// not be supported by all witness-instance pairs. -/// For instance, it is difficult (if not impossible) to do this for `w` and `x` -/// in a plain R1CS. -/// -/// [HyperNova]: https://eprint.iacr.org/2023/573.pdf -pub trait ArithSampler: ArithRelation { - /// Samples a random witness and instance that satisfy the constraint system. - fn sample_witness_instance>( - &self, - params: &CS::ProverParams, - rng: impl RngCore, - ) -> Result<(W, U), Error>; -} - -/// `ArithRelationGadget` defines the in-circuit counterparts of operations -/// specified in `ArithRelation` on constraint systems. -pub trait ArithRelationGadget { - type Evaluation; - - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. - fn eval_relation(&self, w: &WVar, u: &UVar) -> Result; - - /// Generates constraints for enforcing that witness `w` and instance `u` - /// satisfy the constraint system `self` by first computing the evaluation - /// result and then checking the validity of the evaluation result. - fn enforce_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> { - let e = self.eval_relation(w, u)?; - Self::enforce_evaluation(w, u, e) - } - - /// Generates constraints for enforcing that the evaluation result is valid. - /// The witness `w` and instance `u` are also parameters, because the - /// validity check may need information contained in `w` and/or `u`. - fn enforce_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>; -} diff --git a/folding-schemes/src/arith/r1cs/circuits.rs b/folding-schemes/src/arith/r1cs/circuits.rs deleted file mode 100644 index 5900d2efd..000000000 --- a/folding-schemes/src/arith/r1cs/circuits.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::{ - arith::ArithRelationGadget, - utils::gadgets::{EquivalenceGadget, MatrixGadget, SparseMatrixVar, VectorGadget}, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, marker::PhantomData, One}; - -use super::R1CS; - -/// An in-circuit representation of the `R1CS` struct. -/// -/// `M` is for the modulo operation involved in the satisfiability check when -/// the underlying `FVar` is `NonNativeUintVar`. -#[derive(Debug, Clone)] -pub struct R1CSMatricesVar { - _m: PhantomData, - pub A: SparseMatrixVar, - pub B: SparseMatrixVar, - pub C: SparseMatrixVar, -} - -impl> - AllocVar, ConstraintF> for R1CSMatricesVar -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - _mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - Ok(Self { - _m: PhantomData, - A: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?, - B: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?, - C: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?, - }) - }) - } -} - -impl R1CSMatricesVar -where - SparseMatrixVar: MatrixGadget, - [FVar]: VectorGadget, -{ - pub fn eval_at_z(&self, z: &[FVar]) -> Result<(Vec, Vec), SynthesisError> { - // Multiply Cz by z[0] (u) here, allowing this method to be reused for - // both relaxed and unrelaxed R1CS. - let Az = self.A.mul_vector(z)?; - let Bz = self.B.mul_vector(z)?; - let Cz = self.C.mul_vector(z)?; - let uCz = Cz.mul_scalar(&z[0])?; - let AzBz = Az.hadamard(&Bz)?; - Ok((AzBz, uCz)) - } -} - -impl, UVar: AsRef<[FVar]>> ArithRelationGadget - for R1CSMatricesVar -where - SparseMatrixVar: MatrixGadget, - [FVar]: VectorGadget + EquivalenceGadget, - FVar: Clone + One, -{ - /// Evaluation is a tuple of two vectors (`AzBz` and `uCz`) instead of a - /// single vector `AzBz - uCz`, because subtraction is not supported for - /// `FVar = NonNativeUintVar`. - type Evaluation = (Vec, Vec); - - fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { - self.eval_at_z(&[&[FVar::one()], u.as_ref(), w.as_ref()].concat()) - } - - fn enforce_evaluation( - _w: &WVar, - _u: &UVar, - (lhs, rhs): Self::Evaluation, - ) -> Result<(), SynthesisError> { - lhs.enforce_equivalent(&rhs) - } -} - -#[cfg(test)] -pub mod tests { - use ark_crypto_primitives::crh::{ - sha256::{ - constraints::{Sha256Gadget, UnitVar}, - Sha256, - }, - CRHScheme, CRHSchemeGadget, - }; - - use ark_ff::BigInteger; - use ark_pallas::{Fq, Fr, Projective}; - use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar, uint8::UInt8}; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; - use ark_std::{ - cmp::max, - rand::{thread_rng, Rng}, - One, UniformRand, - }; - use ark_vesta::Projective as Projective2; - - use super::*; - use crate::arith::{ - r1cs::{ - extract_r1cs, extract_w_x, - tests::{get_test_r1cs, get_test_z}, - }, - Arith, ArithRelation, - }; - use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; - use crate::folding::{ - circuits::{ - cyclefold::{CycleFoldCommittedInstanceVar, CycleFoldWitnessVar}, - nonnative::uint::NonNativeUintVar, - }, - nova::{ - decider_eth_circuit::WitnessVar, nifs::nova_circuits::CommittedInstanceVar, - CommittedInstance, Witness, - }, - }; - use crate::frontend::{ - utils::{ - cubic_step_native, custom_step_native, CubicFCircuit, CustomFCircuit, WrapperCircuit, - }, - FCircuit, - }; - use crate::{Curve, Error}; - - fn prepare_instances, R: Rng>( - mut rng: R, - r1cs: &R1CS, - z: &[C::ScalarField], - ) -> Result<(Witness, CommittedInstance), Error> { - let (w, x) = r1cs.split_z(z); - - let (cs_pp, _) = CS::setup(&mut rng, max(w.len(), r1cs.A.n_rows))?; - - let mut w = Witness::new::(w, r1cs.A.n_rows, &mut rng); - w.E = r1cs.eval_at_z(z)?; - let mut u = w.commit::(&cs_pp, x)?; - u.u = z[0]; - - Ok((w, u)) - } - - #[test] - fn test_relaxed_r1cs_small_gadget_handcrafted() -> Result<(), Error> { - let rng = &mut thread_rng(); - - let r1cs: R1CS = get_test_r1cs(); - let mut z = get_test_z(3); - z[0] = Fr::rand(rng); - let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z)?; - - let cs = ConstraintSystem::::new_ref(); - - let wVar = WitnessVar::new_witness(cs.clone(), || Ok(w))?; - let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(u))?; - let r1csVar = R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs))?; - - r1csVar.enforce_relation(&wVar, &uVar)?; - assert!(cs.is_satisfied()?); - Ok(()) - } - - // gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been - // initialized. - fn test_relaxed_r1cs_gadget>(circuit: CS) -> Result<(), Error> { - let rng = &mut thread_rng(); - - let cs = ConstraintSystem::::new_ref(); - - circuit.generate_constraints(cs.clone())?; - cs.finalize(); - assert!(cs.is_satisfied()?); - - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - - let r1cs = extract_r1cs::(&cs)?; - let (w, x) = extract_w_x::(&cs); - r1cs.check_relation(&w, &x)?; - let mut z = [vec![Fr::one()], x, w].concat(); - z[0] = Fr::rand(rng); - - let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z)?; - r1cs.check_relation(&w, &u)?; - - // set new CS for the circuit that checks the RelaxedR1CS of our original circuit - let cs = ConstraintSystem::::new_ref(); - // prepare the inputs for our circuit - let wVar = WitnessVar::new_witness(cs.clone(), || Ok(w))?; - let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(u))?; - let r1csVar = R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs))?; - - r1csVar.enforce_relation(&wVar, &uVar)?; - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_relaxed_r1cs_small_gadget_arkworks() -> Result<(), Error> { - let z_i = vec![Fr::from(3_u32)]; - let cubic_circuit = CubicFCircuit::::new(())?; - let circuit = WrapperCircuit::> { - FC: cubic_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(cubic_step_native(z_i)), - }; - - test_relaxed_r1cs_gadget(circuit) - } - - struct Sha256TestCircuit { - _f: PhantomData, - pub x: Vec, - pub y: Vec, - } - impl ConstraintSynthesizer for Sha256TestCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = Vec::>::new_witness(cs.clone(), || Ok(self.x))?; - let y = Vec::>::new_input(cs.clone(), || Ok(self.y))?; - - let unitVar = UnitVar::default(); - let comp_y = as CRHSchemeGadget>::evaluate(&unitVar, &x)?; - comp_y.0.enforce_equal(&y)?; - Ok(()) - } - } - #[test] - fn test_relaxed_r1cs_medium_gadget_arkworks() -> Result<(), Error> { - let x = Fr::from(5_u32).into_bigint().to_bytes_le(); - let y = - ::evaluate(&(), x.clone()).map_err(|_| Error::EvaluationFail)?; - - let circuit = Sha256TestCircuit:: { - _f: PhantomData, - x, - y, - }; - test_relaxed_r1cs_gadget(circuit) - } - - #[test] - fn test_relaxed_r1cs_custom_circuit() -> Result<(), Error> { - let n_constraints = 10_000; - let custom_circuit = CustomFCircuit::::new(n_constraints)?; - let z_i = vec![Fr::from(5_u32)]; - let circuit = WrapperCircuit::> { - FC: custom_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(custom_step_native(z_i, n_constraints)), - }; - test_relaxed_r1cs_gadget(circuit) - } - - #[test] - fn test_relaxed_r1cs_nonnative_circuit() -> Result<(), Error> { - let n_constraints = 10; - let rng = &mut thread_rng(); - - let cs = ConstraintSystem::::new_ref(); - // in practice we would use CycleFoldCircuit, but is a very big circuit (when computed - // non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a - // custom circuit. - let custom_circuit = CustomFCircuit::::new(n_constraints)?; - let z_i = vec![Fq::from(5_u32)]; - let circuit = WrapperCircuit::> { - FC: custom_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(custom_step_native(z_i, n_constraints)), - }; - circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - let (w, x) = extract_w_x::(&cs); - let z = [vec![Fq::rand(rng)], x, w].concat(); - - let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z)?; - - // natively - let cs = ConstraintSystem::::new_ref(); - let wVar = WitnessVar::new_witness(cs.clone(), || Ok(&w))?; - let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(&u))?; - let r1csVar = R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(&r1cs))?; - r1csVar.enforce_relation(&wVar, &uVar)?; - - // non-natively - let cs = ConstraintSystem::::new_ref(); - let wVar = CycleFoldWitnessVar::new_witness(cs.clone(), || Ok(w))?; - let uVar = CycleFoldCommittedInstanceVar::new_witness(cs.clone(), || Ok(u))?; - let r1csVar = - R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs))?; - r1csVar.enforce_relation(&wVar, &uVar)?; - Ok(()) - } -} diff --git a/folding-schemes/src/arith/r1cs/mod.rs b/folding-schemes/src/arith/r1cs/mod.rs deleted file mode 100644 index db714aff4..000000000 --- a/folding-schemes/src/arith/r1cs/mod.rs +++ /dev/null @@ -1,294 +0,0 @@ -use ark_ff::PrimeField; -use ark_relations::gr1cs::ConstraintSystem; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::rand::Rng; - -use super::ccs::CCS; -use super::{Arith, ArithRelation, ArithSerializer}; -use crate::folding::traits::Dummy; -use crate::utils::vec::{ - hadamard, is_zero_vec, mat_vec_mul, vec_scalar_mul, vec_sub, SparseMatrix, -}; -use crate::Error; - -pub mod circuits; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct R1CS { - l: usize, // io len - pub A: SparseMatrix, - pub B: SparseMatrix, - pub C: SparseMatrix, -} - -impl R1CS { - /// Evaluates the R1CS relation at a given vector of variables `z` - pub fn eval_at_z(&self, z: &[F]) -> Result, Error> { - if z.len() != self.A.n_cols { - return Err(Error::NotSameLength( - "z.len()".to_string(), - z.len(), - "number of variables in R1CS".to_string(), - self.A.n_cols, - )); - } - - let Az = mat_vec_mul(&self.A, z)?; - let Bz = mat_vec_mul(&self.B, z)?; - let Cz = mat_vec_mul(&self.C, z)?; - // Multiply Cz by z[0] (u) here, allowing this method to be reused for - // both relaxed and plain R1CS. - let uCz = vec_scalar_mul(&Cz, &z[0]); - let AzBz = hadamard(&Az, &Bz)?; - vec_sub(&AzBz, &uCz) - } -} - -impl Arith for R1CS { - #[inline] - fn degree(&self) -> usize { - 2 - } - - #[inline] - fn n_constraints(&self) -> usize { - self.A.n_rows - } - - #[inline] - fn n_variables(&self) -> usize { - self.A.n_cols - } - - #[inline] - fn n_public_inputs(&self) -> usize { - self.l - } - - #[inline] - fn n_witnesses(&self) -> usize { - self.n_variables() - self.n_public_inputs() - 1 - } - - fn split_z(&self, z: &[P]) -> (Vec

, Vec

) { - (z[self.l + 1..].to_vec(), z[1..self.l + 1].to_vec()) - } -} - -impl, U: AsRef<[F]>> ArithRelation for R1CS { - type Evaluation = Vec; - - fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_at_z(&[&[F::one()], u.as_ref(), w.as_ref()].concat()) - } - - fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { - is_zero_vec(&e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -impl ArithSerializer for R1CS { - fn params_to_le_bytes(&self) -> Vec { - [ - (self.l as u64).to_le_bytes(), - (self.A.n_rows as u64).to_le_bytes(), - (self.A.n_cols as u64).to_le_bytes(), - ] - .concat() - } -} - -impl Dummy<(usize, usize, usize)> for R1CS { - fn dummy((n_constraints, n_variables, n_public_inputs): (usize, usize, usize)) -> Self { - Self { - l: n_public_inputs, - A: SparseMatrix::dummy((n_constraints, n_variables)), - B: SparseMatrix::dummy((n_constraints, n_variables)), - C: SparseMatrix::dummy((n_constraints, n_variables)), - } - } -} - -impl R1CS { - pub fn empty() -> Self { - Self::dummy((0, 0, 0)) - } - pub fn rand(rng: &mut R, n_rows: usize, n_cols: usize) -> Self { - Self { - l: 1, - A: SparseMatrix::rand(rng, n_rows, n_cols), - B: SparseMatrix::rand(rng, n_rows, n_cols), - C: SparseMatrix::rand(rng, n_rows, n_cols), - } - } -} - -impl From> for R1CS { - fn from(ccs: CCS) -> Self { - R1CS:: { - l: ccs.n_public_inputs(), - A: ccs.M[0].clone(), - B: ccs.M[1].clone(), - C: ccs.M[2].clone(), - } - } -} - -/// extracts arkworks ConstraintSystem matrices into crate::utils::vec::SparseMatrix format as R1CS -/// struct. -pub fn extract_r1cs(cs: &ConstraintSystem) -> Result, Error> { - let matrices_map = cs.to_matrices().map_err(|_| { - Error::ConversionError( - "ConstraintSystem".into(), - "ConstraintMatrices".into(), - "The matrices have not been generated yet".into(), - ) - })?; - - // Get the R1CS predicate matrices - let r1cs_matrices = matrices_map.get("R1CS").ok_or_else(|| { - Error::ConversionError( - "ConstraintSystem".into(), - "R1CS matrices".into(), - "No R1CS predicate found in constraint system".into(), - ) - })?; - - // The R1CS predicate should have exactly 3 matrices (A, B, C) - if r1cs_matrices.len() != 3 { - return Err(Error::ConversionError( - "R1CS matrices".into(), - "3 matrices (A, B, C)".into(), - format!("Found {} matrices", r1cs_matrices.len()), - )); - } - - let n_rows = cs.num_constraints(); - let n_cols = cs.num_instance_variables + cs.num_witness_variables; // cs.num_instance_variables already counts the 1 - - let A = SparseMatrix:: { - n_rows, - n_cols, - coeffs: r1cs_matrices[0].clone(), - }; - let B = SparseMatrix:: { - n_rows, - n_cols, - coeffs: r1cs_matrices[1].clone(), - }; - let C = SparseMatrix:: { - n_rows, - n_cols, - coeffs: r1cs_matrices[2].clone(), - }; - - Ok(R1CS:: { - l: cs.num_instance_variables - 1, // -1 to subtract the first '1' - A, - B, - C, - }) -} - -/// extracts the witness and the public inputs from arkworks ConstraintSystem. -pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) { - let witness = cs - .witness_assignment() - .expect("witness_assignment failed") - .to_vec(); - let instance = cs - .instance_assignment() - .expect("instance_assignment failed"); - ( - witness, - // skip the first element which is '1' - instance[1..].to_vec(), - ) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::utils::vec::{ - is_zero_vec, - tests::{to_F_matrix, to_F_vec}, - }; - - use ark_pallas::Fr; - - pub fn get_test_r1cs() -> R1CS { - // R1CS for: x^3 + x + 5 = y (example from article - // https://www.vitalik.ca/general/2016/12/10/qap.html ) - let A = to_F_matrix::(vec![ - vec![0, 1, 0, 0, 0, 0], - vec![0, 0, 0, 1, 0, 0], - vec![0, 1, 0, 0, 1, 0], - vec![5, 0, 0, 0, 0, 1], - ]); - let B = to_F_matrix::(vec![ - vec![0, 1, 0, 0, 0, 0], - vec![0, 1, 0, 0, 0, 0], - vec![1, 0, 0, 0, 0, 0], - vec![1, 0, 0, 0, 0, 0], - ]); - let C = to_F_matrix::(vec![ - vec![0, 0, 0, 1, 0, 0], - vec![0, 0, 0, 0, 1, 0], - vec![0, 0, 0, 0, 0, 1], - vec![0, 0, 1, 0, 0, 0], - ]); - - R1CS:: { l: 1, A, B, C } - } - - pub fn get_test_z(input: usize) -> Vec { - // z = (1, io, w) - to_F_vec(vec![ - 1, - input, // io - input * input * input + input + 5, // x^3 + x + 5 - input * input, // x^2 - input * input * input, // x^2 * x - input * input * input + input, // x^3 + x - ]) - } - - pub fn get_test_z_split(input: usize) -> (F, Vec, Vec) { - // z = (1, io, w) - ( - F::one(), - to_F_vec(vec![ - input, // io - ]), - to_F_vec(vec![ - input * input * input + input + 5, // x^3 + x + 5 - input * input, // x^2 - input * input * input, // x^2 * x - input * input * input + input, // x^3 + x - ]), - ) - } - - #[test] - fn test_eval_r1cs_relation() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let r1cs = get_test_r1cs::(); - let (_, x, mut w) = get_test_z_split::(rng.gen::() as usize); - - let f_w = r1cs.eval_relation(&w, &x)?; - assert!(is_zero_vec(&f_w)); - - w[1] = Fr::from(111); - let f_w = r1cs.eval_relation(&w, &x)?; - assert!(!is_zero_vec(&f_w)); - Ok(()) - } - - #[test] - fn test_check_r1cs_relation() -> Result<(), Error> { - let r1cs = get_test_r1cs::(); - let (_, x, w) = get_test_z_split(5); - r1cs.check_relation(&w, &x)?; - Ok(()) - } -} diff --git a/folding-schemes/src/commitment/ipa.rs b/folding-schemes/src/commitment/ipa.rs deleted file mode 100644 index cf840e3b5..000000000 --- a/folding-schemes/src/commitment/ipa.rs +++ /dev/null @@ -1,718 +0,0 @@ -/// IPA implements the modified Inner Product Argument described in -/// [Halo](https://eprint.iacr.org/2019/1021.pdf). The variable names used follow the paper -/// notation in order to make it more readable. -/// -/// The implementation does the following optimizations in order to reduce the amount of -/// constraints in the circuit: -/// i. computation is done in log time following a modification of the equation 3 in section -/// 3.2 from the paper. -/// ii. s computation is done in 2^{k+1}-2 instead of k*2^k. -use ark_ec::AffineRepr; -use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - convert::ToBitsGadget, - eq::EqGadget, - fields::{emulated_fp::EmulatedFpVar, FieldVar}, - groups::CurveVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{cfg_iter, rand::RngCore, UniformRand, Zero}; -use core::{borrow::Borrow, marker::PhantomData}; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; - -use super::{pedersen::Params as PedersenParams, CommitmentScheme}; -use crate::folding::circuits::CF2; -use crate::transcript::Transcript; -use crate::utils::{ - powers_of, - vec::{vec_add, vec_scalar_mul}, -}; -use crate::{Curve, Error}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof { - a: C::ScalarField, - l: Vec, - r: Vec, - L: Vec, - R: Vec, -} - -/// IPA implements the Inner Product Argument protocol following the CommitmentScheme trait. The -/// `H` parameter indicates if to use the commitment in hiding mode or not. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct IPA { - _c: PhantomData, -} - -/// Implements the CommitmentScheme trait for IPA -impl CommitmentScheme for IPA { - type ProverParams = PedersenParams; - type VerifierParams = PedersenParams; - type Proof = (Proof, C::ScalarField, C::ScalarField); // (proof, v=p(x), r=blinding factor) - type ProverChallenge = (C::ScalarField, C, Vec); - type Challenge = (C::ScalarField, C, Vec); - - fn is_hiding() -> bool { - if H { - return true; - } - false - } - - fn setup( - mut rng: impl RngCore, - len: usize, - ) -> Result<(Self::ProverParams, Self::VerifierParams), Error> { - let generators: Vec = std::iter::repeat_with(|| C::Affine::rand(&mut rng)) - .take(len.next_power_of_two()) - .collect(); - let p = PedersenParams:: { - h: C::rand(&mut rng), - generators, - }; - Ok((p.clone(), p)) - } - - fn commit( - params: &PedersenParams, - a: &[C::ScalarField], - r: &C::ScalarField, // blinding factor - ) -> Result { - if params.generators.len() < a.len() { - return Err(Error::PedersenParamsLen(params.generators.len(), a.len())); - } - if !H && (!r.is_zero()) { - return Err(Error::BlindingNotZero); - } - - // h⋅r + - // use msm_unchecked because we already ensured at the if that lengths match - if !H { - return Ok(C::msm_unchecked(¶ms.generators[..a.len()], a)); - } - Ok(params.h.mul(r) + C::msm_unchecked(¶ms.generators[..a.len()], a)) - } - - fn prove( - params: &Self::ProverParams, - transcript: &mut impl Transcript, - P: &C, // commitment - a: &[C::ScalarField], // vector - blind: &C::ScalarField, - rng: Option<&mut dyn RngCore>, - ) -> Result { - if !a.len().is_power_of_two() { - return Err(Error::NotPowerOfTwo("a".to_string(), a.len())); - } - if !H && (!blind.is_zero()) { - return Err(Error::BlindingNotZero); - } - let d = a.len(); - let k = (f64::from(d as u32).log2()) as usize; - - if params.generators.len() < a.len() { - return Err(Error::PedersenParamsLen(params.generators.len(), a.len())); - } - // blinding factors - let l: Vec; - let r: Vec; - if H { - let rng = rng.ok_or(Error::MissingRandomness)?; - l = std::iter::repeat_with(|| C::ScalarField::rand(rng)) - .take(k) - .collect(); - r = std::iter::repeat_with(|| C::ScalarField::rand(rng)) - .take(k) - .collect(); - } else { - l = vec![]; - r = vec![]; - } - - transcript.absorb_nonnative(P); - let x = transcript.get_challenge(); // challenge value at which we evaluate - let s = transcript.get_challenge(); - let U = C::generator().mul(s); - - let mut a = a.to_owned(); - let mut b = powers_of(x, d); - let v = inner_prod(&a, &b)?; - - let mut G = params.generators.clone(); - - let mut L: Vec = vec![C::zero(); k]; - let mut R: Vec = vec![C::zero(); k]; - - // u challenges - let mut u: Vec = vec![C::ScalarField::zero(); k]; - for j in (0..k).rev() { - let m = a.len() / 2; - - if H { - L[j] = C::msm_unchecked(&G[m..], &a[..m]) - + params.h.mul(l[j]) - + U.mul(inner_prod(&a[..m], &b[m..])?); - R[j] = C::msm_unchecked(&G[..m], &a[m..]) - + params.h.mul(r[j]) - + U.mul(inner_prod(&a[m..], &b[..m])?); - } else { - L[j] = C::msm_unchecked(&G[m..], &a[..m]) + U.mul(inner_prod(&a[..m], &b[m..])?); - R[j] = C::msm_unchecked(&G[..m], &a[m..]) + U.mul(inner_prod(&a[m..], &b[..m])?); - } - // get challenge for the j-th round - transcript.absorb_nonnative(&L[j]); - transcript.absorb_nonnative(&R[j]); - u[j] = transcript.get_challenge(); - - let uj = u[j]; - let uj_inv = u[j] - .inverse() - .ok_or(Error::Other("error on computing inverse".to_string()))?; - - // a_hi * uj^-1 + a_lo * uj - a = vec_add( - &vec_scalar_mul(&a[..m], &uj), - &vec_scalar_mul(&a[m..], &uj_inv), - )?; - // b_lo * uj^-1 + b_hi * uj - b = vec_add( - &vec_scalar_mul(&b[..m], &uj_inv), - &vec_scalar_mul(&b[m..], &uj), - )?; - // G_lo * uj^-1 + G_hi * uj - G = cfg_iter!(G[..m]) - .map(|e| e.into_group().mul(uj_inv)) - .zip(cfg_iter!(G[m..]).map(|e| e.into_group().mul(uj))) - .map(|(a, b)| (a + b).into_affine()) - .collect::>(); - } - - if a.len() != 1 { - return Err(Error::NotExpectedLength(a.len(), 1)); - } - if b.len() != 1 { - return Err(Error::NotExpectedLength(b.len(), 1)); - } - if G.len() != 1 { - return Err(Error::NotExpectedLength(G.len(), 1)); - } - - Ok(( - Proof { - a: a[0], - l: l.clone(), - r: r.clone(), - L, - R, - }, - v, // evaluation at challenge, v=p(x) - *blind, // blind factor - )) - } - - fn prove_with_challenge( - _params: &Self::ProverParams, - _challenge: Self::ProverChallenge, - _a: &[C::ScalarField], // vector - _blind: &C::ScalarField, - _rng: Option<&mut dyn RngCore>, - ) -> Result { - // not supported because the prover logic computes challenges as it advances on the logic - Err(Error::NotSupported("IPA::prove_with_challenge".to_string())) - } - - fn verify( - params: &Self::VerifierParams, - transcript: &mut impl Transcript, - P: &C, // commitment - proof: &Self::Proof, - ) -> Result<(), Error> { - let (p, _r) = (proof.0.clone(), proof.1); - let k = p.L.len(); - - transcript.absorb_nonnative(P); - let x = transcript.get_challenge(); // challenge value at which we evaluate - let s = transcript.get_challenge(); - let U = C::generator().mul(s); - let mut u: Vec = vec![C::ScalarField::zero(); k]; - for i in (0..k).rev() { - transcript.absorb_nonnative(&p.L[i]); - transcript.absorb_nonnative(&p.R[i]); - u[i] = transcript.get_challenge(); - } - let challenge = (x, U, u); - - Self::verify_with_challenge(params, challenge, P, proof) - } - - fn verify_with_challenge( - params: &Self::VerifierParams, - challenge: Self::Challenge, - P: &C, // commitment - proof: &Self::Proof, - ) -> Result<(), Error> { - let (p, v, r) = (proof.0.clone(), proof.1, proof.2); - let (x, U, u) = challenge; - - let k = p.L.len(); - if p.R.len() != k { - return Err(Error::CommitmentVerificationFail); - } - if !H && (!r.is_zero()) { - return Err(Error::BlindingNotZero); - } - if !H && (!p.l.is_empty() || !p.r.is_empty()) { - return Err(Error::CommitmentVerificationFail); - } - if H && (p.l.len() != k || p.r.len() != k) { - return Err(Error::CommitmentVerificationFail); - } - - let P = *P + U.mul(v); // where v=p(x) - - let mut q_0 = P; - let mut r = r; - - // compute u[i]^-1 once - let mut u_invs = vec![C::ScalarField::zero(); u.len()]; - for (j, u_j) in u.iter().enumerate() { - u_invs[j] = u_j - .inverse() - .ok_or(Error::Other("error on computing inverse".to_string()))?; - } - - // compute b & G from s - let s = build_s(&u, &u_invs, k)?; - // b = = - let b = s_b_inner(&u, &x)?; - let d: usize = 2_u64.pow(k as u32) as usize; - if params.generators.len() < d { - return Err(Error::PedersenParamsLen(params.generators.len(), d)); - } - let G = C::msm_unchecked(¶ms.generators, &s); - - for (j, u_j) in u.iter().enumerate() { - let uj2 = u_j.square(); - let uj_inv2 = u_invs[j].square(); - - q_0 = q_0 + p.L[j].mul(uj2) + p.R[j].mul(uj_inv2); - if H { - r = r + p.l[j] * uj2 + p.r[j] * uj_inv2; - } - } - - let q_1 = if H { - G.mul(p.a) + params.h.mul(r) + U.mul(p.a * b) - } else { - G.mul(p.a) + U.mul(p.a * b) - }; - - if q_0 != q_1 { - return Err(Error::CommitmentVerificationFail); - } - Ok(()) - } -} - -/// Computes s such that -/// s = ( -/// u₁⁻¹ u₂⁻¹ … uₖ⁻¹, -/// u₁ u₂⁻¹ … uₖ⁻¹, -/// u₁⁻¹ u₂ … uₖ⁻¹, -/// u₁ u₂ … uₖ⁻¹, -/// ⋮ ⋮ ⋮ -/// u₁ u₂ … uₖ -/// ) -/// Uses Halo2 approach computing $g(X) = \prod\limits_{i=0}^{k-1} (1 + u_{k - 1 - i} X^{2^i})$, -/// taking 2^{k+1}-2. -/// src: https://github.com/zcash/halo2/blob/81729eca91ba4755e247f49c3a72a4232864ec9e/halo2_proofs/src/poly/commitment/verifier.rs#L156 -fn build_s(u: &[F], u_invs: &[F], k: usize) -> Result, Error> { - let d: usize = 2_u64.pow(k as u32) as usize; - let mut s: Vec = vec![F::one(); d]; - for (len, (u_j, u_j_inv)) in u - .iter() - .zip(u_invs) - .enumerate() - .map(|(i, u_j)| (1 << i, u_j)) - { - let (left, right) = s.split_at_mut(len); - let right = &mut right[0..len]; - right.copy_from_slice(left); - for s in left { - *s *= u_j_inv; - } - for s in right { - *s *= u_j; - } - } - Ok(s) -} - -/// Computes (in-circuit) s such that -/// s = ( -/// u₁⁻¹ u₂⁻¹ … uₖ⁻¹, -/// u₁ u₂⁻¹ … uₖ⁻¹, -/// u₁⁻¹ u₂ … uₖ⁻¹, -/// u₁ u₂ … uₖ⁻¹, -/// ⋮ ⋮ ⋮ -/// u₁ u₂ … uₖ -/// ) -/// Uses Halo2 approach computing $g(X) = \prod\limits_{i=0}^{k-1} (1 + u_{k - 1 - i} X^{2^i})$, -/// taking 2^{k+1}-2. -/// src: https://github.com/zcash/halo2/blob/81729eca91ba4755e247f49c3a72a4232864ec9e/halo2_proofs/src/poly/commitment/verifier.rs#L156 -fn build_s_gadget( - u: &[EmulatedFpVar], - u_invs: &[EmulatedFpVar], - k: usize, -) -> Result>, SynthesisError> { - let d: usize = 2_u64.pow(k as u32) as usize; - let mut s: Vec> = vec![EmulatedFpVar::one(); d]; - for (len, (u_j, u_j_inv)) in u - .iter() - .zip(u_invs) - .enumerate() - .map(|(i, u_j)| (1 << i, u_j)) - { - let (left, right) = s.split_at_mut(len); - let right = &mut right[0..len]; - right.clone_from_slice(left); - for s in left { - *s *= u_j_inv; - } - for s in right { - *s *= u_j; - } - } - Ok(s) -} - -fn inner_prod(a: &[F], b: &[F]) -> Result { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a".to_string(), - a.len(), - "b".to_string(), - b.len(), - )); - } - let c = cfg_iter!(a) - .zip(cfg_iter!(b)) - .map(|(a_i, b_i)| *a_i * b_i) - .sum(); - Ok(c) -} - -// g(x, u_1, u_2, ..., u_k) = , naively takes linear, but can compute in log time through -// g(x, u_1, u_2, ..., u_k) = \Prod u_i x^{2^i} + u_i^-1 -fn s_b_inner(u: &[F], x: &F) -> Result { - let mut c: F = F::one(); - let mut x_2_i = *x; // x_2_i is x^{2^i}, starting from x^{2^0}=x - for u_i in u.iter() { - c *= (*u_i * x_2_i) - + u_i - .inverse() - .ok_or(Error::Other("error on computing inverse".to_string()))?; - x_2_i *= x_2_i; - } - Ok(c) -} - -// g(x, u_1, u_2, ..., u_k) = , naively takes linear, but can compute in log time through -// g(x, u_1, u_2, ..., u_k) = \Prod u_i x^{2^i} + u_i^-1 -fn s_b_inner_gadget( - u: &[EmulatedFpVar], - x: &EmulatedFpVar, -) -> Result, SynthesisError> { - let mut c: EmulatedFpVar = EmulatedFpVar::::one(); - let mut x_2_i = x.clone(); // x_2_i is x^{2^i}, starting from x^{2^0}=x - for u_i in u.iter() { - c *= u_i.clone() * x_2_i.clone() + u_i.inverse()?; - x_2_i *= x_2_i.clone(); - } - Ok(c) -} - -pub struct ProofVar { - a: EmulatedFpVar>, - l: Vec>>, - r: Vec>>, - L: Vec, - R: Vec, -} -impl AllocVar, CF2> for ProofVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let a = EmulatedFpVar::>::new_variable( - cs.clone(), - || Ok(val.borrow().a), - mode, - )?; - let l: Vec>> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().l.clone()), mode)?; - let r: Vec>> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().r.clone()), mode)?; - let L: Vec = - Vec::new_variable(cs.clone(), || Ok(val.borrow().L.clone()), mode)?; - let R: Vec = - Vec::new_variable(cs.clone(), || Ok(val.borrow().R.clone()), mode)?; - - Ok(Self { a, l, r, L, R }) - }) - } -} - -/// IPAGadget implements the circuit that verifies an IPA Proof. The `H` parameter indicates if to -/// use the commitment in hiding mode or not, reducing a bit the number of constraints needed in -/// the later case. -pub struct IPAGadget { - _c: PhantomData, -} - -impl IPAGadget { - /// Verify the IPA opening proof, K=log2(d), where d is the degree of the committed polynomial, - /// and H indicates if the commitment is in hiding mode and thus uses blinding factors, if not, - /// there are some constraints saved. - #[allow(clippy::too_many_arguments)] - pub fn verify( - g: &[C::Var], // params.generators - h: &C::Var, // params.h - x: &EmulatedFpVar>, // evaluation point, challenge - v: &EmulatedFpVar>, // value at evaluation point - P: &C::Var, // commitment - p: &ProofVar, - r: &EmulatedFpVar>, // blinding factor - u: &[EmulatedFpVar>; K], // challenges - U: &C::Var, // challenge - ) -> Result>, SynthesisError> { - if p.L.len() != K || p.R.len() != K { - return Err(SynthesisError::Unsatisfiable); - } - - let P_ = U.scalar_mul_le(v.to_bits_le()?.iter())? + P; - let mut q_0 = P_; - let mut r = r.clone(); - - // compute u[i]^-1 once - let mut u_invs = vec![EmulatedFpVar::>::zero(); u.len()]; - for (j, u_j) in u.iter().enumerate() { - u_invs[j] = u_j.inverse()?; - } - - // compute b & G from s - let s = build_s_gadget(u, &u_invs, K)?; - // b = = - let b = s_b_inner_gadget(u, x)?; - // ensure that generators.len() === s.len(): - if g.len() < K { - return Err(SynthesisError::Unsatisfiable); - } - - // msm: G= - let mut G = C::Var::zero(); - let n = s.len(); - if n % 2 == 1 { - G += g[n - 1].scalar_mul_le(s[n - 1].to_bits_le()?.iter())?; - } else { - G += g[n - 1].joint_scalar_mul_be( - &g[n - 2], - s[n - 1].to_bits_le()?.iter(), - s[n - 2].to_bits_le()?.iter(), - )?; - } - for i in (1..n - 2).step_by(2) { - G += g[i - 1].joint_scalar_mul_be( - &g[i], - s[i - 1].to_bits_le()?.iter(), - s[i].to_bits_le()?.iter(), - )?; - } - - for (j, u_j) in u.iter().enumerate() { - let uj2 = u_j.square()?; - let uj_inv2 = u_invs[j].square()?; // cheaper square than inversing the uj2 - - q_0 = q_0 - + p.L[j].scalar_mul_le(uj2.to_bits_le()?.iter())? - + p.R[j].scalar_mul_le(uj_inv2.to_bits_le()?.iter())?; - if H { - r = r + &p.l[j] * &uj2 + &p.r[j] * &uj_inv2; - } - } - - let q_1 = if H { - G.scalar_mul_le(p.a.to_bits_le()?.iter())? - + h.joint_scalar_mul_be( - U, - r.to_bits_le()?.iter(), - (p.a.clone() * b).to_bits_le()?.iter(), - )? - } else { - G.joint_scalar_mul_be( - U, - p.a.to_bits_le()?.iter(), - (p.a.clone() * b).to_bits_le()?.iter(), - )? - }; - // q_0 == q_1 - q_0.is_eq(&q_1) - } -} - -#[cfg(test)] -mod tests { - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_ec::PrimeGroup; - use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; - use ark_r1cs_std::eq::EqGadget; - use ark_relations::gr1cs::ConstraintSystem; - - use super::*; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_ipa() -> Result<(), Error> { - let _ = test_ipa_opt::()?; - let _ = test_ipa_opt::()?; - Ok(()) - } - fn test_ipa_opt() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - const k: usize = 4; - const d: usize = 2_u64.pow(k as u32) as usize; - - // setup params - let (params, _) = IPA::::setup(&mut rng, d)?; - - let poseidon_config = poseidon_canonical_config::(); - // init Prover's transcript - let mut transcript_p = PoseidonSponge::::new(&poseidon_config); - // init Verifier's transcript - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - - // a is the vector that we're committing - let a: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(d) - .collect(); - let r_blind: Fr = if hiding { - Fr::rand(&mut rng) - } else { - Fr::zero() - }; - let cm = IPA::::commit(¶ms, &a, &r_blind)?; - - let proof = IPA::::prove( - ¶ms, - &mut transcript_p, - &cm, - &a, - &r_blind, - Some(&mut rng), - )?; - - IPA::::verify(¶ms, &mut transcript_v, &cm, &proof)?; - Ok(()) - } - - #[test] - fn test_ipa_gadget() -> Result<(), Error> { - let _ = test_ipa_gadget_opt::()?; - let _ = test_ipa_gadget_opt::()?; - Ok(()) - } - fn test_ipa_gadget_opt() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - const k: usize = 3; - const d: usize = 2_u64.pow(k as u32) as usize; - - // setup params - let (params, _) = IPA::::setup(&mut rng, d)?; - - let poseidon_config = poseidon_canonical_config::(); - // init Prover's transcript - let mut transcript_p = PoseidonSponge::::new(&poseidon_config); - // init Verifier's transcript - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - - let mut a: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(d / 2) - .collect(); - a.extend(vec![Fr::zero(); d / 2]); - let r_blind: Fr = if hiding { - Fr::rand(&mut rng) - } else { - Fr::zero() - }; - let cm = IPA::::commit(¶ms, &a, &r_blind)?; - - let proof = IPA::::prove( - ¶ms, - &mut transcript_p, - &cm, - &a, - &r_blind, - Some(&mut rng), - )?; - - IPA::::verify(¶ms, &mut transcript_v, &cm, &proof)?; - - // circuit - let cs = ConstraintSystem::::new_ref(); - - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - transcript_v.absorb_nonnative(&cm); - let challenge = transcript_v.get_challenge(); // challenge value at which we evaluate - let s = transcript_v.get_challenge(); - let U = Projective::generator() * s; - let mut u: Vec = vec![Fr::zero(); k]; - for i in (0..k).rev() { - transcript_v.absorb_nonnative(&proof.0.L[i]); - transcript_v.absorb_nonnative(&proof.0.R[i]); - u[i] = transcript_v.get_challenge(); - } - - // prepare inputs - let gVar = Vec::::new_constant(cs.clone(), params.generators)?; - let hVar = GVar::new_constant(cs.clone(), params.h)?; - let challengeVar = EmulatedFpVar::::new_witness(cs.clone(), || Ok(challenge))?; - let vVar = EmulatedFpVar::::new_witness(cs.clone(), || Ok(proof.1))?; - let cmVar = GVar::new_witness(cs.clone(), || Ok(cm))?; - let proofVar = ProofVar::::new_witness(cs.clone(), || Ok(proof.0))?; - let r_blindVar = EmulatedFpVar::::new_witness(cs.clone(), || Ok(r_blind))?; - let uVar_vec = Vec::>::new_witness(cs.clone(), || Ok(u))?; - let uVar: [EmulatedFpVar; k] = uVar_vec.try_into().map_err(|_| { - Error::ConversionError( - "Vec<_>".to_string(), - "[_; 1]".to_string(), - "variable name: uVar".to_string(), - ) - })?; - let UVar = GVar::new_witness(cs.clone(), || Ok(U))?; - - let v = IPAGadget::::verify::( - &gVar, - &hVar, - &challengeVar, - &vVar, - &cmVar, - &proofVar, - &r_blindVar, - &uVar, - &UVar, - )?; - v.enforce_equal(&Boolean::TRUE)?; - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/commitment/kzg.rs b/folding-schemes/src/commitment/kzg.rs deleted file mode 100644 index f1873cd63..000000000 --- a/folding-schemes/src/commitment/kzg.rs +++ /dev/null @@ -1,311 +0,0 @@ -/// Adaptation of the prover methods and structs from arkworks/poly-commit's KZG10 implementation -/// into the CommitmentScheme trait. -/// -/// The motivation to do so, is that we want to be able to use KZG / Pedersen for committing to -/// vectors indistinctly, and the arkworks KZG10 implementation contains all the methods under the -/// same trait, which requires the Pairing trait, where the prover does not need access to the -/// Pairing but only to G1. -use ark_ec::{pairing::Pairing, CurveGroup, VariableBaseMSM}; -use ark_ff::PrimeField; -use ark_poly::{ - univariate::{DenseOrSparsePolynomial, DensePolynomial}, - DenseUVPolynomial, Polynomial, -}; -use ark_poly_commit::kzg10::{ - Commitment as KZG10Commitment, Proof as KZG10Proof, VerifierKey, KZG10, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Valid}; -use ark_std::rand::RngCore; -use ark_std::{borrow::Cow, fmt::Debug}; -use ark_std::{One, Zero}; -use core::marker::PhantomData; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; - -use super::CommitmentScheme; -use crate::transcript::Transcript; -use crate::utils::vec::poly_from_vec; -use crate::{Curve, Error}; - -/// ProverKey defines a similar struct as in ark_poly_commit::kzg10::Powers, but instead of -/// depending on the Pairing trait it depends on the SonobeCurve trait. -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct ProverKey<'a, C: Curve> { - /// Group elements of the form `β^i G`, for different values of `i`. - pub powers_of_g: Cow<'a, [C::Affine]>, -} - -impl<'a, C: Curve> CanonicalSerialize for ProverKey<'a, C> { - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.powers_of_g.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.powers_of_g.serialized_size(compress) - } -} - -impl<'a, C: Curve> CanonicalDeserialize for ProverKey<'a, C> { - fn deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - ) -> Result { - let powers_of_g_vec = Vec::deserialize_with_mode(reader, compress, validate)?; - Ok(ProverKey { - powers_of_g: ark_std::borrow::Cow::Owned(powers_of_g_vec), - }) - } -} - -impl<'a, C: Curve> Valid for ProverKey<'a, C> { - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - match self.powers_of_g.clone() { - Cow::Borrowed(powers) => powers.to_vec().check(), - Cow::Owned(powers) => powers.check(), - } - } -} - -#[derive(Debug, Clone, Default, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof { - pub eval: C::ScalarField, - pub proof: C, -} - -/// KZG implements the CommitmentScheme trait for the KZG commitment scheme. -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct KZG<'a, E: Pairing, const H: bool = false> { - _a: PhantomData<&'a ()>, - _e: PhantomData, -} - -impl<'a, E: Pairing, const H: bool> CommitmentScheme for KZG<'a, E, H> { - type ProverParams = ProverKey<'a, E::G1>; - type VerifierParams = VerifierKey; - type Proof = Proof; - type ProverChallenge = E::ScalarField; - type Challenge = E::ScalarField; - - fn is_hiding() -> bool { - if H { - return true; - } - false - } - - /// setup returns the tuple (ProverKey, VerifierKey). For real world deployments the setup must - /// be computed in the most trustless way possible, usually through an MPC ceremony. - fn setup( - mut rng: impl RngCore, - len: usize, - ) -> Result<(Self::ProverParams, Self::VerifierParams), Error> { - let len = len.next_power_of_two(); - let universal_params = - KZG10::>::setup(len, false, &mut rng) - .expect("Setup failed"); - let powers_of_g = universal_params.powers_of_g[..=len].to_vec(); - let powers = ProverKey:: { - powers_of_g: ark_std::borrow::Cow::Owned(powers_of_g), - }; - let vk = VerifierKey { - g: universal_params.powers_of_g[0], - gamma_g: universal_params.powers_of_gamma_g[&0], - h: universal_params.h, - beta_h: universal_params.beta_h, - prepared_h: universal_params.prepared_h.clone(), - prepared_beta_h: universal_params.prepared_beta_h.clone(), - }; - Ok((powers, vk)) - } - - /// commit implements the CommitmentScheme commit interface, adapting the implementation from - /// https://github.com/arkworks-rs/poly-commit/tree/c724fa666e935bbba8db5a1421603bab542e15ab/poly-commit/src/kzg10/mod.rs#L178 - /// with the main difference being the removal of the blinding factors and the no-dependency to - /// the Pairing trait. - fn commit( - params: &Self::ProverParams, - v: &[E::ScalarField], - _blind: &E::ScalarField, - ) -> Result { - if !_blind.is_zero() || H { - return Err(Error::NotSupportedYet("hiding".to_string())); - } - - let polynomial = poly_from_vec(v.to_vec())?; - check_degree_is_too_large(polynomial.degree(), params.powers_of_g.len())?; - - let (num_leading_zeros, plain_coeffs) = - skip_first_zero_coeffs_and_convert_to_bigints(&polynomial); - let commitment = ::msm_bigint( - ¶ms.powers_of_g[num_leading_zeros..], - &plain_coeffs, - ); - Ok(commitment) - } - - /// prove implements the CommitmentScheme prove interface, adapting the implementation from - /// https://github.com/arkworks-rs/poly-commit/tree/c724fa666e935bbba8db5a1421603bab542e15ab/poly-commit/src/kzg10/mod.rs#L307 - /// with the main difference being the removal of the blinding factors and the no-dependency to - /// the Pairing trait. - fn prove( - params: &Self::ProverParams, - transcript: &mut impl Transcript, - cm: &E::G1, - v: &[E::ScalarField], - _blind: &E::ScalarField, - _rng: Option<&mut dyn RngCore>, - ) -> Result { - transcript.absorb_nonnative(cm); - let challenge = transcript.get_challenge(); - Self::prove_with_challenge(params, challenge, v, _blind, _rng) - } - - fn prove_with_challenge( - params: &Self::ProverParams, - challenge: Self::ProverChallenge, - v: &[E::ScalarField], - _blind: &E::ScalarField, - _rng: Option<&mut dyn RngCore>, - ) -> Result { - if !_blind.is_zero() || H { - return Err(Error::NotSupportedYet("hiding".to_string())); - } - - let polynomial = poly_from_vec(v.to_vec())?; - check_degree_is_too_large(polynomial.degree(), params.powers_of_g.len())?; - - // Compute q(x) = (p(x) - p(z)) / (x-z). Observe that this quotient does not change with z - // because p(z) is the remainder term. We can therefore omit p(z) when computing the - // quotient. - let divisor = DensePolynomial::::from_coefficients_vec(vec![ - -challenge, - E::ScalarField::one(), - ]); - let (witness_poly, remainder_poly) = DenseOrSparsePolynomial::from(&polynomial) - .divide_with_q_and_r(&DenseOrSparsePolynomial::from(&divisor)) - // the panic inside `divide_with_q_and_r` should never be reached, since the divisor - // polynomial is constructed right before and is set to not be zero. And the `.unwrap` - // should not give an error. - .unwrap(); - - let eval = if remainder_poly.is_zero() { - E::ScalarField::zero() - } else { - remainder_poly[0] - }; - - check_degree_is_too_large(witness_poly.degree(), params.powers_of_g.len())?; - let (num_leading_zeros, witness_coeffs) = - skip_first_zero_coeffs_and_convert_to_bigints(&witness_poly); - let proof = ::msm_bigint( - ¶ms.powers_of_g[num_leading_zeros..], - &witness_coeffs, - ); - - Ok(Proof { eval, proof }) - } - - fn verify( - params: &Self::VerifierParams, - transcript: &mut impl Transcript, - cm: &E::G1, - proof: &Self::Proof, - ) -> Result<(), Error> { - transcript.absorb_nonnative(cm); - let challenge = transcript.get_challenge(); - Self::verify_with_challenge(params, challenge, cm, proof) - } - - fn verify_with_challenge( - params: &Self::VerifierParams, - challenge: Self::Challenge, - cm: &E::G1, - proof: &Self::Proof, - ) -> Result<(), Error> { - if H { - return Err(Error::NotSupportedYet("hiding".to_string())); - } - - // verify the KZG proof using arkworks method - let v = KZG10::>::check( - params, // vk - &KZG10Commitment(cm.into_affine()), - challenge, - proof.eval, - &KZG10Proof:: { - w: proof.proof.into_affine(), - random_v: None, - }, - )?; - if !v { - return Err(Error::CommitmentVerificationFail); - } - Ok(()) - } -} - -fn check_degree_is_too_large( - degree: usize, - num_powers: usize, -) -> Result<(), ark_poly_commit::error::Error> { - let num_coefficients = degree + 1; - if num_coefficients > num_powers { - Err(ark_poly_commit::error::Error::TooManyCoefficients { - num_coefficients, - num_powers, - }) - } else { - Ok(()) - } -} - -fn skip_first_zero_coeffs_and_convert_to_bigints>( - p: &P, -) -> (usize, Vec) { - let mut num_leading_zeros = 0; - while num_leading_zeros < p.coeffs().len() && p.coeffs()[num_leading_zeros].is_zero() { - num_leading_zeros += 1; - } - let coeffs = convert_to_bigints(&p.coeffs()[num_leading_zeros..]); - (num_leading_zeros, coeffs) -} - -fn convert_to_bigints(p: &[F]) -> Vec { - ark_std::cfg_iter!(p) - .map(|s| s.into_bigint()) - .collect::>() -} - -#[cfg(test)] -mod tests { - use ark_bn254::{Bn254, Fr, G1Projective as G1}; - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_std::{test_rng, UniformRand}; - - use super::*; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_kzg_commitment_scheme() -> Result<(), Error> { - let mut rng = &mut test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let transcript_p = &mut PoseidonSponge::::new(&poseidon_config); - let transcript_v = &mut PoseidonSponge::::new(&poseidon_config); - - let n = 10; - let (pk, vk): (ProverKey, VerifierKey) = KZG::::setup(&mut rng, n)?; - - let v: Vec = std::iter::repeat_with(|| Fr::rand(rng)).take(n).collect(); - let cm = KZG::::commit(&pk, &v, &Fr::zero())?; - - let proof = KZG::::prove(&pk, transcript_p, &cm, &v, &Fr::zero(), None)?; - - // verify the proof: - KZG::::verify(&vk, transcript_v, &cm, &proof)?; - Ok(()) - } -} diff --git a/folding-schemes/src/commitment/mod.rs b/folding-schemes/src/commitment/mod.rs deleted file mode 100644 index 0c9301d69..000000000 --- a/folding-schemes/src/commitment/mod.rs +++ /dev/null @@ -1,165 +0,0 @@ -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::fmt::Debug; -use ark_std::rand::RngCore; - -use crate::transcript::Transcript; -use crate::{Curve, Error}; - -pub mod ipa; -pub mod kzg; -pub mod pedersen; - -/// CommitmentScheme defines the vector commitment scheme trait. Where `H` indicates if to use the -/// commitment in hiding mode or not. -pub trait CommitmentScheme: Clone + Debug { - type ProverParams: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; - type VerifierParams: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; - type Proof: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; - type ProverChallenge: Clone + Debug; - type Challenge: Clone + Debug; - - fn is_hiding() -> bool; - - fn setup( - rng: impl RngCore, - len: usize, - ) -> Result<(Self::ProverParams, Self::VerifierParams), Error>; - - fn commit( - params: &Self::ProverParams, - v: &[C::ScalarField], - blind: &C::ScalarField, - ) -> Result; - - fn prove( - params: &Self::ProverParams, - transcript: &mut impl Transcript, - cm: &C, - v: &[C::ScalarField], - blind: &C::ScalarField, - rng: Option<&mut dyn RngCore>, - ) -> Result; - - /// same as `prove` but instead of providing a Transcript to use, providing the already - /// computed challenge - fn prove_with_challenge( - params: &Self::ProverParams, - challenge: Self::ProverChallenge, - v: &[C::ScalarField], - blind: &C::ScalarField, - rng: Option<&mut dyn RngCore>, - ) -> Result; - - fn verify( - params: &Self::VerifierParams, - transcript: &mut impl Transcript, - cm: &C, - proof: &Self::Proof, - ) -> Result<(), Error>; - - /// same as `verify` but instead of providing a Transcript to use, providing the already - /// computed challenge - fn verify_with_challenge( - params: &Self::VerifierParams, - challenge: Self::Challenge, - cm: &C, - proof: &Self::Proof, - ) -> Result<(), Error>; -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_bn254::{Bn254, Fr, G1Projective as G1}; - use ark_crypto_primitives::sponge::{ - poseidon::{PoseidonConfig, PoseidonSponge}, - CryptographicSponge, - }; - use ark_poly_commit::kzg10::VerifierKey; - use ark_std::Zero; - use ark_std::{test_rng, UniformRand}; - - use super::ipa::IPA; - use super::kzg::{ProverKey, KZG}; - use super::pedersen::Pedersen; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_homomorphic_property_using_Commitment_trait() -> Result<(), Error> { - let mut rng = &mut test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let n: usize = 128; - - // set random vector for the test - let v_1: Vec = std::iter::repeat_with(|| Fr::rand(rng)).take(n).collect(); - let v_2: Vec = std::iter::repeat_with(|| Fr::rand(rng)).take(n).collect(); - // set a random challenge for the random linear combination - let r = Fr::rand(rng); - - // setup params for Pedersen & KZG - let (pedersen_params, _) = Pedersen::::setup(&mut rng, n)?; - let (kzg_pk, kzg_vk): (ProverKey, VerifierKey) = KZG::::setup(rng, n)?; - - // test with Pedersen - let _ = test_homomorphic_property_using_Commitment_trait_opt::>( - &poseidon_config, - &pedersen_params, - &pedersen_params, - r, - &v_1, - &v_2, - )?; - // test with IPA - let _ = test_homomorphic_property_using_Commitment_trait_opt::>( - &poseidon_config, - &pedersen_params, - &pedersen_params, - r, - &v_1, - &v_2, - )?; - // test with KZG - let _ = test_homomorphic_property_using_Commitment_trait_opt::>( - &poseidon_config, - &kzg_pk, - &kzg_vk, - r, - &v_1, - &v_2, - )?; - Ok(()) - } - - fn test_homomorphic_property_using_Commitment_trait_opt>( - poseidon_config: &PoseidonConfig, - prover_params: &CS::ProverParams, - verifier_params: &CS::VerifierParams, - r: C::ScalarField, - v_1: &[C::ScalarField], - v_2: &[C::ScalarField], - ) -> Result<(), Error> { - // compute the commitment of the two vectors using the given CommitmentScheme - let cm_1 = CS::commit(prover_params, v_1, &C::ScalarField::zero())?; - let cm_2 = CS::commit(prover_params, v_2, &C::ScalarField::zero())?; - - // random linear combination of the commitments and their witnesses (vectors v_i) - let cm_3 = cm_1 + cm_2.mul(r); - let v_3: Vec = v_1.iter().zip(v_2).map(|(a, b)| *a + (r * b)).collect(); - - // compute the proof of the cm_3 - let transcript_p = &mut PoseidonSponge::::new(poseidon_config); - let proof = CS::prove( - prover_params, - transcript_p, - &cm_3, - &v_3, - &C::ScalarField::zero(), - None, - )?; - - // verify the opening proof - let transcript_v = &mut PoseidonSponge::::new(poseidon_config); - CS::verify(verifier_params, transcript_v, &cm_3, &proof)?; - Ok(()) - } -} diff --git a/folding-schemes/src/commitment/pedersen.rs b/folding-schemes/src/commitment/pedersen.rs deleted file mode 100644 index 753861d22..000000000 --- a/folding-schemes/src/commitment/pedersen.rs +++ /dev/null @@ -1,308 +0,0 @@ -use ark_r1cs_std::{boolean::Boolean, convert::ToBitsGadget, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{marker::PhantomData, rand::RngCore, UniformRand, Zero}; - -use super::CommitmentScheme; -use crate::folding::circuits::CF2; -use crate::transcript::Transcript; -use crate::utils::vec::{vec_add, vec_scalar_mul}; -use crate::{Curve, Error}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof { - pub R: C, - pub u: Vec, - pub r_u: C::ScalarField, // blind -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Params { - pub h: C, - pub generators: Vec, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Pedersen { - _c: PhantomData, -} - -/// Implements the CommitmentScheme trait for Pedersen commitments -impl CommitmentScheme for Pedersen { - type ProverParams = Params; - type VerifierParams = Params; - type Proof = Proof; - type ProverChallenge = (C::ScalarField, Vec, C, C::ScalarField); - type Challenge = C::ScalarField; - - fn is_hiding() -> bool { - if H { - return true; - } - false - } - - fn setup( - mut rng: impl RngCore, - len: usize, - ) -> Result<(Self::ProverParams, Self::VerifierParams), Error> { - let generators: Vec = std::iter::repeat_with(|| C::Affine::rand(&mut rng)) - .take(len.next_power_of_two()) - .collect(); - let p = Params:: { - h: C::rand(&mut rng), - generators, - }; - Ok((p.clone(), p)) - } - - fn commit( - params: &Self::ProverParams, - v: &[C::ScalarField], - r: &C::ScalarField, // blinding factor - ) -> Result { - if params.generators.len() < v.len() { - return Err(Error::PedersenParamsLen(params.generators.len(), v.len())); - } - if !H && (!r.is_zero()) { - return Err(Error::BlindingNotZero); - } - - // h⋅r + - // use msm_unchecked because we already ensured at the if that lengths match - if !H { - return Ok(C::msm_unchecked(¶ms.generators[..v.len()], v)); - } - Ok(params.h.mul(r) + C::msm_unchecked(¶ms.generators[..v.len()], v)) - } - - fn prove( - params: &Self::ProverParams, - transcript: &mut impl Transcript, - cm: &C, - v: &[C::ScalarField], - r: &C::ScalarField, // blinding factor - _rng: Option<&mut dyn RngCore>, - ) -> Result { - transcript.absorb_nonnative(cm); - let r1 = transcript.get_challenge(); - let d = transcript.get_challenges(v.len()); - - // R = h⋅r_1 + - // use msm_unchecked because we already ensured at the if that lengths match - let mut R: C = C::msm_unchecked(¶ms.generators[..d.len()], &d); - if H { - R += params.h.mul(r1); - } - - transcript.absorb_nonnative(&R); - let e = transcript.get_challenge(); - - let challenge = (r1, d, R, e); - Self::prove_with_challenge(params, challenge, v, r, _rng) - } - - fn prove_with_challenge( - params: &Self::ProverParams, - challenge: Self::ProverChallenge, - v: &[C::ScalarField], // vector - r: &C::ScalarField, // blinding factor - _rng: Option<&mut dyn RngCore>, - ) -> Result { - if params.generators.len() < v.len() { - return Err(Error::PedersenParamsLen(params.generators.len(), v.len())); - } - if !H && (!r.is_zero()) { - return Err(Error::BlindingNotZero); - } - let (r1, d, R, e): (C::ScalarField, Vec, C, C::ScalarField) = challenge; - - // u = d + v⋅e - let u = vec_add(&vec_scalar_mul(v, &e), &d)?; - // r_u = e⋅r + r_1 - let mut r_u = C::ScalarField::zero(); - if H { - r_u = e * r + r1; - } - - Ok(Self::Proof { R, u, r_u }) - } - - fn verify( - params: &Self::VerifierParams, - transcript: &mut impl Transcript, - cm: &C, - proof: &Proof, - ) -> Result<(), Error> { - transcript.absorb_nonnative(cm); - transcript.get_challenge(); // r_1 - transcript.get_challenges(proof.u.len()); // d - transcript.absorb_nonnative(&proof.R); - let e = transcript.get_challenge(); - Self::verify_with_challenge(params, e, cm, proof) - } - - fn verify_with_challenge( - params: &Self::VerifierParams, - challenge: Self::Challenge, - cm: &C, - proof: &Proof, - ) -> Result<(), Error> { - if params.generators.len() < proof.u.len() { - return Err(Error::PedersenParamsLen( - params.generators.len(), - proof.u.len(), - )); - } - if !H && (!proof.r_u.is_zero()) { - return Err(Error::BlindingNotZero); - } - - let e = challenge; - - // check that: R + cm⋅e == h⋅r_u + - let lhs = proof.R + cm.mul(e); - // use msm_unchecked because we already ensured at the if that lengths match - let mut rhs = C::msm_unchecked(¶ms.generators[..proof.u.len()], &proof.u); - if H { - rhs += params.h.mul(proof.r_u); - } - if lhs != rhs { - return Err(Error::CommitmentVerificationFail); - } - Ok(()) - } -} - -pub struct PedersenGadget { - _c: PhantomData, -} - -impl PedersenGadget { - pub fn commit( - h: &C::Var, - g: &[C::Var], - v: &[Vec>>], - r: &[Boolean>], - ) -> Result { - let mut res = C::Var::zero(); - if H { - res += h.scalar_mul_le(r.iter())?; - } - let n = v.len(); - if n % 2 == 1 { - res += g[n - 1].scalar_mul_le(v[n - 1].to_bits_le()?.iter())?; - } else { - res += g[n - 1].joint_scalar_mul_be( - &g[n - 2], - v[n - 1].to_bits_le()?.iter(), - v[n - 2].to_bits_le()?.iter(), - )?; - } - for i in (1..n - 1).step_by(2) { - res += g[i - 1].joint_scalar_mul_be( - &g[i], - v[i - 1].to_bits_le()?.iter(), - v[i].to_bits_le()?.iter(), - )?; - } - Ok(res) - } -} - -#[cfg(test)] -mod tests { - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_ff::{BigInteger, PrimeField}; - use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; - use ark_relations::gr1cs::ConstraintSystem; - - use super::*; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_pedersen() -> Result<(), Error> { - let _ = test_pedersen_opt::()?; - let _ = test_pedersen_opt::()?; - Ok(()) - } - fn test_pedersen_opt() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - let n: usize = 10; - // setup params - let (params, _) = Pedersen::::setup(&mut rng, n)?; - let poseidon_config = poseidon_canonical_config::(); - - // init Prover's transcript - let mut transcript_p = PoseidonSponge::::new(&poseidon_config); - // init Verifier's transcript - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - - let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(n) - .collect(); - // blinding factor - let r: Fr = if hiding { - Fr::rand(&mut rng) - } else { - Fr::zero() - }; - let cm = Pedersen::::commit(¶ms, &v, &r)?; - let proof = - Pedersen::::prove(¶ms, &mut transcript_p, &cm, &v, &r, None)?; - Pedersen::::verify(¶ms, &mut transcript_v, &cm, &proof)?; - Ok(()) - } - - #[test] - fn test_pedersen_circuit() -> Result<(), Error> { - let _ = test_pedersen_circuit_opt::(8)?; - let _ = test_pedersen_circuit_opt::(8)?; - let _ = test_pedersen_circuit_opt::(9)?; - let _ = test_pedersen_circuit_opt::(9)?; - Ok(()) - } - fn test_pedersen_circuit_opt(n: usize) -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - // setup params - let (params, _) = Pedersen::::setup(&mut rng, n)?; - - let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(n) - .collect(); - // blinding factor - let r: Fr = if hiding { - Fr::rand(&mut rng) - } else { - Fr::zero() - }; - let cm = Pedersen::::commit(¶ms, &v, &r)?; - - let v_bits: Vec> = v.iter().map(|val| val.into_bigint().to_bits_le()).collect(); - let r_bits: Vec = r.into_bigint().to_bits_le(); - - // circuit - let cs = ConstraintSystem::::new_ref(); - - // prepare inputs - let vVar: Vec>> = v_bits - .iter() - .map(|val_bits| Vec::>::new_witness(cs.clone(), || Ok(val_bits.clone()))) - .collect::>()?; - let rVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits))?; - let gVar = Vec::::new_witness(cs.clone(), || Ok(params.generators))?; - let hVar = GVar::new_witness(cs.clone(), || Ok(params.h))?; - let expected_cmVar = GVar::new_witness(cs.clone(), || Ok(cm))?; - - // use the gadget - let cmVar = PedersenGadget::::commit(&hVar, &gVar, &vVar, &rVar)?; - cmVar.enforce_equal(&expected_cmVar)?; - - assert!(cs.is_satisfied()?); - - Ok(()) - } -} diff --git a/folding-schemes/src/constants.rs b/folding-schemes/src/constants.rs deleted file mode 100644 index e256638f6..000000000 --- a/folding-schemes/src/constants.rs +++ /dev/null @@ -1,5 +0,0 @@ -// used for the RO challenges. -// From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness -// error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this -// case, we keep the size of S close to 2^128. -pub const NOVA_N_BITS_RO: usize = 128; diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs deleted file mode 100644 index edd409157..000000000 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ /dev/null @@ -1,943 +0,0 @@ -/// Contains [CycleFold](https://eprint.iacr.org/2023/1192.pdf) related circuits and functions that -/// are shared across the different folding schemes -use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, Absorb, CryptographicSponge}; -use ark_ec::AffineRepr; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - convert::ToConstraintFieldGadget, - eq::EqGadget, - fields::fp::FpVar, - prelude::CurveVar, - GR1CSVar, -}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, Namespace, SynthesisError, -}; -use ark_std::{borrow::Borrow, fmt::Debug, marker::PhantomData, rand::RngCore, One}; - -use super::{ - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, -}; -use crate::arith::{ - r1cs::{circuits::R1CSMatricesVar, extract_w_x, R1CS}, - ArithRelationGadget, -}; -use crate::commitment::CommitmentScheme; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::{ - nova::nifs::{nova::NIFS, NIFSTrait}, - traits::InputizeNonNative, -}; -use crate::transcript::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar}; -use crate::utils::gadgets::{EquivalenceGadget, VectorGadget}; -use crate::{Curve, Error}; - -/// Re-export the Nova committed instance as `CycleFoldCommittedInstance` and -/// witness as `CycleFoldWitness`, for clarity and consistency -pub use crate::folding::nova::{ - CommittedInstance as CycleFoldCommittedInstance, Witness as CycleFoldWitness, -}; - -impl InputizeNonNative> for CycleFoldCommittedInstance { - /// Returns the internal representation in the same order as how the value - /// is allocated in `CycleFoldCommittedInstanceVar::new_input`. - fn inputize_nonnative(&self) -> Vec> { - [ - self.u.inputize_nonnative(), - self.x.inputize_nonnative(), - self.cmE.inputize(), - self.cmW.inputize(), - ] - .concat() - } -} - -/// CycleFoldCommittedInstanceVar is the CycleFold CommittedInstance represented -/// in folding verifier circuit -#[derive(Debug, Clone)] -pub struct CycleFoldCommittedInstanceVar { - pub cmE: C::Var, - pub u: NonNativeUintVar>, - pub cmW: C::Var, - pub x: Vec>>, -} - -impl AllocVar, CF2> - for CycleFoldCommittedInstanceVar -{ - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let u = - NonNativeUintVar::>::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; - let x: Vec>> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; - let cmE = C::Var::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; - let cmW = C::Var::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; - - Ok(Self { cmE, u, cmW, x }) - }) - } -} - -impl AbsorbNonNative for CycleFoldCommittedInstance { - // Compatible with the in-circuit `CycleFoldCommittedInstanceVar::to_native_sponge_field_elements` - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - self.u.to_native_sponge_field_elements(dest); - self.x.to_native_sponge_field_elements(dest); - let (cmE_x, cmE_y) = self.cmE.into_affine().xy().unwrap_or_default(); - let (cmW_x, cmW_y) = self.cmW.into_affine().xy().unwrap_or_default(); - cmE_x.to_sponge_field_elements(dest); - cmE_y.to_sponge_field_elements(dest); - cmW_x.to_sponge_field_elements(dest); - cmW_y.to_sponge_field_elements(dest); - } -} - -impl AbsorbNonNativeGadget for CycleFoldCommittedInstanceVar { - /// Extracts the underlying field elements from `CycleFoldCommittedInstanceVar`, in the order - /// of `u`, `x`, `cmE.x`, `cmE.y`, `cmW.x`, `cmW.y`, `cmE.is_inf || cmW.is_inf` (|| is for - /// concat). - fn to_native_sponge_field_elements(&self) -> Result>>, SynthesisError> { - let mut cmE_elems = self.cmE.to_constraint_field()?; - let mut cmW_elems = self.cmW.to_constraint_field()?; - - // See `transcript/poseidon.rs: TranscriptVar::absorb_point` for details - // why the last element is unnecessary. - cmE_elems.pop(); - cmW_elems.pop(); - - Ok([ - self.u.to_native_sponge_field_elements()?, - self.x - .iter() - .map(|i| i.to_native_sponge_field_elements()) - .collect::, _>>()? - .concat(), - cmE_elems, - cmW_elems, - ] - .concat()) - } -} - -impl CycleFoldCommittedInstanceVar { - /// Creates a new `CycleFoldCommittedInstanceVar` from the given components. - pub fn new_incoming_from_components>( - cmW: C2::Var, - r_bits: &[Boolean>], - points: Vec>, - ) -> Result { - // Construct the public inputs `x` from `r_bits` and `points`. - // Note that the underlying field can only safely store - // `CF1::::MODULUS_BIT_SIZE - 1` bits, but `r_bits` may be longer - // than that. - // Thus, we need to chunk `r_bits` into pieces and convert each piece - // to a `NonNativeUintVar`. - let x = r_bits - .chunks(CF1::::MODULUS_BIT_SIZE as usize - 1) - .map(|bits| { - let mut bits = bits.to_vec(); - bits.resize(CF1::::MODULUS_BIT_SIZE as usize, Boolean::FALSE); - NonNativeUintVar::from(&bits) - }) - .chain(points.into_iter().flat_map(|p| [p.x, p.y])) - .collect::>(); - Ok(Self { - // `cmE` is always zero for incoming instances - cmE: C2::Var::zero(), - // `u` is always one for incoming instances - u: NonNativeUintVar::new_constant(ConstraintSystemRef::None, CF1::::one())?, - cmW, - x, - }) - } -} - -impl CycleFoldCommittedInstance { - /// hash_cyclefold implements the committed instance hash compatible with the - /// in-circuit implementation `CycleFoldCommittedInstanceVar::hash`. - /// Returns `H(U_i)`, where `U_i` is a `CycleFoldCommittedInstance`. - pub fn hash_cyclefold>(&self, sponge: &T) -> C::BaseField { - let mut sponge = sponge.clone(); - sponge.absorb_nonnative(self); - sponge.squeeze_field_elements(1)[0] - } -} - -impl CycleFoldCommittedInstanceVar { - /// hash implements the committed instance hash compatible with the native - /// implementation `CycleFoldCommittedInstance::hash_cyclefold`. - /// Returns `H(U_i)`, where `U` is a `CycleFoldCommittedInstanceVar`. - /// - /// Additionally it returns the vector of the field elements from the self - /// parameters, so they can be reused in other gadgets without recalculating - /// (reconstraining) them. - #[allow(clippy::type_complexity)] - pub fn hash, S>>( - &self, - sponge: &T, - ) -> Result<(FpVar>, Vec>>), SynthesisError> { - let mut sponge = sponge.clone(); - let U_vec = self.to_native_sponge_field_elements()?; - sponge.absorb(&U_vec)?; - Ok(( - // `unwrap` is safe because the sponge is guaranteed to return a single element - sponge.squeeze_field_elements(1)?.pop().unwrap(), - U_vec, - )) - } -} - -/// In-circuit representation of the Witness associated to the CommittedInstance, but with -/// non-native representation, since it is used to represent the CycleFold witness. This struct is -/// used in the Decider circuit. -#[derive(Debug, Clone)] -pub struct CycleFoldWitnessVar { - pub E: Vec>>, - pub rE: NonNativeUintVar>, - pub W: Vec>>, - pub rW: NonNativeUintVar>, -} - -impl AllocVar, CF2> for CycleFoldWitnessVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let E = Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; - let rE = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; - - let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; - let rW = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?; - - Ok(Self { E, rE, W, rW }) - }) - } -} - -/// This is the gadget used in the AugmentedFCircuit to verify the CycleFold instances folding, -/// which checks the correct RLC of u,x,cmE,cmW (hence the name containing 'Full', since it checks -/// all the RLC values, not only the native ones). It assumes that ci2.cmE=0, ci2.u=1. -pub struct NIFSFullGadget { - _c: PhantomData, -} - -impl NIFSFullGadget { - pub fn fold_committed_instance( - r_bits: Vec>>, - cmT: C::Var, - ci1: CycleFoldCommittedInstanceVar, - // ci2 is assumed to be always with cmE=0, u=1 (checks done previous to this method) - ci2: CycleFoldCommittedInstanceVar, - ) -> Result, SynthesisError> { - // r_nonnat is equal to r_bits just that in a different format - let r_nonnat = { - let mut bits = r_bits.clone(); - bits.resize(CF1::::MODULUS_BIT_SIZE as usize, Boolean::FALSE); - NonNativeUintVar::from(&bits) - }; - Ok(CycleFoldCommittedInstanceVar { - cmE: cmT.scalar_mul_le(r_bits.iter())? + ci1.cmE, - cmW: ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?, - u: ci1.u.add_no_align(&r_nonnat)?.modulo::>()?, - x: ci1 - .x - .iter() - .zip(ci2.x) - .map(|(a, b)| { - a.add_no_align(&r_nonnat.mul_no_align(&b)?)? - .modulo::>() - }) - .collect::, _>>()?, - }) - } - - pub fn verify( - // assumes that r_bits is equal to r_nonnat just that in a different format - r_bits: Vec>>, - cmT: C::Var, - ci1: CycleFoldCommittedInstanceVar, - // ci2 is assumed to be always with cmE=0, u=1 (checks done previous to this method) - ci2: CycleFoldCommittedInstanceVar, - ci3: CycleFoldCommittedInstanceVar, - ) -> Result<(), SynthesisError> { - let ci = Self::fold_committed_instance(r_bits, cmT, ci1, ci2)?; - - ci.cmE.enforce_equal(&ci3.cmE)?; - ci.u.enforce_equal_unaligned(&ci3.u)?; - ci.cmW.enforce_equal(&ci3.cmW)?; - for (x, y) in ci.x.iter().zip(ci3.x.iter()) { - x.enforce_equal_unaligned(y)?; - } - - Ok(()) - } -} - -impl ArithRelationGadget, CycleFoldCommittedInstanceVar> - for R1CSMatricesVar, NonNativeUintVar>> -{ - type Evaluation = (Vec>>, Vec>>); - - fn eval_relation( - &self, - w: &CycleFoldWitnessVar, - u: &CycleFoldCommittedInstanceVar, - ) -> Result { - self.eval_at_z(&[&[u.u.clone()][..], &u.x, &w.W].concat()) - } - - fn enforce_evaluation( - w: &CycleFoldWitnessVar, - _u: &CycleFoldCommittedInstanceVar, - (AzBz, uCz): Self::Evaluation, - ) -> Result<(), SynthesisError> { - EquivalenceGadget::>::enforce_equivalent(&AzBz[..], &uCz.add(&w.E)?[..]) - } -} - -/// CycleFoldChallengeGadget computes the RO challenge used for the CycleFold instances NIFS, it contains a -/// rust-native and a in-circuit compatible versions. -pub struct CycleFoldChallengeGadget { - _c: PhantomData, // Nova's Curve2, the one used for the CycleFold circuit -} -impl CycleFoldChallengeGadget { - pub fn get_challenge_native>( - transcript: &mut T, - U_i: &CycleFoldCommittedInstance, - u_i: &CycleFoldCommittedInstance, - cmT: C, - ) -> Vec { - transcript.absorb_nonnative(U_i); - transcript.absorb_nonnative(u_i); - transcript.absorb_point(&cmT); - transcript.squeeze_bits(NOVA_N_BITS_RO) - } - - // compatible with the native get_challenge_native - pub fn get_challenge_gadget>( - transcript: &mut T, - U_i_vec: &[FpVar], - u_i: &CycleFoldCommittedInstanceVar, - cmT: &C::Var, - ) -> Result>, SynthesisError> { - transcript.absorb(&U_i_vec)?; - transcript.absorb_nonnative(u_i)?; - transcript.absorb_point(cmT)?; - transcript.squeeze_bits(NOVA_N_BITS_RO) - } -} - -/// [`CycleFoldConfig`] controls the behavior of [`CycleFoldCircuit`]. -/// -/// Looking ahead, the circuit computes the random linear combination of points, -/// which is essentially done by iteratively computing `P = (P + p_i) * r_i`, -/// where `P` is the folded point, `p_i` is the input point, and `r_i` is the -/// randomness. -pub trait CycleFoldConfig: Sized + Default { - /// `N_INPUT_POINTS` specifies the number of input points that are folded in - /// [`CycleFoldCircuit`] via random linear combinations. - const N_INPUT_POINTS: usize; - /// `N_UNIQUE_RANDOMNESSES` specifies the number of *unique* randomnesses - /// allocated in [`CycleFoldCircuit`]. Although the linear combination in - /// general consists of multiple randomnesses, some folding schemes (such as - /// Nova and HyperNova) only need a single one. Thus, by setting this value, - /// the circuit can learn how many randomnesses are used and how long the - /// public inputs vector should be. - const N_UNIQUE_RANDOMNESSES: usize; - /// `RANDOMNESS_BIT_LENGTH` is the maximum bit length of a randomness `r_i`. - const RANDOMNESS_BIT_LENGTH: usize; - /// `FIELD_CAPACITY` is the maximum number of bits that can be stored in a - /// field element. - /// - /// By default, `FIELD_CAPACITY` is set to `MODULUS_BIT_SIZE - 1`. - /// - /// Given a randomness `r_i` with `RANDOMNESS_BIT_LENGTH` bits, we need - /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY` field elements to represent it - /// *compactly* in-circuit. - const FIELD_CAPACITY: usize = CF2::::MODULUS_BIT_SIZE as usize - 1; - - /// Public inputs length for the [`CycleFoldCircuit`], which depends on the - /// above constants defined by the concrete folding scheme. For example: - /// * In Nova, this is `|r| + |p_1| + |p_2| + |P|` - /// * In HyperNova, this is `|r| + |p_i| * n_points + |P|`. - /// * In ProtoGalaxy, this is `|[..., r_i, ...]| + |p_i| * n_points + |P|`. - /// - /// As explained above, `|r|` (i.e., the length of a single randomness) is - /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY`. - /// When there are multiple randomnesses, the length of `|[..., r_i, ...]|` - /// is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY`, as - /// the bits of all randomnesses are concatenated before being packed into - /// field elements. - /// The length of a point `p_i` when treated as public inputs is 2, as we - /// only need the `x` and `y` coordinates of the point. - /// - /// Thus, `IO_LEN` is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY + 2 * (N_INPUT_POINTS + 1)`. - const IO_LEN: usize = { - (Self::RANDOMNESS_BIT_LENGTH * Self::N_UNIQUE_RANDOMNESSES).div_ceil(Self::FIELD_CAPACITY) - + 2 * (Self::N_INPUT_POINTS + 1) - }; - - /// `alloc_points` allocates the points that are going to be folded in the - /// [`CycleFoldCircuit`] via random linear combinations. - /// - /// The implementation must allocate the points as *witness* variables (i.e. - /// by calling [`AllocVar::new_witness`]) first, then mark them as public - /// inputs by calling [`CycleFoldConfig::mark_point_as_public`], and finally - /// return the allocated witness variables. - /// - /// While it is possible to allocate the points as public inputs directly, - /// we do not use this approach because this will create a longer vector of - /// public inputs, which is not ideal for the augmented step circuit on the - /// primary curve. - fn alloc_points(&self, cs: ConstraintSystemRef>) -> Result, SynthesisError>; - - /// `alloc_randomnesses` allocates the randomnesses used as coefficients of - /// the random linear combinations in the `CycleFoldCircuit`. - /// - /// The implementation must allocate the randomnesses as *witness* variables - /// (i.e. by calling [`AllocVar::new_witness`]) first, then mark them as - /// public inputs by calling [`CycleFoldConfig::mark_point_as_public`], and - /// finally return the allocated witness variables. - /// - /// See [`CycleFoldConfig::alloc_points`] for the reason why they need to be - /// allocated as witness variables first and converted to public later. - /// - /// In addition, because the circuit computes `P = (P + p_i) * r_i` for each - /// `i` from `N_INPUT_POINTS - 1` down to `0`, the actual linear combination - /// is `P = r_0 * p_0 + (r_0 r_1) * p_1 + (r_0 r_1 r_2) * p_2 + ...`. Thus, - /// to compute `P = R_0 p_0 + R_1 p_1 + R_2 p_2 + ...`, the implementation - /// should return `r_0 = R_0, r_1 = R_1 / R_0, ..., r_i = R_i / R_{i - 1}`. - /// A special case is `R_i = R^i`, where the allocated randomnesses become - /// `r_0 = 1, r_1 = r_2 = ... = R`. - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError>; - - /// `mark_point_as_public` marks a point as public. - /// - /// The final vector of public inputs is shorter than the result of calling - /// [`AllocVar::new_input`], because we only need the x and y coordinates of - /// the point, but the `infinity` flag is not necessary. - fn mark_point_as_public(point: &C::Var) -> Result<(), SynthesisError> { - for x in &point.to_constraint_field()?[..2] { - // This line "converts" `x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `x`. - // While comparing `x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `x` computed in-circuit. - FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(x)?; - } - Ok(()) - } - - /// `mark_randomness_as_public` marks randomness as public. - /// - /// The final vector of public inputs is shorter than the result of calling - /// [`AllocVar::new_input`], because we pack the bits of randomness into - /// a compact field elements. - fn mark_randomness_as_public(r: &[Boolean>]) -> Result<(), SynthesisError> { - for bits in r.chunks(Self::FIELD_CAPACITY) { - let x = Boolean::le_bits_to_fp(bits)?; - FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(&x)?; - } - Ok(()) - } - - /// `build_circuit` creates a new [`CycleFoldCircuit`] with `self` as the - /// configuration. - fn build_circuit(self) -> CycleFoldCircuit { - CycleFoldCircuit { - _c: PhantomData, - cfg: self, - } - } -} - -#[derive(Debug, Clone)] -pub struct CycleFoldCircuit> { - _c: PhantomData, - cfg: CFG, -} - -impl> Default for CycleFoldCircuit { - fn default() -> Self { - CFG::default().build_circuit() - } -} - -impl> ConstraintSynthesizer> for CycleFoldCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let rs = self.cfg.alloc_randomnesses(cs.clone())?; - let points = self.cfg.alloc_points(cs.clone())?; - - #[cfg(test)] - { - assert_eq!(CFG::N_INPUT_POINTS, points.len()); - assert_eq!(CFG::N_INPUT_POINTS, rs.len()); - for r in &rs { - assert_eq!(CFG::RANDOMNESS_BIT_LENGTH, r.len()); - } - } - - // A slightly optimized version of `scalar_mul_le`. - fn point_mul( - point: &C::Var, - r: &[Boolean>], - ) -> Result { - if r.is_constant() { - let r = CF1::::from( as PrimeField>::BigInt::from_bits_le(&r.value()?)); - if r.is_one() { - return Ok(point.clone()); - } - } - point.scalar_mul_le(r.iter()) - } - - // Given a vector of points (over the primary curve) that are obtained - // from the instances of the folding scheme, we fold them *natively* in - // the CycleFold circuit (over the secondary curve). - // * In Nova, we need to compute P = p_0 + R * p_1. - // - for the cmW we're computing: U_i1.cmW = U_i.cmW + R * u_i.cmW - // - for the cmE we're computing: U_i1.cmE = U_i.cmE + R * cmT + R^2 * u_i.cmE, where u_i.cmE - // is assumed to be 0, so, U_i1.cmE = U_i.cmE + R * cmT - // * In HyperNova, we need to compute P = p_0 + R * p_1 + R^2 * p_2 + ... + R^{n-1} * p_{n-1}. - // * In ProtoGalaxy, we need to compute P = R_0 * p_0 + R_1 * p_1 + R_2 * p_2 + ... + R_{n-1} * p_{n-1}. - // - // To handle HyperNova more efficiently (with less constraints), we do - // P = ((((p_{n-1} * R) + p_{n-2}) * R + p_{n-3}) * R + ...) * R + p_0. - // This can be done iteratively by computing P = (P + p_i) * R. - // - // We further generalize this to support ProtoGalaxy, which now becomes - // P = (((((p_{n-1} * r_{n-1}) + p_{n-2}) * r_{n-2} + p_{n-3}) * r_{n-3} + ...) * r_1 + p_0) * r_0 - // - // Here, r_0 = 1, r_1 = r_2 = ... = r_{n-1} = R for Nova and HyperNova, - // and r_i = R_i / R_{i - 1} for ProtoGalaxy. - let mut p_folded = point_mul::( - &points[CFG::N_INPUT_POINTS - 1], - &rs[CFG::N_INPUT_POINTS - 1], - )?; - for i in (0..CFG::N_INPUT_POINTS - 1).rev() { - p_folded = point_mul::(&(p_folded + &points[i]), &rs[i])?; - } - - CFG::mark_point_as_public(&p_folded)?; - - Ok(()) - } -} - -impl> CycleFoldCircuit { - /// Generates a pair of incoming instance and witness for the CycleFold - /// circuit. - pub fn generate_incoming_instance_witness< - C2: Curve, BaseField = CF1>, - CS2: CommitmentScheme, - const H: bool, - >( - self, - cf_cs_params: &CS2::ProverParams, - mut rng: impl RngCore, - ) -> Result<(CycleFoldWitness, CycleFoldCommittedInstance), Error> { - let cs2 = ConstraintSystem::new_ref(); - self.generate_constraints(cs2.clone())?; - - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let (cf_w_i, cf_x_i) = extract_w_x(&cs2); - - #[cfg(test)] - assert_eq!(cf_x_i.len(), CFG::IO_LEN); - - // generate cyclefold instances - let cf_w_i = CycleFoldWitness::::new::(cf_w_i, cs2.num_constraints(), &mut rng); - let cf_u_i = cf_w_i.commit::(cf_cs_params, cf_x_i)?; - - Ok((cf_w_i, cf_u_i)) - } -} - -/// [`CycleFoldAugmentationGadget`] implements methods for folding multiple -/// CycleFold instances, both natively and in the augmented step circuit. -pub struct CycleFoldAugmentationGadget; - -impl CycleFoldAugmentationGadget { - #[allow(clippy::too_many_arguments, clippy::type_complexity)] - pub fn fold_native, const H: bool>( - transcript: &mut impl Transcript>, - cf_r1cs: &R1CS, - cf_cs_params: &CS::ProverParams, - mut cf_W: CycleFoldWitness, // witness of the running instance - mut cf_U: CycleFoldCommittedInstance, // running instance - cf_ws: Vec>, // witnesses of the incoming instances - cf_us: Vec>, // incoming instances - ) -> Result< - ( - CycleFoldWitness, // W_i1 - CycleFoldCommittedInstance, // U_i1 - Vec, // cmT - ), - Error, - > { - assert_eq!(cf_ws.len(), cf_us.len()); - let mut cf_cmTs = vec![]; - - for (cf_w, cf_u) in cf_ws.into_iter().zip(cf_us) { - // compute T* and cmT* for CycleFoldCircuit - let (cf_T, cf_cmT) = NIFS::>, H>::compute_cyclefold_cmT( - cf_cs_params, - cf_r1cs, - &cf_w, - &cf_u, - &cf_W, - &cf_U, - )?; - cf_cmTs.push(cf_cmT); - - let cf_r_bits = - CycleFoldChallengeGadget::get_challenge_native(transcript, &cf_U, &cf_u, cf_cmT); - let cf_r_Fq = CF1::::from( as PrimeField>::BigInt::from_bits_le(&cf_r_bits)); - - (cf_W, cf_U) = CycleFoldNIFS::::prove( - cf_r_Fq, &cf_W, &cf_U, &cf_w, &cf_u, &cf_T, cf_cmT, - )?; - - #[cfg(test)] - { - use crate::{arith::ArithRelation, folding::traits::CommittedInstanceOps}; - cf_u.check_incoming()?; - cf_r1cs.check_relation(&cf_w, &cf_u)?; - cf_r1cs.check_relation(&cf_W, &cf_U)?; - } - } - - Ok((cf_W, cf_U, cf_cmTs)) - } - - pub fn fold_gadget( - transcript: &mut impl TranscriptVar, S>, - mut cf_U: CycleFoldCommittedInstanceVar, - cf_us: Vec>, - cf_cmTs: Vec, - ) -> Result, SynthesisError> { - assert_eq!(cf_us.len(), cf_cmTs.len()); - - // Fold the incoming CycleFold instances into the running CycleFold - // instance in a iterative way, since `NIFSFullGadget` only supports - // folding one incoming instance at a time. - for (cf_u, cmT) in cf_us.into_iter().zip(cf_cmTs) { - let cf_r_bits = CycleFoldChallengeGadget::get_challenge_gadget( - transcript, - &cf_U.to_native_sponge_field_elements()?, - &cf_u, - &cmT, - )?; - // Fold the current incoming CycleFold instance `cf_u` into the - // running CycleFold instance `cf_U`. - cf_U = NIFSFullGadget::fold_committed_instance(cf_r_bits, cmT, cf_U, cf_u)?; - } - - Ok(cf_U) - } -} - -/// CycleFoldNIFS is a wrapper on top of Nova's NIFS, which just replaces the `prove` and `verify` -/// methods to use a different ChallengeGadget, but internally reuses the other Nova's NIFS -/// methods. -/// It is a custom implementation that does not follow the NIFSTrait because it needs to work over -/// different fields than the main NIFS impls (Nova, Mova, Ova). Could be abstracted, but it's a -/// tradeoff between overcomplexity at the NIFSTrait and the (not much) need of generalization at -/// the CycleFoldNIFS. -pub struct CycleFoldNIFS, const H: bool = false> { - _c2: PhantomData, - _cs: PhantomData, -} -impl, const H: bool> CycleFoldNIFS { - fn prove( - cf_r_Fq: C2::ScalarField, // C2::Fr==C1::Fq - cf_W_i: &CycleFoldWitness, - cf_U_i: &CycleFoldCommittedInstance, - cf_w_i: &CycleFoldWitness, - cf_u_i: &CycleFoldCommittedInstance, - aux_p: &[C2::ScalarField], // = cf_T - aux_v: C2, // = cf_cmT - ) -> Result<(CycleFoldWitness, CycleFoldCommittedInstance), Error> { - let w = NIFS::, H>::fold_witness( - cf_r_Fq, - cf_W_i, - cf_w_i, - &aux_p.to_vec(), - )?; - let ci = Self::verify(cf_r_Fq, cf_U_i, cf_u_i, &aux_v)?; - Ok((w, ci)) - } - fn verify( - r: C2::ScalarField, - U_i: &CycleFoldCommittedInstance, - u_i: &CycleFoldCommittedInstance, - cmT: &C2, // VerifierAux - ) -> Result, Error> { - Ok( - NIFS::, H>::fold_committed_instances( - r, U_i, u_i, cmT, - ), - ) - } -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{constraints::GVar, Fq, Fr, G1Projective as Projective}; - use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; - use ark_r1cs_std::GR1CSVar; - use ark_std::{One, UniformRand, Zero}; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::CommittedInstance; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::utils::get_cm_coordinates; - - struct TestCycleFoldConfig { - r: CF1, - points: Vec, - } - - impl Default for TestCycleFoldConfig { - fn default() -> Self { - let r = CF1::::zero(); - let points = vec![C::zero(); N]; - Self { r, points } - } - } - - impl CycleFoldConfig for TestCycleFoldConfig { - const RANDOMNESS_BIT_LENGTH: usize = NOVA_N_BITS_RO; - const N_INPUT_POINTS: usize = N; - const N_UNIQUE_RANDOMNESSES: usize = 1; - - fn alloc_points( - &self, - cs: ConstraintSystemRef>, - ) -> Result, SynthesisError> { - let points = Vec::new_witness(cs.clone(), || Ok(self.points.clone()))?; - for point in &points { - Self::mark_point_as_public(point)?; - } - Ok(points) - } - - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError> { - let one = &CF1::::one().into_bigint().to_bits_le()[..NOVA_N_BITS_RO]; - let r = &self.r.into_bigint().to_bits_le()[..NOVA_N_BITS_RO]; - let one_var = Vec::new_constant(cs.clone(), one)?; - let r_var = Vec::new_witness(cs.clone(), || Ok(r))?; - Self::mark_randomness_as_public(&r_var)?; - Ok([vec![one_var], vec![r_var; N - 1]].concat()) - } - } - - #[test] - fn test_CycleFoldCircuit_n_points_constraints() -> Result<(), Error> { - const n: usize = 16; - let mut rng = ark_std::test_rng(); - - // points to random-linear-combine - let points: Vec = std::iter::repeat_with(|| Projective::rand(&mut rng)) - .take(n) - .collect(); - - use std::ops::Mul; - let rho_raw = Fq::rand(&mut rng); - let rho_bits = rho_raw.into_bigint().to_bits_le()[..NOVA_N_BITS_RO].to_vec(); - let rho_Fq = - Fq::from_bigint(BigInteger::from_bits_le(&rho_bits)).ok_or(Error::OutOfBounds)?; - let rho_Fr = - Fr::from_bigint(BigInteger::from_bits_le(&rho_bits)).ok_or(Error::OutOfBounds)?; - let mut res = Projective::zero(); - use ark_std::One; - let mut rho_i = Fr::one(); - for point_i in points.iter() { - res += point_i.mul(rho_i); - rho_i *= rho_Fr; - } - - // cs is the Constraint System on the Curve Cycle auxiliary curve constraints field - // (E1::Fq=E2::Fr) - let cs = ConstraintSystem::::new_ref(); - - let x: Vec = [ - vec![rho_Fq], - points.iter().flat_map(get_cm_coordinates).collect(), - get_cm_coordinates(&res), - ] - .concat(); - let cf_circuit = TestCycleFoldConfig:: { r: rho_Fr, points }.build_circuit(); - cf_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - // `instance_assignment[0]` is the constant term 1 - assert_eq!(&cs.borrow().unwrap().instance_assignment()?[1..], &x); - Ok(()) - } - - #[test] - fn test_nifs_full_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::rand(&mut rng); - let mut transcript_v = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - // prepare the committed instances to test in-circuit - let ci: Vec> = (0..2) - .into_iter() - .map(|_| CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }) - .collect(); - let (ci1, mut ci2) = (ci[0].clone(), ci[1].clone()); - // make the 2nd instance a 'fresh' instance (ie. cmE=0, u=1) - ci2.cmE = Projective::zero(); - ci2.u = Fr::one(); - - let cmT = Projective::rand(&mut rng); // random only for testing - let (ci3, r_bits) = NIFS::, PoseidonSponge>::verify( - &mut transcript_v, - &ci1, - &ci2, - &cmT, - )?; - - let cs = ConstraintSystem::::new_ref(); - let r_bitsVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits))?; - let ci1Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(ci1.clone()) - })?; - let ci2Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(ci2.clone()) - })?; - let ci3Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(ci3.clone()) - })?; - let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT))?; - - NIFSFullGadget::::verify(r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var)?; - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_cyclefold_challenge_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fq::from(42u32); // only for test - let mut transcript = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - let u_i = CycleFoldCommittedInstance:: { - cmE: Projective::zero(), // zero on purpose, so we test also the zero point case - u: Fr::zero(), - cmW: Projective::rand(&mut rng), - x: std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(TestCycleFoldConfig::::IO_LEN) - .collect(), - }; - let U_i = CycleFoldCommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(TestCycleFoldConfig::::IO_LEN) - .collect(), - }; - let cmT = Projective::rand(&mut rng); // random only for testing - - // compute the challenge natively - let r_bits = CycleFoldChallengeGadget::::get_challenge_native( - &mut transcript, - &U_i, - &u_i, - cmT, - ); - - let cs = ConstraintSystem::::new_ref(); - let u_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(u_i.clone()) - })?; - let U_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(U_i.clone()) - })?; - let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT))?; - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let mut transcript_var = - PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - - let r_bitsVar = CycleFoldChallengeGadget::::get_challenge_gadget( - &mut transcript_var, - &U_iVar.to_native_sponge_field_elements()?, - &u_iVar, - &cmTVar, - )?; - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - let rVar = Boolean::le_bits_to_fp(&r_bitsVar)?; - let r = Fq::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds)?; - assert_eq!(rVar.value()?, r); - assert_eq!(r_bitsVar.value()?, r_bits); - Ok(()) - } - - #[test] - fn test_cyclefold_hash_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fq::from(42u32); // only for test - let sponge = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - let U_i = CycleFoldCommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(TestCycleFoldConfig::::IO_LEN) - .collect(), - }; - let h = U_i.hash_cyclefold(&sponge); - - let cs = ConstraintSystem::::new_ref(); - let U_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(U_i.clone()) - })?; - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let (hVar, _) = U_iVar.hash(&PoseidonSpongeVar::new_with_pp_hash( - &poseidon_config, - &pp_hashVar, - )?)?; - hVar.enforce_equal(&FpVar::new_witness(cs.clone(), || Ok(h))?)?; - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/decider/mod.rs b/folding-schemes/src/folding/circuits/decider/mod.rs deleted file mode 100644 index d455b6339..000000000 --- a/folding-schemes/src/folding/circuits/decider/mod.rs +++ /dev/null @@ -1,205 +0,0 @@ -use ark_crypto_primitives::sponge::{ - poseidon::constraints::PoseidonSpongeVar, CryptographicSponge, -}; -use ark_ff::PrimeField; -use ark_poly::Polynomial; -use ark_r1cs_std::{ - fields::{fp::FpVar, FieldVar}, - poly::{domain::Radix2DomainVar, evaluations::univariate::EvaluationsVar}, -}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::log2; - -use crate::folding::traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps}; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::utils::vec::poly_from_vec; -use crate::{arith::ArithRelation, folding::circuits::CF1}; -use crate::{Curve, Error}; - -pub mod off_chain; -pub mod on_chain; - -/// Gadget that computes the KZG challenges. -/// It also offers the rust native implementation compatible with the gadget. -pub struct KZGChallengesGadget {} - -impl KZGChallengesGadget { - pub fn get_challenges_native>, U: CommittedInstanceOps>( - transcript: &mut T, - U_i: &U, - ) -> Vec> { - let mut challenges = vec![]; - for cm in U_i.get_commitments() { - transcript.absorb_nonnative(&cm); - challenges.push(transcript.get_challenge()); - } - challenges - } - - pub fn get_challenges_gadget< - C: Curve, - S: CryptographicSponge, - T: TranscriptVar, S>, - U: CommittedInstanceVarOps, - >( - transcript: &mut T, - U_i: &U, - ) -> Result>>, SynthesisError> { - let mut challenges = vec![]; - for cm in U_i.get_commitments() { - transcript.absorb_nonnative(&cm)?; - challenges.push(transcript.get_challenge()?); - } - Ok(challenges) - } -} - -/// Gadget that interpolates the polynomial from the given vector and returns -/// its evaluation at the given point. -/// It also offers the rust native implementation compatible with the gadget. -pub struct EvalGadget {} - -impl EvalGadget { - pub fn evaluate_native(v: &[F], point: F) -> Result { - let mut v = v.to_vec(); - v.resize(v.len().next_power_of_two(), F::zero()); - - Ok(poly_from_vec(v)?.evaluate(&point)) - } - - pub fn evaluate_gadget( - v: &[FpVar], - point: &FpVar, - ) -> Result, SynthesisError> { - let mut v = v.to_vec(); - v.resize(v.len().next_power_of_two(), FpVar::zero()); - let n = v.len() as u64; - let gen = F::get_root_of_unity(n).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; - // `unwrap` below is safe because `Radix2DomainVar::new` only fails if - // `offset.enforce_not_equal(&FpVar::zero())` returns an error. - // But in our case, `offset` is `FpVar::one()`, i.e., both operands of - // `enforce_not_equal` are constants. - // Consequently, `FpVar`'s implementation of `enforce_not_equal` will - // always return `Ok(())`. - let domain = Radix2DomainVar::new(gen, log2(v.len()) as u64, FpVar::one()).unwrap(); - - let evaluations_var = EvaluationsVar::from_vec_and_domain(v, domain, true); - evaluations_var.interpolate_and_evaluate(point) - } -} - -/// This is a temporary workaround for step 6 (running NIFS.V for group elements -/// in circuit) in an NIFS-agnostic way, because different folding schemes have -/// different interfaces of folding verification now. -/// -/// In the future, we may introduce a better solution that uses a trait for all -/// folding schemes that specifies their native and in-circuit behaviors. -pub trait DeciderEnabledNIFS< - C: Curve, - RU: CommittedInstanceOps, // Running instance - IU: CommittedInstanceOps, // Incoming instance - W: WitnessOps>, - A: ArithRelation, -> -{ - type ProofDummyCfg; - type Proof: Dummy; - type RandomnessDummyCfg; - type Randomness: Dummy; - - /// Fold the field elements in `U` and `u` inside the circuit. - /// - /// `U_vec` is `U` expressed as a vector of `FpVar`s, which can be reused - /// before or after calling this function to save constraints. - #[allow(clippy::too_many_arguments)] - fn fold_field_elements_gadget( - arith: &A, - transcript: &mut PoseidonSpongeVar>, - U: RU::Var, - U_vec: Vec>>, - u: IU::Var, - proof: Self::Proof, - randomness: Self::Randomness, - ) -> Result; - - /// Fold the group elements (i.e., commitments) in `U` and `u` outside the - /// circuit. - fn fold_group_elements_native( - U_commitments: &[C], - u_commitments: &[C], - proof: Option, - randomness: Self::Randomness, - ) -> Result, Error>; -} - -#[cfg(test)] -pub mod tests { - use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, poseidon::PoseidonSponge, - }; - use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::UniformRand; - - use super::*; - use crate::folding::nova::{nifs::nova_circuits::CommittedInstanceVar, CommittedInstance}; - use crate::transcript::poseidon::poseidon_canonical_config; - - // checks that the gadget and native implementations of the challenge computation match - #[test] - fn test_kzg_challenge_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let mut transcript = PoseidonSponge::::new(&poseidon_config); - - let U_i = CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }; - - // compute the challenge natively - let challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i); - - let cs = ConstraintSystem::::new_ref(); - let U_iVar = - CommittedInstanceVar::::new_witness(cs.clone(), || Ok(U_i.clone()))?; - let mut transcript_var = PoseidonSpongeVar::::new(cs.clone(), &poseidon_config); - - let challenges_var = - KZGChallengesGadget::get_challenges_gadget(&mut transcript_var, &U_iVar)?; - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(challenges_var.value()?, challenges); - Ok(()) - } - - #[test] - fn test_polynomial_interpolation() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let n = 12; - let l = 1 << n; - - let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(l) - .collect(); - let challenge = Fr::rand(&mut rng); - - use ark_poly::Polynomial; - let polynomial = poly_from_vec(v.to_vec())?; - let eval = polynomial.evaluate(&challenge); - - let cs = ConstraintSystem::::new_ref(); - let vVar = Vec::>::new_witness(cs.clone(), || Ok(v))?; - let challengeVar = FpVar::::new_witness(cs.clone(), || Ok(challenge))?; - - let evalVar = EvalGadget::evaluate_gadget(&vVar, &challengeVar)?; - - assert_eq!(evalVar.value()?, eval); - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/decider/off_chain.rs b/folding-schemes/src/folding/circuits/decider/off_chain.rs deleted file mode 100644 index 3e29beab1..000000000 --- a/folding-schemes/src/folding/circuits/decider/off_chain.rs +++ /dev/null @@ -1,306 +0,0 @@ -/// This file implements a generic offchain decider circuit. -/// For ethereum use cases, use the `GenericOnchainDeciderCircuit`. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html -use ark_crypto_primitives::sponge::{ - constraints::AbsorbGadget, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, -}; -use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::{marker::PhantomData, Zero}; - -use crate::{ - arith::{ - r1cs::{circuits::R1CSMatricesVar, R1CS}, - ArithRelation, ArithRelationGadget, - }, - folding::{ - circuits::{ - cyclefold::{ - CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldWitness, - }, - decider::{EvalGadget, KZGChallengesGadget}, - nonnative::affine::NonNativeAffineVar, - CF1, CF2, - }, - nova::{decider_eth_circuit::WitnessVar, nifs::nova_circuits::CommittedInstanceVar}, - traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps}, - }, - transcript::TranscriptVar, - Curve, -}; - -use super::DeciderEnabledNIFS; - -/// Circuit that implements part of the in-circuit checks needed for the offchain verification over -/// the Curve2's BaseField (=Curve1's ScalarField). -pub struct GenericOffchainDeciderCircuit1< - C1: Curve, - C2: Curve, - RU: CommittedInstanceOps, // Running instance - IU: CommittedInstanceOps, // Incoming instance - W: WitnessOps>, // Witness - A: ArithRelation, // Constraint system - AVar: ArithRelationGadget, // In-circuit representation of `A` - D: DeciderEnabledNIFS, -> { - pub _avar: PhantomData, - /// Constraint system of the Augmented Function circuit - pub arith: A, - pub poseidon_config: PoseidonConfig>, - /// public params hash - pub pp_hash: CF1, - pub i: CF1, - /// initial state - pub z_0: Vec>, - /// current i-th state - pub z_i: Vec>, - /// Folding scheme instances - pub U_i: RU, - pub W_i: W, - pub u_i: IU, - pub w_i: W, - pub U_i1: RU, - pub W_i1: W, - - /// Helper for folding verification - pub proof: D::Proof, - pub randomness: D::Randomness, - - /// CycleFold running instance - pub cf_U_i: CycleFoldCommittedInstance, - - /// KZG challenges - pub kzg_challenges: Vec>, - pub kzg_evaluations: Vec>, -} - -impl< - C1: Curve, - C2: Curve, BaseField = CF1>, - RU: CommittedInstanceOps + for<'a> Dummy<&'a A>, - IU: CommittedInstanceOps + for<'a> Dummy<&'a A>, - W: WitnessOps> + for<'a> Dummy<&'a A>, - A: ArithRelation, - AVar: ArithRelationGadget + AllocVar>, - D: DeciderEnabledNIFS, - > - Dummy<( - A, - &R1CS>, - PoseidonConfig>, - D::ProofDummyCfg, - D::RandomnessDummyCfg, - usize, - usize, - )> for GenericOffchainDeciderCircuit1 -{ - fn dummy( - ( - arith, - cf_arith, - poseidon_config, - proof_config, - randomness_config, - state_len, - num_commitments, - ): ( - A, - &R1CS>, - PoseidonConfig>, - D::ProofDummyCfg, - D::RandomnessDummyCfg, - usize, - usize, - ), - ) -> Self { - Self { - _avar: PhantomData, - poseidon_config, - pp_hash: Zero::zero(), - i: Zero::zero(), - z_0: vec![Zero::zero(); state_len], - z_i: vec![Zero::zero(); state_len], - U_i: RU::dummy(&arith), - W_i: W::dummy(&arith), - u_i: IU::dummy(&arith), - w_i: W::dummy(&arith), - U_i1: RU::dummy(&arith), - W_i1: W::dummy(&arith), - proof: D::Proof::dummy(proof_config), - randomness: D::Randomness::dummy(randomness_config), - cf_U_i: CycleFoldCommittedInstance::dummy(cf_arith), - kzg_challenges: vec![Zero::zero(); num_commitments], - kzg_evaluations: vec![Zero::zero(); num_commitments], - arith, - } - } -} - -impl< - C1: Curve, - C2: Curve, BaseField = CF1>, - RU: CommittedInstanceOps, - IU: CommittedInstanceOps, - W: WitnessOps>, - A: ArithRelation, - AVar: ArithRelationGadget + AllocVar>, - D: DeciderEnabledNIFS, - > ConstraintSynthesizer> - for GenericOffchainDeciderCircuit1 -where - RU::Var: AbsorbGadget> + CommittedInstanceVarOps>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let arith = AVar::new_witness(cs.clone(), || Ok(&self.arith))?; - - let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; - let i = FpVar::new_input(cs.clone(), || Ok(self.i))?; - let z_0 = Vec::new_input(cs.clone(), || Ok(self.z_0))?; - let z_i = Vec::new_input(cs.clone(), || Ok(self.z_i))?; - - let u_i = IU::Var::new_witness(cs.clone(), || Ok(self.u_i))?; - let U_i = RU::Var::new_witness(cs.clone(), || Ok(self.U_i))?; - // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) - let U_i1_commitments = Vec::>::new_input(cs.clone(), || { - Ok(self.U_i1.get_commitments()) - })?; - let U_i1 = RU::Var::new_witness(cs.clone(), || Ok(self.U_i1))?; - let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; - U_i1.get_commitments().enforce_equal(&U_i1_commitments)?; - - let cf_U_i = - CycleFoldCommittedInstanceVar::::new_input(cs.clone(), || Ok(self.cf_U_i))?; - - // allocate the inputs for the checks 7.1 and 7.2 - let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; - let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; - - // `sponge` is for digest computation. - // notice that `pp_hash` has already been absorbed during init. - let sponge = PoseidonSpongeVar::new_with_pp_hash(&self.poseidon_config, &pp_hash)?; - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - // 1. enforce `U_{i+1}` and `W_{i+1}` satisfy `arith` - arith.enforce_relation(&W_i1, &U_i1)?; - - // 2. enforce `u_i` is an incoming instance - u_i.enforce_incoming()?; - - // 3. u_i.x[0] == H(i, z_0, z_i, U_i), u_i.x[1] == H(cf_U_i) - let (u_i_x, U_i_vec) = U_i.hash(&sponge, &i, &z_0, &z_i)?; - let (cf_u_i_x, _) = cf_U_i.hash(&sponge)?; - u_i.get_public_inputs().enforce_equal(&[u_i_x, cf_u_i_x])?; - - // 6.1. partially enforce `NIFS.V(U_i, u_i) = U_{i+1}`. - D::fold_field_elements_gadget( - &self.arith, - &mut transcript, - U_i, - U_i_vec, - u_i, - self.proof, - self.randomness, - )? - .enforce_partial_equal(&U_i1)?; - - // 7.1. compute and check KZG challenges - KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? - .enforce_equal(&kzg_challenges)?; - - // 7.2. check the claimed evaluations - for (((v, _r), c), e) in W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .zip(&kzg_evaluations) - { - // The randomness `_r` is currently not used. - EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; - } - - Ok(()) - } -} - -/// Circuit that implements part of the in-circuit checks needed for the offchain verification over -/// the Curve1's BaseField (=Curve2's ScalarField). -pub struct GenericOffchainDeciderCircuit2 { - /// R1CS of the CycleFold circuit - pub cf_arith: R1CS>, - pub poseidon_config: PoseidonConfig>, - /// public params hash - pub pp_hash: CF1, - - /// CycleFold running instance - pub cf_U_i: CycleFoldCommittedInstance, - pub cf_W_i: CycleFoldWitness, - - /// KZG challenges - pub kzg_challenges: Vec>, - pub kzg_evaluations: Vec>, -} - -impl Dummy<(R1CS>, PoseidonConfig>, usize)> - for GenericOffchainDeciderCircuit2 -{ - fn dummy( - (cf_arith, poseidon_config, num_commitments): ( - R1CS>, - PoseidonConfig>, - usize, - ), - ) -> Self { - Self { - poseidon_config, - pp_hash: Zero::zero(), - cf_U_i: CycleFoldCommittedInstance::dummy(&cf_arith), - cf_W_i: CycleFoldWitness::dummy(&cf_arith), - kzg_challenges: vec![Zero::zero(); num_commitments], - kzg_evaluations: vec![Zero::zero(); num_commitments], - cf_arith, - } - } -} - -impl ConstraintSynthesizer> for GenericOffchainDeciderCircuit2 { - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let cf_r1cs = R1CSMatricesVar::, FpVar>>::new_witness(cs.clone(), || { - Ok(self.cf_arith.clone()) - })?; - - let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; - - let cf_U_i = CommittedInstanceVar::new_input(cs.clone(), || Ok(self.cf_U_i))?; - let cf_W_i = WitnessVar::new_witness(cs.clone(), || Ok(self.cf_W_i))?; - - // allocate the inputs for the checks 4.1 and 4.2 - let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; - let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; - - // `transcript` is for challenge generation. - let mut transcript = PoseidonSpongeVar::new_with_pp_hash(&self.poseidon_config, &pp_hash)?; - - // 5. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs` - cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; - - // 4.1. compute and check KZG challenges - KZGChallengesGadget::get_challenges_gadget(&mut transcript, &cf_U_i)? - .enforce_equal(&kzg_challenges)?; - - // 4.2. check the claimed evaluations - for (((v, _r), c), e) in cf_W_i - .get_openings() - .iter() - .zip(&kzg_challenges) - .zip(&kzg_evaluations) - { - // The randomness `_r` is currently not used. - EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; - } - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/decider/on_chain.rs b/folding-schemes/src/folding/circuits/decider/on_chain.rs deleted file mode 100644 index 798662316..000000000 --- a/folding-schemes/src/folding/circuits/decider/on_chain.rs +++ /dev/null @@ -1,315 +0,0 @@ -/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, -/// other more efficient approaches can be used. -use ark_crypto_primitives::sponge::{ - constraints::AbsorbGadget, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, -}; -use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::{marker::PhantomData, Zero}; - -use crate::{ - arith::{r1cs::R1CS, ArithRelation, ArithRelationGadget}, - commitment::pedersen::Params as PedersenParams, - folding::{ - circuits::{ - cyclefold::{ - CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldWitness, - }, - decider::{EvalGadget, KZGChallengesGadget}, - nonnative::affine::NonNativeAffineVar, - CF1, CF2, - }, - traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps}, - }, - transcript::TranscriptVar, - Curve, -}; - -use super::DeciderEnabledNIFS; - -/// A generic circuit tailored for the onchain (Ethereum's EVM) verification of -/// IVC proofs, where we support IVC built upon any folding scheme. -/// -/// Specifically, `GenericDeciderEthCircuit` implements the in-circuit version -/// of the IVC verification algorithm, which essentially checks the following: -/// - `R_arith(W_i, U_i)`: -/// The running instance `U_i` and witness `W_i` satisfy `arith`, -/// and the commitments in `U_i` open to the values in `W_i`. -/// - `R_arith(w_i, u_i)`: -/// The incoming instance `u_i` and witness `w_i` satisfy `arith`, -/// and the commitments in `u_i` open to the values in `w_i`. -/// - `R_cf_arith(cf_W_i, cf_U_i)`: -/// The CycleFold instance `cf_U_i` and witness `cf_W_i` satisfy `cf_arith`, -/// and the commitments in `cf_U_i` open to the values in `cf_W_i`. -/// - `u_i` contains the correct hash of the initial and final states. -/// -/// To reduce the number of relation checks, the prover, before invoking the -/// circuit, further folds `U_i, u_i` into `U_{i+1}`, and `W_i, w_i` into -/// `W_{i+1}`. -/// Now, the circuit only needs to perform two relation checks, i.e., -/// `R_arith(W_{i+1}, U_{i+1})` and `R_cf_arith(cf_W_i, cf_U_i)`, plus a few -/// constraints for enforcing the correct hash in `u_i` and the correct folding -/// from `U_i, u_i` to `U_{i+1}`. -/// -/// We further reduce the circuit size by avoiding the non-native commitment -/// checks involved in `R_arith(W_{i+1}, U_{i+1})`. -/// Now, we now only check the satisfiability of the constraint system `arith` -/// with the witness `W_{i+1}` and instance `U_{i+1}` in the circuit, but the -/// actual commitment checks are done with the help of KZG. -/// -/// For more details, see [https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html]. -pub struct GenericOnchainDeciderCircuit< - C1: Curve, - C2: Curve, - RU: CommittedInstanceOps, // Running instance - IU: CommittedInstanceOps, // Incoming instance - W: WitnessOps>, // Witness - A: ArithRelation, // Constraint system - AVar: ArithRelationGadget, // In-circuit representation of `A` - D: DeciderEnabledNIFS, -> { - pub _avar: PhantomData, - /// Constraint system of the Augmented Function circuit - pub arith: A, - /// R1CS of the CycleFold circuit - pub cf_arith: R1CS>, - /// CycleFold PedersenParams over C2 - pub cf_pedersen_params: PedersenParams, - pub poseidon_config: PoseidonConfig>, - /// public params hash - pub pp_hash: CF1, - pub i: CF1, - /// initial state - pub z_0: Vec>, - /// current i-th state - pub z_i: Vec>, - /// Folding scheme instances - pub U_i: RU, - pub W_i: W, - pub u_i: IU, - pub w_i: W, - pub U_i1: RU, - pub W_i1: W, - - /// Helper for folding verification - pub proof: D::Proof, - pub randomness: D::Randomness, - - /// CycleFold running instance - pub cf_U_i: CycleFoldCommittedInstance, - pub cf_W_i: CycleFoldWitness, - - /// KZG challenges - pub kzg_challenges: Vec>, - pub kzg_evaluations: Vec>, -} - -impl< - C1: Curve, - C2: Curve, BaseField = CF1>, - RU: CommittedInstanceOps + for<'a> Dummy<&'a A>, - IU: CommittedInstanceOps + for<'a> Dummy<&'a A>, - W: WitnessOps> + for<'a> Dummy<&'a A>, - A: ArithRelation, - AVar: ArithRelationGadget + AllocVar>, - D: DeciderEnabledNIFS, - > - Dummy<( - A, - R1CS>, - PedersenParams, - PoseidonConfig>, - D::ProofDummyCfg, - D::RandomnessDummyCfg, - usize, - usize, - )> for GenericOnchainDeciderCircuit -{ - fn dummy( - ( - arith, - cf_arith, - cf_pedersen_params, - poseidon_config, - proof_config, - randomness_config, - state_len, - num_commitments, - ): ( - A, - R1CS>, - PedersenParams, - PoseidonConfig>, - D::ProofDummyCfg, - D::RandomnessDummyCfg, - usize, - usize, - ), - ) -> Self { - Self { - _avar: PhantomData, - cf_pedersen_params, - poseidon_config, - pp_hash: Zero::zero(), - i: Zero::zero(), - z_0: vec![Zero::zero(); state_len], - z_i: vec![Zero::zero(); state_len], - U_i: RU::dummy(&arith), - W_i: W::dummy(&arith), - u_i: IU::dummy(&arith), - w_i: W::dummy(&arith), - U_i1: RU::dummy(&arith), - W_i1: W::dummy(&arith), - proof: D::Proof::dummy(proof_config), - randomness: D::Randomness::dummy(randomness_config), - cf_U_i: CycleFoldCommittedInstance::dummy(&cf_arith), - cf_W_i: CycleFoldWitness::dummy(&cf_arith), - kzg_challenges: vec![Zero::zero(); num_commitments], - kzg_evaluations: vec![Zero::zero(); num_commitments], - arith, - cf_arith, - } - } -} - -impl< - C1: Curve, - C2: Curve, BaseField = CF1>, - RU: CommittedInstanceOps, - IU: CommittedInstanceOps, - W: WitnessOps>, - A: ArithRelation, - AVar: ArithRelationGadget + AllocVar>, - D: DeciderEnabledNIFS, - > ConstraintSynthesizer> for GenericOnchainDeciderCircuit -where - RU::Var: AbsorbGadget> + CommittedInstanceVarOps>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let arith = AVar::new_witness(cs.clone(), || Ok(&self.arith))?; - - let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; - let i = FpVar::new_input(cs.clone(), || Ok(self.i))?; - let z_0 = Vec::new_input(cs.clone(), || Ok(self.z_0))?; - let z_i = Vec::new_input(cs.clone(), || Ok(self.z_i))?; - - let u_i = IU::Var::new_witness(cs.clone(), || Ok(self.u_i))?; - let U_i = RU::Var::new_witness(cs.clone(), || Ok(self.U_i))?; - // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) - let U_i1_commitments = Vec::>::new_input(cs.clone(), || { - Ok(self.U_i1.get_commitments()) - })?; - let U_i1 = RU::Var::new_witness(cs.clone(), || Ok(self.U_i1))?; - let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; - U_i1.get_commitments().enforce_equal(&U_i1_commitments)?; - - let cf_U_i = - CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.cf_U_i))?; - - // allocate the inputs for the check 7.1 and 7.2 - let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; - let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; - - // `sponge` is for digest computation. - let sponge = PoseidonSpongeVar::new_with_pp_hash(&self.poseidon_config, &pp_hash)?; - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - // NOTE: we use the same enumeration as in - // https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html - // in order to make it easier to reason about. - - // 1. enforce `U_{i+1}` and `W_{i+1}` satisfy `arith` - arith.enforce_relation(&W_i1, &U_i1)?; - - // 2. enforce `u_i` is an incoming instance - u_i.enforce_incoming()?; - - // 3. u_i.x[0] == H(i, z_0, z_i, U_i), u_i.x[1] == H(cf_U_i) - let (u_i_x, U_i_vec) = U_i.hash(&sponge, &i, &z_0, &z_i)?; - let (cf_u_i_x, _) = cf_U_i.hash(&sponge)?; - u_i.get_public_inputs().enforce_equal(&[u_i_x, cf_u_i_x])?; - - #[cfg(feature = "light-test")] - log::warn!("[WARNING]: Running with the 'light-test' feature, skipping the big part of the DeciderEthCircuit.\n Only for testing purposes."); - - // The following two checks (and their respective allocations) are disabled for normal - // tests since they take several millions of constraints and would take several minutes - // (and RAM) to run the test. It is active by default, and not active only when - // 'light-test' feature is used. - #[cfg(not(feature = "light-test"))] - { - // imports here instead of at the top of the file, so we avoid having multiple - // `#[cfg(not(test))]` - use crate::{ - arith::r1cs::circuits::R1CSMatricesVar, - commitment::pedersen::PedersenGadget, - folding::circuits::{ - cyclefold::CycleFoldWitnessVar, nonnative::uint::NonNativeUintVar, - }, - }; - use ark_r1cs_std::{convert::ToBitsGadget, groups::CurveVar}; - let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || Ok(self.cf_W_i))?; - // 4. check Pedersen commitments of cf_U_i.{cmE, cmW} - let H = C2::Var::constant(self.cf_pedersen_params.h); - let G = self - .cf_pedersen_params - .generators - .iter() - .map(|&g| C2::Var::constant(g.into())) - .collect::>(); - let cf_W_i_E_bits = cf_W_i - .E - .iter() - .map(|E_i| E_i.to_bits_le()) - .collect::, _>>()?; - let cf_W_i_W_bits = cf_W_i - .W - .iter() - .map(|W_i| W_i.to_bits_le()) - .collect::, _>>()?; - PedersenGadget::::commit(&H, &G, &cf_W_i_E_bits, &cf_W_i.rE.to_bits_le()?)? - .enforce_equal(&cf_U_i.cmE)?; - PedersenGadget::::commit(&H, &G, &cf_W_i_W_bits, &cf_W_i.rW.to_bits_le()?)? - .enforce_equal(&cf_U_i.cmW)?; - - let cf_r1cs = R1CSMatricesVar::, NonNativeUintVar>>::new_constant( - ConstraintSystemRef::None, - self.cf_arith, - )?; - - // 5. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs` - cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; - } - - // 6.1. partially enforce `NIFS.V(U_i, u_i) = U_{i+1}`. - D::fold_field_elements_gadget( - &self.arith, - &mut transcript, - U_i, - U_i_vec, - u_i, - self.proof, - self.randomness, - )? - .enforce_partial_equal(&U_i1)?; - - // 7.1. compute and check KZG challenges - KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? - .enforce_equal(&kzg_challenges)?; - - // 7.2. check the claimed evaluations - for (((v, _r), c), e) in W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .zip(&kzg_evaluations) - { - // The randomness `_r` is currently not used. - EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; - } - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/mod.rs b/folding-schemes/src/folding/circuits/mod.rs deleted file mode 100644 index 5b6af02bc..000000000 --- a/folding-schemes/src/folding/circuits/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -/// Circuits and gadgets shared across the different folding schemes. -use ark_ec::{CurveGroup, PrimeGroup}; -use ark_ff::Field; - -pub mod cyclefold; -pub mod decider; -pub mod nonnative; -pub mod sum_check; -pub mod utils; - -/// CF1 uses the ScalarField of the given C. CF1 represents the ConstraintField used for the main -/// folding circuit which is over E1::Fr, where E1 is the main curve where we do the folding. -/// In CF1, the points of C can not be natively represented. -pub type CF1 = ::ScalarField; -/// CF2 uses the BaseField of the given C. CF2 represents the ConstraintField used for the -/// CycleFold circuit which is over E2::Fr=E1::Fq, where E2 is the auxiliary curve (from -/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf) approach) where we check the folding of the -/// commitments (elliptic curve points). -/// In CF2, the points of C can be natively represented. -pub type CF2 = <::BaseField as Field>::BasePrimeField; diff --git a/folding-schemes/src/folding/circuits/nonnative/affine.rs b/folding-schemes/src/folding/circuits/nonnative/affine.rs deleted file mode 100644 index c7d7bb98d..000000000 --- a/folding-schemes/src/folding/circuits/nonnative/affine.rs +++ /dev/null @@ -1,241 +0,0 @@ -use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig, SWFlags}, - AffineRepr, CurveGroup, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - eq::EqGadget, - fields::fp::FpVar, - prelude::Boolean, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; -use ark_std::{borrow::Borrow, One, Zero}; - -use crate::{ - folding::traits::{Inputize, InputizeNonNative}, - transcript::{AbsorbNonNative, AbsorbNonNativeGadget}, - Curve, Field, -}; - -use super::uint::NonNativeUintVar; - -/// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native -/// field, over the constraint field. It is not intended to perform operations, but just to contain -/// the affine coordinates in order to perform hash operations of the point. -#[derive(Debug, Clone)] -pub struct NonNativeAffineVar { - pub x: NonNativeUintVar, - pub y: NonNativeUintVar, -} - -impl AllocVar for NonNativeAffineVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let affine = val.borrow().into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - - let x = NonNativeUintVar::new_variable(cs.clone(), || Ok(x), mode)?; - let y = NonNativeUintVar::new_variable(cs.clone(), || Ok(y), mode)?; - - Ok(Self { x, y }) - }) - } -} - -impl GR1CSVar for NonNativeAffineVar { - type Value = C; - - fn cs(&self) -> ConstraintSystemRef { - self.x.cs().or(self.y.cs()) - } - - fn value(&self) -> Result { - let x = C::BaseField::from_le_bytes_mod_order(&self.x.value()?.to_bytes_le()); - let y = C::BaseField::from_le_bytes_mod_order(&self.y.value()?.to_bytes_le()); - // Below is a workaround to convert the `x` and `y` coordinates to a - // point. This is because the `SonobeCurve` trait does not provide a - // method to construct a point from `BaseField` elements. - let mut bytes = vec![]; - // `unwrap` below is safe because serialization of a `PrimeField` value - // only fails if the serialization flag has more than 8 bits, but here - // we call `serialize_uncompressed` which uses an empty flag. - x.serialize_uncompressed(&mut bytes).unwrap(); - // `unwrap` below is also safe, because the bit size of `SWFlags` is 2. - y.serialize_with_flags( - &mut bytes, - if x.is_zero() && y.is_zero() { - SWFlags::PointAtInfinity - } else if y <= -y { - SWFlags::YIsPositive - } else { - SWFlags::YIsNegative - }, - ) - .unwrap(); - // `unwrap` below is safe because `bytes` is constructed from the `x` - // and `y` coordinates of a valid point, and these coordinates are - // serialized in the same way as the `SonobeCurve` implementation. - Ok(C::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) - } -} - -impl EqGadget for NonNativeAffineVar { - fn is_eq(&self, other: &Self) -> Result, SynthesisError> { - let mut result = Boolean::TRUE; - if self.x.0.len() != other.x.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.y.0.len() != other.y.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - for (l, r) in self - .x - .0 - .iter() - .chain(&self.y.0) - .zip(other.x.0.iter().chain(&other.y.0)) - { - if l.ub != r.ub { - return Err(SynthesisError::Unsatisfiable); - } - result &= l.v.is_eq(&r.v)?; - } - Ok(result) - } - - fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { - if self.x.0.len() != other.x.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.y.0.len() != other.y.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - for (l, r) in self - .x - .0 - .iter() - .chain(&self.y.0) - .zip(other.x.0.iter().chain(&other.y.0)) - { - if l.ub != r.ub { - return Err(SynthesisError::Unsatisfiable); - } - l.v.enforce_equal(&r.v)?; - } - Ok(()) - } -} - -impl NonNativeAffineVar { - pub fn zero() -> Self { - // `unwrap` below is safe because we are allocating a constant value, - // which is guaranteed to succeed. - Self::new_constant(ConstraintSystemRef::None, C::zero()).unwrap() - } -} - -impl> AbsorbNonNative for Projective

{ - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - let affine = self.into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - - [x, y].to_native_sponge_field_elements(dest); - } -} - -impl AbsorbNonNativeGadget for NonNativeAffineVar { - fn to_native_sponge_field_elements( - &self, - ) -> Result>, SynthesisError> { - [&self.x, &self.y].to_native_sponge_field_elements() - } -} - -impl> Inputize for Projective

{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `ProjectiveVar::new_input`. - fn inputize(&self) -> Vec { - let affine = self.into_affine(); - match affine.xy() { - Some((x, y)) => vec![x, y, One::one()], - None => vec![Zero::zero(), One::one(), Zero::zero()], - } - } -} - -impl> InputizeNonNative for Projective

{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeAffineVar::new_input`. - fn inputize_nonnative(&self) -> Vec { - let affine = self.into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - - [x, y].inputize_nonnative() - } -} - -#[cfg(test)] -mod tests { - use ark_pallas::{Fq, Fr, PallasConfig, Projective}; - use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::UniformRand; - - use super::*; - use crate::Error; - - #[test] - fn test_alloc_zero() { - let cs = ConstraintSystem::::new_ref(); - - // dealing with the 'zero' point should not panic when doing the unwrap - let p = Projective::zero(); - assert!(NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p)).is_ok()); - } - - #[test] - fn test_improved_to_hash_preimage() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - // check that point_to_nonnative_limbs returns the expected values - let mut rng = ark_std::test_rng(); - let p = Projective::rand(&mut rng); - let pVar = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - pVar.to_native_sponge_field_elements()?.value()?, - p.to_native_sponge_field_elements_as_vec() - ); - Ok(()) - } - - #[test] - fn test_inputize() -> Result<(), Error> { - // check that point_to_nonnative_limbs returns the expected values - let mut rng = ark_std::test_rng(); - let p = Projective::rand(&mut rng); - - let cs = ConstraintSystem::::new_ref(); - let pVar = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - [pVar.x.0.value()?, pVar.y.0.value()?].concat(), - p.inputize_nonnative() - ); - - let cs = ConstraintSystem::::new_ref(); - let pVar = ProjectiveVar::>::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - vec![pVar.x.value()?, pVar.y.value()?, pVar.z.value()?], - p.inputize() - ); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/nonnative/mod.rs b/folding-schemes/src/folding/circuits/nonnative/mod.rs deleted file mode 100644 index 497b9870f..000000000 --- a/folding-schemes/src/folding/circuits/nonnative/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod affine; -pub mod uint; diff --git a/folding-schemes/src/folding/circuits/nonnative/uint.rs b/folding-schemes/src/folding/circuits/nonnative/uint.rs deleted file mode 100644 index cb3cdd444..000000000 --- a/folding-schemes/src/folding/circuits/nonnative/uint.rs +++ /dev/null @@ -1,1036 +0,0 @@ -use std::{ - borrow::Borrow, - cmp::{max, min}, -}; - -use ark_ff::{BigInteger, Fp, FpConfig, One, PrimeField, Zero}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - convert::ToBitsGadget, - fields::{fp::FpVar, FieldVar}, - prelude::EqGadget, - select::CondSelectGadget, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use num_bigint::BigUint; -use num_integer::Integer; - -use crate::{ - folding::traits::{Inputize, InputizeNonNative}, - transcript::{AbsorbNonNative, AbsorbNonNativeGadget}, - utils::gadgets::{EquivalenceGadget, MatrixGadget, SparseMatrixVar, VectorGadget}, - Field, -}; - -/// `LimbVar` represents a single limb of a non-native unsigned integer in the -/// circuit. -/// The limb value `v` should be small enough to fit into `FpVar`, and we also -/// store an upper bound `ub` for the limb value, which is treated as a constant -/// in the circuit and is used for efficient equality checks and some arithmetic -/// operations. -#[derive(Debug, Clone)] -pub struct LimbVar { - pub v: FpVar, - pub ub: BigUint, -} - -impl]>> From for LimbVar { - fn from(bits: B) -> Self { - Self { - // `Boolean::le_bits_to_fp` will return an error if the internal - // invocation of `Boolean::enforce_in_field_le` fails. - // However, this method is only called when the length of `bits` is - // greater than `F::MODULUS_BIT_SIZE`, which should not happen in - // our case where `bits` is guaranteed to be short. - v: Boolean::le_bits_to_fp(bits.as_ref()).unwrap(), - ub: (BigUint::one() << bits.as_ref().len()) - BigUint::one(), - } - } -} - -impl Default for LimbVar { - fn default() -> Self { - Self { - v: FpVar::zero(), - ub: BigUint::zero(), - } - } -} - -impl GR1CSVar for LimbVar { - type Value = F; - - fn cs(&self) -> ConstraintSystemRef { - self.v.cs() - } - - fn value(&self) -> Result { - self.v.value() - } -} - -impl CondSelectGadget for LimbVar { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - // We only allow selecting between two values with the same upper bound - assert_eq!(true_value.ub, false_value.ub); - Ok(Self { - v: cond.select(&true_value.v, &false_value.v)?, - ub: true_value.ub.clone(), - }) - } -} - -impl LimbVar { - /// Add two `LimbVar`s. - /// Returns `None` if the upper bound of the sum is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the sum as a `LimbVar`. - pub fn add(&self, other: &Self) -> Option { - let ubound = &self.ub + &other.ub; - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: &self.v + &other.v, - ub: ubound, - }) - } else { - None - } - } - - /// Add multiple `LimbVar`s. - /// Returns `None` if the upper bound of the sum is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the sum as a `LimbVar`. - pub fn add_many(limbs: &[Self]) -> Option { - let ubound = limbs.iter().map(|l| &l.ub).sum(); - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: if limbs.is_constant() { - FpVar::constant(limbs.value().unwrap_or_default().into_iter().sum()) - } else { - limbs.iter().map(|l| &l.v).sum() - }, - ub: ubound, - }) - } else { - None - } - } - - /// Multiply two `LimbVar`s. - /// Returns `None` if the upper bound of the product is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the product as a `LimbVar`. - pub fn mul(&self, other: &Self) -> Option { - let ubound = &self.ub * &other.ub; - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: &self.v * &other.v, - ub: ubound, - }) - } else { - None - } - } - - pub fn zero() -> Self { - Self::default() - } - - pub fn constant(v: F) -> Self { - Self { - v: FpVar::constant(v), - ub: v.into(), - } - } -} - -impl ToBitsGadget for LimbVar { - fn to_bits_le(&self) -> Result>, SynthesisError> { - let cs = self.cs(); - - let bits = &self - .v - .value() - .unwrap_or_default() - .into_bigint() - .to_bits_le()[..self.ub.bits() as usize]; - let bits = if cs.is_none() { - Vec::new_constant(cs, bits)? - } else { - Vec::new_witness(cs, || Ok(bits))? - }; - - Boolean::le_bits_to_fp(&bits)?.enforce_equal(&self.v)?; - - Ok(bits) - } -} - -/// `NonNativeUintVar` represents a non-native unsigned integer (BigUint) in the -/// circuit. -/// We apply [xJsnark](https://akosba.github.io/papers/xjsnark.pdf)'s techniques -/// for efficient operations on `NonNativeUintVar`. -/// Note that `NonNativeUintVar` is different from arkworks' `NonNativeFieldVar` -/// in that the latter runs the expensive `reduce` (`align` + `modulo` in our -/// terminology) after each arithmetic operation, while the former only reduces -/// the integer when explicitly called. -#[derive(Debug, Clone)] -pub struct NonNativeUintVar(pub Vec>); - -impl NonNativeUintVar { - pub const fn bits_per_limb() -> usize { - assert!(F::MODULUS_BIT_SIZE > 250); - // For a `F` with order > 250 bits, 55 is chosen for optimizing the most - // expensive part `Az∘Bz` when checking the R1CS relation for CycleFold. - // Consider using `NonNativeUintVar` to represent the base field `Fq`. - // Since 250 / 55 = 4.46, the `NonNativeUintVar` has 5 limbs. - // Now, the multiplication of two `NonNativeUintVar`s has 9 limbs, and - // each limb has at most 2^{55 * 2} * 5 = 112.3 bits. - // For a 1400x1400 matrix `A`, the multiplication of `A`'s row and `z` - // is the sum of 1400 `NonNativeUintVar`s, each with 9 limbs. - // Thus, the maximum bit length of limbs of each element in `Az` is - // 2^{55 * 2} * 5 * 1400 = 122.7 bits. - // Finally, in the hadamard product of `Az` and `Bz`, every element has - // 17 limbs, whose maximum bit length is (2^{55 * 2} * 5 * 1400)^2 * 9 - // = 248.7 bits and is less than the native field `Fr`. - // Thus, 55 allows us to compute `Az∘Bz` without the expensive alignment - // operation. - // - // TODO: either make it a global const, or compute an optimal value - // based on the modulus size. - 55 - } -} - -struct BoundedBigUint(BigUint, usize); - -impl AllocVar for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let cs = cs.into().cs(); - let v = f()?; - let BoundedBigUint(x, l) = v.borrow(); - - let mut limbs = vec![]; - for chunk in (0..*l) - .map(|i| x.bit(i as u64)) - .collect::>() - .chunks(Self::bits_per_limb()) - { - let limb = F::from(F::BigInt::from_bits_le(chunk)); - let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; - Self::enforce_bit_length(&limb, chunk.len())?; - limbs.push(LimbVar { - v: limb, - ub: (BigUint::one() << chunk.len()) - BigUint::one(), - }); - } - - Ok(Self(limbs)) - } -} - -impl AllocVar for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let cs = cs.into().cs(); - let v = f()?; - assert_eq!(G::extension_degree(), 1); - // `unwrap` is safe because `G` is a field with extension degree 1, and - // thus `G::to_base_prime_field_elements` should return an iterator with - // exactly one element. - let v = v.borrow().to_base_prime_field_elements().next().unwrap(); - - let mut limbs = vec![]; - - for chunk in v.into_bigint().to_bits_le().chunks(Self::bits_per_limb()) { - let limb = F::from(F::BigInt::from_bits_le(chunk)); - let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; - Self::enforce_bit_length(&limb, chunk.len())?; - limbs.push(LimbVar { - v: limb, - ub: (BigUint::one() << chunk.len()) - BigUint::one(), - }); - } - - Ok(Self(limbs)) - } -} - -impl GR1CSVar for NonNativeUintVar { - type Value = BigUint; - - fn cs(&self) -> ConstraintSystemRef { - self.0.cs() - } - - fn value(&self) -> Result { - let mut r = BigUint::zero(); - - for limb in self.0.value()?.into_iter().rev() { - r <<= Self::bits_per_limb(); - r += Into::::into(limb); - } - - Ok(r) - } -} - -impl NonNativeUintVar { - /// Enforce `self` to be less than `other`, where `self` and `other` should - /// be aligned. - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 - pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { - let len = max(self.0.len(), other.0.len()); - let zero = LimbVar::zero(); - - // Compute the difference between limbs of `other` and `self`. - // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by - // `0`, and an unknown limb by `?`. - // Then, for `self < other`, `delta` should look like: - // ? ? ... ? ? + 0 0 ... 0 0 - let delta = (0..len) - .map(|i| { - let x = &self.0.get(i).unwrap_or(&zero).v; - let y = &other.0.get(i).unwrap_or(&zero).v; - y - x - }) - .collect::>(); - - // `helper` is a vector of booleans that indicates if the corresponding - // limb of `delta` is the first (searching from MSB) positive limb. - // For example, if `delta` is: - // - + ... + - + 0 0 ... 0 0 - // <---- search in this direction -------- - // Then `helper` should be: - // F F ... F F T F F ... F F - let helper = { - let cs = self.cs().or(other.cs()); - let mut helper = vec![false; len]; - for i in (0..len).rev() { - let delta = delta[i].value().unwrap_or_default().into_bigint(); - if !delta.is_zero() && delta < F::MODULUS_MINUS_ONE_DIV_TWO { - helper[i] = true; - break; - } - } - if cs.is_none() { - Vec::>::new_constant(cs, helper)? - } else { - Vec::new_witness(cs, || Ok(helper))? - } - }; - - // `p` is the first positive limb in `delta`. - let mut p = FpVar::::zero(); - // `r` is the sum of all bits in `helper`, which should be 1 when `self` - // is less than `other`, as there should be more than one positive limb - // in `delta`, and thus exactly one true bit in `helper`. - let mut r = FpVar::zero(); - for (b, d) in helper.into_iter().zip(delta) { - // Choose the limb `d` only if `b` is true. - p += b.select(&d, &FpVar::zero())?; - // Either `r` or `d` should be zero. - // Consider the same example as above: - // - + ... + - + 0 0 ... 0 0 - // F F ... F F T F F ... F F - // |-----------| - // `r = 0` in this range (before/when we meet the first positive limb) - // |---------| - // `d = 0` in this range (after we meet the first positive limb) - // This guarantees that for every bit after the true bit in `helper`, - // the corresponding limb in `delta` is zero. - (&r * &d).enforce_equal(&FpVar::zero())?; - // Add the current bit to `r`. - r += FpVar::from(b); - } - - // Ensure that `r` is exactly 1. This guarantees that there is exactly - // one true value in `helper`. - r.enforce_equal(&FpVar::one())?; - // Ensure that `p` is positive, i.e., - // `0 <= p - 1 < 2^bits_per_limb < F::MODULUS_MINUS_ONE_DIV_TWO`. - // This guarantees that the true value in `helper` corresponds to a - // positive limb in `delta`. - Self::enforce_bit_length(&(p - FpVar::one()), Self::bits_per_limb())?; - - Ok(()) - } - - /// Enforce `self` to be equal to `other`, where `self` and `other` are not - /// necessarily aligned. - /// - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 - /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 - /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 - pub fn enforce_equal_unaligned(&self, other: &Self) -> Result<(), SynthesisError> { - let len = min(self.0.len(), other.0.len()); - - // Group the limbs of `self` and `other` so that each group nearly - // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. - // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` - // is the initial number of bits in a limb, just as what we do in grade - // school arithmetic, e.g., - // 5 9 - // x 7 3 - // ------------- - // 15 27 - // 35 63 - // ------------- <- When grouping 35, 15 + 63, and 27, we are computing - // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 - // Note that this is different from the concatenation `x_0 || x_1 ...`, - // since the bit-length of each limb is not necessarily the initial size - // `W`. - let (steps, x, y, rest) = { - // `steps` stores the size of each grouped limb. - let mut steps = vec![]; - // `x_grouped` stores the grouped limbs of `self`. - let mut x_grouped = vec![]; - // `y_grouped` stores the grouped limbs of `other`. - let mut y_grouped = vec![]; - let mut i = 0; - while i < len { - let mut j = i; - // The current grouped limbs of `self` and `other`. - let mut xx = LimbVar::zero(); - let mut yy = LimbVar::zero(); - while j < len { - let shift = BigUint::one() << (Self::bits_per_limb() * (j - i)); - assert!(shift < F::MODULUS_MINUS_ONE_DIV_TWO.into()); - let shift = LimbVar::constant(shift.into()); - match ( - // Try to group `x` and `y` into `xx` and `yy`. - self.0[j].mul(&shift).and_then(|x| xx.add(&x)), - other.0[j].mul(&shift).and_then(|y| yy.add(&y)), - ) { - // Update the result if successful. - (Some(x), Some(y)) => (xx, yy) = (x, y), - // Break the loop if the upper bound of the result exceeds - // the maximum capacity. - _ => break, - } - j += 1; - } - // Store the grouped limbs and their size. - steps.push((j - i) * Self::bits_per_limb()); - x_grouped.push(xx); - y_grouped.push(yy); - // Start the next group - i = j; - } - let remaining_limbs = &(if i < self.0.len() { self } else { other }).0[i..]; - let rest = if remaining_limbs.is_empty() { - FpVar::zero() - } else { - // If there is any remaining limb, the first one should be the - // final carry (which will be checked later), and the following - // ones should be zero. - - // Enforce the remaining limbs to be zero. - // Instead of doing that one by one, we check if their sum is - // zero using a single constraint. - // This is sound, as the upper bounds of the limbs and their sum - // are guaranteed to be less than `F::MODULUS_MINUS_ONE_DIV_TWO` - // (i.e., all of them are "non-negative"), implying that all - // limbs should be zero to make the sum zero. - LimbVar::add_many(&remaining_limbs[1..]) - .ok_or(SynthesisError::Unsatisfiable)? - .v - .enforce_equal(&FpVar::zero())?; - remaining_limbs[0].v.clone() - }; - (steps, x_grouped, y_grouped, rest) - }; - let n = steps.len(); - // `c` stores the current carry of `x_i - y_i` - let mut c = FpVar::::zero(); - // For each group, check the last `step_i` bits of `x_i` and `y_i` are - // equal. - // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. - // However, this is only true for `i = 0`, and we need to consider carry - // values `diff >> step_i` for `i > 0`. - // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` - // and derive the next `c` by computing `diff >> step_i`. - // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` - // and enforce it to be small (soundness holds because for `a` that does - // not divide `b`, `b / a` in the field will be very large. - for i in 0..n { - let step = steps[i]; - c = (&x[i].v - &y[i].v + &c) - .mul_by_inverse_unchecked(&FpVar::constant(F::from(BigUint::one() << step)))?; - if i != n - 1 { - // Unlike the code mentioned above which add some offset to the - // diff `x_i - y_i + c` to make it always positive, we directly - // check if the absolute value of the diff is small. - Self::enforce_abs_bit_length( - &c, - (max(&x[i].ub, &y[i].ub).bits() as usize) - .checked_sub(step) - .unwrap_or_default(), - )?; - } else { - // For the final carry, we need to ensure that it equals the - // remaining limb `rest`. - c.enforce_equal(&rest)?; - } - } - - Ok(()) - } -} - -impl ToBitsGadget for NonNativeUintVar { - fn to_bits_le(&self) -> Result>, SynthesisError> { - Ok(self - .0 - .iter() - .map(|limb| limb.to_bits_le()) - .collect::, _>>()? - .concat()) - } -} - -impl CondSelectGadget for NonNativeUintVar { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - assert_eq!(true_value.0.len(), false_value.0.len()); - let mut v = vec![]; - for i in 0..true_value.0.len() { - v.push(cond.select(&true_value.0[i], &false_value.0[i])?); - } - Ok(Self(v)) - } -} - -impl NonNativeUintVar { - pub fn ubound(&self) -> BigUint { - let mut r = BigUint::zero(); - - for i in self.0.iter().rev() { - r <<= Self::bits_per_limb(); - r += &i.ub; - } - - r - } - - fn enforce_bit_length(x: &FpVar, length: usize) -> Result>, SynthesisError> { - let cs = x.cs(); - - let bits = &x.value().unwrap_or_default().into_bigint().to_bits_le()[..length]; - let bits = if cs.is_none() { - Vec::new_constant(cs, bits)? - } else { - Vec::new_witness(cs, || Ok(bits))? - }; - - Boolean::le_bits_to_fp(&bits)?.enforce_equal(x)?; - - Ok(bits) - } - - fn enforce_abs_bit_length( - x: &FpVar, - length: usize, - ) -> Result>, SynthesisError> { - let cs = x.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - let is_neg = Boolean::new_variable( - cs.clone(), - || Ok(x.value().unwrap_or_default().into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO), - mode, - )?; - let bits = Vec::new_variable( - cs.clone(), - || { - Ok({ - let x = x.value().unwrap_or_default(); - let mut bits = if is_neg.value().unwrap_or_default() { - -x - } else { - x - } - .into_bigint() - .to_bits_le(); - bits.resize(length, false); - bits - }) - }, - mode, - )?; - - // Below is equivalent to but more efficient than - // `Boolean::le_bits_to_fp(&bits)?.enforce_equal(&is_neg.select(&x.negate()?, &x)?)?` - // Note that this enforces: - // 1. The claimed absolute value `is_neg.select(&x.negate()?, &x)?` has - // exactly `length` bits. - // 2. `is_neg` is indeed the sign of `x`, i.e., `is_neg = false` when - // `0 <= x < (|F| - 1) / 2`, and `is_neg = true` when - // `(|F| - 1) / 2 <= x < F`, thus the claimed absolute value is - // correct. - // If `is_neg` is incorrect, then: - // a. `0 <= x < (|F| - 1) / 2`, but `is_neg = true`, then - // `is_neg.select(&x.negate()?, &x)?` returns `|F| - x`, - // which is greater than `(|F| - 1) / 2` and cannot fit in - // `length` bits (given that `length` is small). - // b. `(|F| - 1) / 2 <= x < F`, but `is_neg = false`, then - // `is_neg.select(&x.negate()?, &x)?` returns `x`, which is - // greater than `(|F| - 1) / 2` and cannot fit in `length` - // bits. - FpVar::from(is_neg).mul_equals(&x.double()?, &(x - Boolean::le_bits_to_fp(&bits)?))?; - - Ok(bits) - } - - /// Compute `self + other`, without aligning the limbs. - pub fn add_no_align(&self, other: &Self) -> Result { - let mut z = vec![LimbVar::zero(); max(self.0.len(), other.0.len())]; - for (i, v) in self.0.iter().enumerate() { - z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; - } - for (i, v) in other.0.iter().enumerate() { - z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; - } - Ok(Self(z)) - } - - /// Compute `self * other`, without aligning the limbs. - /// Implements the O(n) approach described in xJsnark, Section IV.B.1) - pub fn mul_no_align(&self, other: &Self) -> Result { - let len = self.0.len() + other.0.len() - 1; - if self.is_constant() || other.is_constant() { - // Use the naive approach for constant operands, which costs no - // constraints. - let z = (0..len) - .map(|i| { - let start = max(i + 1, other.0.len()) - other.0.len(); - let end = min(i + 1, self.0.len()); - LimbVar::add_many( - &(start..end) - .map(|j| self.0[j].mul(&other.0[i - j])) - .collect::>>()?, - ) - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable)?; - return Ok(Self(z)); - } - let cs = self.cs().or(other.cs()); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - // Compute the result `z` outside the circuit and provide it as hints. - let z = { - let mut z = vec![(F::zero(), BigUint::zero()); len]; - for i in 0..self.0.len() { - for j in 0..other.0.len() { - z[i + j].0 += self.0[i].value().unwrap_or_default() - * other.0[j].value().unwrap_or_default(); - z[i + j].1 += &self.0[i].ub * &other.0[j].ub; - } - } - z.into_iter() - .map(|(v, ub)| { - assert!(ub < F::MODULUS_MINUS_ONE_DIV_TWO.into()); - Ok(LimbVar { - v: FpVar::new_variable(cs.clone(), || Ok(v), mode)?, - ub, - }) - }) - .collect::, _>>()? - }; - for c in 1..=len { - let c = F::from(c as u64); - let mut t = F::one(); - let mut c_powers = vec![]; - for _ in 0..len { - c_powers.push(t); - t *= c; - } - // `l = Σ self[i] c^i` - let l = self - .0 - .iter() - .zip(&c_powers) - .map(|(v, t)| (&v.v * *t)) - .collect::>() - .iter() - .sum::>(); - // `r = Σ other[i] c^i` - let r = other - .0 - .iter() - .zip(&c_powers) - .map(|(v, t)| (&v.v * *t)) - .collect::>() - .iter() - .sum::>(); - // `o = Σ z[i] c^i` - let o = z - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .collect::>() - .iter() - .sum::>(); - // Enforce `o = l * r` - l.mul_equals(&r, &o)?; - } - - Ok(Self(z)) - } - - /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. - pub fn modulo(&self) -> Result { - let cs = self.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - let m: BigUint = M::MODULUS.into(); - // Provide the quotient and remainder as hints - let (q, r) = { - let v = self.value().unwrap_or_default(); - let (q, r) = v.div_rem(&m); - let q_ubound = self.ubound().div_ceil(&m); - let r_ubound = &m; - ( - Self::new_variable( - cs.clone(), - || Ok(BoundedBigUint(q, q_ubound.bits() as usize)), - mode, - )?, - Self::new_variable( - cs.clone(), - || Ok(BoundedBigUint(r, r_ubound.bits() as usize)), - mode, - )?, - ) - }; - - let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; - // Enforce `self = q * m + r` - q.mul_no_align(&m)? - .add_no_align(&r)? - .enforce_equal_unaligned(self)?; - // Enforce `r < m` (and `r >= 0` already holds) - r.enforce_lt(&m)?; - - Ok(r) - } - - /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. - pub fn enforce_congruent(&self, other: &Self) -> Result<(), SynthesisError> { - let cs = self.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - let m: BigUint = M::MODULUS.into(); - let bits = (max(self.ubound(), other.ubound()) / &m).bits() as usize; - // Provide the quotient `|x - y| / m` and a boolean indicating if `x > y` - // as hints. - let (q, is_ge) = { - let x = self.value().unwrap_or_default(); - let y = other.value().unwrap_or_default(); - let (d, b) = if x > y { - ((x - y) / &m, true) - } else { - ((y - x) / &m, false) - }; - ( - Self::new_variable(cs.clone(), || Ok(BoundedBigUint(d, bits)), mode)?, - Boolean::new_variable(cs.clone(), || Ok(b), mode)?, - ) - }; - - let zero = Self::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), bits))?; - let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; - let l = self.add_no_align(&is_ge.select(&zero, &q)?.mul_no_align(&m)?)?; - let r = other.add_no_align(&is_ge.select(&q, &zero)?.mul_no_align(&m)?)?; - // If `self >= other`, enforce `self = other + q * m` - // Otherwise, enforce `self + q * m = other` - // Soundness holds because if `self` and `other` are not congruent, then - // one can never find a `q` satisfying either equation above. - l.enforce_equal_unaligned(&r) - } -} - -impl EquivalenceGadget for NonNativeUintVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { - self.enforce_congruent::(other) - } -} - -impl]>> From for NonNativeUintVar { - fn from(bits: B) -> Self { - Self( - bits.as_ref() - .chunks(Self::bits_per_limb()) - .map(LimbVar::from) - .collect::>(), - ) - } -} - -impl, const N: usize> AbsorbNonNative for Fp { - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - let num_limbs = (Fp::::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb); - - let mut limbs = self - .into_bigint() - .to_bits_le() - .chunks(bits_per_limb) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect::>(); - limbs.resize(num_limbs, F::zero()); - - dest.extend(&limbs) - } -} - -impl AbsorbNonNativeGadget for NonNativeUintVar { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - - let limbs = self - .to_bits_le()? - .chunks(bits_per_limb) - .map(Boolean::le_bits_to_fp) - .collect::, _>>()?; - - Ok(limbs) - } -} - -impl, const N: usize> Inputize for Fp { - /// Returns the internal representation in the same order as how the value - /// is allocated in `FpVar::new_input`. - fn inputize(&self) -> Vec { - vec![*self] - } -} - -impl InputizeNonNative for P { - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeUintVar::new_input`. - fn inputize_nonnative(&self) -> Vec { - self.into_bigint() - .to_bits_le() - .chunks(NonNativeUintVar::::bits_per_limb()) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect() - } -} - -impl VectorGadget> for [NonNativeUintVar] { - fn add(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.add_no_align(y)) - .collect() - } - - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.mul_no_align(y)) - .collect() - } - - fn mul_scalar( - &self, - other: &NonNativeUintVar, - ) -> Result>, SynthesisError> { - self.iter().map(|x| x.mul_no_align(other)).collect() - } -} - -impl MatrixGadget> for SparseMatrixVar> { - fn mul_vector( - &self, - v: &[NonNativeUintVar], - ) -> Result>, SynthesisError> { - self.coeffs - .iter() - .map(|row| { - let len = row - .iter() - .map(|(value, col_i)| value.0.len() + v[*col_i].0.len() - 1) - .max() - .unwrap_or(0); - // This is a combination of `mul_no_align` and `add_no_align` - // that results in more flattened `LinearCombination`s. - // Consequently, `ConstraintSystem::inline_all_lcs` costs less - // time, thus making trusted setup and proof generation faster. - (0..len) - .map(|i| { - LimbVar::add_many( - &row.iter() - .flat_map(|(value, col_i)| { - let start = max(i + 1, v[*col_i].0.len()) - v[*col_i].0.len(); - let end = min(i + 1, value.0.len()); - (start..end).map(|j| value.0[j].mul(&v[*col_i].0[i - j])) - }) - .collect::>>()?, - ) - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable) - .map(NonNativeUintVar) - }) - .collect::, _>>() - } -} - -#[cfg(test)] -mod tests { - use ark_ff::Field; - use ark_pallas::{Fq, Fr}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand}; - use num_bigint::RandBigInt; - - use super::*; - use crate::Error; - - #[test] - fn test_mul_biguint() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let size = 256; - - let rng = &mut test_rng(); - let a = rng.gen_biguint(size as u64); - let b = rng.gen_biguint(size as u64); - let ab = &a * &b; - let aab = &a * &ab; - let abb = &ab * &b; - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(a, size)))?; - let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(b, size)))?; - let ab_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(ab, size * 2)))?; - let aab_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(aab, size * 3)))?; - let abb_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(abb, size * 3)))?; - - a_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_equal_unaligned(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_mul_fq() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - let a = Fq::rand(rng); - let b = Fq::rand(rng); - let ab = a * b; - let aab = a * ab; - let abb = ab * b; - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(b))?; - let ab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(ab))?; - let aab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(aab))?; - let abb_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(abb))?; - - a_var - .mul_no_align(&b_var)? - .enforce_congruent::(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_congruent::(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_congruent::(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_pow() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - - let a = Fq::rand(rng); - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - - let mut r_var = a_var.clone(); - for _ in 0..16 { - r_var = r_var.mul_no_align(&r_var)?.modulo::()?; - } - r_var = r_var.mul_no_align(&a_var)?.modulo::()?; - assert_eq!(a.pow([65537u64]), Fq::from(r_var.value()?)); - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_vec_vec_mul() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - - let len = 1000; - - let rng = &mut test_rng(); - let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); - - let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; - let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; - let c_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(c))?; - - let mut r_var = - NonNativeUintVar::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), 0))?; - for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { - r_var = r_var.add_no_align(&a.mul_no_align(&b)?)?; - } - r_var.enforce_congruent::(&c_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/sum_check.rs b/folding-schemes/src/folding/circuits/sum_check.rs deleted file mode 100644 index 5a7c49d9d..000000000 --- a/folding-schemes/src/folding/circuits/sum_check.rs +++ /dev/null @@ -1,259 +0,0 @@ -/// Heavily inspired from testudo: https://github.com/cryptonetlab/testudo/tree/master -/// Some changes: -/// - Typings to better stick to ark_poly's API -/// - Uses `folding-schemes`' own `TranscriptVar` trait and `PoseidonTranscriptVar` struct -/// - API made closer to gadgets found in `folding-schemes` -use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, Absorb, CryptographicSponge}; -use ark_ff::PrimeField; -use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use std::{borrow::Borrow, marker::PhantomData}; - -use crate::utils::espresso::sum_check::SumCheck; -use crate::utils::virtual_polynomial::VPAuxInfo; -use crate::{ - transcript::TranscriptVar, - utils::sum_check::{structs::IOPProof, IOPSumCheck}, -}; - -#[derive(Clone, Debug)] -pub struct DensePolynomialVar { - pub coeffs: Vec>, -} - -impl AllocVar, F> for DensePolynomialVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|c| { - let cs = cs.into(); - let cp: &DensePolynomial = c.borrow(); - let mut coeffs_var = Vec::>::with_capacity(cp.coeffs.len()); - for coeff in cp.coeffs.iter() { - let coeff_var = FpVar::::new_variable(cs.clone(), || Ok(coeff), mode)?; - coeffs_var.push(coeff_var); - } - Ok(Self { coeffs: coeffs_var }) - }) - } -} - -impl DensePolynomialVar { - pub fn eval_at_zero(&self) -> FpVar { - if self.coeffs.is_empty() { - return FpVar::::zero(); - } - self.coeffs[0].clone() - } - - pub fn eval_at_one(&self) -> FpVar { - if self.coeffs.is_empty() { - return FpVar::::zero(); - } - let mut res = self.coeffs[0].clone(); - for i in 1..self.coeffs.len() { - res = &res + &self.coeffs[i]; - } - res - } - - pub fn evaluate(&self, r: &FpVar) -> FpVar { - if self.coeffs.is_empty() { - return FpVar::::zero(); - } - let mut eval = self.coeffs[0].clone(); - let mut power = r.clone(); - - for i in 1..self.coeffs.len() { - eval += &power * &self.coeffs[i]; - power *= r; - } - eval - } -} - -#[derive(Clone, Debug)] -pub struct IOPProofVar { - // We have to be generic over a CurveGroup because instantiating a IOPProofVar will call IOPSumCheck which requires a CurveGroup - pub proofs: Vec>, - pub claim: FpVar, -} - -impl AllocVar, F> for IOPProofVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|c| { - let cs = cs.into(); - let cp: &IOPProof = c.borrow(); - let claim = IOPSumCheck::>::extract_sum(cp); - let claim = FpVar::::new_variable(cs.clone(), || Ok(claim), mode)?; - let mut proofs = Vec::>::with_capacity(cp.proofs.len()); - for proof in cp.proofs.iter() { - let poly = DensePolynomial::from_coefficients_slice(&proof.coeffs); - let proof = DensePolynomialVar::::new_variable(cs.clone(), || Ok(poly), mode)?; - proofs.push(proof); - } - Ok(Self { proofs, claim }) - }) - } -} - -#[derive(Clone, Debug)] -pub struct VPAuxInfoVar { - pub num_variables: FpVar, - pub max_degree: FpVar, -} - -impl AllocVar, F> for VPAuxInfoVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|c| { - let cs = cs.into(); - let cp: &VPAuxInfo = c.borrow(); - let num_variables = FpVar::::new_variable( - cs.clone(), - || Ok(F::from(cp.num_variables as u64)), - mode, - )?; - let max_degree = - FpVar::::new_variable(cs.clone(), || Ok(F::from(cp.max_degree as u64)), mode)?; - Ok(Self { - num_variables, - max_degree, - }) - }) - } -} - -#[derive(Debug, Clone)] -pub struct SumCheckVerifierGadget { - _f: PhantomData, -} - -impl SumCheckVerifierGadget { - #[allow(clippy::type_complexity)] - pub fn verify>( - iop_proof_var: &IOPProofVar, - poly_aux_info_var: &VPAuxInfoVar, - transcript_var: &mut T, - enabled: Boolean, - ) -> Result<(Vec>, Vec>), SynthesisError> { - let mut e_vars = vec![iop_proof_var.claim.clone()]; - let mut r_vars: Vec> = Vec::new(); - transcript_var.absorb(&poly_aux_info_var.num_variables)?; - transcript_var.absorb(&poly_aux_info_var.max_degree)?; - - for poly_var in iop_proof_var.proofs.iter() { - let res = poly_var.eval_at_one() + poly_var.eval_at_zero(); - let e_var = e_vars.last().ok_or(SynthesisError::Unsatisfiable)?; - res.conditional_enforce_equal(e_var, &enabled)?; - transcript_var.absorb(&poly_var.coeffs)?; - let r_i_var = transcript_var.get_challenge()?; - e_vars.push(poly_var.evaluate(&r_i_var)); - r_vars.push(r_i_var); - } - - Ok((e_vars, r_vars)) - } -} - -#[cfg(test)] -mod tests { - use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, - }; - use ark_pallas::Fr; - use ark_poly::{DenseMultilinearExtension, MultilinearExtension, Polynomial}; - use ark_r1cs_std::GR1CSVar; - use ark_relations::gr1cs::ConstraintSystem; - use std::sync::Arc; - - use super::*; - use crate::{ - transcript::poseidon::poseidon_canonical_config, - utils::virtual_polynomial::VirtualPolynomial, Error, - }; - - pub type TestSumCheckProof = (VirtualPolynomial, PoseidonConfig, IOPProof); - - /// Primarily used for testing the sumcheck gadget - /// Returns a random virtual polynomial, the poseidon config used and the associated sumcheck proof - pub fn get_test_sumcheck_proof( - num_vars: usize, - ) -> Result, Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config: PoseidonConfig = poseidon_canonical_config::(); - let mut poseidon_transcript_prove = PoseidonSponge::::new(&poseidon_config); - let poly_mle = DenseMultilinearExtension::rand(num_vars, &mut rng); - let virtual_poly = VirtualPolynomial::new_from_mle(&Arc::new(poly_mle), F::ONE); - let sum_check: IOPProof = IOPSumCheck::>::prove( - &virtual_poly, - &mut poseidon_transcript_prove, - )?; - Ok((virtual_poly, poseidon_config, sum_check)) - } - - #[test] - fn test_sum_check_circuit() -> Result<(), Error> { - for num_vars in 1..15 { - let cs = ConstraintSystem::::new_ref(); - let (virtual_poly, poseidon_config, sum_check) = - get_test_sumcheck_proof::(num_vars)?; - let mut poseidon_var: PoseidonSpongeVar = - PoseidonSpongeVar::new(cs.clone(), &poseidon_config); - let iop_proof_var = IOPProofVar::::new_witness(cs.clone(), || Ok(&sum_check))?; - let poly_aux_info_var = - VPAuxInfoVar::::new_witness(cs.clone(), || Ok(virtual_poly.aux_info))?; - let enabled = Boolean::::new_witness(cs.clone(), || Ok(true))?; - let res = SumCheckVerifierGadget::::verify( - &iop_proof_var, - &poly_aux_info_var, - &mut poseidon_var, - enabled, - ); - - assert!(res.is_ok()); - let (circuit_evals, r_challenges) = res?; - - // 1. assert claim from circuit is equal to the one from the sum-check - let claim: Fr = IOPSumCheck::>::extract_sum(&sum_check); - assert_eq!(circuit_evals[0].value()?, claim); - - // 2. assert that all in-circuit evaluations are equal to the ones from the sum-check - for ((proof, point), circuit_eval) in sum_check - .proofs - .iter() - .zip(sum_check.point.iter()) - .zip(circuit_evals.iter().skip(1)) - // we skip the first one since it's the above checked claim - { - let poly = DensePolynomial::from_coefficients_slice(&proof.coeffs); - let eval = poly.evaluate(point); - assert_eq!(eval, circuit_eval.value()?); - } - - // 3. assert that all challenges are equal to the ones from the sum-check - for (point, r_challenge) in sum_check.point.iter().zip(r_challenges.iter()) { - assert_eq!(*point, r_challenge.value()?); - } - - assert!(cs.is_satisfied()?); - } - Ok(()) - } -} diff --git a/folding-schemes/src/folding/circuits/utils.rs b/folding-schemes/src/folding/circuits/utils.rs deleted file mode 100644 index da6e579c4..000000000 --- a/folding-schemes/src/folding/circuits/utils.rs +++ /dev/null @@ -1,75 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; -use ark_relations::gr1cs::SynthesisError; -use std::marker::PhantomData; - -/// EqEval is a gadget for computing $\tilde{eq}(a, b) = \prod_{i=1}^{l}(a_i \cdot b_i + (1 - a_i)(1 - b_i))$ -/// :warning: This is not the ark_r1cs_std::eq::EqGadget -pub struct EqEvalGadget { - _f: PhantomData, -} - -impl EqEvalGadget { - /// Gadget to evaluate eq polynomial. - /// Follows the implementation of `eq_eval` found in this crate. - pub fn eq_eval(x: &[FpVar], y: &[FpVar]) -> Result, SynthesisError> { - if x.len() != y.len() { - return Err(SynthesisError::Unsatisfiable); - } - if x.is_empty() || y.is_empty() { - return Err(SynthesisError::AssignmentMissing); - } - let mut e = FpVar::::one(); - for (xi, yi) in x.iter().zip(y.iter()) { - let xi_yi = xi * yi; - e *= xi_yi.clone() + xi_yi - xi - yi + F::one(); - } - Ok(e) - } -} - -#[cfg(test)] -mod tests { - use ark_ff::Field; - use ark_pallas::Fr; - use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand}; - - use super::EqEvalGadget; - use crate::utils::virtual_polynomial::eq_eval; - use crate::Error; - - #[test] - pub fn test_eq_eval_gadget() -> Result<(), Error> { - let mut rng = test_rng(); - let cs = ConstraintSystem::::new_ref(); - - for i in 1..20 { - let x_vec: Vec = (0..i).map(|_| Fr::rand(&mut rng)).collect(); - let y_vec: Vec = (0..i).map(|_| Fr::rand(&mut rng)).collect(); - let x: Vec> = x_vec - .iter() - .map(|x| FpVar::::new_witness(cs.clone(), || Ok(x))) - .collect::, _>>()?; - let y: Vec> = y_vec - .iter() - .map(|y| FpVar::::new_witness(cs.clone(), || Ok(y))) - .collect::, _>>()?; - let expected_eq_eval = eq_eval::(&x_vec, &y_vec)?; - let gadget_eq_eval: FpVar = EqEvalGadget::::eq_eval(&x, &y)?; - assert_eq!(expected_eq_eval, gadget_eq_eval.value()?); - } - - let x: Vec> = vec![]; - let y: Vec> = vec![]; - let gadget_eq_eval = EqEvalGadget::::eq_eval(&x, &y); - assert!(gadget_eq_eval.is_err()); - - let x: Vec> = vec![]; - let y: Vec> = vec![FpVar::::new_witness(cs.clone(), || Ok(&Fr::ONE))?]; - let gadget_eq_eval = EqEvalGadget::::eq_eval(&x, &y); - assert!(gadget_eq_eval.is_err()); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/cccs.rs b/folding-schemes/src/folding/hypernova/cccs.rs deleted file mode 100644 index 3f8306598..000000000 --- a/folding-schemes/src/folding/hypernova/cccs.rs +++ /dev/null @@ -1,263 +0,0 @@ -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::PrimeField; -use ark_serialize::CanonicalDeserialize; -use ark_serialize::CanonicalSerialize; -use ark_std::{rand::Rng, sync::Arc, One, Zero}; - -use super::circuits::CCCSVar; -use super::Witness; -use crate::arith::{ccs::CCS, Arith, ArithRelation}; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::CF1; -use crate::folding::traits::Inputize; -use crate::folding::traits::{CommittedInstanceOps, Dummy}; -use crate::utils::mle::dense_vec_to_dense_mle; -use crate::utils::vec::{is_zero_vec, mat_vec_mul}; -use crate::utils::virtual_polynomial::{build_eq_x_r_vec, VirtualPolynomial}; -use crate::{Curve, Error}; - -/// Committed CCS instance -#[derive(Debug, Clone, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)] -pub struct CCCS { - // Commitment to witness - pub C: C, - // Public input/output - pub x: Vec, -} - -impl CCS { - pub fn to_cccs, const H: bool>( - &self, - rng: &mut R, - cs_params: &CS::ProverParams, - z: &[F], - ) -> Result<(CCCS, Witness), Error> - where - // enforce that CCS's F is the C::ScalarField - C: Curve, - { - let (w, x) = self.split_z(z); - - // if the commitment scheme is set to be hiding, set the random blinding parameter - let r_w = if CS::is_hiding() { - F::rand(rng) - } else { - F::zero() - }; - let C = CS::commit(cs_params, &w, &r_w)?; - - Ok((CCCS:: { C, x }, Witness:: { w, r_w })) - } - - /// Computes q(x) = \sum^q c_i * \prod_{j \in S_i} ( \sum_{y \in {0,1}^s'} M_j(x, y) * z(y) ) - /// polynomial over x - pub fn compute_q(&self, z: &[F]) -> Result, Error> { - let mut q_x = VirtualPolynomial::::new(self.s); - for (S_i, &c_i) in self.S.iter().zip(&self.c) { - let mut Q_k = vec![]; - for &j in S_i { - Q_k.push(Arc::new(dense_vec_to_dense_mle( - self.s, - &mat_vec_mul(&self.M[j], z)?, - ))); - } - q_x.add_mle_list(Q_k, c_i)?; - } - Ok(q_x) - } - - /// Computes Q(x) = eq(beta, x) * q(x) - /// = eq(beta, x) * \sum^q c_i * \prod_{j \in S_i} ( \sum_{y \in {0,1}^s'} M_j(x, y) * z(y) ) - /// polynomial over x - pub fn compute_Q(&self, z: &[F], beta: &[F]) -> Result, Error> { - let eq_beta = build_eq_x_r_vec(beta)?; - let eq_beta_mle = Arc::new(dense_vec_to_dense_mle(self.s, &eq_beta)); - - let mut Q = VirtualPolynomial::::new(self.s); - for (S_i, &c_i) in self.S.iter().zip(&self.c) { - let mut Q_k = vec![]; - for &j in S_i { - Q_k.push(Arc::new(dense_vec_to_dense_mle( - self.s, - &mat_vec_mul(&self.M[j], z)?, - ))); - } - Q_k.push(eq_beta_mle.clone()); - Q.add_mle_list(Q_k, c_i)?; - } - Ok(Q) - } -} - -impl Dummy<&CCS>> for CCCS { - fn dummy(ccs: &CCS>) -> Self { - Self { - C: C::zero(), - x: vec![CF1::::zero(); ccs.n_public_inputs()], - } - } -} - -impl ArithRelation>, CCCS> for CCS> { - type Evaluation = Vec>; - - fn eval_relation(&self, w: &Witness>, u: &CCCS) -> Result { - // evaluate CCCS relation - self.eval_at_z(&[&[CF1::::one()][..], &u.x, &w.w].concat()) - } - - /// Perform the check of the CCCS instance described at section 4.1, - /// notice that this method does not check the commitment correctness - fn check_evaluation( - _w: &Witness>, - _u: &CCCS, - e: Self::Evaluation, - ) -> Result<(), Error> { - // A CCCS relation is satisfied if the q(x) multivariate polynomial evaluates to zero in - // the hypercube, evaluating over the whole boolean hypercube for a normal-sized instance - // would take too much, this checks the CCS relation of the CCCS. - is_zero_vec(&e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -impl Absorb for CCCS { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.C.to_native_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - } -} - -impl CommittedInstanceOps for CCCS { - type Var = CCCSVar; - - fn get_commitments(&self) -> Vec { - vec![self.C] - } - - fn is_incoming(&self) -> bool { - true - } -} - -impl Inputize> for CCCS { - /// Returns the internal representation in the same order as how the value - /// is allocated in `CCCSVar::new_input`. - fn inputize(&self) -> Vec> { - [&self.C.inputize_nonnative()[..], &self.x].concat() - } -} - -#[cfg(test)] -pub mod tests { - use ark_pallas::Fr; - use ark_std::test_rng; - use ark_std::UniformRand; - - use super::*; - use crate::arith::ccs::tests::{get_test_ccs, get_test_z}; - use crate::utils::hypercube::BooleanHypercube; - - /// Do some sanity checks on q(x). It's a multivariable polynomial and it should evaluate to zero inside the - /// hypercube, but to not-zero outside the hypercube. - #[test] - fn test_compute_q() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs = get_test_ccs::(); - let z = get_test_z(3); - - let q = ccs.compute_q(&z)?; - - // Evaluate inside the hypercube - for x in BooleanHypercube::new(ccs.s) { - assert_eq!(Fr::zero(), q.evaluate(&x)?); - } - - // Evaluate outside the hypercube - let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - assert_ne!(Fr::zero(), q.evaluate(&beta)?); - Ok(()) - } - - /// Perform some sanity checks on Q(x). - #[test] - fn test_compute_Q() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs: CCS = get_test_ccs(); - let z = get_test_z(3); - let (w, x) = ccs.split_z(&z); - ccs.check_relation(&w, &x)?; - - let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - - // Compute Q(x) = eq(beta, x) * q(x). - let Q = ccs.compute_Q(&z, &beta)?; - - // Let's consider the multilinear polynomial G(x) = \sum_{y \in {0, 1}^s} eq(x, y) q(y) - // which interpolates the multivariate polynomial q(x) inside the hypercube. - // - // Observe that summing Q(x) inside the hypercube, directly computes G(\beta). - // - // Now, G(x) is multilinear and agrees with q(x) inside the hypercube. Since q(x) vanishes inside the - // hypercube, this means that G(x) also vanishes in the hypercube. Since G(x) is multilinear and vanishes - // inside the hypercube, this makes it the zero polynomial. - // - // Hence, evaluating G(x) at a random beta should give zero. - - // Now sum Q(x) evaluations in the hypercube and expect it to be 0 - let r = BooleanHypercube::new(ccs.s) - .map(|x| Q.evaluate(&x)) - .collect::, _>>()? - .into_iter() - .fold(Fr::zero(), |acc, result| acc + result); - assert_eq!(r, Fr::zero()); - Ok(()) - } - - /// The polynomial G(x) (see above) interpolates q(x) inside the hypercube. - /// Summing Q(x) over the hypercube is equivalent to evaluating G(x) at some point. - /// This test makes sure that G(x) agrees with q(x) inside the hypercube, but not outside - #[test] - fn test_Q_against_q() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs: CCS = get_test_ccs(); - let z = get_test_z(3); - let (w, x) = ccs.split_z(&z); - ccs.check_relation(&w, &x)?; - - // Now test that if we create Q(x) with eq(d,y) where d is inside the hypercube, \sum Q(x) should be G(d) which - // should be equal to q(d), since G(x) interpolates q(x) inside the hypercube - let q = ccs.compute_q(&z)?; - for d in BooleanHypercube::new(ccs.s) { - let Q_at_d = ccs.compute_Q(&z, &d)?; - - // Get G(d) by summing over Q_d(x) over the hypercube - let G_at_d = BooleanHypercube::new(ccs.s) - .map(|x| Q_at_d.evaluate(&x)) - .collect::, _>>()? - .into_iter() - .fold(Fr::zero(), |acc, result| acc + result); - assert_eq!(G_at_d, q.evaluate(&d)?); - } - - // Now test that they should disagree outside of the hypercube - let r: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - let Q_at_r = ccs.compute_Q(&z, &r)?; - - // Get G(d) by summing over Q_d(x) over the hypercube - let G_at_r = BooleanHypercube::new(ccs.s) - .map(|x| Q_at_r.evaluate(&x)) - .collect::, _>>()? - .into_iter() - .fold(Fr::zero(), |acc, result| acc + result); - - assert_ne!(G_at_r, q.evaluate(&r)?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/circuits.rs b/folding-schemes/src/folding/hypernova/circuits.rs deleted file mode 100644 index c352da23f..000000000 --- a/folding-schemes/src/folding/hypernova/circuits.rs +++ /dev/null @@ -1,1389 +0,0 @@ -/// Implementation of [HyperNova](https://eprint.iacr.org/2023/573.pdf) circuits -use ark_crypto_primitives::sponge::{ - constraints::AbsorbGadget, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, - CryptographicSponge, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - uint8::UInt8, - GR1CSVar, -}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, Namespace, SynthesisError, - SynthesisMode, -}; -#[cfg(test)] -use ark_std::One; -use ark_std::{fmt::Debug, iter::Sum, Zero}; -use core::{borrow::Borrow, marker::PhantomData}; - -use super::{ - cccs::CCCS, - lcccs::LCCCS, - nimfs::{NIMFSProof, NIMFS}, - HyperNovaCycleFoldConfig, Witness, -}; -use crate::arith::{ - ccs::CCS, - r1cs::{extract_r1cs, R1CS}, - Arith, -}; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::{ - circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, - CycleFoldConfig, - }, - nonnative::affine::NonNativeAffineVar, - sum_check::{IOPProofVar, SumCheckVerifierGadget, VPAuxInfoVar}, - utils::EqEvalGadget, - CF1, - }, - nova::get_r1cs_from_cs, - traits::{CommittedInstanceVarOps, Dummy}, -}; -use crate::frontend::FCircuit; -use crate::transcript::{AbsorbNonNativeGadget, Transcript, TranscriptVar}; -use crate::utils::virtual_polynomial::VPAuxInfo; -use crate::{Curve, Error}; - -/// Committed CCS instance -#[derive(Debug, Clone)] -pub struct CCCSVar { - // Commitment to witness - pub C: NonNativeAffineVar, - // Public io - pub x: Vec>>, -} - -impl AllocVar, CF1> for CCCSVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let C = NonNativeAffineVar::::new_variable(cs.clone(), || Ok(val.borrow().C), mode)?; - let x: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; - - Ok(Self { C, x }) - }) - } -} - -impl CommittedInstanceVarOps for CCCSVar { - type PointVar = NonNativeAffineVar; - - fn get_commitments(&self) -> Vec { - vec![self.C.clone()] - } - - fn get_public_inputs(&self) -> &[FpVar>] { - &self.x - } - - fn enforce_incoming(&self) -> Result<(), SynthesisError> { - // `CCCSVar` is always the incoming instance - Ok(()) - } - - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { - self.x.enforce_equal(&other.x) - } -} - -impl AbsorbGadget for CCCSVar { - fn to_sponge_bytes(&self) -> Result>, SynthesisError> { - FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) - } - - fn to_sponge_field_elements(&self) -> Result>, SynthesisError> { - Ok([&self.C.to_native_sponge_field_elements()?, &self.x[..]].concat()) - } -} - -/// Linearized Committed CCS instance -#[derive(Debug, Clone)] -pub struct LCCCSVar { - // Commitment to witness - pub C: NonNativeAffineVar, - // Relaxation factor of z for folded LCCCS - pub u: FpVar>, - // Public io - pub x: Vec>>, - // Random evaluation point for the v_i - pub r_x: Vec>>, - // Vector of v_i - pub v: Vec>>, -} - -impl AllocVar, CF1> for LCCCSVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let C = NonNativeAffineVar::::new_variable(cs.clone(), || Ok(val.borrow().C), mode)?; - let u = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; - let x: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; - let r_x: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().r_x.clone()), mode)?; - let v: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().v.clone()), mode)?; - - Ok(Self { C, u, x, r_x, v }) - }) - } -} - -impl AbsorbGadget for LCCCSVar { - fn to_sponge_bytes(&self) -> Result>, SynthesisError> { - FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) - } - - fn to_sponge_field_elements(&self) -> Result>, SynthesisError> { - Ok([ - &self.C.to_native_sponge_field_elements()?, - &[self.u.clone()][..], - &self.x, - &self.r_x, - &self.v, - ] - .concat()) - } -} - -impl CommittedInstanceVarOps for LCCCSVar { - type PointVar = NonNativeAffineVar; - - fn get_commitments(&self) -> Vec { - vec![self.C.clone()] - } - - fn get_public_inputs(&self) -> &[FpVar>] { - &self.x - } - - fn enforce_incoming(&self) -> Result<(), SynthesisError> { - // `LCCCSVar` is always the running instance - Err(SynthesisError::Unsatisfiable) - } - - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { - self.u.enforce_equal(&other.u)?; - self.x.enforce_equal(&other.x)?; - self.r_x.enforce_equal(&other.r_x)?; - self.v.enforce_equal(&other.v) - } -} - -/// ProofVar defines a multifolding proof -#[derive(Debug)] -pub struct ProofVar { - pub sc_proof: IOPProofVar, - #[allow(clippy::type_complexity)] - pub sigmas_thetas: (Vec>>>, Vec>>>), -} -impl AllocVar, CF1> for ProofVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let sc_proof = IOPProofVar::::new_variable( - cs.clone(), - || Ok(val.borrow().sc_proof.clone()), - mode, - )?; - let sigmas: Vec>>> = val - .borrow() - .sigmas_thetas - .0 - .iter() - .map(|sigmas_i| Vec::new_variable(cs.clone(), || Ok(sigmas_i.clone()), mode)) - .collect::>>>, SynthesisError>>()?; - let thetas: Vec>>> = val - .borrow() - .sigmas_thetas - .1 - .iter() - .map(|thetas_i| Vec::new_variable(cs.clone(), || Ok(thetas_i.clone()), mode)) - .collect::>>>, SynthesisError>>()?; - - Ok(Self { - sc_proof, - sigmas_thetas: (sigmas.clone(), thetas.clone()), - }) - }) - } -} - -pub struct NIMFSGadget { - _c: PhantomData, -} -impl NIMFSGadget { - /// Runs (in-circuit) the NIMFS.V, which outputs the new folded LCCCS instance together with - /// the rho_powers, which will be used in other parts of the AugmentedFCircuit - #[allow(clippy::type_complexity)] - pub fn verify>( - cs: ConstraintSystemRef>, - // only used the CCS params, not the matrices - ccs: &CCS, - transcript: &mut T, - running_instances: &[LCCCSVar], // U - new_instances: &[CCCSVar], // u - proof: ProofVar, - enabled: Boolean, - ) -> Result<(LCCCSVar, Vec>>), SynthesisError> { - // absorb instances to transcript - transcript.absorb(&running_instances)?; - transcript.absorb(&new_instances)?; - - // get the challenges - let gamma_scalar_raw = C::ScalarField::from_le_bytes_mod_order(b"gamma"); - let gamma_scalar: FpVar> = - FpVar::>::new_constant(cs.clone(), gamma_scalar_raw)?; - transcript.absorb(&gamma_scalar)?; - let gamma: FpVar> = transcript.get_challenge()?; - - let beta_scalar_raw = C::ScalarField::from_le_bytes_mod_order(b"beta"); - let beta_scalar: FpVar> = - FpVar::>::new_constant(cs.clone(), beta_scalar_raw)?; - transcript.absorb(&beta_scalar)?; - let beta: Vec>> = transcript.get_challenges(ccs.s)?; - - let vp_aux_info_raw = VPAuxInfo:: { - max_degree: ccs.degree() + 1, - num_variables: ccs.s, - phantom: PhantomData::, - }; - let vp_aux_info = VPAuxInfoVar::>::new_witness(cs.clone(), || Ok(vp_aux_info_raw))?; - - // sumcheck - // first, compute the expected sumcheck sum: \sum gamma^j v_j - let mut sum_v_j_gamma = FpVar::>::zero(); - let mut gamma_j = FpVar::::one(); - for running_instance in running_instances.iter() { - for j in 0..running_instance.v.len() { - gamma_j *= gamma.clone(); - sum_v_j_gamma += running_instance.v[j].clone() * gamma_j.clone(); - } - } - - // verify the interactive part of the sumcheck - let (e_vars, r_vars) = SumCheckVerifierGadget::::verify( - &proof.sc_proof, - &vp_aux_info, - transcript, - enabled.clone(), - )?; - - // extract the randomness from the sumcheck - let r_x_prime = r_vars.clone(); - - // verify the claim c - let computed_c = compute_c_gadget( - ccs, - proof.sigmas_thetas.0.clone(), // sigmas - proof.sigmas_thetas.1.clone(), // thetas - gamma, - beta, - running_instances - .iter() - .map(|lcccs| lcccs.r_x.clone()) - .collect(), - r_x_prime.clone(), - )?; - computed_c.conditional_enforce_equal(&e_vars[e_vars.len() - 1], &enabled)?; - - // get the folding challenge - let rho_scalar_raw = C::ScalarField::from_le_bytes_mod_order(b"rho"); - let rho_scalar: FpVar> = FpVar::>::new_constant(cs.clone(), rho_scalar_raw)?; - transcript.absorb(&rho_scalar)?; - let rho_bits: Vec>> = transcript.get_challenge_nbits(NOVA_N_BITS_RO)?; - let rho = Boolean::le_bits_to_fp(&rho_bits)?; - - // Self::fold will return the folded instance - let folded_lcccs = Self::fold( - running_instances, - new_instances, - proof.sigmas_thetas, - r_x_prime, - rho, - )?; - // return the rho_bits so it can be used in other parts of the AugmentedFCircuit - Ok((folded_lcccs, rho_bits)) - } - - /// Runs (in-circuit) the verifier side of the fold, computing the new folded LCCCS instance - #[allow(clippy::type_complexity)] - fn fold( - lcccs: &[LCCCSVar], - cccs: &[CCCSVar], - sigmas_thetas: (Vec>>>, Vec>>>), - r_x_prime: Vec>>, - rho: FpVar>, - ) -> Result, SynthesisError> { - let (sigmas, thetas) = (sigmas_thetas.0.clone(), sigmas_thetas.1.clone()); - let mut u_folded: FpVar> = FpVar::zero(); - let mut x_folded: Vec>> = vec![FpVar::zero(); lcccs[0].x.len()]; - let mut v_folded: Vec>> = vec![FpVar::zero(); sigmas[0].len()]; - - let mut rho_i = FpVar::one(); - for i in 0..(lcccs.len() + cccs.len()) { - let u: FpVar>; - let x: Vec>>; - let v: Vec>>; - if i < lcccs.len() { - u = lcccs[i].u.clone(); - x = lcccs[i].x.clone(); - v = sigmas[i].clone(); - } else { - u = FpVar::one(); - x = cccs[i - lcccs.len()].x.clone(); - v = thetas[i - lcccs.len()].clone(); - } - - u_folded += rho_i.clone() * u; - x_folded = x_folded - .iter() - .zip( - x.iter() - .map(|x_i| x_i * rho_i.clone()) - .collect::>>>(), - ) - .map(|(a_i, b_i)| a_i + b_i) - .collect(); - - v_folded = v_folded - .iter() - .zip( - v.iter() - .map(|x_i| x_i * rho_i.clone()) - .collect::>>>(), - ) - .map(|(a_i, b_i)| a_i + b_i) - .collect(); - - // compute the next power of rho - rho_i *= rho.clone(); - } - - // return the folded instance, together with the rho's powers vector so they can be used in - // other parts of the AugmentedFCircuit - Ok(LCCCSVar:: { - // C this is later overwritten by the U_{i+1}.C value checked by the cyclefold circuit - C: lcccs[0].C.clone(), - u: u_folded, - x: x_folded, - r_x: r_x_prime, - v: v_folded, - }) - } -} - -/// Computes c from the step 5 in section 5 of HyperNova, adapted to multiple LCCCS & CCCS -/// instances: -/// $$ -/// c = \sum_{i \in [\mu]} \left(\sum_{j \in [t]} \gamma^{i \cdot t + j} \cdot e_i \cdot \sigma_{i,j} \right) + -/// \sum_{k \in [\nu]} \gamma^{\mu \cdot t+k} \cdot e_k \cdot \left( \sum_{i=1}^q c_i \cdot \prod_{j \in S_i} -/// \theta_{k,j} \right) -/// $$ -#[allow(clippy::too_many_arguments)] -fn compute_c_gadget( - ccs: &CCS, - vec_sigmas: Vec>>, - vec_thetas: Vec>>, - gamma: FpVar, - beta: Vec>, - vec_r_x: Vec>>, - vec_r_x_prime: Vec>, -) -> Result, SynthesisError> { - let mut e_lcccs = Vec::new(); - for r_x in vec_r_x.iter() { - e_lcccs.push(EqEvalGadget::eq_eval(r_x, &vec_r_x_prime)?); - } - - let mut c = FpVar::::zero(); - let mut current_gamma = FpVar::::one(); - for i in 0..vec_sigmas.len() { - for sigma in &vec_sigmas[i] { - c += current_gamma.clone() * e_lcccs[i].clone() * sigma; - current_gamma *= gamma.clone(); - } - } - - let e_k = EqEvalGadget::eq_eval(&beta, &vec_r_x_prime)?; - #[allow(clippy::needless_range_loop)] - for k in 0..vec_thetas.len() { - let prods = ccs.S.iter().zip(&ccs.c).map(|(S_i, &c_i)| { - let mut prod = FpVar::::one(); - for &j in S_i { - prod *= &vec_thetas[k][j]; - } - prod * c_i - }); - let sum = FpVar::sum(prods); - c += current_gamma.clone() * e_k.clone() * sum; - current_gamma *= gamma.clone(); - } - Ok(c) -} - -/// `AugmentedFCircuit` enhances the original step function `F`, so that it can -/// be used in recursive arguments such as IVC. -/// -/// The method for converting `F` to `AugmentedFCircuit` (`F'`) is defined in -/// [Nova](https://eprint.iacr.org/2021/370.pdf), where `AugmentedFCircuit` not -/// only invokes `F`, but also adds additional constraints for verifying the -/// correct folding of primary instances (i.e., the instances over `C1`). -/// In the paper, the primary instances are Nova's `CommittedInstance`, but we -/// extend this method to support using HyperNova's `LCCCS` and `CCCS` instances -/// as primary instances. -/// -/// Furthermore, to reduce circuit size over `C2`, we implement the constraints -/// defined in [CycleFold](https://eprint.iacr.org/2023/1192.pdf). These extra -/// constraints verify the correct folding of CycleFold instances. -/// -/// For multi-instance folding, one needs to specify the const generics below: -/// * `MU` - the number of LCCCS instances to be folded -/// * `NU` - the number of CCCS instances to be folded -#[derive(Debug, Clone)] -pub struct AugmentedFCircuit< - C1: Curve, - C2: Curve, - FC: FCircuit>, - const MU: usize, - const NU: usize, -> { - pub(super) poseidon_config: PoseidonConfig>, - pub(super) ccs: CCS, // CCS of the AugmentedFCircuit - pub(super) pp_hash: Option>, - pub(super) i: Option>, - pub(super) i_usize: Option, - pub(super) z_0: Option>, - pub(super) z_i: Option>, - pub(super) external_inputs: Option, - pub(super) U_i: Option>, - pub(super) Us: Option>>, // other U_i's to be folded that are not the main running instance - pub(super) u_i_C: Option, // u_i.C - pub(super) us: Option>>, // other u_i's to be folded that are not the main incoming instance - pub(super) U_i1_C: Option, // U_{i+1}.C - pub(super) F: FC, // F circuit - pub(super) nimfs_proof: Option>, - - // cyclefold verifier on C1 - pub(super) cf_u_i_cmW: Option, // input, cf_u_i.cmW - pub(super) cf_U_i: Option>, // input, RelaxedR1CS CycleFold instance - pub(super) cf_cmT: Option, -} - -impl AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - pub fn default( - poseidon_config: &PoseidonConfig>, - F_circuit: FC, - ccs: CCS, - ) -> Result { - if MU < 1 || NU < 1 { - return Err(Error::CantBeZero("mu,nu".to_string())); - } - Ok(Self { - poseidon_config: poseidon_config.clone(), - ccs, - pp_hash: None, - i: None, - i_usize: None, - z_0: None, - z_i: None, - external_inputs: None, - U_i: None, - Us: None, - u_i_C: None, - us: None, - U_i1_C: None, - F: F_circuit, - nimfs_proof: None, - cf_u_i_cmW: None, - cf_U_i: None, - cf_cmT: None, - }) - } - - pub fn empty( - poseidon_config: &PoseidonConfig>, - F: FC, // FCircuit - ccs: Option>, - ) -> Result { - // create the initial ccs by converting from a dummy r1cs with m = 0, - // n = 0, and l = 2 (i.e., 0 constraints, and 0 variables, and 2 public - // inputs). - // Here, `m` and `n` will be overwritten by the `compute_concrete_ccs` - // method. - let mut initial_ccs = CCS::from(R1CS::dummy((0, 0, 2))); - // Although `s = log(m)` is undefined for `m = 0`, we set it to 1 here - // because the circuit internally calls `IOPSumCheck::extract_sum` which - // will panic if `s = 0` (0 is arkworks' fallback value for `log(0)`). - // Similarly, `s` will also be overwritten by `compute_concrete_ccs`. - initial_ccs.s = 1; - let mut augmented_f_circuit = Self::default(poseidon_config, F, initial_ccs)?; - augmented_f_circuit.ccs = ccs - .ok_or(()) - .or_else(|_| augmented_f_circuit.compute_concrete_ccs())?; - Ok(augmented_f_circuit) - } - - /// This method computes the CCS parameters. This is used because there is a circular - /// dependency between the AugmentedFCircuit CCS and the CCS parameters m & n & s. - /// For a stable FCircuit circuit, the CCS parameters can be computed in advance and can be - /// feed in as parameter for the AugmentedFCircuit::empty method to avoid computing them there. - pub fn compute_concrete_ccs(&self) -> Result, Error> { - let r1cs = get_r1cs_from_cs::>(self.clone())?; - let mut ccs = CCS::from(r1cs); - - let z_0 = vec![C1::ScalarField::zero(); self.F.state_len()]; - let mut W_i = Witness::::dummy(&ccs); - let mut U_i = LCCCS::::dummy(&ccs); - let mut w_i = W_i.clone(); - let mut u_i = CCCS::::dummy(&ccs); - - let n_iters = 2; - for _ in 0..n_iters { - let Us = vec![U_i.clone(); MU - 1]; - let Ws = vec![W_i.clone(); MU - 1]; - let us = vec![u_i.clone(); NU - 1]; - let ws = vec![w_i.clone(); NU - 1]; - - let all_Us = [vec![U_i.clone()], Us.clone()].concat(); - let all_us = [vec![u_i.clone()], us.clone()].concat(); - let all_Ws = [vec![W_i.clone()], Ws].concat(); - let all_ws = [vec![w_i.clone()], ws].concat(); - - let mut transcript_p = PoseidonSponge::new_with_pp_hash( - &self.poseidon_config.clone(), - C1::ScalarField::zero(), - ); - let (nimfs_proof, U_i1, _, _) = NIMFS::>::prove( - &mut transcript_p, - &ccs, - &all_Us, - &all_us, - &all_Ws, - &all_ws, - )?; - - let augmented_f_circuit = Self { - poseidon_config: self.poseidon_config.clone(), - ccs: ccs.clone(), - pp_hash: Some(C1::ScalarField::zero()), - i: Some(C1::ScalarField::zero()), - i_usize: Some(0), - z_0: Some(z_0.clone()), - z_i: Some(z_0.clone()), - external_inputs: Some(FC::ExternalInputs::default()), - U_i: Some(U_i.clone()), - Us: Some(Us), - u_i_C: Some(u_i.C), - us: Some(us), - U_i1_C: Some(U_i1.C), - F: self.F.clone(), - nimfs_proof: Some(nimfs_proof), - // cyclefold values - cf_u_i_cmW: None, - cf_U_i: None, - cf_cmT: None, - }; - - ccs = augmented_f_circuit.compute_ccs()?; - // prepare instances for next loop iteration - u_i = CCCS::::dummy(&ccs); - w_i = Witness::::dummy(&ccs); - W_i = Witness::::dummy(&ccs); - U_i = LCCCS::::dummy(&ccs); - } - Ok(ccs) - } - - /// Returns the CCS out of the AugmentedFCircuit. - /// Notice that in order to be able to internally call the `extract_r1cs` function, this method - /// calls the `cs.finalize` method which consumes a noticeable portion of the time. If the CCS - /// is not needed, directly generate the ConstraintSystem without calling the `finalize` method - /// will save computing time. - #[allow(clippy::type_complexity)] - pub fn compute_ccs(&self) -> Result, Error> { - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - self.clone().generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - let ccs = CCS::from(r1cs); - - Ok(ccs) - } -} - -impl AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - pub fn compute_next_state( - self, - cs: ConstraintSystemRef>, - ) -> Result>>, SynthesisError> { - let pp_hash = FpVar::>::new_witness(cs.clone(), || { - Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) - })?; - let i = FpVar::>::new_witness(cs.clone(), || { - Ok(self.i.unwrap_or_else(CF1::::zero)) - })?; - let z_0 = Vec::>>::new_witness(cs.clone(), || { - Ok(self - .z_0 - .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) - })?; - let z_i = Vec::>>::new_witness(cs.clone(), || { - Ok(self - .z_i - .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) - })?; - let external_inputs = FC::ExternalInputsVar::new_witness(cs.clone(), || { - Ok(self.external_inputs.unwrap_or_default()) - })?; - - let U_dummy = LCCCS::::dummy(&self.ccs); - let u_dummy = CCCS::::dummy(&self.ccs); - - let U_i = - LCCCSVar::::new_witness(cs.clone(), || Ok(self.U_i.unwrap_or(U_dummy.clone())))?; - let Us = Vec::>::new_witness(cs.clone(), || { - Ok(self.Us.unwrap_or(vec![U_dummy.clone(); MU - 1])) - })?; - let us = Vec::>::new_witness(cs.clone(), || { - Ok(self.us.unwrap_or(vec![u_dummy.clone(); NU - 1])) - })?; - let U_i1_C = NonNativeAffineVar::new_witness(cs.clone(), || { - Ok(self.U_i1_C.unwrap_or_else(C1::zero)) - })?; - let nimfs_proof_dummy = NIMFSProof::::dummy((&self.ccs, MU, NU)); - let nimfs_proof = ProofVar::::new_witness(cs.clone(), || { - Ok(self.nimfs_proof.unwrap_or(nimfs_proof_dummy)) - })?; - - let cf_u_dummy = - CycleFoldCommittedInstance::dummy(HyperNovaCycleFoldConfig::::IO_LEN); - let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.cf_U_i.unwrap_or(cf_u_dummy.clone())) - })?; - let cf_cmT = C2::Var::new_witness(cs.clone(), || Ok(self.cf_cmT.unwrap_or_else(C2::zero)))?; - - let sponge = PoseidonSpongeVar::::new_with_pp_hash( - &self.poseidon_config, - &pp_hash, - )?; - let mut transcript = sponge.clone(); - - let is_basecase = i.is_zero()?; - let is_not_basecase = !&is_basecase; - - // Primary Part - // P.1. Compute u_i.x - // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash(&sponge, &i, &z_0, &z_i)?; - // u_i.x[1] = H(cf_U_i) - let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge)?; - - // P.2. Construct u_i - let u_i = CCCSVar:: { - // u_i.C is provided by the prover as witness - C: NonNativeAffineVar::::new_witness(cs.clone(), || { - Ok(self.u_i_C.unwrap_or(C1::zero())) - })?, - // u_i.x is computed in step 1 - x: vec![u_i_x, cf_u_i_x], - }; - - let all_Us = [vec![U_i.clone()], Us].concat(); - let all_us = [vec![u_i.clone()], us].concat(); - - // P.3. NIMFS.verify, obtains U_{i+1} by folding [U_i] & [u_i]. - // Notice that NIMFSGadget::fold_committed_instance does not fold C. We set `U_i1.C` to - // unconstrained witnesses `U_i1_C` respectively. Its correctness will be checked on the - // other curve. - let (mut U_i1, rho_bits) = NIMFSGadget::::verify( - cs.clone(), - &self.ccs.clone(), - &mut transcript, - &all_Us, - &all_us, - nimfs_proof, - is_not_basecase.clone(), - )?; - U_i1.C = U_i1_C; - - // P.4.a compute and check the first output of F' - - // get z_{i+1} from the F circuit - let i_usize = self.i_usize.unwrap_or(0); - let z_i1 = self - .F - .generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?; - - let (u_i1_x, _) = - U_i1.clone() - .hash(&sponge, &(i + FpVar::>::one()), &z_0, &z_i1)?; - let (u_i1_x_base, _) = LCCCSVar::new_constant(cs.clone(), U_dummy)?.hash( - &sponge, - &FpVar::>::one(), - &z_0, - &z_i1, - )?; - let x = is_basecase.select(&u_i1_x_base, &u_i1_x)?; - // This line "converts" `x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `x`. - // While comparing `x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `x` computed in-circuit. - FpVar::new_input(cs.clone(), || x.value())?.enforce_equal(&x)?; - - // CycleFold part - // C.1. Compute `cf_u_i.x` - // C.2. Construct `cf_u_i` - let cf_u_i = CycleFoldCommittedInstanceVar::new_incoming_from_components( - // `cf_u_i.cmW` is provided by the prover as witness. - C2::Var::new_witness(cs.clone(), || Ok(self.cf_u_i_cmW.unwrap_or(C2::zero())))?, - // To construct `cf_u_i.x`, we need to provide the randomness - // `rho_bits` and the `C` component in LCCCS and CCCS instances - // `all_Us`, `all_us` and `U_{i+1}`. - &rho_bits, - all_Us - .into_iter() - .map(|U| U.C) - .chain(all_us.into_iter().map(|u| u.C)) - .chain(vec![U_i1.C]) - .collect(), - )?; - - // C.3. nifs.verify (fold_committed_instance), obtains cf_U_{i+1} by folding cf_u_i & cf_U_i. - let cf_U_i1 = CycleFoldAugmentationGadget::fold_gadget( - &mut transcript, - cf_U_i, - vec![cf_u_i], - vec![cf_cmT], - )?; - - // Back to Primary Part - // P.4.b compute and check the second output of F' - // Base case: u_{i+1}.x[1] == H(cf_U_{\bot}) - // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) - let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge)?; - let (cf_u_i1_x_base, _) = - CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? - .hash(&sponge)?; - let cf_x = is_basecase.select(&cf_u_i1_x_base, &cf_u_i1_x)?; - // This line "converts" `cf_x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `cf_x`. - // While comparing `cf_x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `cf_x` computed in-circuit. - FpVar::new_input(cs.clone(), || cf_x.value())?.enforce_equal(&cf_x)?; - - Ok(z_i1) - } -} - -impl ConstraintSynthesizer> - for AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - self.compute_next_state(cs).map(|_| ()) - } -} - -#[cfg(test)] -mod tests { - use ark_bn254::{Fq, Fr, G1Projective as Projective}; - use ark_crypto_primitives::sponge::Absorb; - use ark_grumpkin::Projective as Projective2; - use ark_std::{cmp::max, test_rng, time::Instant, UniformRand}; - - use super::*; - use crate::{ - arith::{ - ccs::tests::{get_test_ccs, get_test_z}, - r1cs::extract_w_x, - ArithRelation, - }, - commitment::{pedersen::Pedersen, CommitmentScheme}, - folding::{ - circuits::cyclefold::{CycleFoldCircuit, CycleFoldWitness}, - hypernova::utils::{compute_c, compute_sigmas_thetas}, - traits::CommittedInstanceOps, - }, - frontend::utils::{cubic_step_native, CubicFCircuit}, - transcript::{poseidon::poseidon_canonical_config, Transcript}, - }; - - #[test] - pub fn test_compute_c_gadget() -> Result<(), Error> { - // number of LCCCS & CCCS instances to fold in a single step - let mu = 32; - let nu = 42; - - let mut z_lcccs = Vec::new(); - for i in 0..mu { - let z = get_test_z(i + 3); - z_lcccs.push(z); - } - let mut z_cccs = Vec::new(); - for i in 0..nu { - let z = get_test_z(i + 3); - z_cccs.push(z); - } - - let ccs: CCS = get_test_ccs(); - - let mut rng = test_rng(); - let gamma: Fr = Fr::rand(&mut rng); - let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - // Create the LCCCS instances out of z_lcccs - let mut lcccs_instances = Vec::new(); - for z_i in z_lcccs.iter() { - let (inst, _) = ccs.to_lcccs::<_, _, Pedersen, true>( - &mut rng, - &pedersen_params, - z_i, - )?; - lcccs_instances.push(inst); - } - // Create the CCCS instance out of z_cccs - let mut cccs_instances = Vec::new(); - for z_i in z_cccs.iter() { - let (inst, _) = - ccs.to_cccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, z_i)?; - cccs_instances.push(inst); - } - - let sigmas_thetas = compute_sigmas_thetas(&ccs, &z_lcccs, &z_cccs, &r_x_prime)?; - - let expected_c = compute_c( - &ccs, - &sigmas_thetas, - gamma, - &beta, - &lcccs_instances - .iter() - .map(|lcccs| lcccs.r_x.clone()) - .collect(), - &r_x_prime, - )?; - - let cs = ConstraintSystem::::new_ref(); - let mut vec_sigmas = Vec::new(); - let mut vec_thetas = Vec::new(); - for sigmas in sigmas_thetas.0 { - vec_sigmas.push(Vec::>::new_witness(cs.clone(), || { - Ok(sigmas.clone()) - })?); - } - for thetas in sigmas_thetas.1 { - vec_thetas.push(Vec::>::new_witness(cs.clone(), || { - Ok(thetas.clone()) - })?); - } - let vec_r_x: Vec>> = lcccs_instances - .iter() - .map(|lcccs| Vec::>::new_witness(cs.clone(), || Ok(lcccs.r_x.clone()))) - .collect::, _>>()?; - let vec_r_x_prime = Vec::>::new_witness(cs.clone(), || Ok(r_x_prime.clone()))?; - let gamma_var = FpVar::::new_witness(cs.clone(), || Ok(gamma))?; - let beta_var = Vec::>::new_witness(cs.clone(), || Ok(beta.clone()))?; - - let computed_c = compute_c_gadget( - &ccs, - vec_sigmas, - vec_thetas, - gamma_var, - beta_var, - vec_r_x, - vec_r_x_prime, - )?; - - assert_eq!(expected_c, computed_c.value()?); - Ok(()) - } - - /// Test that generates mu>1 and nu>1 instances, and folds them in a single multifolding step, - /// to verify the folding in the NIMFSGadget circuit - #[test] - pub fn test_nimfs_gadget_verify() -> Result<(), Error> { - let mut rng = test_rng(); - - // Create a basic CCS circuit - let ccs = get_test_ccs::(); - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let mu = 32; - let nu = 42; - - // Generate a mu LCCCS & nu CCCS satisfying witness - let mut z_lcccs = Vec::new(); - for i in 0..mu { - let z = get_test_z(i + 3); - z_lcccs.push(z); - } - let mut z_cccs = Vec::new(); - for i in 0..nu { - let z = get_test_z(nu + i + 3); - z_cccs.push(z); - } - - // Create the LCCCS instances out of z_lcccs - let mut lcccs_instances = Vec::new(); - let mut w_lcccs = Vec::new(); - for z_i in z_lcccs.iter() { - let (running_instance, w) = ccs.to_lcccs::<_, _, Pedersen, false>( - &mut rng, - &pedersen_params, - z_i, - )?; - lcccs_instances.push(running_instance); - w_lcccs.push(w); - } - // Create the CCCS instance out of z_cccs - let mut cccs_instances = Vec::new(); - let mut w_cccs = Vec::new(); - for z_i in z_cccs.iter() { - let (new_instance, w) = - ccs.to_cccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, z_i)?; - cccs_instances.push(new_instance); - w_cccs.push(w); - } - - // Prover's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for test - let mut transcript_p: PoseidonSponge = - PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - // Verifier's transcript - let mut transcript_v: PoseidonSponge = transcript_p.clone(); - - // Run the prover side of the multifolding - let (proof, folded_lcccs, folded_witness, _) = - NIMFS::>::prove( - &mut transcript_p, - &ccs, - &lcccs_instances, - &cccs_instances, - &w_lcccs, - &w_cccs, - )?; - - // Run the verifier side of the multifolding - let folded_lcccs_v = NIMFS::>::verify( - &mut transcript_v, - &ccs, - &lcccs_instances, - &cccs_instances, - proof.clone(), - )?; - assert_eq!(folded_lcccs, folded_lcccs_v); - - // Check that the folded LCCCS instance is a valid instance with respect to the folded witness - ccs.check_relation(&folded_witness, &folded_lcccs)?; - - // allocate circuit inputs - let cs = ConstraintSystem::::new_ref(); - let lcccs_instancesVar = - Vec::>::new_witness(cs.clone(), || Ok(lcccs_instances.clone()))?; - let cccs_instancesVar = - Vec::>::new_witness(cs.clone(), || Ok(cccs_instances.clone()))?; - let proofVar = ProofVar::::new_witness(cs.clone(), || Ok(proof.clone()))?; - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let mut transcriptVar = - PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - - let enabled = Boolean::::new_witness(cs.clone(), || Ok(true))?; - let (folded_lcccsVar, _) = NIMFSGadget::::verify( - cs.clone(), - &ccs, - &mut transcriptVar, - &lcccs_instancesVar, - &cccs_instancesVar, - proofVar, - enabled, - )?; - assert!(cs.is_satisfied()?); - assert_eq!(folded_lcccsVar.u.value()?, folded_lcccs.u); - Ok(()) - } - - /// test that checks the native LCCCS.to_sponge_{bytes,field_elements} vs - /// the R1CS constraints version - #[test] - pub fn test_lcccs_to_sponge_preimage() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs = get_test_ccs(); - let z1 = get_test_z::(3); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let (lcccs, _) = ccs.to_lcccs::<_, _, Pedersen, true>( - &mut rng, - &pedersen_params, - &z1, - )?; - let bytes = lcccs.to_sponge_bytes_as_vec(); - let field_elements = lcccs.to_sponge_field_elements_as_vec(); - - let cs = ConstraintSystem::::new_ref(); - - let lcccsVar = LCCCSVar::::new_witness(cs.clone(), || Ok(lcccs))?; - let bytes_var = lcccsVar.to_sponge_bytes()?; - let field_elements_var = lcccsVar.to_sponge_field_elements()?; - - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(bytes_var.value()?, bytes); - assert_eq!(field_elements_var.value()?, field_elements); - Ok(()) - } - - /// test that checks the native LCCCS.hash vs the R1CS constraints version - #[test] - pub fn test_lcccs_hash() -> Result<(), Error> { - let mut rng = test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for test - let sponge = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - let ccs = get_test_ccs(); - let z1 = get_test_z::(3); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let i = Fr::from(3_u32); - let z_0 = vec![Fr::from(3_u32)]; - let z_i = vec![Fr::from(3_u32)]; - let (lcccs, _) = ccs.to_lcccs::<_, _, Pedersen, true>( - &mut rng, - &pedersen_params, - &z1, - )?; - let h = lcccs.clone().hash(&sponge, i, &z_0, &z_i); - - let cs = ConstraintSystem::::new_ref(); - - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let spongeVar = PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - let iVar = FpVar::::new_witness(cs.clone(), || Ok(i))?; - let z_0Var = Vec::>::new_witness(cs.clone(), || Ok(z_0.clone()))?; - let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i.clone()))?; - let lcccsVar = LCCCSVar::::new_witness(cs.clone(), || Ok(lcccs))?; - let (hVar, _) = lcccsVar.clone().hash(&spongeVar, &iVar, &z_0Var, &z_iVar)?; - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(hVar.value()?, h); - Ok(()) - } - - #[test] - pub fn test_augmented_f_circuit() -> Result<(), Error> { - let mut rng = test_rng(); - let poseidon_config = poseidon_canonical_config::(); - // public params hash - let pp_hash = Fr::from(42u32); // only for test - let sponge = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - const MU: usize = 3; - const NU: usize = 3; - - let start = Instant::now(); - let F_circuit = CubicFCircuit::::new(())?; - let mut augmented_f_circuit = - AugmentedFCircuit::, MU, NU>::empty( - &poseidon_config, - F_circuit, - None, - )?; - let ccs = augmented_f_circuit.ccs.clone(); - println!("AugmentedFCircuit & CCS generation: {:?}", start.elapsed()); - println!("CCS m x n: {} x {}", ccs.n_constraints(), ccs.n_variables()); - - // CycleFold circuit - let cs2 = ConstraintSystem::::new_ref(); - let cf_circuit = - CycleFoldCircuit::<_, HyperNovaCycleFoldConfig>::default(); - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - println!( - "CF m x n: {} x {}", - cf_r1cs.n_constraints(), - cf_r1cs.n_variables() - ); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - let (cf_pedersen_params, _) = Pedersen::::setup( - &mut rng, - max(cf_r1cs.n_constraints(), cf_r1cs.n_witnesses()), - )?; - - // first step - let z_0 = vec![Fr::from(3_u32)]; - let mut z_i = z_0.clone(); - - // prepare the dummy instances - let W_dummy = Witness::::dummy(&ccs); - let U_dummy = LCCCS::::dummy(&ccs); - let w_dummy = W_dummy.clone(); - let u_dummy = CCCS::::dummy(&ccs); - let (cf_W_dummy, cf_U_dummy): ( - CycleFoldWitness, - CycleFoldCommittedInstance, - ) = cf_r1cs.dummy_witness_instance(); - - // set the initial dummy instances - let mut W_i = W_dummy.clone(); - let mut U_i = U_dummy.clone(); - let mut w_i = w_dummy.clone(); - let mut u_i = u_dummy.clone(); - let mut cf_W_i = cf_W_dummy.clone(); - let mut cf_U_i = cf_U_dummy.clone(); - u_i.x = vec![ - U_i.hash(&sponge, Fr::zero(), &z_0, &z_i), - cf_U_i.hash_cyclefold(&sponge), - ]; - - let n_steps: usize = 4; - let mut iFr = Fr::zero(); - for i in 0..n_steps { - let start = Instant::now(); - - // for this test, let Us & us be just an array of copies of the U_i & u_i respectively - let Us = vec![U_i.clone(); MU - 1]; - let Ws = vec![W_i.clone(); MU - 1]; - let us = vec![u_i.clone(); NU - 1]; - let ws = vec![w_i.clone(); NU - 1]; - let all_Us = [vec![U_i.clone()], Us.clone()].concat(); - let all_us = [vec![u_i.clone()], us.clone()].concat(); - let all_Ws = [vec![W_i.clone()], Ws].concat(); - let all_ws = [vec![w_i.clone()], ws].concat(); - - let z_i1 = cubic_step_native(z_i.clone()); - - let (U_i1, W_i1); - - let u_i1_x; - let cf_u_i1_x; - - if i == 0 { - W_i1 = Witness::::dummy(&ccs); - U_i1 = LCCCS::dummy(&ccs); - - u_i1_x = U_i1.hash(&sponge, Fr::one(), &z_0, &z_i1); - - // hash the initial (dummy) CycleFold instance, which is used as the 2nd public - // input in the AugmentedFCircuit - cf_u_i1_x = cf_U_i.hash_cyclefold(&sponge); - - augmented_f_circuit = - AugmentedFCircuit::, MU, NU> { - poseidon_config: poseidon_config.clone(), - ccs: ccs.clone(), - pp_hash: Some(pp_hash), - i: Some(Fr::zero()), - i_usize: Some(0), - z_0: Some(z_0.clone()), - z_i: Some(z_i.clone()), - external_inputs: Some(()), - U_i: Some(U_i.clone()), - Us: Some(Us.clone()), - u_i_C: Some(u_i.C), - us: Some(us.clone()), - U_i1_C: Some(U_i1.C), - F: F_circuit, - nimfs_proof: None, - - // cyclefold values - cf_u_i_cmW: None, - cf_U_i: None, - cf_cmT: None, - }; - } else { - let mut transcript_p: PoseidonSponge = sponge.clone(); - let (rho, nimfs_proof); - (nimfs_proof, U_i1, W_i1, rho) = NIMFS::>::prove( - &mut transcript_p, - &ccs, - &all_Us, - &all_us, - &all_Ws, - &all_ws, - )?; - - // sanity check: check the folded instance relation - ccs.check_relation(&W_i1, &U_i1)?; - - u_i1_x = U_i1.hash(&sponge, iFr + Fr::one(), &z_0, &z_i1); - - // CycleFold part: - let cf_config = HyperNovaCycleFoldConfig:: { - r: rho, - points: [ - vec![U_i.clone().C], - Us.iter().map(|Us_i| Us_i.C).collect(), - vec![u_i.clone().C], - us.iter().map(|us_i| us_i.C).collect(), - ] - .concat(), - }; - - // ensure that the CycleFoldCircuit is well defined - assert_eq!( - cf_config.points.len(), - HyperNovaCycleFoldConfig::::N_INPUT_POINTS - ); - - let (cf_w_i, cf_u_i) = cf_config - .build_circuit() - .generate_incoming_instance_witness::<_, Pedersen<_>, false>( - &cf_pedersen_params, - &mut rng, - )?; - let (cf_W_i1, cf_U_i1, cf_cmTs) = - CycleFoldAugmentationGadget::fold_native::<_, Pedersen<_>, false>( - &mut transcript_p, - &cf_r1cs, - &cf_pedersen_params, - cf_W_i, - cf_U_i.clone(), - vec![cf_w_i], - vec![cf_u_i.clone()], - )?; - - // hash the CycleFold folded instance, which is used as the 2nd public input in the - // AugmentedFCircuit - cf_u_i1_x = cf_U_i1.hash_cyclefold(&sponge); - - augmented_f_circuit = - AugmentedFCircuit::, MU, NU> { - poseidon_config: poseidon_config.clone(), - ccs: ccs.clone(), - pp_hash: Some(pp_hash), - i: Some(iFr), - i_usize: Some(i), - z_0: Some(z_0.clone()), - z_i: Some(z_i.clone()), - external_inputs: Some(()), - U_i: Some(U_i.clone()), - Us: Some(Us.clone()), - u_i_C: Some(u_i.C), - us: Some(us.clone()), - U_i1_C: Some(U_i1.C), - F: F_circuit, - nimfs_proof: Some(nimfs_proof), - - // cyclefold values - cf_u_i_cmW: Some(cf_u_i.cmW), - cf_U_i: Some(cf_U_i), - cf_cmT: Some(cf_cmTs[0]), - }; - - // assign the next round instances - cf_W_i = cf_W_i1; - cf_U_i = cf_U_i1; - } - - let cs = ConstraintSystem::::new_ref(); - augmented_f_circuit - .clone() - .generate_constraints(cs.clone())?; - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - assert!(cs.is_satisfied()?); - - let (r1cs_w_i1, r1cs_x_i1) = extract_w_x::(&cs); // includes 1 and public inputs - assert_eq!(r1cs_x_i1[0], u_i1_x); - let r1cs_z = [vec![Fr::one()], r1cs_x_i1.clone(), r1cs_w_i1.clone()].concat(); - // compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we - // assign them directly to w_i, u_i. - (u_i, w_i) = ccs.to_cccs::<_, _, Pedersen, false>( - &mut rng, - &pedersen_params, - &r1cs_z, - )?; - ccs.check_relation(&w_i, &u_i)?; - - // sanity checks - assert_eq!(w_i.w, r1cs_w_i1); - assert_eq!(u_i.x, r1cs_x_i1); - assert_eq!(u_i.x[0], u_i1_x); - assert_eq!(u_i.x[1], cf_u_i1_x); - let expected_u_i1_x = U_i1.hash(&sponge, iFr + Fr::one(), &z_0, &z_i1); - let expected_cf_U_i1_x = cf_U_i.hash_cyclefold(&sponge); - // u_i is already u_i1 at this point, check that has the expected value at x[0] - assert_eq!(u_i.x[0], expected_u_i1_x); - assert_eq!(u_i.x[1], expected_cf_U_i1_x); - - // set values for next iteration - iFr += Fr::one(); - // assign z_{i+1} into z_i - z_i = z_i1.clone(); - U_i = U_i1.clone(); - W_i = W_i1.clone(); - - // check the new LCCCS instance relation - ccs.check_relation(&W_i, &U_i)?; - // check the new CCCS instance relation - ccs.check_relation(&w_i, &u_i)?; - - // check the CycleFold instance relation - cf_r1cs.check_relation(&cf_W_i, &cf_U_i)?; - - println!("augmented_f_circuit step {}: {:?}", i, start.elapsed()); - } - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs deleted file mode 100644 index 8389a1cdc..000000000 --- a/folding-schemes/src/folding/hypernova/decider_eth.rs +++ /dev/null @@ -1,441 +0,0 @@ -/// This file implements the HyperNova's onchain (Ethereum's EVM) decider. -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_snark::SNARK; -use ark_std::rand::{CryptoRng, RngCore}; -use ark_std::{One, Zero}; -use core::marker::PhantomData; - -pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::decider_eth_circuit::DeciderHyperNovaGadget; -use super::HyperNova; -use crate::commitment::{ - kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme, -}; -use crate::folding::circuits::decider::DeciderEnabledNIFS; -use crate::folding::nova::decider_eth::VerifierParam; -use crate::folding::traits::{Dummy, WitnessOps}; -use crate::frontend::FCircuit; -use crate::{Curve, Error}; -use crate::{Decider as DeciderTrait, FoldingScheme}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof -where - C1: Curve, - CS1: CommitmentScheme, - S: SNARK, -{ - snark_proof: S::Proof, - kzg_proof: CS1::Proof, - // rho used at the last fold, U_{i+1}=NIMFS.V(rho, U_i, u_i), it is checked in-circuit - rho: C1::ScalarField, - // the KZG challenge is provided by the prover, but in-circuit it is checked to match - // the in-circuit computed computed one. - kzg_challenge: C1::ScalarField, -} - -/// Onchain Decider, for ethereum use cases -#[derive(Clone, Debug)] -pub struct Decider { - _c1: PhantomData, - _c2: PhantomData, - _fc: PhantomData, - _cs1: PhantomData, - _cs2: PhantomData, - _s: PhantomData, - _fs: PhantomData, -} - -impl DeciderTrait - for Decider -where - C1: Curve, - C2: Curve, - FC: FCircuit, - // CS1 is a KZG commitment, where challenge is C1::Fr elem - CS1: CommitmentScheme< - C1, - ProverChallenge = C1::ScalarField, - Challenge = C1::ScalarField, - Proof = KZGProof, - >, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - S: SNARK, - FS: FoldingScheme, - // constrain FS into HyperNova, since this is a Decider specifically for HyperNova - HyperNova: From, - crate::folding::hypernova::ProverParams: - From<>::ProverParam>, - crate::folding::hypernova::VerifierParams: - From<>::VerifierParam>, -{ - type PreprocessorParam = ((FS::ProverParam, FS::VerifierParam), usize); - type ProverParam = (S::ProvingKey, CS1::ProverParams); - type Proof = Proof; - type VerifierParam = VerifierParam; - type PublicInput = Vec; - type CommittedInstance = Vec; - - fn preprocess( - mut rng: impl RngCore + CryptoRng, - ((pp, vp), state_len): Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - // get the FoldingScheme prover & verifier params from HyperNova - let hypernova_pp: as FoldingScheme< - C1, - C2, - FC, - >>::ProverParam = pp.into(); - let hypernova_vp: as FoldingScheme< - C1, - C2, - FC, - >>::VerifierParam = vp.into(); - let pp_hash = hypernova_vp.pp_hash()?; - - let s = hypernova_vp.ccs.s; - let t = hypernova_vp.ccs.t; - - let circuit = DeciderEthCircuit::::dummy(( - hypernova_vp.ccs, - hypernova_vp.cf_r1cs, - hypernova_pp.cf_cs_pp, - hypernova_pp.poseidon_config, - (s, t, MU, NU), - (), - state_len, - 1, // HyperNova's LCCCS contains 1 commitment - )); - - // get the Groth16 specific setup for the circuit - let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng) - .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; - - let pp = (g16_pk, hypernova_pp.cs_pp); - - let vp = Self::VerifierParam { - pp_hash, - snark_vp: g16_vk, - cs_vp: hypernova_vp.cs_vp, - }; - Ok((pp, vp)) - } - - fn prove( - mut rng: impl RngCore + CryptoRng, - pp: Self::ProverParam, - folding_scheme: FS, - ) -> Result { - let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - - let circuit = DeciderEthCircuit::::try_from(HyperNova::from(folding_scheme))?; - - let rho = circuit.randomness; - - // get the challenges that have been already computed when preparing the circuit inputs in - // the above `try_from` call - let kzg_challenges = circuit.kzg_challenges.clone(); - - // generate KZG proofs - let kzg_proofs = circuit - .W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| { - CS1::prove_with_challenge(&cs_pk, c, v, &C1::ScalarField::zero(), None) - }) - .collect::, _>>()?; - - let snark_proof = - S::prove(&snark_pk, circuit, &mut rng).map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self::Proof { - snark_proof, - rho, - kzg_proof: (kzg_proofs.len() == 1) - .then(|| kzg_proofs[0].clone()) - .ok_or(Error::NotExpectedLength(kzg_proofs.len(), 1))?, - kzg_challenge: (kzg_challenges.len() == 1) - .then(|| kzg_challenges[0]) - .ok_or(Error::NotExpectedLength(kzg_challenges.len(), 1))?, - }) - } - - fn verify( - vp: Self::VerifierParam, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - // we don't use the instances at the verifier level, since we check them in-circuit - running_commitments: &Self::CommittedInstance, - incoming_commitments: &Self::CommittedInstance, - proof: &Self::Proof, - ) -> Result { - if i <= C1::ScalarField::one() { - return Err(Error::NotEnoughSteps); - } - - let Self::VerifierParam { - pp_hash, - snark_vp, - cs_vp, - } = vp; - - // 6.2. Fold the commitments - let C = DeciderHyperNovaGadget::fold_group_elements_native( - running_commitments, - incoming_commitments, - None, - proof.rho, - )?[0]; - - // Note: the NIMFS proof is checked inside the DeciderEthCircuit, which ensures that the - // 'proof.U_i1' is correctly computed - let public_input: Vec = [ - &[pp_hash, i][..], - &z_0, - &z_i, - &C.inputize_nonnative(), - &[proof.kzg_challenge, proof.kzg_proof.eval, proof.rho], - ] - .concat(); - - let snark_v = S::verify(&snark_vp, &public_input, &proof.snark_proof) - .map_err(|e| Error::Other(e.to_string()))?; - if !snark_v { - return Err(Error::SNARKVerificationFail); - } - - // 7.3. Verify the KZG proof - // we're at the Ethereum EVM case, so the CS1 is KZG commitments - CS1::verify_with_challenge(&cs_vp, proof.kzg_challenge, &C, &proof.kzg_proof)?; - - Ok(true) - } -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{Bn254, Fr, G1Projective as Projective}; - use ark_groth16::Groth16; - use ark_grumpkin::Projective as Projective2; - use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Validate}; - - use super::*; - use crate::commitment::{kzg::KZG, pedersen::Pedersen}; - use crate::folding::hypernova::cccs::CCCS; - use crate::folding::hypernova::lcccs::LCCCS; - use crate::folding::hypernova::PreprocessorParam; - use crate::folding::traits::CommittedInstanceOps; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_decider() -> Result<(), Error> { - const MU: usize = 1; - const NU: usize = 1; - // use HyperNova as FoldingScheme - type HN = HyperNova< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - MU, - NU, - false, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - HN, // here we define the FoldingScheme to use - MU, - NU, - >; - - let mut rng = rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); - let hypernova_params = HN::preprocess(&mut rng, &prep_param)?; - - let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone())?; - hypernova.prove_step(&mut rng, (), Some((vec![], vec![])))?; - hypernova.prove_step(&mut rng, (), Some((vec![], vec![])))?; // do a 2nd step - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (hypernova_params, F_circuit.state_len()))?; - - // decider proof generation - let proof = D::prove(rng, decider_pp, hypernova.clone())?; - - // decider proof verification - let verified = D::verify( - decider_vp, - hypernova.i, - hypernova.z_0, - hypernova.z_i, - &hypernova.U_i.get_commitments(), - &hypernova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - Ok(()) - } - - #[test] - fn test_decider_serialization() -> Result<(), Error> { - const MU: usize = 1; - const NU: usize = 1; - // use HyperNova as FoldingScheme - type HN = HyperNova< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - MU, - NU, - false, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - HN, // here we define the FoldingScheme to use - MU, - NU, - >; - - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let prep_param = PreprocessorParam::new(poseidon_config.clone(), F_circuit); - let hypernova_params = HN::preprocess(&mut rng, &prep_param)?; - - let mut rng = rand::rngs::OsRng; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (hypernova_params.clone(), F_circuit.state_len()))?; - - let mut hypernova_pp_serialized = vec![]; - hypernova_params - .0 - .clone() - .serialize_compressed(&mut hypernova_pp_serialized)?; - let mut hypernova_vp_serialized = vec![]; - hypernova_params - .1 - .clone() - .serialize_compressed(&mut hypernova_vp_serialized)?; - - let hypernova_pp_deserialized = HN::pp_deserialize_with_mode( - hypernova_pp_serialized.as_slice(), - Compress::Yes, - Validate::No, - (), // FCircuit's Params - )?; - - let hypernova_vp_deserialized = HN::vp_deserialize_with_mode( - hypernova_vp_serialized.as_slice(), - Compress::Yes, - Validate::No, - (), // FCircuit's Params - )?; - - let hypernova_params = (hypernova_pp_deserialized, hypernova_vp_deserialized); - let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone())?; - - hypernova.prove_step(&mut rng, (), Some((vec![], vec![])))?; - hypernova.prove_step(&mut rng, (), Some((vec![], vec![])))?; - - // decider proof generation - let proof = D::prove(rng, decider_pp, hypernova.clone())?; - - let verified = D::verify( - decider_vp.clone(), - hypernova.i, - hypernova.z_0.clone(), - hypernova.z_i.clone(), - &hypernova.U_i.get_commitments(), - &hypernova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - - // The rest of this test will serialize the data and deserialize it back, and use it to - // verify the proof: - - // serialize the verifier_params, proof and public inputs - let mut decider_vp_serialized = vec![]; - decider_vp.serialize_compressed(&mut decider_vp_serialized)?; - let mut proof_serialized = vec![]; - proof.serialize_compressed(&mut proof_serialized)?; - // serialize the public inputs in a single packet - let mut public_inputs_serialized = vec![]; - hypernova - .i - .serialize_compressed(&mut public_inputs_serialized)?; - hypernova - .z_0 - .serialize_compressed(&mut public_inputs_serialized)?; - hypernova - .z_i - .serialize_compressed(&mut public_inputs_serialized)?; - hypernova - .U_i - .serialize_compressed(&mut public_inputs_serialized)?; - hypernova - .u_i - .serialize_compressed(&mut public_inputs_serialized)?; - - // deserialize back the verifier_params, proof and public inputs - let decider_vp_deserialized = - VerifierParam::< - Projective, - as CommitmentScheme>::VerifierParams, - as SNARK>::VerifyingKey, - >::deserialize_compressed(&mut decider_vp_serialized.as_slice())?; - - let proof_deserialized = - Proof::, Groth16>::deserialize_compressed( - &mut proof_serialized.as_slice(), - )?; - - let mut reader = public_inputs_serialized.as_slice(); - let i_deserialized = Fr::deserialize_compressed(&mut reader)?; - let z_0_deserialized = Vec::::deserialize_compressed(&mut reader)?; - let z_i_deserialized = Vec::::deserialize_compressed(&mut reader)?; - let _U_i = LCCCS::::deserialize_compressed(&mut reader)?; - let _u_i = CCCS::::deserialize_compressed(&mut reader)?; - - let verified = D::verify( - decider_vp_deserialized, - i_deserialized, - z_0_deserialized.clone(), - z_i_deserialized.clone(), - &hypernova.U_i.get_commitments(), - &hypernova.u_i.get_commitments(), - &proof_deserialized, - )?; - assert!(verified); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs deleted file mode 100644 index 218a062d3..000000000 --- a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs +++ /dev/null @@ -1,313 +0,0 @@ -/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, -/// other more efficient approaches can be used. -use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - eq::EqGadget, - fields::fp::FpVar, - GR1CSVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, log2, marker::PhantomData}; - -use super::{ - circuits::{CCCSVar, LCCCSVar, NIMFSGadget, ProofVar as NIMFSProofVar}, - nimfs::{NIMFSProof, NIMFS}, - HyperNova, Witness, CCCS, LCCCS, -}; -use crate::arith::{ - ccs::{circuits::CCSMatricesVar, CCS}, - ArithRelationGadget, -}; -use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; -use crate::folding::circuits::{ - decider::{ - on_chain::GenericOnchainDeciderCircuit, DeciderEnabledNIFS, EvalGadget, KZGChallengesGadget, - }, - CF1, -}; -use crate::folding::traits::{WitnessOps, WitnessVarOps}; -use crate::frontend::FCircuit; -use crate::transcript::Transcript; -use crate::utils::gadgets::{eval_mle, MatrixGadget}; -use crate::{Curve, Error}; - -impl ArithRelationGadget>, LCCCSVar> for CCSMatricesVar> { - type Evaluation = Vec>>; - - fn eval_relation( - &self, - w: &WitnessVar>, - u: &LCCCSVar, - ) -> Result { - let z = [&[u.u.clone()][..], &u.x, &w.w].concat(); - - self.M - .iter() - .map(|M_j| { - let s = log2(M_j.n_rows) as usize; - let Mz = M_j.mul_vector(&z)?; - Ok(eval_mle(s, Mz, u.r_x.clone())) - }) - .collect() - } - - fn enforce_evaluation( - _w: &WitnessVar>, - u: &LCCCSVar, - v: Self::Evaluation, - ) -> Result<(), SynthesisError> { - v.enforce_equal(&u.v) - } -} - -/// In-circuit representation of the Witness associated to the CommittedInstance. -#[derive(Debug, Clone)] -pub struct WitnessVar { - pub w: Vec>, - pub r_w: FpVar, -} - -impl AllocVar, F> for WitnessVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let w: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().w.clone()), mode)?; - let r_w = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; - - Ok(Self { w, r_w }) - }) - } -} - -impl WitnessVarOps for WitnessVar { - fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { - vec![(&self.w, self.r_w.clone())] - } -} - -pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< - C1, - C2, - LCCCS, - CCCS, - Witness>, - CCS>, - CCSMatricesVar>, - DeciderHyperNovaGadget, ->; - -impl< - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - const MU: usize, - const NU: usize, - const H: bool, - > TryFrom> for DeciderEthCircuit -{ - type Error = Error; - - fn try_from(hn: HyperNova) -> Result { - // compute the U_{i+1}, W_{i+1}, by folding the last running & incoming instances - let mut transcript = PoseidonSponge::new_with_pp_hash(&hn.poseidon_config, hn.pp_hash); - let (nimfs_proof, U_i1, W_i1, rho) = NIMFS::>::prove( - &mut transcript, - &hn.ccs, - &[hn.U_i.clone()], - &[hn.u_i.clone()], - &[hn.W_i.clone()], - &[hn.w_i.clone()], - )?; - - // compute the KZG challenges used as inputs in the circuit - let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); - - // get KZG evals - let kzg_evaluations = W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) - .collect::, _>>()?; - - Ok(Self { - _avar: PhantomData, - arith: hn.ccs, - cf_arith: hn.cf_r1cs, - cf_pedersen_params: hn.cf_cs_pp, - poseidon_config: hn.poseidon_config, - pp_hash: hn.pp_hash, - i: hn.i, - z_0: hn.z_0, - z_i: hn.z_i, - U_i: hn.U_i, - W_i: hn.W_i, - u_i: hn.u_i, - w_i: hn.w_i, - U_i1, - W_i1, - proof: nimfs_proof, - randomness: rho, - cf_U_i: hn.cf_U_i, - cf_W_i: hn.cf_W_i, - kzg_challenges, - kzg_evaluations, - }) - } -} - -pub struct DeciderHyperNovaGadget; - -impl DeciderEnabledNIFS, CCCS, Witness, CCS>> - for DeciderHyperNovaGadget -{ - type ProofDummyCfg = (usize, usize, usize, usize); - type Proof = NIMFSProof; - type Randomness = CF1; - type RandomnessDummyCfg = (); - - fn fold_field_elements_gadget( - arith: &CCS>, - transcript: &mut PoseidonSpongeVar>, - U: LCCCSVar, - _U_vec: Vec>>, - u: CCCSVar, - proof: Self::Proof, - randomness: Self::Randomness, - ) -> Result, SynthesisError> { - let cs = U.u.cs(); - let nimfs_proof = NIMFSProofVar::::new_witness(cs.clone(), || Ok(proof))?; - let rho = FpVar::>::new_input(cs.clone(), || Ok(randomness))?; - let (computed_U_i1, rho_bits) = NIMFSGadget::::verify( - cs.clone(), - arith, - transcript, - &[U], - &[u], - nimfs_proof, - Boolean::TRUE, // enabled - )?; - Boolean::le_bits_to_fp(&rho_bits)?.enforce_equal(&rho)?; - Ok(computed_U_i1) - } - - fn fold_group_elements_native( - U_commitments: &[C], - u_commitments: &[C], - _: Option, - r: Self::Randomness, - ) -> Result, Error> { - let U_C = U_commitments[0]; - let u_C = u_commitments[0]; - let C = U_C + u_C.mul(r); - Ok(vec![C]) - } -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - use ark_std::{test_rng, UniformRand}; - - use super::*; - use crate::arith::{r1cs::R1CS, Arith}; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::PreprocessorParam; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::FoldingScheme; - - #[test] - fn test_lcccs_checker_gadget() -> Result<(), Error> { - let mut rng = test_rng(); - let n_rows = 2_u32.pow(5) as usize; - let n_cols = 2_u32.pow(5) as usize; - let r1cs = R1CS::::rand(&mut rng, n_rows, n_cols); - let ccs = CCS::from(r1cs); - let z: Vec = (0..n_cols).map(|_| Fr::rand(&mut rng)).collect(); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let (lcccs, w) = ccs.to_lcccs::<_, Projective, Pedersen, false>( - &mut rng, - &pedersen_params, - &z, - )?; - - let cs = ConstraintSystem::::new_ref(); - - // CCS's (sparse) matrices are constants in the circuit - let ccs_mat = CCSMatricesVar::::new_constant(cs.clone(), ccs.clone())?; - let w_var = WitnessVar::new_witness(cs.clone(), || Ok(w))?; - let lcccs_var = LCCCSVar::new_input(cs.clone(), || Ok(lcccs))?; - - ccs_mat.enforce_relation(&w_var, &lcccs_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_decider_circuit() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - const MU: usize = 1; - const NU: usize = 1; - - type HN = HyperNova< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - MU, - NU, - false, - >; - let prep_param = PreprocessorParam::< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - false, - >::new(poseidon_config, F_circuit); - let hn_params = HN::preprocess(&mut rng, &prep_param)?; - - // generate a Nova instance and do a step of it - let mut hypernova = HN::init(&hn_params, F_circuit, z_0.clone())?; - hypernova.prove_step(&mut rng, (), None)?; - - let ivc_proof = hypernova.ivc_proof(); - HN::verify(hn_params.1, ivc_proof)?; - - // load the DeciderEthCircuit from the generated Nova instance - let decider_circuit = DeciderEthCircuit::::try_from(hypernova)?; - - let cs = ConstraintSystem::::new_ref(); - - // generate the constraints and check that are satisfied by the inputs - decider_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - dbg!(cs.num_constraints()); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/lcccs.rs b/folding-schemes/src/folding/hypernova/lcccs.rs deleted file mode 100644 index 1336cd495..000000000 --- a/folding-schemes/src/folding/hypernova/lcccs.rs +++ /dev/null @@ -1,281 +0,0 @@ -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::PrimeField; -use ark_poly::Polynomial; -use ark_serialize::CanonicalDeserialize; -use ark_serialize::CanonicalSerialize; -use ark_std::rand::Rng; -use ark_std::Zero; - -use super::circuits::LCCCSVar; -use super::Witness; -use crate::arith::ccs::CCS; -use crate::arith::{Arith, ArithRelation}; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::CF1; -use crate::folding::traits::Inputize; -use crate::folding::traits::{CommittedInstanceOps, Dummy}; -use crate::utils::mle::dense_vec_to_dense_mle; -use crate::utils::vec::mat_vec_mul; -use crate::{Curve, Error}; - -/// Linearized Committed CCS instance -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct LCCCS { - // Commitment to witness - pub C: C, - // Relaxation factor of z for folded LCCCS - pub u: C::ScalarField, - // Public input/output - pub x: Vec, - // Random evaluation point for the v_i - pub r_x: Vec, - // Vector of v_i - pub v: Vec, -} - -impl CCS { - pub fn to_lcccs, const H: bool>( - &self, - rng: &mut R, - cs_params: &CS::ProverParams, - z: &[F], - ) -> Result<(LCCCS, Witness), Error> - where - // enforce that CCS's F is the C::ScalarField - C: Curve, - { - let (w, x) = self.split_z(z); - // if the commitment scheme is set to be hiding, set the random blinding parameter - let r_w = if CS::is_hiding() { - F::rand(rng) - } else { - F::zero() - }; - let C = CS::commit(cs_params, &w, &r_w)?; - - let r_x: Vec = (0..self.s).map(|_| F::rand(rng)).collect(); - - // compute v_j - let v = self - .M - .iter() - .map(|M_j| { - let Mz = dense_vec_to_dense_mle(self.s, &mat_vec_mul(M_j, z)?); - Ok(Mz.evaluate(&r_x)) - }) - .collect::>()?; - - Ok(( - LCCCS:: { - C, - u: z[0], - x, - r_x, - v, - }, - Witness:: { w, r_w }, - )) - } -} - -impl Dummy<&CCS>> for LCCCS { - fn dummy(ccs: &CCS>) -> Self { - Self { - C: C::zero(), - u: CF1::::zero(), - x: vec![CF1::::zero(); ccs.n_public_inputs()], - r_x: vec![CF1::::zero(); ccs.s], - v: vec![CF1::::zero(); ccs.t], - } - } -} - -impl ArithRelation>, LCCCS> for CCS> { - type Evaluation = Vec>; - - /// Perform the check of the LCCCS instance described at section 4.2, - /// notice that this method does not check the commitment correctness - fn eval_relation(&self, w: &Witness>, u: &LCCCS) -> Result { - let z = [&[u.u][..], &u.x, &w.w].concat(); - - self.M - .iter() - .map(|M_j| { - let Mz_mle = dense_vec_to_dense_mle(self.s, &mat_vec_mul(M_j, &z)?); - Ok(Mz_mle.evaluate(&u.r_x)) - }) - .collect() - } - - fn check_evaluation( - _w: &Witness>, - u: &LCCCS, - e: Self::Evaluation, - ) -> Result<(), Error> { - (u.v == e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -impl Absorb for LCCCS { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.C.to_native_sponge_field_elements(dest); - self.u.to_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - self.r_x.to_sponge_field_elements(dest); - self.v.to_sponge_field_elements(dest); - } -} - -impl CommittedInstanceOps for LCCCS { - type Var = LCCCSVar; - - fn get_commitments(&self) -> Vec { - vec![self.C] - } - - fn is_incoming(&self) -> bool { - false - } -} - -impl Inputize> for LCCCS { - /// Returns the internal representation in the same order as how the value - /// is allocated in `LCCCS::new_input`. - fn inputize(&self) -> Vec> { - [ - &self.C.inputize_nonnative(), - &[self.u][..], - &self.x, - &self.r_x, - &self.v, - ] - .concat() - } -} - -#[cfg(test)] -pub mod tests { - use ark_pallas::{Fr, Projective}; - use ark_std::{sync::Arc, test_rng, One, UniformRand}; - - use super::*; - use crate::arith::{ - ccs::tests::{get_test_ccs, get_test_z}, - r1cs::R1CS, - ArithRelation, - }; - use crate::commitment::pedersen::Pedersen; - use crate::utils::hypercube::BooleanHypercube; - use crate::utils::virtual_polynomial::{build_eq_x_r_vec, VirtualPolynomial}; - - // method for testing - pub fn compute_Ls( - ccs: &CCS, - lcccs: &LCCCS, - z: &[C::ScalarField], - ) -> Result>, Error> { - let eq_rx = build_eq_x_r_vec(&lcccs.r_x)?; - let eq_rx_mle = Arc::new(dense_vec_to_dense_mle(ccs.s, &eq_rx)); - - let Ls = ccs - .M - .iter() - .map(|M_j| { - let mut L = VirtualPolynomial::::new(ccs.s); - let Mz = vec![ - Arc::new(dense_vec_to_dense_mle(ccs.s, &mat_vec_mul(M_j, z)?)), - eq_rx_mle.clone(), - ]; - L.add_mle_list(Mz, C::ScalarField::one())?; - Ok(L) - }) - .collect::, Error>>()?; - Ok(Ls) - } - - #[test] - /// Test linearized CCCS v_j against the L_j(x) - fn test_lcccs_v_j() -> Result<(), Error> { - let mut rng = test_rng(); - - let n_rows = 2_u32.pow(5) as usize; - let n_cols = 2_u32.pow(5) as usize; - let r1cs = R1CS::::rand(&mut rng, n_rows, n_cols); - let ccs = CCS::from(r1cs); - let z: Vec = (0..n_cols).map(|_| Fr::rand(&mut rng)).collect(); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let (lcccs, _) = ccs.to_lcccs::<_, Projective, Pedersen, false>( - &mut rng, - &pedersen_params, - &z, - )?; - // with our test vector coming from R1CS, v should have length 3 - assert_eq!(lcccs.v.len(), 3); - - let vec_L_j_x = compute_Ls(&ccs, &lcccs, &z)?; - assert_eq!(vec_L_j_x.len(), lcccs.v.len()); - - for (v_i, L_j_x) in lcccs.v.into_iter().zip(vec_L_j_x) { - let sum_L_j_x = BooleanHypercube::new(ccs.s) - .map(|y| L_j_x.evaluate(&y)) - .collect::, _>>()? - .into_iter() - .fold(Fr::zero(), |acc, result| acc + result); - assert_eq!(v_i, sum_L_j_x); - } - Ok(()) - } - - /// Given a bad z, check that the v_j should not match with the L_j(x) - #[test] - fn test_bad_v_j() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs = get_test_ccs(); - let z = get_test_z(3); - let (w, x) = ccs.split_z(&z); - ccs.check_relation(&w, &x)?; - - // Mutate z so that the relation does not hold - let mut bad_z = z.clone(); - bad_z[3] = Fr::zero(); - let (bad_w, bad_x) = ccs.split_z(&bad_z); - assert!(ccs.check_relation(&bad_w, &bad_x).is_err()); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - // Compute v_j with the right z - let (lcccs, _) = ccs.to_lcccs::<_, Projective, Pedersen, false>( - &mut rng, - &pedersen_params, - &z, - )?; - // with our test vector coming from R1CS, v should have length 3 - assert_eq!(lcccs.v.len(), 3); - - // Bad compute L_j(x) with the bad z - let vec_L_j_x = compute_Ls(&ccs, &lcccs, &bad_z)?; - assert_eq!(vec_L_j_x.len(), lcccs.v.len()); - - // Make sure that the LCCCS is not satisfied given these L_j(x) - // i.e. summing L_j(x) over the hypercube should not give v_j for all j - let mut satisfied = true; - for (v_i, L_j_x) in lcccs.v.into_iter().zip(vec_L_j_x) { - let sum_L_j_x = BooleanHypercube::new(ccs.s) - .map(|y| L_j_x.evaluate(&y)) - .collect::, _>>()? - .into_iter() - .fold(Fr::zero(), |acc, result| acc + result); - if v_i != sum_L_j_x { - satisfied = false; - } - } - assert!(!satisfied); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs deleted file mode 100644 index 2cae32341..000000000 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ /dev/null @@ -1,1094 +0,0 @@ -/// Implements the scheme described in [HyperNova](https://eprint.iacr.org/2023/573.pdf) -use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{alloc::AllocVar, boolean::Boolean, GR1CSVar}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, SerializationError}; -use ark_std::{cmp::max, fmt::Debug, rand::RngCore, One, Zero}; - -pub mod cccs; -pub mod circuits; -pub mod decider_eth; -pub mod decider_eth_circuit; -pub mod lcccs; -pub mod nimfs; -pub mod utils; - -use cccs::CCCS; -use circuits::AugmentedFCircuit; -use decider_eth_circuit::WitnessVar; -use lcccs::LCCCS; -use nimfs::NIMFS; - -use crate::arith::{ - ccs::CCS, - r1cs::{extract_w_x, R1CS}, - Arith, ArithRelation, -}; -use crate::commitment::CommitmentScheme; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::{ - circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCircuit, CycleFoldCommittedInstance, - CycleFoldConfig, CycleFoldWitness, - }, - CF1, CF2, - }, - nova::{get_r1cs_from_cs, PreprocessorParam}, - traits::{CommittedInstanceOps, Dummy, WitnessOps}, -}; -use crate::frontend::FCircuit; -use crate::transcript::{poseidon::poseidon_canonical_config, Transcript}; -use crate::utils::pp_hash; -use crate::{Curve, Error, FoldingScheme, MultiFolding}; - -/// Configuration for HyperNova's CycleFold circuit -pub struct HyperNovaCycleFoldConfig { - r: CF1, - points: Vec, -} - -impl Default for HyperNovaCycleFoldConfig { - fn default() -> Self { - Self { - r: CF1::::zero(), - points: vec![C::zero(); MU + NU], - } - } -} - -impl CycleFoldConfig - for HyperNovaCycleFoldConfig -{ - const RANDOMNESS_BIT_LENGTH: usize = NOVA_N_BITS_RO; - const N_INPUT_POINTS: usize = MU + NU; - const N_UNIQUE_RANDOMNESSES: usize = 1; - - fn alloc_points(&self, cs: ConstraintSystemRef>) -> Result, SynthesisError> { - let points = Vec::new_witness(cs.clone(), || Ok(self.points.clone()))?; - for point in &points { - Self::mark_point_as_public(point)?; - } - Ok(points) - } - - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError> { - let one = &CF1::::one().into_bigint().to_bits_le()[..NOVA_N_BITS_RO]; - let r = &self.r.into_bigint().to_bits_le()[..NOVA_N_BITS_RO]; - let one_var = Vec::new_constant(cs.clone(), one)?; - let r_var = Vec::new_witness(cs.clone(), || Ok(r))?; - Self::mark_randomness_as_public(&r_var)?; - Ok([vec![one_var], vec![r_var; MU + NU - 1]].concat()) - } -} - -/// Witness for the LCCCS & CCCS, containing the w vector, and the r_w used as randomness in the Pedersen commitment. -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Witness { - pub w: Vec, - pub r_w: F, -} - -impl Witness { - pub fn new(w: Vec) -> Self { - // note: at the current version, we don't use the blinding factors and we set them to 0 - // always. - Self { w, r_w: F::zero() } - } -} - -impl Dummy<&CCS> for Witness { - fn dummy(ccs: &CCS) -> Self { - Self::new(vec![F::zero(); ccs.n_witnesses()]) - } -} - -impl WitnessOps for Witness { - type Var = WitnessVar; - - fn get_openings(&self) -> Vec<(&[F], F)> { - vec![(&self.w, self.r_w)] - } -} - -/// Proving parameters for HyperNova-based IVC -#[derive(Debug, Clone)] -pub struct ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// Proving parameters of the underlying commitment scheme over C1 - pub cs_pp: CS1::ProverParams, - /// Proving parameters of the underlying commitment scheme over C2 - pub cf_cs_pp: CS2::ProverParams, - /// CCS of the Augmented Function circuit - /// If ccs is set, it will be used, if not, it will be computed at runtime - pub ccs: Option>, -} - -impl< - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, - > CanonicalSerialize for ProverParams -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: Compress, - ) -> Result<(), SerializationError> { - self.cs_pp.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_pp.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: Compress) -> usize { - self.cs_pp.serialized_size(compress) + self.cf_cs_pp.serialized_size(compress) - } -} - -/// Verification parameters for HyperNova-based IVC -#[derive(Debug, Clone)] -pub struct VerifierParams< - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, -> { - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// CCS of the Augmented step circuit - pub ccs: CCS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - /// Verification parameters of the underlying commitment scheme over C1 - pub cs_vp: CS1::VerifierParams, - /// Verification parameters of the underlying commitment scheme over C2 - pub cf_cs_vp: CS2::VerifierParams, -} - -impl CanonicalSerialize for VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.cs_vp.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_vp.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.cs_vp.serialized_size(compress) + self.cf_cs_vp.serialized_size(compress) - } -} - -impl VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// returns the hash of the public parameters of HyperNova - pub fn pp_hash(&self) -> Result { - pp_hash::( - &self.ccs, - &self.cf_r1cs, - &self.cs_vp, - &self.cf_cs_vp, - &self.poseidon_config, - ) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] -pub struct IVCProof -where - C1: Curve, - C2: Curve, -{ - pub i: C1::ScalarField, - pub z_0: Vec, - pub z_i: Vec, - pub W_i: Witness, - pub U_i: LCCCS, - pub w_i: Witness, - pub u_i: CCCS, - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -/// Implements HyperNova+CycleFold's IVC, described in -/// [HyperNova](https://eprint.iacr.org/2023/573.pdf) and -/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf), following the FoldingScheme trait -/// -/// For multi-instance folding, one needs to specify the const generics below: -/// * `MU` - the number of LCCCS instances to be folded -/// * `NU` - the number of CCCS instances to be folded -#[derive(Clone, Debug)] -pub struct HyperNova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// CCS of the Augmented Function circuit - pub ccs: CCS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - pub poseidon_config: PoseidonConfig, - /// CommitmentScheme::ProverParams over C1 - pub cs_pp: CS1::ProverParams, - /// CycleFold CommitmentScheme::ProverParams, over C2 - pub cf_cs_pp: CS2::ProverParams, - /// F circuit, the circuit that is being folded - pub F: FC, - /// public params hash - pub pp_hash: C1::ScalarField, - pub i: C1::ScalarField, - /// initial state - pub z_0: Vec, - /// current i-th state - pub z_i: Vec, - /// HyperNova instances - pub W_i: Witness, - pub U_i: LCCCS, - pub w_i: Witness, - pub u_i: CCCS, - - /// CycleFold running instance - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -impl MultiFolding - for HyperNova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - C1: Curve, -{ - type RunningInstance = (LCCCS, Witness); - type IncomingInstance = (CCCS, Witness); - type MultiInstance = (Vec, Vec); - - /// Creates a new LCCS instance for the given state, which satisfies the HyperNova.CCS. This - /// method can be used to generate the 'other' LCCS instances to be folded in the multi-folding - /// step. - fn new_running_instance( - &self, - mut rng: impl RngCore, - state: Vec, - external_inputs: FC::ExternalInputs, - ) -> Result { - let r1cs_z = self.new_instance_generic(state, external_inputs)?; - // compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we - // assign them directly to w_i, u_i. - let (U_i, W_i) = self - .ccs - .to_lcccs::<_, _, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; - - #[cfg(test)] - self.ccs.check_relation(&W_i, &U_i)?; - - Ok((U_i, W_i)) - } - - /// Creates a new CCCS instance for the given state, which satisfies the HyperNova.CCS. This - /// method can be used to generate the 'other' CCCS instances to be folded in the multi-folding - /// step. - fn new_incoming_instance( - &self, - mut rng: impl RngCore, - state: Vec, - external_inputs: FC::ExternalInputs, - ) -> Result { - let r1cs_z = self.new_instance_generic(state, external_inputs)?; - // compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we - // assign them directly to w_i, u_i. - let (u_i, w_i) = self - .ccs - .to_cccs::<_, _, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; - - #[cfg(test)] - self.ccs.check_relation(&w_i, &u_i)?; - - Ok((u_i, w_i)) - } -} - -impl - HyperNova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - C1: Curve, -{ - /// internal helper for new_running_instance & new_incoming_instance methods, returns the R1CS - /// z=[u,x,w] vector to be used to create the LCCCS & CCCS fresh instances. - fn new_instance_generic( - &self, - state: Vec, - external_inputs: FC::ExternalInputs, - ) -> Result, Error> { - // prepare the initial dummy instances - let U_i = LCCCS::::dummy(&self.ccs); - let mut u_i = CCCS::::dummy(&self.ccs); - let (_, cf_U_i): (CycleFoldWitness, CycleFoldCommittedInstance) = - self.cf_r1cs.dummy_witness_instance(); - - let sponge = PoseidonSponge::::new_with_pp_hash( - &self.poseidon_config, - self.pp_hash, - ); - - u_i.x = vec![ - U_i.hash( - &sponge, - C1::ScalarField::zero(), // i - &self.z_0, - &state, - ), - cf_U_i.hash_cyclefold(&sponge), - ]; - let us = vec![u_i.clone(); NU - 1]; - - // compute u_{i+1}.x - let U_i1 = LCCCS::dummy(&self.ccs); - - let augmented_f_circuit = AugmentedFCircuit:: { - poseidon_config: self.poseidon_config.clone(), - ccs: self.ccs.clone(), - pp_hash: Some(self.pp_hash), - i: Some(C1::ScalarField::zero()), - i_usize: Some(0), - z_0: Some(self.z_0.clone()), - z_i: Some(state.clone()), - external_inputs: Some(external_inputs), - U_i: Some(U_i.clone()), - Us: None, - u_i_C: Some(u_i.C), - us: Some(us), - U_i1_C: Some(U_i1.C), - F: self.F.clone(), - nimfs_proof: None, - - // cyclefold values - cf_u_i_cmW: None, - cf_U_i: None, - cf_cmT: None, - }; - - let cs = ConstraintSystem::::new_ref(); - augmented_f_circuit.generate_constraints(cs.clone())?; - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - - #[cfg(test)] - assert!(cs.is_satisfied()?); - - let (r1cs_w_i1, r1cs_x_i1) = extract_w_x::(&cs); // includes 1 and public inputs - - let r1cs_z = [ - vec![C1::ScalarField::one()], - r1cs_x_i1.clone(), - r1cs_w_i1.clone(), - ] - .concat(); - Ok(r1cs_z) - } -} - -impl - FoldingScheme for HyperNova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - C1: Curve, -{ - /// Reuse Nova's PreprocessorParam. - type PreprocessorParam = PreprocessorParam; - type ProverParam = ProverParams; - type VerifierParam = VerifierParams; - type RunningInstance = (LCCCS, Witness); - type IncomingInstance = (CCCS, Witness); - type MultiCommittedInstanceWithWitness = - (Vec, Vec); - type CFInstance = (CycleFoldCommittedInstance, CycleFoldWitness); - type IVCProof = IVCProof; - - fn pp_deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, - ) -> Result { - let poseidon_config = poseidon_canonical_config::(); - - // generate the r1cs & cf_r1cs needed for the VerifierParams. In this way we avoid needing - // to serialize them, saving significant space in the VerifierParams serialized size. - - // main circuit R1CS: - let f_circuit = FC::new(fc_params)?; - let augmented_F_circuit = AugmentedFCircuit::::empty( - &poseidon_config, - f_circuit.clone(), - None, - )?; - let ccs = augmented_F_circuit.ccs; - - let cs_pp = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_pp = CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - - Ok(ProverParams { - poseidon_config, - cs_pp, - cf_cs_pp, - ccs: Some(ccs), - }) - } - - fn vp_deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, - ) -> Result { - let poseidon_config = poseidon_canonical_config::(); - - // generate the r1cs & cf_r1cs needed for the VerifierParams. In this way we avoid needing - // to serialize them, saving significant space in the VerifierParams serialized size. - - // main circuit R1CS: - let f_circuit = FC::new(fc_params)?; - let augmented_F_circuit = AugmentedFCircuit::::empty( - &poseidon_config, - f_circuit.clone(), - None, - )?; - let ccs = augmented_F_circuit.ccs; - - // CycleFold circuit R1CS - let cf_circuit = CycleFoldCircuit::<_, HyperNovaCycleFoldConfig>::default(); - let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; - - let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - - Ok(VerifierParams { - poseidon_config, - ccs, - cf_r1cs, - cs_vp, - cf_cs_vp, - }) - } - - fn preprocess( - mut rng: impl RngCore, - prep_param: &Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - if MU < 1 || NU < 1 { - return Err(Error::CantBeZero("mu,nu".to_string())); - } - - let augmented_f_circuit = AugmentedFCircuit::::empty( - &prep_param.poseidon_config, - prep_param.F.clone(), - None, - )?; - let ccs = augmented_f_circuit.ccs.clone(); - - let cf_circuit = CycleFoldCircuit::<_, HyperNovaCycleFoldConfig>::default(); - let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; - - // if cs params exist, use them, if not, generate new ones - let (cs_pp, cs_vp) = match (&prep_param.cs_pp, &prep_param.cs_vp) { - (Some(cs_pp), Some(cs_vp)) => (cs_pp.clone(), cs_vp.clone()), - // `CS1` is for committing to HyperNova's witness vector `w`, so we - // set `len` to the number of witnesses in `r1cs`. - _ => CS1::setup(&mut rng, ccs.n_witnesses())?, - }; - let (cf_cs_pp, cf_cs_vp) = match (&prep_param.cf_cs_pp, &prep_param.cf_cs_vp) { - (Some(cf_cs_pp), Some(cf_cs_vp)) => (cf_cs_pp.clone(), cf_cs_vp.clone()), - _ => CS2::setup( - &mut rng, - // `CS2` is for committing to CycleFold's witness vector `w` and - // error term `e`, where the length of `e` is the number of - // constraints, so we set `len` to the maximum of `e` and `w`'s - // lengths. - max(cf_r1cs.n_constraints(), cf_r1cs.n_witnesses()), - )?, - }; - - let pp = ProverParams:: { - poseidon_config: prep_param.poseidon_config.clone(), - cs_pp, - cf_cs_pp, - ccs: Some(ccs.clone()), - }; - let vp = VerifierParams:: { - poseidon_config: prep_param.poseidon_config.clone(), - ccs, - cf_r1cs, - cs_vp: cs_vp.clone(), - cf_cs_vp: cf_cs_vp.clone(), - }; - Ok((pp, vp)) - } - - /// Initializes the HyperNova+CycleFold's IVC for the given parameters and initial state `z_0`. - fn init( - params: &(Self::ProverParam, Self::VerifierParam), - F: FC, - z_0: Vec, - ) -> Result { - let (pp, vp) = params; - if MU < 1 || NU < 1 { - return Err(Error::CantBeZero("mu,nu".to_string())); - } - - // compute the public params hash - let pp_hash = vp.pp_hash()?; - - // `sponge` is for digest computation. - let sponge = - PoseidonSponge::::new_with_pp_hash(&pp.poseidon_config, pp_hash); - - // prepare the HyperNova's AugmentedFCircuit and CycleFold's circuits and obtain its CCS - // and R1CS respectively - let augmented_f_circuit = AugmentedFCircuit::::empty( - &pp.poseidon_config, - F.clone(), - pp.ccs.clone(), - )?; - let ccs = augmented_f_circuit.ccs.clone(); - - let cf_circuit = CycleFoldCircuit::<_, HyperNovaCycleFoldConfig>::default(); - let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; - - // setup the dummy instances - let W_dummy = Witness::::dummy(&ccs); - let U_dummy = LCCCS::::dummy(&ccs); - let w_dummy = W_dummy.clone(); - let mut u_dummy = CCCS::::dummy(&ccs); - let (cf_W_dummy, cf_U_dummy): (CycleFoldWitness, CycleFoldCommittedInstance) = - cf_r1cs.dummy_witness_instance(); - u_dummy.x = vec![ - U_dummy.hash(&sponge, C1::ScalarField::zero(), &z_0, &z_0), - cf_U_dummy.hash_cyclefold(&sponge), - ]; - - // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the - // R1CS that we're working with. - Ok(Self { - ccs, - cf_r1cs, - poseidon_config: pp.poseidon_config.clone(), - cs_pp: pp.cs_pp.clone(), - cf_cs_pp: pp.cf_cs_pp.clone(), - F, - pp_hash, - i: C1::ScalarField::zero(), - z_0: z_0.clone(), - z_i: z_0, - W_i: W_dummy, - U_i: U_dummy, - w_i: w_dummy, - u_i: u_dummy, - // cyclefold running instance - cf_W_i: cf_W_dummy, - cf_U_i: cf_U_dummy, - }) - } - - /// Implements IVC.P of HyperNova+CycleFold - fn prove_step( - &mut self, - mut rng: impl RngCore, - external_inputs: FC::ExternalInputs, - other_instances: Option, - ) -> Result<(), Error> { - // ensure that commitments are blinding if user has specified so. - - if H { - let blinding_commitments = if self.i == C1::ScalarField::zero() { - vec![self.w_i.r_w] - } else { - vec![self.w_i.r_w, self.W_i.r_w] - }; - if blinding_commitments.contains(&C1::ScalarField::zero()) { - return Err(Error::IncorrectBlinding( - H, - format!("{blinding_commitments:?}"), - )); - } - } - - let (Us, Ws, us, ws) = if MU > 1 || NU > 1 { - let other_instances = other_instances.ok_or(Error::MissingOtherInstances(MU, NU))?; - - #[allow(clippy::type_complexity)] - let (lcccs, cccs): ( - Vec<(LCCCS, Witness)>, - Vec<(CCCS, Witness)>, - ) = other_instances; - - // recall, mu & nu is the number of all the LCCCS & CCCS respectively, including the - // running and incoming instances that are not part of the 'other_instances', hence the +1 - // in the couple of following checks. - if lcccs.len() + 1 != MU { - return Err(Error::NotSameLength( - "other_instances.lcccs.len()".to_string(), - lcccs.len(), - "hypernova.mu".to_string(), - MU, - )); - } - if cccs.len() + 1 != NU { - return Err(Error::NotSameLength( - "other_instances.cccs.len()".to_string(), - cccs.len(), - "hypernova.nu".to_string(), - NU, - )); - } - - let (Us, Ws): (Vec>, Vec>) = - lcccs.into_iter().unzip(); - let (us, ws): (Vec>, Vec>) = cccs.into_iter().unzip(); - (Us, Ws, us, ws) - } else { - (vec![], vec![], vec![], vec![]) - }; - - let augmented_f_circuit: AugmentedFCircuit; - - if self.z_i.len() != self.F.state_len() { - return Err(Error::NotSameLength( - "z_i.len()".to_string(), - self.z_i.len(), - "F.state_len()".to_string(), - self.F.state_len(), - )); - } - - if self.i > C1::ScalarField::from_le_bytes_mod_order(&usize::MAX.to_le_bytes()) { - return Err(Error::MaxStep); - } - - let i_usize; - - #[cfg(target_pointer_width = "64")] - { - let mut i_bytes: [u8; 8] = [0; 8]; - i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..8]); - i_usize = usize::from_le_bytes(i_bytes); - } - - #[cfg(target_pointer_width = "32")] - { - let mut i_bytes: [u8; 4] = [0; 4]; - i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..4]); - i_usize = usize::from_le_bytes(i_bytes); - } - - let (U_i1, mut W_i1); - - if self.i == C1::ScalarField::zero() { - W_i1 = Witness::::dummy(&self.ccs); - W_i1.r_w = self.W_i.r_w; - U_i1 = LCCCS::dummy(&self.ccs); - - augmented_f_circuit = AugmentedFCircuit:: { - poseidon_config: self.poseidon_config.clone(), - ccs: self.ccs.clone(), - pp_hash: Some(self.pp_hash), - i: Some(C1::ScalarField::zero()), - i_usize: Some(0), - z_0: Some(self.z_0.clone()), - z_i: Some(self.z_i.clone()), - external_inputs: Some(external_inputs.clone()), - U_i: Some(self.U_i.clone()), - Us: Some(Us), - u_i_C: Some(self.u_i.C), - us: Some(us), - U_i1_C: Some(U_i1.C), - F: self.F.clone(), - nimfs_proof: None, - - // cyclefold values - cf_u_i_cmW: None, - cf_U_i: None, - cf_cmT: None, - }; - } else { - let mut transcript_p: PoseidonSponge = - PoseidonSponge::::new_with_pp_hash( - &self.poseidon_config, - self.pp_hash, - ); - - let (all_Us, all_us, all_Ws, all_ws) = ( - [&[self.U_i.clone()][..], &Us].concat(), - [&[self.u_i.clone()][..], &us].concat(), - [vec![self.W_i.clone()], Ws].concat(), - [vec![self.w_i.clone()], ws].concat(), - ); - - let (rho, nimfs_proof); - (nimfs_proof, U_i1, W_i1, rho) = NIMFS::>::prove( - &mut transcript_p, - &self.ccs, - &all_Us, - &all_us, - &all_Ws, - &all_ws, - )?; - - // sanity check: check the folded instance relation - #[cfg(test)] - self.ccs.check_relation(&W_i1, &U_i1)?; - - // CycleFold part: - let (cf_w_i, cf_u_i) = HyperNovaCycleFoldConfig:: { - r: rho, - points: [ - all_Us.iter().map(|Us_i| Us_i.C).collect::>(), - all_us.iter().map(|us_i| us_i.C).collect::>(), - ] - .concat(), - } - .build_circuit() - .generate_incoming_instance_witness::<_, CS2, H>(&self.cf_cs_pp, &mut rng)?; - - let (cf_W_i1, cf_U_i1, cf_cmTs) = CycleFoldAugmentationGadget::fold_native::<_, CS2, H>( - &mut transcript_p, - &self.cf_r1cs, - &self.cf_cs_pp, - self.cf_W_i.clone(), - self.cf_U_i.clone(), - vec![cf_w_i], - vec![cf_u_i.clone()], - )?; - - augmented_f_circuit = AugmentedFCircuit:: { - poseidon_config: self.poseidon_config.clone(), - ccs: self.ccs.clone(), - pp_hash: Some(self.pp_hash), - i: Some(self.i), - i_usize: Some(i_usize), - z_0: Some(self.z_0.clone()), - z_i: Some(self.z_i.clone()), - external_inputs: Some(external_inputs), - U_i: Some(self.U_i.clone()), - Us: Some(Us), - u_i_C: Some(self.u_i.C), - us: Some(us), - U_i1_C: Some(U_i1.C), - F: self.F.clone(), - nimfs_proof: Some(nimfs_proof), - - // cyclefold values - cf_u_i_cmW: Some(cf_u_i.cmW), - cf_U_i: Some(self.cf_U_i.clone()), - cf_cmT: Some(cf_cmTs[0]), - }; - - // assign the next round instances - self.cf_W_i = cf_W_i1; - self.cf_U_i = cf_U_i1; - } - - let cs = ConstraintSystem::::new_ref(); - let z_i1 = augmented_f_circuit - .compute_next_state(cs.clone())? - .value()?; - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - - #[cfg(test)] - assert!(cs.is_satisfied()?); - - let (r1cs_w_i1, r1cs_x_i1) = extract_w_x::(&cs); // includes 1 and public inputs - - let r1cs_z = [ - vec![C1::ScalarField::one()], - r1cs_x_i1.clone(), - r1cs_w_i1.clone(), - ] - .concat(); - // compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we - // assign them directly to w_i, u_i. - let (u_i, w_i) = self - .ccs - .to_cccs::<_, C1, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; - self.u_i = u_i.clone(); - self.w_i = w_i.clone(); - - // set values for next iteration - self.i += C1::ScalarField::one(); - // assign z_{i+1} into z_i - self.z_i = z_i1; - self.U_i = U_i1.clone(); - self.W_i = W_i1.clone(); - - #[cfg(test)] - { - // check the new LCCCS instance relation - self.ccs.check_relation(&self.W_i, &self.U_i)?; - // check the new CCCS instance relation - self.ccs.check_relation(&self.w_i, &self.u_i)?; - } - - Ok(()) - } - - fn state(&self) -> Vec { - self.z_i.clone() - } - - fn ivc_proof(&self) -> Self::IVCProof { - Self::IVCProof { - i: self.i, - z_0: self.z_0.clone(), - z_i: self.z_i.clone(), - W_i: self.W_i.clone(), - U_i: self.U_i.clone(), - w_i: self.w_i.clone(), - u_i: self.u_i.clone(), - cf_W_i: self.cf_W_i.clone(), - cf_U_i: self.cf_U_i.clone(), - } - } - - fn from_ivc_proof( - ivc_proof: Self::IVCProof, - fcircuit_params: FC::Params, - params: (Self::ProverParam, Self::VerifierParam), - ) -> Result { - let IVCProof { - i, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - let (pp, vp) = params; - - let f_circuit = FC::new(fcircuit_params)?; - let augmented_f_circuit = AugmentedFCircuit::::empty( - &pp.poseidon_config, - f_circuit.clone(), - None, - )?; - let cf_circuit = CycleFoldCircuit::<_, HyperNovaCycleFoldConfig>::default(); - - let ccs = augmented_f_circuit.ccs.clone(); - let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; - - Ok(Self { - ccs, - cf_r1cs, - poseidon_config: pp.poseidon_config, - cs_pp: pp.cs_pp, - cf_cs_pp: pp.cf_cs_pp, - F: f_circuit, - pp_hash: vp.pp_hash()?, - i, - z_0, - z_i, - w_i, - u_i, - W_i, - U_i, - cf_W_i, - cf_U_i, - }) - } - - /// Implements IVC.V of Hyp.clone()erNova+CycleFold. Notice that this method does not include the - /// commitments verification, which is done in the Decider. - fn verify(vp: Self::VerifierParam, ivc_proof: Self::IVCProof) -> Result<(), Error> { - let Self::IVCProof { - i: num_steps, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - - if num_steps == C1::ScalarField::zero() { - if z_0 != z_i { - return Err(Error::IVCVerificationFail); - } - return Ok(()); - } - // `sponge` is for digest computation. - let sponge = - PoseidonSponge::::new_with_pp_hash(&vp.poseidon_config, vp.pp_hash()?); - - if u_i.x.len() != 2 || U_i.x.len() != 2 { - return Err(Error::IVCVerificationFail); - } - - // check that u_i's output points to the running instance - // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, num_steps, &z_0, &z_i); - if expected_u_i_x != u_i.x[0] { - return Err(Error::IVCVerificationFail); - } - // u_i.X[1] == H(cf_U_i) - let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&sponge); - if expected_cf_u_i_x != u_i.x[1] { - return Err(Error::IVCVerificationFail); - } - - // check LCCCS satisfiability - vp.ccs.check_relation(&W_i, &U_i)?; - // check CCCS satisfiability - vp.ccs.check_relation(&w_i, &u_i)?; - - // check CycleFold's RelaxedR1CS satisfiability - vp.cf_r1cs.check_relation(&cf_W_i, &cf_U_i)?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::commitment::kzg::KZG; - use ark_bn254::{Bn254, Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - use ark_std::UniformRand; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - pub fn test_ivc() -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - - // run the test using Pedersen commitments on both sides of the curve cycle - let _ = test_ivc_opt::, Pedersen, false>( - poseidon_config.clone(), - F_circuit, - )?; - - let _ = test_ivc_opt::, Pedersen, true>( - poseidon_config.clone(), - F_circuit, - )?; - - // run the test using KZG for the commitments on the main curve, and Pedersen for the - // commitments on the secondary curve - let _ = - test_ivc_opt::, Pedersen, false>(poseidon_config, F_circuit)?; - Ok(()) - } - - #[allow(clippy::type_complexity)] - // test_ivc allowing to choose the CommitmentSchemes - pub fn test_ivc_opt< - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, - >( - poseidon_config: PoseidonConfig, - F_circuit: CubicFCircuit, - ) -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - const MU: usize = 2; - const NU: usize = 3; - - type HN = - HyperNova, CS1, CS2, MU, NU, H>; - - let prep_param = - PreprocessorParam::, CS1, CS2, H>::new( - poseidon_config.clone(), - F_circuit, - ); - let hypernova_params = HN::preprocess(&mut rng, &prep_param)?; - - let z_0 = vec![Fr::from(3_u32)]; - let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone())?; - - let (w_i_blinding, W_i_blinding) = if H { - (Fr::rand(&mut rng), Fr::rand(&mut rng)) - } else { - (Fr::zero(), Fr::zero()) - }; - hypernova.w_i.r_w = w_i_blinding; - hypernova.W_i.r_w = W_i_blinding; - - let num_steps: usize = 3; - for _ in 0..num_steps { - // prepare some new instances to fold in the multifolding step - let mut lcccs = vec![]; - for j in 0..MU - 1 { - let instance_state = vec![Fr::from(j as u32 + 85_u32)]; - let (U, W) = hypernova.new_running_instance(&mut rng, instance_state, ())?; - lcccs.push((U, W)); - } - let mut cccs = vec![]; - for j in 0..NU - 1 { - let instance_state = vec![Fr::from(j as u32 + 15_u32)]; - let (u, w) = hypernova.new_incoming_instance(&mut rng, instance_state, ())?; - cccs.push((u, w)); - } - - hypernova.prove_step(&mut rng, (), Some((lcccs, cccs)))?; - } - assert_eq!(Fr::from(num_steps as u32), hypernova.i); - - let ivc_proof = hypernova.ivc_proof(); - HN::verify( - hypernova_params.1.clone(), // verifier_params - ivc_proof, - )?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/nimfs.rs b/folding-schemes/src/folding/hypernova/nimfs.rs deleted file mode 100644 index 4098dc163..000000000 --- a/folding-schemes/src/folding/hypernova/nimfs.rs +++ /dev/null @@ -1,732 +0,0 @@ -use ark_ff::{BigInteger, Field, PrimeField}; -use ark_poly::univariate::DensePolynomial; -use ark_poly::{DenseUVPolynomial, Polynomial}; -use ark_std::{fmt::Debug, marker::PhantomData, One, Zero}; - -use super::{ - cccs::CCCS, - lcccs::LCCCS, - utils::{compute_c, compute_g, compute_sigmas_thetas}, - Witness, -}; -use crate::arith::{ccs::CCS, Arith}; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::circuits::CF1; -use crate::folding::traits::Dummy; -use crate::transcript::Transcript; -use crate::utils::sum_check::structs::{IOPProof as SumCheckProof, IOPProverMessage}; -use crate::utils::sum_check::{IOPSumCheck, SumCheck}; -use crate::utils::virtual_polynomial::VPAuxInfo; -use crate::{Curve, Error}; - -/// NIMFSProof defines a multifolding proof -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct NIMFSProof { - pub sc_proof: SumCheckProof, - pub sigmas_thetas: SigmasThetas, -} - -impl Dummy<(usize, usize, usize, usize)> for NIMFSProof { - fn dummy((s, t, mu, nu): (usize, usize, usize, usize)) -> Self { - // use 'C::ScalarField::one()' instead of 'zero()' to enforce the NIMFSProof to have the - // same in-circuit representation to match the number of constraints of an actual proof. - NIMFSProof:: { - sc_proof: SumCheckProof:: { - point: vec![C::ScalarField::one(); s], - proofs: vec![ - IOPProverMessage { - coeffs: vec![C::ScalarField::one(); t + 1] - }; - s - ], - }, - sigmas_thetas: SigmasThetas( - vec![vec![C::ScalarField::one(); t]; mu], - vec![vec![C::ScalarField::one(); t]; nu], - ), - } - } -} - -impl Dummy<(&CCS>, usize, usize)> for NIMFSProof { - fn dummy((ccs, mu, nu): (&CCS>, usize, usize)) -> Self { - NIMFSProof::dummy((ccs.s, ccs.t, mu, nu)) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SigmasThetas(pub Vec>, pub Vec>); - -#[derive(Debug)] -/// Implements the Non-Interactive Multi Folding Scheme described in section 5 of -/// [HyperNova](https://eprint.iacr.org/2023/573.pdf) -pub struct NIMFS> { - pub _c: PhantomData, - pub _t: PhantomData, -} - -impl> NIMFS { - pub fn fold( - lcccs: &[LCCCS], - cccs: &[CCCS], - sigmas_thetas: &SigmasThetas, - r_x_prime: Vec, - rho: C::ScalarField, - ) -> LCCCS { - let (sigmas, thetas) = (sigmas_thetas.0.clone(), sigmas_thetas.1.clone()); - let mut C_folded = C::zero(); - let mut u_folded = C::ScalarField::zero(); - let mut x_folded: Vec = vec![C::ScalarField::zero(); lcccs[0].x.len()]; - let mut v_folded: Vec = vec![C::ScalarField::zero(); sigmas[0].len()]; - - let mut rho_i = C::ScalarField::one(); - for i in 0..(lcccs.len() + cccs.len()) { - let c: C; - let u: C::ScalarField; - let x: Vec; - let v: Vec; - if i < lcccs.len() { - c = lcccs[i].C; - u = lcccs[i].u; - x = lcccs[i].x.clone(); - v = sigmas[i].clone(); - } else { - c = cccs[i - lcccs.len()].C; - u = C::ScalarField::one(); - x = cccs[i - lcccs.len()].x.clone(); - v = thetas[i - lcccs.len()].clone(); - } - - C_folded += c.mul(rho_i); - u_folded += rho_i * u; - x_folded = x_folded - .iter() - .zip( - x.iter() - .map(|x_i| *x_i * rho_i) - .collect::>(), - ) - .map(|(a_i, b_i)| *a_i + b_i) - .collect(); - - v_folded = v_folded - .iter() - .zip( - v.iter() - .map(|x_i| *x_i * rho_i) - .collect::>(), - ) - .map(|(a_i, b_i)| *a_i + b_i) - .collect(); - - // compute the next power of rho - rho_i *= rho; - } - - LCCCS:: { - C: C_folded, - u: u_folded, - x: x_folded, - r_x: r_x_prime, - v: v_folded, - } - } - - pub fn fold_witness( - w_lcccs: &[Witness], - w_cccs: &[Witness], - rho: C::ScalarField, - ) -> Witness { - let mut w_folded: Vec = vec![C::ScalarField::zero(); w_lcccs[0].w.len()]; - let mut r_w_folded = C::ScalarField::zero(); - - let mut rho_i = C::ScalarField::one(); - for i in 0..(w_lcccs.len() + w_cccs.len()) { - // let rho_i = rho.pow([i as u64]); - let w: Vec; - let r_w: C::ScalarField; - - if i < w_lcccs.len() { - w = w_lcccs[i].w.clone(); - r_w = w_lcccs[i].r_w; - } else { - w = w_cccs[i - w_lcccs.len()].w.clone(); - r_w = w_cccs[i - w_lcccs.len()].r_w; - } - - w_folded = w_folded - .iter() - .zip( - w.iter() - .map(|x_i| *x_i * rho_i) - .collect::>(), - ) - .map(|(a_i, b_i)| *a_i + b_i) - .collect(); - - r_w_folded += rho_i * r_w; - - // compute the next power of rho - rho_i *= rho; - } - Witness { - w: w_folded, - r_w: r_w_folded, - } - } - - /// Performs the multifolding prover. Given μ LCCCS instances and ν CCS instances, fold them - /// into a single LCCCS instance. Since this is the prover, also fold their witness. - /// Returns the final folded LCCCS, the folded witness, and the multifolding proof, which - /// contains the sumcheck proof and the helper sumcheck claim sigmas and thetas. - #[allow(clippy::type_complexity)] - pub fn prove( - transcript: &mut impl Transcript, - ccs: &CCS, - running_instances: &[LCCCS], - new_instances: &[CCCS], - w_lcccs: &[Witness], - w_cccs: &[Witness], - ) -> Result< - ( - NIMFSProof, - LCCCS, - Witness, - C::ScalarField, // rho - ), - Error, - > { - // absorb instances to transcript - transcript.absorb(&running_instances); - transcript.absorb(&new_instances); - - if running_instances.is_empty() { - return Err(Error::Empty); - } - if new_instances.is_empty() { - return Err(Error::Empty); - } - - // construct the LCCCS z vector from the relaxation factor, public IO and witness - let mut z_lcccs = Vec::new(); - for (i, running_instance) in running_instances.iter().enumerate() { - let z_1: Vec = [ - vec![running_instance.u], - running_instance.x.clone(), - w_lcccs[i].w.to_vec(), - ] - .concat(); - z_lcccs.push(z_1); - } - // construct the CCCS z vector from the public IO and witness - let mut z_cccs = Vec::new(); - for (i, new_instance) in new_instances.iter().enumerate() { - let z_2: Vec = [ - vec![C::ScalarField::one()], - new_instance.x.clone(), - w_cccs[i].w.to_vec(), - ] - .concat(); - z_cccs.push(z_2); - } - - // Step 1: Get some challenges - let gamma_scalar = C::ScalarField::from_le_bytes_mod_order(b"gamma"); - let beta_scalar = C::ScalarField::from_le_bytes_mod_order(b"beta"); - transcript.absorb(&gamma_scalar); - let gamma: C::ScalarField = transcript.get_challenge(); - transcript.absorb(&beta_scalar); - let beta: Vec = transcript.get_challenges(ccs.s); - - // Compute g(x) - let g = compute_g(ccs, running_instances, &z_lcccs, &z_cccs, gamma, &beta)?; - - // Step 3: Run the sumcheck prover - let sumcheck_proof = IOPSumCheck::::prove(&g, transcript) - .map_err(|err| Error::SumCheckProveError(err.to_string()))?; - - // Step 2: dig into the sumcheck and extract r_x_prime - let r_x_prime = sumcheck_proof.point.clone(); - - // Step 4: compute sigmas and thetas - let sigmas_thetas = compute_sigmas_thetas(ccs, &z_lcccs, &z_cccs, &r_x_prime)?; - - // Step 6: Get the folding challenge - let rho_scalar = C::ScalarField::from_le_bytes_mod_order(b"rho"); - transcript.absorb(&rho_scalar); - let rho_bits: Vec = transcript.get_challenge_nbits(NOVA_N_BITS_RO); - let rho: C::ScalarField = C::ScalarField::from( - ::BigInt::from_bits_le(&rho_bits), - ); - - // Step 7: Create the folded instance - let folded_lcccs = Self::fold( - running_instances, - new_instances, - &sigmas_thetas, - r_x_prime, - rho, - ); - - // Step 8: Fold the witnesses - let folded_witness = Self::fold_witness(w_lcccs, w_cccs, rho); - - Ok(( - NIMFSProof:: { - sc_proof: sumcheck_proof, - sigmas_thetas, - }, - folded_lcccs, - folded_witness, - rho, - )) - } - - /// Performs the multifolding verifier. Given μ LCCCS instances and ν CCS instances, fold them - /// into a single LCCCS instance. - /// Returns the folded LCCCS instance. - pub fn verify( - transcript: &mut impl Transcript, - ccs: &CCS, - running_instances: &[LCCCS], - new_instances: &[CCCS], - proof: NIMFSProof, - ) -> Result, Error> { - // absorb instances to transcript - transcript.absorb(&running_instances); - transcript.absorb(&new_instances); - - if running_instances.is_empty() { - return Err(Error::Empty); - } - if new_instances.is_empty() { - return Err(Error::Empty); - } - - // Step 1: Get some challenges - let gamma_scalar = C::ScalarField::from_le_bytes_mod_order(b"gamma"); - transcript.absorb(&gamma_scalar); - let gamma: C::ScalarField = transcript.get_challenge(); - - let beta_scalar = C::ScalarField::from_le_bytes_mod_order(b"beta"); - transcript.absorb(&beta_scalar); - let beta: Vec = transcript.get_challenges(ccs.s); - - let vp_aux_info = VPAuxInfo:: { - max_degree: ccs.degree() + 1, - num_variables: ccs.s, - phantom: PhantomData::, - }; - - // Step 3: Start verifying the sumcheck - // First, compute the expected sumcheck sum: \sum gamma^j v_j - let mut sum_v_j_gamma = C::ScalarField::zero(); - for (i, running_instance) in running_instances.iter().enumerate() { - for j in 0..running_instance.v.len() { - let gamma_j = gamma.pow([(i * ccs.t + j) as u64]); - sum_v_j_gamma += running_instance.v[j] * gamma_j; - } - } - - // Verify the interactive part of the sumcheck - let sumcheck_subclaim = IOPSumCheck::::verify( - sum_v_j_gamma, - &proof.sc_proof, - &vp_aux_info, - transcript, - ) - .map_err(|err| Error::SumCheckVerifyError(err.to_string()))?; - - // Step 2: Dig into the sumcheck claim and extract the randomness used - let r_x_prime = sumcheck_subclaim.point.clone(); - - // Step 5: Finish verifying sumcheck (verify the claim c) - let c = compute_c( - ccs, - &proof.sigmas_thetas, - gamma, - &beta, - &running_instances - .iter() - .map(|lcccs| lcccs.r_x.clone()) - .collect(), - &r_x_prime, - )?; - - // check that the g(r_x') from the sumcheck proof is equal to the computed c from sigmas&thetas - if c != sumcheck_subclaim.expected_evaluation { - return Err(Error::NotEqual); - } - - // Sanity check: we can also compute g(r_x') from the proof last evaluation value, and - // should be equal to the previously obtained values. - let g_on_rxprime_from_sumcheck_last_eval = DensePolynomial::from_coefficients_slice( - &proof.sc_proof.proofs.last().ok_or(Error::Empty)?.coeffs, - ) - .evaluate(r_x_prime.last().ok_or(Error::Empty)?); - if g_on_rxprime_from_sumcheck_last_eval != c { - return Err(Error::NotEqual); - } - if g_on_rxprime_from_sumcheck_last_eval != sumcheck_subclaim.expected_evaluation { - return Err(Error::NotEqual); - } - - // Step 6: Get the folding challenge - let rho_scalar = C::ScalarField::from_le_bytes_mod_order(b"rho"); - transcript.absorb(&rho_scalar); - let rho_bits: Vec = transcript.get_challenge_nbits(NOVA_N_BITS_RO); - let rho: C::ScalarField = C::ScalarField::from( - ::BigInt::from_bits_le(&rho_bits), - ); - - // Step 7: Compute the folded instance - Ok(Self::fold( - running_instances, - new_instances, - &proof.sigmas_thetas, - r_x_prime, - rho, - )) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - use ark_std::{test_rng, UniformRand}; - - use crate::arith::{ - ccs::tests::{get_test_ccs, get_test_z}, - ArithRelation, - }; - use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_fold() -> Result<(), Error> { - let ccs = get_test_ccs(); - let z1 = get_test_z::(3); - let z2 = get_test_z::(4); - let (w1, x1) = ccs.split_z(&z1); - let (w2, x2) = ccs.split_z(&z2); - ccs.check_relation(&w1, &x1)?; - ccs.check_relation(&w2, &x2)?; - - let mut rng = test_rng(); - let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - - let sigmas_thetas = compute_sigmas_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime)?; - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let (lcccs, w1) = ccs.to_lcccs::<_, Projective, Pedersen, false>( - &mut rng, - &pedersen_params, - &z1, - )?; - let (cccs, w2) = ccs.to_cccs::<_, Projective, Pedersen, false>( - &mut rng, - &pedersen_params, - &z2, - )?; - - ccs.check_relation(&w1, &lcccs)?; - ccs.check_relation(&w2, &cccs)?; - - let mut rng = test_rng(); - let rho = Fr::rand(&mut rng); - - let folded = NIMFS::>::fold( - &[lcccs], - &[cccs], - &sigmas_thetas, - r_x_prime, - rho, - ); - - let w_folded = NIMFS::>::fold_witness(&[w1], &[w2], rho); - - // check lcccs relation - ccs.check_relation(&w_folded, &folded)?; - Ok(()) - } - - /// Perform multifolding of an LCCCS instance with a CCCS instance (as described in the paper) - #[test] - pub fn test_basic_multifolding() -> Result<(), Error> { - let mut rng = test_rng(); - - // Create a basic CCS circuit - let ccs = get_test_ccs::(); - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - // Generate a satisfying witness - let z_1 = get_test_z(3); - // Generate another satisfying witness - let z_2 = get_test_z(4); - - // Create the LCCCS instance out of z_1 - let (running_instance, w1) = - ccs.to_lcccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z_1)?; - // Create the CCCS instance out of z_2 - let (new_instance, w2) = - ccs.to_cccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z_2)?; - - // Prover's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from_le_bytes_mod_order(b"init init"); - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - // Verifier's transcript - let mut transcript_v = transcript_p.clone(); - - // Run the prover side of the multifolding - let (proof, folded_lcccs, folded_witness, _) = - NIMFS::>::prove( - &mut transcript_p, - &ccs, - &[running_instance.clone()], - &[new_instance.clone()], - &[w1], - &[w2], - )?; - - // Run the verifier side of the multifolding - let folded_lcccs_v = NIMFS::>::verify( - &mut transcript_v, - &ccs, - &[running_instance.clone()], - &[new_instance.clone()], - proof, - )?; - assert_eq!(folded_lcccs, folded_lcccs_v); - - // Check that the folded LCCCS instance is a valid instance with respect to the folded witness - ccs.check_relation(&folded_witness, &folded_lcccs)?; - Ok(()) - } - - /// Perform multiple steps of multifolding of an LCCCS instance with a CCCS instance - #[test] - pub fn test_multifolding_two_instances_multiple_steps() -> Result<(), Error> { - let mut rng = test_rng(); - - let ccs = get_test_ccs::(); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - // LCCCS witness - let z_1 = get_test_z(2); - let (mut running_instance, mut w1) = - ccs.to_lcccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z_1)?; - - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from_le_bytes_mod_order(b"init init"); - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - // Verifier's transcript - let mut transcript_v = transcript_p.clone(); - - let n: usize = 10; - for i in 3..n { - // CCS witness - let z_2 = get_test_z(i); - - let (new_instance, w2) = - ccs.to_cccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z_2)?; - - // run the prover side of the multifolding - let (proof, folded_lcccs, folded_witness, _) = - NIMFS::>::prove( - &mut transcript_p, - &ccs, - &[running_instance.clone()], - &[new_instance.clone()], - &[w1], - &[w2], - )?; - - // run the verifier side of the multifolding - let folded_lcccs_v = NIMFS::>::verify( - &mut transcript_v, - &ccs, - &[running_instance.clone()], - &[new_instance.clone()], - proof, - )?; - assert_eq!(folded_lcccs, folded_lcccs_v); - - // check that the folded instance with the folded witness holds the LCCCS relation - ccs.check_relation(&folded_witness, &folded_lcccs)?; - - running_instance = folded_lcccs; - w1 = folded_witness; - } - Ok(()) - } - - /// Test that generates mu>1 and nu>1 instances, and folds them in a single multifolding step. - #[test] - pub fn test_multifolding_mu_nu_instances() -> Result<(), Error> { - let mut rng = test_rng(); - - // Create a basic CCS circuit - let ccs = get_test_ccs::(); - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let mu = 10; - let nu = 15; - - // Generate a mu LCCCS & nu CCCS satisfying witness - let mut z_lcccs = Vec::new(); - for i in 0..mu { - let z = get_test_z(i + 3); - z_lcccs.push(z); - } - let mut z_cccs = Vec::new(); - for i in 0..nu { - let z = get_test_z(nu + i + 3); - z_cccs.push(z); - } - - // Create the LCCCS instances out of z_lcccs - let mut lcccs_instances = Vec::new(); - let mut w_lcccs = Vec::new(); - for z_i in z_lcccs.iter() { - let (running_instance, w) = - ccs.to_lcccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, z_i)?; - lcccs_instances.push(running_instance); - w_lcccs.push(w); - } - // Create the CCCS instance out of z_cccs - let mut cccs_instances = Vec::new(); - let mut w_cccs = Vec::new(); - for z_i in z_cccs.iter() { - let (new_instance, w) = - ccs.to_cccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, z_i)?; - cccs_instances.push(new_instance); - w_cccs.push(w); - } - - // Prover's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from_le_bytes_mod_order(b"init init"); - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - // Verifier's transcript - let mut transcript_v = transcript_p.clone(); - - // Run the prover side of the multifolding - let (proof, folded_lcccs, folded_witness, _) = - NIMFS::>::prove( - &mut transcript_p, - &ccs, - &lcccs_instances, - &cccs_instances, - &w_lcccs, - &w_cccs, - )?; - - // Run the verifier side of the multifolding - let folded_lcccs_v = NIMFS::>::verify( - &mut transcript_v, - &ccs, - &lcccs_instances, - &cccs_instances, - proof, - )?; - assert_eq!(folded_lcccs, folded_lcccs_v); - - // Check that the folded LCCCS instance is a valid instance with respect to the folded witness - ccs.check_relation(&folded_witness, &folded_lcccs)?; - Ok(()) - } - - /// Test that generates mu>1 and nu>1 instances, and folds them in a single multifolding step - /// and repeats the process by doing multiple steps. - #[test] - pub fn test_multifolding_mu_nu_instances_multiple_steps() -> Result<(), Error> { - let mut rng = test_rng(); - - // Create a basic CCS circuit - let ccs = get_test_ccs::(); - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from_le_bytes_mod_order(b"init init"); - // Prover's transcript - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - // Verifier's transcript - let mut transcript_v = transcript_p.clone(); - - let n_steps = 3; - - // number of LCCCS & CCCS instances in each multifolding step - let mu = 10; - let nu = 15; - - // Generate a mu LCCCS & nu CCCS satisfying witness, for each step - for step in 0..n_steps { - let mut z_lcccs = Vec::new(); - for i in 0..mu { - let z = get_test_z(step + i + 3); - z_lcccs.push(z); - } - let mut z_cccs = Vec::new(); - for i in 0..nu { - let z = get_test_z(nu + i + 3); - z_cccs.push(z); - } - - // Create the LCCCS instances out of z_lcccs - let mut lcccs_instances = Vec::new(); - let mut w_lcccs = Vec::new(); - for z_i in z_lcccs.iter() { - let (running_instance, w) = ccs.to_lcccs::<_, _, Pedersen, false>( - &mut rng, - &pedersen_params, - z_i, - )?; - lcccs_instances.push(running_instance); - w_lcccs.push(w); - } - // Create the CCCS instance out of z_cccs - let mut cccs_instances = Vec::new(); - let mut w_cccs = Vec::new(); - for z_i in z_cccs.iter() { - let (new_instance, w) = ccs.to_cccs::<_, _, Pedersen, false>( - &mut rng, - &pedersen_params, - z_i, - )?; - cccs_instances.push(new_instance); - w_cccs.push(w); - } - - // Run the prover side of the multifolding - let (proof, folded_lcccs, folded_witness, _) = - NIMFS::>::prove( - &mut transcript_p, - &ccs, - &lcccs_instances, - &cccs_instances, - &w_lcccs, - &w_cccs, - )?; - - // Run the verifier side of the multifolding - let folded_lcccs_v = NIMFS::>::verify( - &mut transcript_v, - &ccs, - &lcccs_instances, - &cccs_instances, - proof, - )?; - - assert_eq!(folded_lcccs, folded_lcccs_v); - - // Check that the folded LCCCS instance is a valid instance with respect to the folded witness - ccs.check_relation(&folded_witness, &folded_lcccs)?; - } - Ok(()) - } -} diff --git a/folding-schemes/src/folding/hypernova/utils.rs b/folding-schemes/src/folding/hypernova/utils.rs deleted file mode 100644 index 2ed0f07a7..000000000 --- a/folding-schemes/src/folding/hypernova/utils.rs +++ /dev/null @@ -1,333 +0,0 @@ -use ark_ff::PrimeField; -use ark_poly::MultilinearExtension; -use ark_std::One; -use std::sync::Arc; - -use super::lcccs::LCCCS; -use super::nimfs::SigmasThetas; -use crate::arith::ccs::CCS; -use crate::utils::mle::dense_vec_to_dense_mle; -use crate::utils::vec::mat_vec_mul; -use crate::utils::virtual_polynomial::{build_eq_x_r_vec, eq_eval, VirtualPolynomial}; -use crate::{Curve, Error}; - -/// Compute the arrays of sigma_i and theta_i from step 4 corresponding to the LCCCS and CCCS -/// instances -pub fn compute_sigmas_thetas( - ccs: &CCS, - z_lcccs: &[Vec], - z_cccs: &[Vec], - r_x_prime: &[F], -) -> Result, Error> { - let mut sigmas: Vec> = Vec::new(); - for z_lcccs_i in z_lcccs { - let sigma_i = ccs - .M - .iter() - .map(|M_j| { - let Mz = dense_vec_to_dense_mle(ccs.s, &mat_vec_mul(M_j, z_lcccs_i)?); - Ok(Mz.fix_variables(r_x_prime)[0]) - }) - .collect::, Error>>()?; - sigmas.push(sigma_i); - } - - let mut thetas: Vec> = Vec::new(); - for z_cccs_i in z_cccs { - let theta_i = ccs - .M - .iter() - .map(|M_j| { - let Mz = dense_vec_to_dense_mle(ccs.s, &mat_vec_mul(M_j, z_cccs_i)?); - Ok(Mz.fix_variables(r_x_prime)[0]) - }) - .collect::, Error>>()?; - thetas.push(theta_i); - } - Ok(SigmasThetas(sigmas, thetas)) -} - -/// Computes c from the step 5 in section 5 of HyperNova, adapted to multiple LCCCS & CCCS -/// instances: -/// $$ -/// c = \sum_{i \in [\mu]} \left(\sum_{j \in [t]} \gamma^{i \cdot t + j} \cdot e_i \cdot \sigma_{i,j} \right) + -/// \sum_{k \in [\nu]} \gamma^{\mu \cdot t+k} \cdot e_k \cdot \left( \sum_{i=1}^q c_i \cdot \prod_{j \in S_i} -/// \theta_{k,j} \right) -/// $$ -pub fn compute_c( - ccs: &CCS, - st: &SigmasThetas, - gamma: F, - beta: &[F], - vec_r_x: &Vec>, - r_x_prime: &[F], -) -> Result { - let (vec_sigmas, vec_thetas) = (st.0.clone(), st.1.clone()); - let mut c = F::zero(); - - let mut e_lcccs = Vec::new(); - for r_x in vec_r_x { - e_lcccs.push(eq_eval(r_x, r_x_prime)?); - } - for (i, sigmas) in vec_sigmas.iter().enumerate() { - // (sum gamma^j * e_i * sigma_j) - for (j, sigma_j) in sigmas.iter().enumerate() { - let gamma_j = gamma.pow([((i * ccs.t + j) as u64)]); - c += gamma_j * e_lcccs[i] * sigma_j; - } - } - - let mu = vec_sigmas.len(); - let e2 = eq_eval(beta, r_x_prime)?; - for (k, thetas) in vec_thetas.iter().enumerate() { - // + gamma^{t+1} * e2 * sum c_i * prod theta_j - let prods = ccs.S.iter().zip(&ccs.c).map(|(S_i, &c_i)| { - let mut prod = F::one(); - for &j in S_i { - prod *= thetas[j]; - } - c_i * prod - }); - let lhs = F::sum(prods); - let gamma_t1 = gamma.pow([(mu * ccs.t + k) as u64]); - c += gamma_t1 * e2 * lhs; - } - Ok(c) -} - -/// Compute g(x) polynomial for the given inputs. -pub fn compute_g( - ccs: &CCS, - running_instances: &[LCCCS], - z_lcccs: &[Vec], - z_cccs: &[Vec], - gamma: C::ScalarField, - beta: &[C::ScalarField], -) -> Result, Error> { - assert_eq!(running_instances.len(), z_lcccs.len()); - - let mut g = VirtualPolynomial::::new(ccs.s); - - let mu = z_lcccs.len(); - let nu = z_cccs.len(); - - let mut gamma_pow = C::ScalarField::one(); - for i in 0..mu { - // L_j - let eq_rx = build_eq_x_r_vec(&running_instances[i].r_x)?; - let eq_rx_mle = dense_vec_to_dense_mle(ccs.s, &eq_rx); - for M_j in ccs.M.iter() { - let mut L_i_j = vec![dense_vec_to_dense_mle( - ccs.s, - &mat_vec_mul(M_j, &z_lcccs[i])?, - )]; - L_i_j.push(eq_rx_mle.clone()); - g.add_mle_list(L_i_j.iter().map(|v| Arc::new(v.clone())), gamma_pow)?; - gamma_pow *= gamma; - } - } - - let eq_beta = build_eq_x_r_vec(beta)?; - let eq_beta_mle = Arc::new(dense_vec_to_dense_mle(ccs.s, &eq_beta)); - - #[allow(clippy::needless_range_loop)] - for k in 0..nu { - // Q_k - for (S_i, &c_i) in ccs.S.iter().zip(&ccs.c) { - let mut Q_k = vec![]; - for &j in S_i { - Q_k.push(Arc::new(dense_vec_to_dense_mle( - ccs.s, - &mat_vec_mul(&ccs.M[j], &z_cccs[k])?, - ))); - } - Q_k.push(eq_beta_mle.clone()); - g.add_mle_list(Q_k, c_i * gamma_pow)?; - } - gamma_pow *= gamma; - } - - Ok(g) -} - -#[cfg(test)] -pub mod tests { - use ark_ff::Field; - use ark_pallas::{Fr, Projective}; - use ark_std::test_rng; - use ark_std::UniformRand; - use ark_std::Zero; - - use super::*; - use crate::arith::{ - ccs::tests::{get_test_ccs, get_test_z}, - Arith, ArithRelation, - }; - use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; - use crate::folding::hypernova::lcccs::tests::compute_Ls; - use crate::utils::hypercube::BooleanHypercube; - use crate::utils::mle::matrix_to_dense_mle; - use crate::utils::multilinear_polynomial::tests::fix_last_variables; - - /// Given M(x,y) matrix and a random field element `r`, test that ~M(r,y) is an s'-variable polynomial which - /// compresses every column j of the M(x,y) matrix by performing a random linear combination between the elements - /// of the column and the values eq_i(r) where i is the row of that element - /// - /// For example, for matrix M: - /// - /// [2, 3, 4, 4 - /// 4, 4, 3, 2 - /// 2, 8, 9, 2 - /// 9, 4, 2, 0] - /// - /// The polynomial ~M(r,y) is a polynomial in F^2 which evaluates to the following values in the hypercube: - /// - M(00) = 2*eq_00(r) + 4*eq_10(r) + 2*eq_01(r) + 9*eq_11(r) - /// - M(10) = 3*eq_00(r) + 4*eq_10(r) + 8*eq_01(r) + 4*eq_11(r) - /// - M(01) = 4*eq_00(r) + 3*eq_10(r) + 9*eq_01(r) + 2*eq_11(r) - /// - M(11) = 4*eq_00(r) + 2*eq_10(r) + 2*eq_01(r) + 0*eq_11(r) - /// - /// This is used by HyperNova in LCCCS to perform a verifier-chosen random linear combination between the columns - /// of the matrix and the z vector. This technique is also used extensively in "An Algebraic Framework for - /// Universal and Updatable SNARKs". - #[test] - fn test_compute_M_r_y_compression() -> Result<(), Error> { - let mut rng = test_rng(); - - // s = 2, s' = 3 - let ccs = get_test_ccs::(); - - let M = ccs.M[0].clone().to_dense(); - let M_mle = matrix_to_dense_mle(ccs.M[0].clone()); - - // Fix the polynomial ~M(r,y) - let r: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - let M_r_y = fix_last_variables(&M_mle, &r); - - // compute M_r_y the other way around - for j in 0..M[0].len() { - // Go over every column of M - let column_j: Vec = M.clone().iter().map(|x| x[j]).collect(); - // and perform the random lincomb between the elements of the column and eq_i(r) - let rlc = BooleanHypercube::new(ccs.s) - .enumerate() - .map(|(i, x)| column_j[i] * eq_eval(&x, &r).unwrap()) - .fold(Fr::zero(), |acc, result| acc + result); - - assert_eq!(M_r_y.evaluations[j], rlc); - } - Ok(()) - } - - #[test] - fn test_compute_sigmas_thetas() -> Result<(), Error> { - let ccs = get_test_ccs(); - let z1 = get_test_z(3); - let z2 = get_test_z(4); - let (w1, x1) = ccs.split_z(&z1); - let (w2, x2) = ccs.split_z(&z2); - ccs.check_relation(&w1, &x1)?; - ccs.check_relation(&w2, &x2)?; - - let mut rng = test_rng(); - let gamma: Fr = Fr::rand(&mut rng); - let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - - // Initialize a multifolding object - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - let (lcccs_instance, _) = - ccs.to_lcccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z1)?; - - let sigmas_thetas = compute_sigmas_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime)?; - - let g = compute_g( - &ccs, - &[lcccs_instance.clone()], - &[z1.clone()], - &[z2.clone()], - gamma, - &beta, - )?; - - // we expect g(r_x_prime) to be equal to: - // c = (sum gamma^j * e1 * sigma_j) + gamma^{t+1} * e2 * sum c_i * prod theta_j - // from compute_c - let expected_c = g.evaluate(&r_x_prime)?; - let c = compute_c::( - &ccs, - &sigmas_thetas, - gamma, - &beta, - &vec![lcccs_instance.r_x], - &r_x_prime, - )?; - assert_eq!(c, expected_c); - Ok(()) - } - - #[test] - fn test_compute_g() -> Result<(), Error> { - let mut rng = test_rng(); - - // generate test CCS & z vectors - let ccs: CCS = get_test_ccs(); - let z1 = get_test_z(3); - let z2 = get_test_z(4); - let (w1, x1) = ccs.split_z(&z1); - let (w2, x2) = ccs.split_z(&z2); - ccs.check_relation(&w1, &x1)?; - ccs.check_relation(&w2, &x2)?; - - let gamma: Fr = Fr::rand(&mut rng); - let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); - - // Initialize a multifolding object - let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n_witnesses())?; - let (lcccs_instance, _) = - ccs.to_lcccs::<_, _, Pedersen, false>(&mut rng, &pedersen_params, &z1)?; - - // Compute g(x) with that r_x - let g = compute_g::( - &ccs, - &[lcccs_instance.clone()], - &[z1.clone()], - &[z2.clone()], - gamma, - &beta, - )?; - - // evaluate g(x) over x \in {0,1}^s - let mut g_on_bhc = Fr::zero(); - for x in BooleanHypercube::new(ccs.s) { - g_on_bhc += g.evaluate(&x)?; - } - - // Q(x) over bhc is assumed to be zero, as checked in the test 'test_compute_Q' - assert_ne!(g_on_bhc, Fr::zero()); - - let mut sum_v_j_gamma = Fr::zero(); - for j in 0..lcccs_instance.v.len() { - let gamma_j = gamma.pow([j as u64]); - sum_v_j_gamma += lcccs_instance.v[j] * gamma_j; - } - - // evaluating g(x) over the boolean hypercube should give the same result as evaluating the - // sum of gamma^j * v_j over j \in [t] - assert_eq!(g_on_bhc, sum_v_j_gamma); - - // evaluate sum_{j \in [t]} (gamma^j * Lj(x)) over x \in {0,1}^s - let mut sum_Lj_on_bhc = Fr::zero(); - let vec_L = compute_Ls(&ccs, &lcccs_instance, &z1)?; - for x in BooleanHypercube::new(ccs.s) { - for (j, Lj) in vec_L.iter().enumerate() { - let gamma_j = gamma.pow([j as u64]); - sum_Lj_on_bhc += Lj.evaluate(&x)? * gamma_j; - } - } - - // evaluating g(x) over the boolean hypercube should give the same result as evaluating the - // sum of gamma^j * Lj(x) over the boolean hypercube - assert_eq!(g_on_bhc, sum_Lj_on_bhc); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/mod.rs b/folding-schemes/src/folding/mod.rs deleted file mode 100644 index a2b23a8d7..000000000 --- a/folding-schemes/src/folding/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -pub mod circuits; -pub mod hypernova; -pub mod nova; -pub mod protogalaxy; -pub mod traits; - -#[cfg(test)] -pub mod tests { - - use ark_pallas::{Fr, Projective as G1}; - use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; - use ark_vesta::Projective as G2; - use std::io::Write; - - use crate::commitment::pedersen::Pedersen; - use crate::folding::{ - hypernova::HyperNova, - nova::{Nova, PreprocessorParam as NovaPreprocessorParam}, - protogalaxy::ProtoGalaxy, - }; - use crate::frontend::utils::CubicFCircuit; - use crate::frontend::FCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::FoldingScheme; - use crate::{Curve, Error}; - - /// tests the IVC proofs and its serializers for the 3 implemented IVCs: Nova, HyperNova and - /// ProtoGalaxy. - #[test] - fn test_serialize_ivc_nova_hypernova_protogalaxy() -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - type FC = CubicFCircuit; - let f_circuit = FC::new(())?; - - // test Nova - type N = Nova, Pedersen, false>; - let prep_param = NovaPreprocessorParam::new(poseidon_config.clone(), f_circuit); - test_serialize_ivc_opt::("nova".to_string(), prep_param.clone())?; - - // test HyperNova - type HN = HyperNova< - G1, - G2, - FC, - Pedersen, - Pedersen, - 1, // mu - 1, // nu - false, - >; - test_serialize_ivc_opt::("hypernova".to_string(), prep_param)?; - - // test ProtoGalaxy - type P = ProtoGalaxy, Pedersen>; - let prep_param = (poseidon_config, f_circuit); - test_serialize_ivc_opt::("protogalaxy".to_string(), prep_param)?; - Ok(()) - } - - fn test_serialize_ivc_opt< - C1: Curve, - C2: Curve, - FC: FCircuit, - FS: FoldingScheme, - >( - name: String, - prep_param: FS::PreprocessorParam, - ) -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let F_circuit = FC::new(())?; - - let fs_params = FS::preprocess(&mut rng, &prep_param)?; - - let z_0 = vec![C1::ScalarField::from(3_u32)]; - let mut fs = FS::init(&fs_params, F_circuit, z_0.clone())?; - - // perform multiple IVC steps (internally folding) - let num_steps: usize = 3; - for _ in 0..num_steps { - fs.prove_step(&mut rng, FC::ExternalInputs::default(), None)?; - } - - // verify the IVCProof - let ivc_proof: FS::IVCProof = fs.ivc_proof(); - FS::verify(fs_params.1.clone(), ivc_proof.clone())?; - - // serialize the IVCProof and store it in a file - let mut writer = vec![]; - assert!(ivc_proof.serialize_compressed(&mut writer).is_ok()); - - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .open(format!("./ivc_proof-{}.serialized", name))?; - file.write_all(&writer)?; - - // read the IVCProof from the file deserializing it - let bytes = std::fs::read(format!("./ivc_proof-{}.serialized", name))?; - let deserialized_ivc_proof = FS::IVCProof::deserialize_compressed(bytes.as_slice())?; - // verify deserialized IVCProof - FS::verify(fs_params.1.clone(), deserialized_ivc_proof.clone())?; - - // build the FS from the given IVCProof, FC::Params, ProverParams and VerifierParams - let mut new_fs = FS::from_ivc_proof(deserialized_ivc_proof, (), fs_params.clone())?; - - // serialize the Nova params - let mut fs_pp_serialized = vec![]; - fs_params.0.serialize_compressed(&mut fs_pp_serialized)?; - let mut fs_vp_serialized = vec![]; - fs_params.1.serialize_compressed(&mut fs_vp_serialized)?; - - // deserialize the Nova params. This would be done by the client reading from a file - let _fs_pp_deserialized = FS::pp_deserialize_with_mode( - &mut fs_pp_serialized.as_slice(), - ark_serialize::Compress::Yes, - ark_serialize::Validate::Yes, - (), // FCircuit's Params - )?; - - // perform several IVC steps on both the original FS instance and the recovered from the - // serialization new FS instance - let num_steps: usize = 3; - for _ in 0..num_steps { - new_fs.prove_step(&mut rng, FC::ExternalInputs::default(), None)?; - fs.prove_step(&mut rng, FC::ExternalInputs::default(), None)?; - } - - // check that the IVCProofs from both FS instances are equal - assert_eq!(new_fs.ivc_proof(), fs.ivc_proof()); - - let fs_vp_deserialized = FS::vp_deserialize_with_mode( - &mut fs_vp_serialized.as_slice(), - ark_serialize::Compress::Yes, - ark_serialize::Validate::Yes, - (), // fcircuit_params - )?; - - // get the IVCProof - let ivc_proof: FS::IVCProof = new_fs.ivc_proof(); - - // serialize IVCProof - let mut ivc_proof_serialized = vec![]; - assert!(ivc_proof - .serialize_compressed(&mut ivc_proof_serialized) - .is_ok()); - // deserialize IVCProof - let ivc_proof_deserialized = - FS::IVCProof::deserialize_compressed(ivc_proof_serialized.as_slice())?; - - // verify the last IVCProof from the recovered from serialization FS - FS::verify(fs_vp_deserialized.clone(), ivc_proof_deserialized)?; - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs deleted file mode 100644 index 9cea0284b..000000000 --- a/folding-schemes/src/folding/nova/circuits.rs +++ /dev/null @@ -1,370 +0,0 @@ -/// contains [Nova](https://eprint.iacr.org/2021/370.pdf) related circuits -use ark_crypto_primitives::sponge::poseidon::{ - constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge, -}; -use ark_r1cs_std::{ - alloc::AllocVar, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::{fmt::Debug, Zero}; - -use super::{ - nifs::{ - nova_circuits::{CommittedInstanceVar, NIFSGadget}, - NIFSGadgetTrait, - }, - CommittedInstance, NovaCycleFoldConfig, -}; -use crate::folding::circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, - CycleFoldConfig, - }, - nonnative::affine::NonNativeAffineVar, - CF1, -}; -use crate::folding::traits::{CommittedInstanceVarOps, Dummy}; -use crate::frontend::FCircuit; -use crate::transcript::TranscriptVar; -use crate::Curve; - -/// `AugmentedFCircuit` enhances the original step function `F`, so that it can -/// be used in recursive arguments such as IVC. -/// -/// The method for converting `F` to `AugmentedFCircuit` (`F'`) is defined in -/// [Nova](https://eprint.iacr.org/2021/370.pdf), where `AugmentedFCircuit` not -/// only invokes `F`, but also adds additional constraints for verifying the -/// correct folding of primary instances (i.e., Nova's `CommittedInstance`s over -/// `C1`). -/// -/// Furthermore, to reduce circuit size over `C2`, we implement the constraints -/// defined in [CycleFold](https://eprint.iacr.org/2023/1192.pdf). These extra -/// constraints verify the correct folding of CycleFold instances. -#[derive(Debug, Clone)] -pub struct AugmentedFCircuit>> { - pub(super) poseidon_config: PoseidonConfig>, - pub(super) pp_hash: Option>, - pub(super) i: Option>, - pub(super) i_usize: Option, - pub(super) z_0: Option>, - pub(super) z_i: Option>, - pub(super) external_inputs: Option, - pub(super) u_i_cmW: Option, - pub(super) U_i: Option>, - pub(super) U_i1_cmE: Option, - pub(super) U_i1_cmW: Option, - pub(super) cmT: Option, - pub(super) F: FC, // F circuit - - // cyclefold verifier on C1 - // Here 'cf1, cf2' are for each of the CycleFold circuits, corresponding to the fold of cmW and - // cmE respectively - pub(super) cf1_u_i_cmW: Option, // input - pub(super) cf2_u_i_cmW: Option, // input - pub(super) cf_U_i: Option>, // input - pub(super) cf1_cmT: Option, - pub(super) cf2_cmT: Option, -} - -impl>> AugmentedFCircuit { - pub fn empty(poseidon_config: &PoseidonConfig>, F_circuit: FC) -> Self { - Self { - poseidon_config: poseidon_config.clone(), - pp_hash: None, - i: None, - i_usize: None, - z_0: None, - z_i: None, - external_inputs: None, - u_i_cmW: None, - U_i: None, - U_i1_cmE: None, - U_i1_cmW: None, - cmT: None, - F: F_circuit, - // cyclefold values - cf1_u_i_cmW: None, - cf2_u_i_cmW: None, - cf_U_i: None, - cf1_cmT: None, - cf2_cmT: None, - } - } -} - -impl AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - pub fn compute_next_state( - self, - cs: ConstraintSystemRef>, - ) -> Result>>, SynthesisError> { - let pp_hash = FpVar::>::new_witness(cs.clone(), || { - Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) - })?; - let i = FpVar::>::new_witness(cs.clone(), || { - Ok(self.i.unwrap_or_else(CF1::::zero)) - })?; - let z_0 = Vec::>>::new_witness(cs.clone(), || { - Ok(self - .z_0 - .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) - })?; - let z_i = Vec::>>::new_witness(cs.clone(), || { - Ok(self - .z_i - .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) - })?; - let external_inputs = FC::ExternalInputsVar::new_witness(cs.clone(), || { - Ok(self.external_inputs.unwrap_or_default()) - })?; - - let u_dummy = CommittedInstance::dummy(2); - let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.U_i.unwrap_or(u_dummy.clone())) - })?; - let U_i1_cmE = NonNativeAffineVar::new_witness(cs.clone(), || { - Ok(self.U_i1_cmE.unwrap_or_else(C1::zero)) - })?; - let U_i1_cmW = NonNativeAffineVar::new_witness(cs.clone(), || { - Ok(self.U_i1_cmW.unwrap_or_else(C1::zero)) - })?; - - let cmT = - NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; - - let cf_u_dummy = CycleFoldCommittedInstance::dummy(NovaCycleFoldConfig::::IO_LEN); - let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.cf_U_i.unwrap_or(cf_u_dummy.clone())) - })?; - let cf1_cmT = - C2::Var::new_witness(cs.clone(), || Ok(self.cf1_cmT.unwrap_or_else(C2::zero)))?; - let cf2_cmT = - C2::Var::new_witness(cs.clone(), || Ok(self.cf2_cmT.unwrap_or_else(C2::zero)))?; - - // `sponge` is for digest computation. - let sponge = PoseidonSpongeVar::::new_with_pp_hash( - &self.poseidon_config, - &pp_hash, - )?; - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - let is_basecase = i.is_zero()?; - - // Primary Part - // P.1. Compute u_i.x - // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &i, &z_0, &z_i)?; - // u_i.x[1] = H(cf_U_i) - let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge)?; - - // P.2. Construct u_i - let u_i = CommittedInstanceVar { - // u_i.cmE = cm(0) - cmE: NonNativeAffineVar::new_constant(cs.clone(), C1::zero())?, - // u_i.u = 1 - u: FpVar::one(), - // u_i.cmW is provided by the prover as witness - cmW: NonNativeAffineVar::new_witness(cs.clone(), || { - Ok(self.u_i_cmW.unwrap_or(C1::zero())) - })?, - // u_i.x is computed in step 1 - x: vec![u_i_x, cf_u_i_x], - }; - - // P.3. nifs.verify, obtains U_{i+1} by folding u_i & U_i. - // Notice that NIFSGadget::verify does not fold cmE & cmW. - // We set `U_i1.cmE` and `U_i1.cmW` to unconstrained witnesses `U_i1_cmE` and `U_i1_cmW` - // respectively. - // The correctness of them will be checked on the other curve. - let (mut U_i1, r_bits) = NIFSGadget::< - C1, - PoseidonSponge, - PoseidonSpongeVar, - >::verify( - &mut transcript, - U_i.clone(), - U_i_vec, - u_i.clone(), - Some(cmT.clone()), - )?; - U_i1.cmE = U_i1_cmE; - U_i1.cmW = U_i1_cmW; - - // P.4.a compute and check the first output of F' - - // get z_{i+1} from the F circuit - let i_usize = self.i_usize.unwrap_or(0); - let z_i1 = self - .F - .generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?; - - // Base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{\bot}) - // Non-base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{i+1}) - let (u_i1_x, _) = - U_i1.clone() - .hash(&sponge, &(i + FpVar::>::one()), &z_0, &z_i1)?; - let (u_i1_x_base, _) = CommittedInstanceVar::new_constant(cs.clone(), u_dummy)?.hash( - &sponge, - &FpVar::>::one(), - &z_0, - &z_i1, - )?; - let x = is_basecase.select(&u_i1_x_base, &u_i1_x)?; - // This line "converts" `x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `x`. - // While comparing `x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `x` computed in-circuit. - FpVar::new_input(cs.clone(), || x.value())?.enforce_equal(&x)?; - - // CycleFold part - // C.1. Compute cf1_u_i.x and cf2_u_i.x - // C.2. Construct `cf1_u_i` and `cf2_u_i` - let cf1_u_i = CycleFoldCommittedInstanceVar::new_incoming_from_components( - // `cf1_u_i.cmW` is provided by the prover as witness. - C2::Var::new_witness(cs.clone(), || Ok(self.cf1_u_i_cmW.unwrap_or(C2::zero())))?, - // To construct `cf1_u_i.x`, we need to provide the randomness - // `r_bits` and the `cmW` component in committed instances `U_i`, - // `u_i`, and `U_{i+1}`. - &r_bits, - vec![U_i.cmW, u_i.cmW, U_i1.cmW], - )?; - let cf2_u_i = CycleFoldCommittedInstanceVar::new_incoming_from_components( - // `cf2_u_i.cmW` is provided by the prover as witness. - C2::Var::new_witness(cs.clone(), || Ok(self.cf2_u_i_cmW.unwrap_or(C2::zero())))?, - // To construct `cf2_u_i.x`, we need to provide the randomness - // `r_bits`, the `cmE` component in running instances `U_i` and - // `U_{i+1}`, and the cross term commitment `cmT`. - &r_bits, - vec![U_i.cmE, cmT, U_i1.cmE], - )?; - - // C.3. nifs.verify, obtains cf_U_{i+1} by folding cf1_u_i and cf2_u_i into cf_U. - let cf_U_i1 = CycleFoldAugmentationGadget::fold_gadget( - &mut transcript, - cf_U_i, - vec![cf1_u_i, cf2_u_i], - vec![cf1_cmT, cf2_cmT], - )?; - - // Back to Primary Part - // P.4.b compute and check the second output of F' - // Base case: u_{i+1}.x[1] == H(cf_U_{\bot}) - // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) - let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge)?; - let (cf_u_i1_x_base, _) = - CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? - .hash(&sponge)?; - let cf_x = is_basecase.select(&cf_u_i1_x_base, &cf_u_i1_x)?; - // This line "converts" `cf_x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `cf_x`. - // While comparing `cf_x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `cf_x` computed in-circuit. - FpVar::new_input(cs.clone(), || cf_x.value())?.enforce_equal(&cf_x)?; - - Ok(z_i1) - } -} - -impl ConstraintSynthesizer> for AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - self.compute_next_state(cs).map(|_| ()) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, poseidon::PoseidonSponge}; - use ark_ff::{BigInteger, PrimeField}; - - use ark_r1cs_std::prelude::Boolean; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::UniformRand; - - use crate::folding::nova::nifs::nova::ChallengeGadget; - use crate::transcript::{poseidon::poseidon_canonical_config, Transcript}; - use crate::Error; - - // checks that the gadget and native implementations of the challenge computation match - #[test] - fn test_challenge_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for testing - let mut transcript = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - let u_i = CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }; - let U_i = CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }; - let cmT = Projective::rand(&mut rng); - - // compute the challenge natively - let r_bits = - ChallengeGadget::>::get_challenge_native( - &mut transcript, - &U_i, - &u_i, - Some(&cmT), - ); - let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds)?; - - let cs = ConstraintSystem::::new_ref(); - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let u_iVar = - CommittedInstanceVar::::new_witness(cs.clone(), || Ok(u_i.clone()))?; - let U_iVar = - CommittedInstanceVar::::new_witness(cs.clone(), || Ok(U_i.clone()))?; - let cmTVar = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(cmT))?; - let mut transcriptVar = - PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - - // compute the challenge in-circuit - let r_bitsVar = - ChallengeGadget::>::get_challenge_gadget( - &mut transcriptVar, - U_iVar.to_sponge_field_elements()?, - u_iVar, - Some(cmTVar), - )?; - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - let rVar = Boolean::le_bits_to_fp(&r_bitsVar)?; - assert_eq!(rVar.value()?, r); - assert_eq!(r_bitsVar.value()?, r_bits); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/decider.rs b/folding-schemes/src/folding/nova/decider.rs deleted file mode 100644 index 20641b1b7..000000000 --- a/folding-schemes/src/folding/nova/decider.rs +++ /dev/null @@ -1,429 +0,0 @@ -/// This file implements the offchain decider. For ethereum use cases, use the -/// DeciderEth from decider_eth.rs file. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html -use ark_ff::{BigInteger, PrimeField}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_snark::SNARK; -use ark_std::rand::{CryptoRng, RngCore}; -use ark_std::{One, Zero}; -use core::marker::PhantomData; - -use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2}; -use super::decider_eth_circuit::DeciderNovaGadget; -use super::Nova; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::cyclefold::CycleFoldCommittedInstance; -use crate::folding::circuits::decider::DeciderEnabledNIFS; -use crate::folding::traits::{ - CommittedInstanceOps, Dummy, Inputize, InputizeNonNative, WitnessOps, -}; -use crate::frontend::FCircuit; -use crate::transcript::poseidon::poseidon_custom_config; -use crate::{Curve, Error}; -use crate::{Decider as DeciderTrait, FoldingScheme}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Proof -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - S1: SNARK, - S2: SNARK, -{ - c1_snark_proof: S1::Proof, - c2_snark_proof: S2::Proof, - cs1_proofs: [CS1::Proof; 2], - cs2_proofs: [CS2::Proof; 2], - // cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are - // checked in-circuit - cmT: C1, - r: C1::ScalarField, - // cyclefold committed instance - cf_U_final: CycleFoldCommittedInstance, - // the CS challenges are provided by the prover, but in-circuit they are checked to match the - // in-circuit computed computed ones. - cs1_challenges: [C1::ScalarField; 2], - cs2_challenges: [C2::ScalarField; 2], -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct ProverParam -where - CS1_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S1_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - CS2_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S2_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize, -{ - pub c1_snark_pp: S1_ProvingKey, - pub c1_cs_pp: CS1_ProvingKey, - pub c2_snark_pp: S2_ProvingKey, - pub c2_cs_pp: CS2_ProvingKey, -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct VerifierParam -where - C1: Curve, - CS1_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S1_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - CS2_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S2_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, -{ - pub pp_hash: C1::ScalarField, - pub c1_snark_vp: S1_VerifyingKey, - pub c1_cs_vp: CS1_VerifyingKey, - pub c2_snark_vp: S2_VerifyingKey, - pub c2_cs_vp: CS2_VerifyingKey, -} - -/// Onchain Decider, for ethereum use cases -#[derive(Clone, Debug)] -pub struct Decider { - _c1: PhantomData, - _c2: PhantomData, - _fc: PhantomData, - _cs1: PhantomData, - _cs2: PhantomData, - _s1: PhantomData, - _s2: PhantomData, - _fs: PhantomData, -} - -impl DeciderTrait - for Decider -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme< - C1, - ProverChallenge = C1::ScalarField, - Challenge = C1::ScalarField, - Proof = crate::commitment::kzg::Proof, - >, - CS2: CommitmentScheme< - C2, - ProverChallenge = C2::ScalarField, - Challenge = C2::ScalarField, - Proof = crate::commitment::kzg::Proof, - >, - S1: SNARK, - S2: SNARK, - FS: FoldingScheme, - // constrain FS into Nova, since this is a Decider specifically for Nova - Nova: From, - crate::folding::nova::ProverParams: - From<>::ProverParam>, - crate::folding::nova::VerifierParams: - From<>::VerifierParam>, -{ - type PreprocessorParam = ((FS::ProverParam, FS::VerifierParam), usize); - type ProverParam = - ProverParam; - type Proof = Proof; - type VerifierParam = VerifierParam< - C1, - CS1::VerifierParams, - S1::VerifyingKey, - CS2::VerifierParams, - S2::VerifyingKey, - >; - type PublicInput = Vec; - type CommittedInstance = Vec; - - fn preprocess( - mut rng: impl RngCore + CryptoRng, - ((pp, vp), state_len): Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - // get the FoldingScheme prover & verifier params from Nova - let nova_pp: as FoldingScheme>::ProverParam = - pp.into(); - let nova_vp: as FoldingScheme< - C1, - C2, - FC, - >>::VerifierParam = vp.into(); - let pp_hash = nova_vp.pp_hash()?; - - let poseidon_config1 = nova_vp.poseidon_config; - // Create a poseidon config on `C2`'s scalar field for `circuit2`, with - // the same parameters (`full_rounds` etc.) as `circuit1` to ensure the - // security level is the same. - // Note: `ark` and `mds` will be different because they depend on the - // field, but they will not affect the security level. - let poseidon_config2 = poseidon_custom_config( - poseidon_config1.full_rounds, - poseidon_config1.partial_rounds, - poseidon_config1.alpha, - poseidon_config1.rate, - poseidon_config1.capacity, - ); - - let circuit1 = DeciderCircuit1::::dummy(( - nova_vp.r1cs, - &nova_vp.cf_r1cs, - poseidon_config1, - (), - (), - state_len, - 2, // Nova's running CommittedInstance contains 2 commitments - )); - let circuit2 = DeciderCircuit2::::dummy(( - nova_vp.cf_r1cs, - poseidon_config2, - 2, // Nova's running CommittedInstance contains 2 commitments - )); - - // get the Groth16 specific setup for the circuits - let (c1_g16_pk, c1_g16_vk) = S1::circuit_specific_setup(circuit1, &mut rng) - .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; - let (c2_g16_pk, c2_g16_vk) = S2::circuit_specific_setup(circuit2, &mut rng) - .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; - - let pp = Self::ProverParam { - c1_snark_pp: c1_g16_pk, - c1_cs_pp: nova_pp.cs_pp, - c2_snark_pp: c2_g16_pk, - c2_cs_pp: nova_pp.cf_cs_pp, - }; - let vp = Self::VerifierParam { - pp_hash, - c1_snark_vp: c1_g16_vk, - c1_cs_vp: nova_vp.cs_vp, - c2_snark_vp: c2_g16_vk, - c2_cs_vp: nova_vp.cf_cs_vp, - }; - Ok((pp, vp)) - } - - fn prove( - mut rng: impl RngCore + CryptoRng, - pp: Self::ProverParam, - fs: FS, - ) -> Result { - let circuit1 = DeciderCircuit1::::try_from(Nova::from(fs.clone()))?; - let circuit2 = DeciderCircuit2::::try_from(Nova::from(fs))?; - - let cmT = circuit1.proof; - let r = circuit1.randomness; - let cf_U_final = circuit1.cf_U_i.clone(); - - let c1_kzg_challenges = circuit1.kzg_challenges.clone(); - let c1_kzg_proofs = circuit1 - .W_i1 - .get_openings() - .iter() - .zip(&c1_kzg_challenges) - .map(|((v, _), &c)| { - CS1::prove_with_challenge(&pp.c1_cs_pp, c, v, &C1::ScalarField::zero(), None) - }) - .collect::, _>>()?; - let c2_kzg_challenges = circuit2.kzg_challenges.clone(); - let c2_kzg_proofs = circuit2 - .cf_W_i - .get_openings() - .iter() - .zip(&c2_kzg_challenges) - .map(|((v, _), &c)| { - CS2::prove_with_challenge(&pp.c2_cs_pp, c, v, &C2::ScalarField::zero(), None) - }) - .collect::, _>>()?; - - let c1_snark_proof = S1::prove(&pp.c1_snark_pp, circuit1, &mut rng) - .map_err(|e| Error::Other(e.to_string()))?; - let c2_snark_proof = S2::prove(&pp.c2_snark_pp, circuit2, &mut rng) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self::Proof { - c1_snark_proof, - c2_snark_proof, - cs1_proofs: c1_kzg_proofs - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - cs2_proofs: c2_kzg_proofs - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - cmT, - r, - cf_U_final, - cs1_challenges: c1_kzg_challenges - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - cs2_challenges: c2_kzg_challenges - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - }) - } - - fn verify( - vp: Self::VerifierParam, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - // we don't use the instances at the verifier level, since we check them in-circuit - running_commitments: &Self::CommittedInstance, - incoming_commitments: &Self::CommittedInstance, - proof: &Self::Proof, - ) -> Result { - if i <= C1::ScalarField::one() { - return Err(Error::NotEnoughSteps); - } - - // 6.2. Fold the commitments - let U_final_commitments = DeciderNovaGadget::fold_group_elements_native( - running_commitments, - incoming_commitments, - Some(proof.cmT), - proof.r, - )?; - let cf_U = proof.cf_U_final.clone(); - - // snark proof 1 - let c1_public_input = [ - &[vp.pp_hash, i][..], - &z_0, - &z_i, - &U_final_commitments.inputize_nonnative(), - &cf_U.inputize_nonnative(), - &proof.cs1_challenges, - &proof.cs1_proofs.iter().map(|p| p.eval).collect::>(), - &proof.cmT.inputize_nonnative(), - ] - .concat(); - - let c1_snark_v = S1::verify(&vp.c1_snark_vp, &c1_public_input, &proof.c1_snark_proof) - .map_err(|e| Error::Other(e.to_string()))?; - if !c1_snark_v { - return Err(Error::SNARKVerificationFail); - } - - // snark proof 2 - // migrate pp_hash from C1::Fr to C1::Fq - let pp_hash_Fq = - C2::ScalarField::from_le_bytes_mod_order(&vp.pp_hash.into_bigint().to_bytes_le()); - let c2_public_input: Vec = [ - &[pp_hash_Fq][..], - &cf_U.inputize(), - &proof.cs2_challenges, - &proof.cs2_proofs.iter().map(|p| p.eval).collect::>(), - ] - .concat(); - - let c2_snark_v = S2::verify(&vp.c2_snark_vp, &c2_public_input, &proof.c2_snark_proof) - .map_err(|e| Error::Other(e.to_string()))?; - if !c2_snark_v { - return Err(Error::SNARKVerificationFail); - } - - // 7.3. check C1 commitments (main instance commitments) - for ((cm, &c), pi) in U_final_commitments - .iter() - .zip(&proof.cs1_challenges) - .zip(&proof.cs1_proofs) - { - CS1::verify_with_challenge(&vp.c1_cs_vp, c, cm, pi)?; - } - - // 4.3. check C2 commitments (CycleFold instance commitments) - for ((cm, &c), pi) in cf_U - .get_commitments() - .iter() - .zip(&proof.cs2_challenges) - .zip(&proof.cs2_proofs) - { - CS2::verify_with_challenge(&vp.c2_cs_vp, c, cm, pi)?; - } - - Ok(true) - } -} - -#[cfg(test)] -pub mod tests { - use ark_groth16::Groth16; - - // Note: do not use the MNTx_298 curves in practice, these are just for tests. Use the MNTx_753 - // curves instead. - use ark_mnt4_298::{Fr, G1Projective as Projective, MNT4_298 as MNT4}; - use ark_mnt6_298::{G1Projective as Projective2, MNT6_298 as MNT6}; - use std::time::Instant; - - use super::*; - use crate::commitment::kzg::KZG; - use crate::folding::nova::PreprocessorParam; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_decider() -> Result<(), Error> { - // use Nova as FoldingScheme - type N = Nova< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, MNT4>, - KZG<'static, MNT6>, - false, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, MNT4>, - KZG<'static, MNT6>, - Groth16, - Groth16, - N, // here we define the FoldingScheme to use - >; - - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let start = Instant::now(); - let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &prep_param)?; - println!("Nova preprocess, {:?}", start.elapsed()); - - let start = Instant::now(); - let mut nova = N::init(&nova_params, F_circuit, z_0.clone())?; - println!("Nova initialized, {:?}", start.elapsed()); - let start = Instant::now(); - nova.prove_step(&mut rng, (), None)?; - println!("prove_step, {:?}", start.elapsed()); - nova.prove_step(&mut rng, (), None)?; // do a 2nd step - - let mut rng = rand::rngs::OsRng; - - // prepare the Decider prover & verifier params - let start = Instant::now(); - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params, F_circuit.state_len()))?; - println!("Decider preprocess, {:?}", start.elapsed()); - - // decider proof generation - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("Decider prove, {:?}", start.elapsed()); - - // decider proof verification - let start = Instant::now(); - let verified = D::verify( - decider_vp, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/decider_circuits.rs b/folding-schemes/src/folding/nova/decider_circuits.rs deleted file mode 100644 index 707691ce4..000000000 --- a/folding-schemes/src/folding/nova/decider_circuits.rs +++ /dev/null @@ -1,226 +0,0 @@ -/// This file implements the offchain decider circuit. For ethereum use cases, use the -/// DeciderEthCircuit. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html -use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::fields::fp::FpVar; -use core::marker::PhantomData; - -use super::{ - decider_eth_circuit::DeciderNovaGadget, - nifs::{nova::NIFS, NIFSTrait}, - CommittedInstance, Nova, Witness, -}; -use crate::{ - arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, - commitment::CommitmentScheme, - folding::{ - circuits::{ - decider::{ - off_chain::{GenericOffchainDeciderCircuit1, GenericOffchainDeciderCircuit2}, - EvalGadget, KZGChallengesGadget, - }, - CF1, - }, - traits::WitnessOps, - }, - frontend::FCircuit, - transcript::{poseidon::poseidon_custom_config, Transcript}, - Curve, Error, -}; - -/// Circuit that implements part of the in-circuit checks needed for the offchain verification over -/// the Curve2's BaseField (=Curve1's ScalarField). -pub type DeciderCircuit1 = GenericOffchainDeciderCircuit1< - C1, - C2, - CommittedInstance, - CommittedInstance, - Witness, - R1CS>, - R1CSMatricesVar, FpVar>>, - DeciderNovaGadget, ->; - -impl< - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, - > TryFrom> for DeciderCircuit1 -{ - type Error = Error; - - fn try_from(nova: Nova) -> Result { - let mut transcript = PoseidonSponge::new_with_pp_hash(&nova.poseidon_config, nova.pp_hash); - // pp_hash is absorbed to transcript at the NIFS::prove call - - // compute the U_{i+1}, W_{i+1} - let (W_i1, U_i1, cmT, r_bits) = NIFS::, H>::prove( - &nova.cs_pp, - &nova.r1cs.clone(), - &mut transcript, - &nova.W_i, - &nova.U_i, - &nova.w_i, - &nova.u_i, - )?; - let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - // compute the KZG challenges used as inputs in the circuit - let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); - - // get KZG evals - let kzg_evaluations = W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) - .collect::, _>>()?; - - Ok(Self { - _avar: PhantomData, - arith: nova.r1cs, - poseidon_config: nova.poseidon_config, - pp_hash: nova.pp_hash, - i: nova.i, - z_0: nova.z_0, - z_i: nova.z_i, - U_i: nova.U_i, - W_i: nova.W_i, - u_i: nova.u_i, - w_i: nova.w_i, - U_i1, - W_i1, - proof: cmT, - randomness: r_Fr, - cf_U_i: nova.cf_U_i, - kzg_challenges, - kzg_evaluations, - }) - } -} - -/// Circuit that implements part of the in-circuit checks needed for the offchain verification over -/// the Curve1's BaseField (=Curve2's ScalarField). -pub type DeciderCircuit2 = GenericOffchainDeciderCircuit2; - -impl< - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, - > TryFrom> for DeciderCircuit2 -{ - type Error = Error; - - fn try_from(nova: Nova) -> Result { - // Create a poseidon config on `C2`'s scalar field for `circuit2`, with - // the same parameters (`full_rounds` etc.) as `circuit1` to ensure the - // security level is the same. - // Note: `ark` and `mds` will be different because they depend on the - // field, but they will not affect the security level. - let poseidon_config = poseidon_custom_config( - nova.poseidon_config.full_rounds, - nova.poseidon_config.partial_rounds, - nova.poseidon_config.alpha, - nova.poseidon_config.rate, - nova.poseidon_config.capacity, - ); - let pp_hash_Fq = - C2::ScalarField::from_le_bytes_mod_order(&nova.pp_hash.into_bigint().to_bytes_le()); - let mut transcript = - PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash_Fq); - - // compute the KZG challenges used as inputs in the circuit - let kzg_challenges = - KZGChallengesGadget::get_challenges_native(&mut transcript, &nova.cf_U_i); - - // get KZG evals - let kzg_evaluations = nova - .cf_W_i - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) - .collect::, _>>()?; - - Ok(Self { - cf_arith: nova.cf_r1cs, - poseidon_config, - pp_hash: pp_hash_Fq, - cf_U_i: nova.cf_U_i, - cf_W_i: nova.cf_W_i, - kzg_challenges, - kzg_evaluations, - }) - } -} - -#[cfg(test)] -pub mod tests { - use ark_pallas::{Fq, Fr, Projective}; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - use ark_vesta::Projective as Projective2; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::PreprocessorParam; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::FoldingScheme; - - #[test] - fn test_decider_circuits() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - type N = Nova< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - false, - >; - - let prep_param = PreprocessorParam::< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - false, - >::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &prep_param)?; - - // generate a Nova instance and do a step of it - let mut nova = N::init(&nova_params, F_circuit, z_0.clone())?; - nova.prove_step(&mut rng, (), None)?; - // verify the IVC - let ivc_proof = nova.ivc_proof(); - N::verify(nova_params.1, ivc_proof)?; - - // load the DeciderCircuit 1 & 2 from the Nova instance - let decider_circuit1 = DeciderCircuit1::::try_from(nova.clone())?; - let decider_circuit2 = DeciderCircuit2::::try_from(nova)?; - - // generate the constraints of both circuits and check that are satisfied by the inputs - let cs1 = ConstraintSystem::::new_ref(); - decider_circuit1.generate_constraints(cs1.clone())?; - assert!(cs1.is_satisfied()?); - let cs2 = ConstraintSystem::::new_ref(); - decider_circuit2.generate_constraints(cs2.clone())?; - assert!(cs2.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs deleted file mode 100644 index 155d9e278..000000000 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ /dev/null @@ -1,499 +0,0 @@ -/// This file implements the Nova's onchain (Ethereum's EVM) decider. For non-ethereum use cases, -/// the Decider from decider.rs file will be more efficient. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_snark::SNARK; -use ark_std::{ - rand::{CryptoRng, RngCore}, - One, Zero, -}; -use core::marker::PhantomData; - -pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::decider_eth_circuit::DeciderNovaGadget; -use super::Nova; -use crate::folding::circuits::decider::DeciderEnabledNIFS; -use crate::folding::traits::{InputizeNonNative, WitnessOps}; -use crate::frontend::FCircuit; -use crate::{ - commitment::{kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme}, - folding::traits::Dummy, -}; -use crate::{Curve, Error}; -use crate::{Decider as DeciderTrait, FoldingScheme}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof -where - C: Curve, - CS: CommitmentScheme, - S: SNARK, -{ - snark_proof: S::Proof, - kzg_proofs: [CS::Proof; 2], - // cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are - // checked in-circuit - cmT: C, - r: C::ScalarField, - // the KZG challenges are provided by the prover, but in-circuit they are checked to match - // the in-circuit computed ones. - kzg_challenges: [C::ScalarField; 2], -} - -impl Proof -where - C: Curve, - CS: CommitmentScheme, - S: SNARK, -{ - pub fn snark_proof(&self) -> &S::Proof { - &self.snark_proof - } - - pub fn kzg_proofs(&self) -> &[CS::Proof; 2] { - &self.kzg_proofs - } - - pub fn cmT(&self) -> &C { - &self.cmT - } - - pub fn r(&self) -> C::ScalarField { - self.r - } - - pub fn kzg_challenges(&self) -> [C::ScalarField; 2] { - self.kzg_challenges - } -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct VerifierParam -where - C1: Curve, - CS_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, -{ - pub pp_hash: C1::ScalarField, - pub snark_vp: S_VerifyingKey, - pub cs_vp: CS_VerifyingKey, -} - -/// Onchain Decider, for ethereum use cases -#[derive(Clone, Debug)] -pub struct Decider { - _c1: PhantomData, - _c2: PhantomData, - _fc: PhantomData, - _cs1: PhantomData, - _cs2: PhantomData, - _s: PhantomData, - _fs: PhantomData, -} - -impl DeciderTrait - for Decider -where - C1: Curve, - C2: Curve, - FC: FCircuit, - // CS1 is a KZG commitment, where challenge is C1::Fr elem - CS1: CommitmentScheme< - C1, - ProverChallenge = C1::ScalarField, - Challenge = C1::ScalarField, - Proof = KZGProof, - >, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - S: SNARK, - FS: FoldingScheme, - // constrain FS into Nova, since this is a Decider specifically for Nova - Nova: From, - crate::folding::nova::ProverParams: - From<>::ProverParam>, - crate::folding::nova::VerifierParams: - From<>::VerifierParam>, -{ - type PreprocessorParam = ((FS::ProverParam, FS::VerifierParam), usize); - type ProverParam = (S::ProvingKey, CS1::ProverParams); - type Proof = Proof; - type VerifierParam = VerifierParam; - type PublicInput = Vec; - type CommittedInstance = Vec; - - fn preprocess( - mut rng: impl RngCore + CryptoRng, - ((pp, vp), state_len): Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - // get the FoldingScheme prover & verifier params from Nova - let nova_pp: as FoldingScheme>::ProverParam = - pp.into(); - let nova_vp: as FoldingScheme< - C1, - C2, - FC, - >>::VerifierParam = vp.into(); - - let pp_hash = nova_vp.pp_hash()?; - - let circuit = DeciderEthCircuit::::dummy(( - nova_vp.r1cs, - nova_vp.cf_r1cs, - nova_pp.cf_cs_pp, - nova_pp.poseidon_config, - (), - (), - state_len, - 2, // Nova's running CommittedInstance contains 2 commitments - )); - - // get the Groth16 specific setup for the circuit - let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng) - .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; - - let pp = (g16_pk, nova_pp.cs_pp); - let vp = Self::VerifierParam { - pp_hash, - snark_vp: g16_vk, - cs_vp: nova_vp.cs_vp, - }; - Ok((pp, vp)) - } - - fn prove( - mut rng: impl RngCore + CryptoRng, - pp: Self::ProverParam, - folding_scheme: FS, - ) -> Result { - let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - - let circuit = DeciderEthCircuit::::try_from(Nova::from(folding_scheme))?; - - let cmT = circuit.proof; - let r = circuit.randomness; - - // get the challenges that have been already computed when preparing the circuit inputs in - // the above `try_from` call - let kzg_challenges = circuit.kzg_challenges.clone(); - - // generate KZG proofs - let kzg_proofs = circuit - .W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| { - CS1::prove_with_challenge(&cs_pk, c, v, &C1::ScalarField::zero(), None) - }) - .collect::, _>>()?; - - let snark_proof = - S::prove(&snark_pk, circuit, &mut rng).map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self::Proof { - snark_proof, - cmT, - r, - kzg_proofs: kzg_proofs - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - kzg_challenges: kzg_challenges - .try_into() - .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, - }) - } - - fn verify( - vp: Self::VerifierParam, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - // we don't use the instances at the verifier level, since we check them in-circuit - running_commitments: &Self::CommittedInstance, - incoming_commitments: &Self::CommittedInstance, - proof: &Self::Proof, - ) -> Result { - if i <= C1::ScalarField::one() { - return Err(Error::NotEnoughSteps); - } - - let Self::VerifierParam { - pp_hash, - snark_vp, - cs_vp, - } = vp; - - // 6.2. Fold the commitments - let U_final_commitments = DeciderNovaGadget::fold_group_elements_native( - running_commitments, - incoming_commitments, - Some(proof.cmT), - proof.r, - )?; - - let public_input = [ - &[pp_hash, i][..], - &z_0, - &z_i, - &U_final_commitments.inputize_nonnative(), - &proof.kzg_challenges, - &proof.kzg_proofs.iter().map(|p| p.eval).collect::>(), - &proof.cmT.inputize_nonnative(), - ] - .concat(); - - let snark_v = S::verify(&snark_vp, &public_input, &proof.snark_proof) - .map_err(|e| Error::Other(e.to_string()))?; - if !snark_v { - return Err(Error::SNARKVerificationFail); - } - - // 7.3. Verify the KZG proofs - for ((cm, &c), pi) in U_final_commitments - .iter() - .zip(&proof.kzg_challenges) - .zip(&proof.kzg_proofs) - { - // we're at the Ethereum EVM case, so the CS1 is KZG commitments - CS1::verify_with_challenge(&cs_vp, c, cm, pi)?; - } - - Ok(true) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::commitment::kzg::KZG; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::{PreprocessorParam, ProverParams as NovaProverParams}; - use crate::folding::traits::CommittedInstanceOps; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use ark_bn254::{Bn254, Fr, G1Projective as Projective}; - use ark_groth16::Groth16; - use ark_grumpkin::Projective as Projective2; - use std::time::Instant; - - #[test] - fn test_decider() -> Result<(), Error> { - // use Nova as FoldingScheme - type N = Nova< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - N, // here we define the FoldingScheme to use - >; - - let mut rng = rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let preprocessor_param = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &preprocessor_param)?; - - let start = Instant::now(); - let mut nova = N::init(&nova_params, F_circuit, z_0.clone())?; - println!("Nova initialized, {:?}", start.elapsed()); - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params, F_circuit.state_len()))?; - - let start = Instant::now(); - nova.prove_step(&mut rng, (), None)?; - println!("prove_step, {:?}", start.elapsed()); - nova.prove_step(&mut rng, (), None)?; // do a 2nd step - - // decider proof generation - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("Decider prove, {:?}", start.elapsed()); - - // decider proof verification - let start = Instant::now(); - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); - - // decider proof verification using the deserialized data - let verified = D::verify( - decider_vp, - nova.i, - nova.z_0, - nova.z_i, - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - Ok(()) - } - - // Test to check the serialization and deserialization of diverse Decider related parameters. - // This test is the same test as `test_decider` but it serializes values and then uses the - // deserialized values to continue the checks. - #[test] - fn test_decider_serialization() -> Result<(), Error> { - // use Nova as FoldingScheme - type N = Nova< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - false, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - N, // here we define the FoldingScheme to use - >; - - let mut rng = rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let preprocessor_param = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &preprocessor_param)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (nova_params.clone(), F_circuit.state_len()))?; - - // serialize the Nova params. These params are the trusted setup of the commitment schemes used - // (ie. KZG & Pedersen in this case) - let mut nova_pp_serialized = vec![]; - nova_params - .0 - .serialize_compressed(&mut nova_pp_serialized)?; - let mut nova_vp_serialized = vec![]; - nova_params - .1 - .serialize_compressed(&mut nova_vp_serialized)?; - // deserialize the Nova params. This would be done by the client reading from a file - let nova_pp_deserialized = NovaProverParams::< - Projective, - Projective2, - KZG<'static, Bn254>, - Pedersen, - >::deserialize_compressed( - &mut nova_pp_serialized.as_slice() - )?; - let nova_vp_deserialized = , - >>::vp_deserialize_with_mode( - &mut nova_vp_serialized.as_slice(), - ark_serialize::Compress::Yes, - ark_serialize::Validate::Yes, - (), // fcircuit_params - )?; - - // initialize nova again, but from the deserialized parameters - let nova_params = (nova_pp_deserialized, nova_vp_deserialized); - let mut nova = N::init(&nova_params, F_circuit, z_0)?; - - let start = Instant::now(); - nova.prove_step(&mut rng, (), None)?; - println!("prove_step, {:?}", start.elapsed()); - nova.prove_step(&mut rng, (), None)?; // do a 2nd step - - // decider proof generation - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, nova.clone())?; - println!("Decider prove, {:?}", start.elapsed()); - - // decider proof verification - let start = Instant::now(); - let verified = D::verify( - decider_vp.clone(), - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); - - // The rest of this test will serialize the data and deserialize it back, and use it to - // verify the proof: - - // serialize the verifier_params, proof and public inputs - let mut decider_vp_serialized = vec![]; - decider_vp.serialize_compressed(&mut decider_vp_serialized)?; - let mut proof_serialized = vec![]; - proof.serialize_compressed(&mut proof_serialized)?; - // serialize the public inputs in a single packet - let mut public_inputs_serialized = vec![]; - nova.i.serialize_compressed(&mut public_inputs_serialized)?; - nova.z_0 - .serialize_compressed(&mut public_inputs_serialized)?; - nova.z_i - .serialize_compressed(&mut public_inputs_serialized)?; - - // deserialize back the verifier_params, proof and public inputs - let decider_vp_deserialized = - VerifierParam::< - Projective, - as CommitmentScheme>::VerifierParams, - as SNARK>::VerifyingKey, - >::deserialize_compressed(&mut decider_vp_serialized.as_slice())?; - let proof_deserialized = - Proof::, Groth16>::deserialize_compressed( - &mut proof_serialized.as_slice(), - )?; - - // deserialize the public inputs from the single packet 'public_inputs_serialized' - let mut reader = public_inputs_serialized.as_slice(); - let i_deserialized = Fr::deserialize_compressed(&mut reader)?; - let z_0_deserialized = Vec::::deserialize_compressed(&mut reader)?; - let z_i_deserialized = Vec::::deserialize_compressed(&mut reader)?; - - // decider proof verification using the deserialized data - let verified = D::verify( - decider_vp_deserialized, - i_deserialized, - z_0_deserialized, - z_i_deserialized, - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof_deserialized, - )?; - assert!(verified); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs deleted file mode 100644 index e03853fa1..000000000 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ /dev/null @@ -1,258 +0,0 @@ -/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, -/// other more efficient approaches can be used. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html -use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, - GR1CSVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, marker::PhantomData}; - -use super::{ - nifs::nova_circuits::{CommittedInstanceVar, NIFSGadget}, - nifs::{nova::NIFS, NIFSGadgetTrait, NIFSTrait}, - CommittedInstance, Nova, Witness, -}; -use crate::{ - arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, - commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, - folding::{ - circuits::{ - decider::{ - on_chain::GenericOnchainDeciderCircuit, DeciderEnabledNIFS, EvalGadget, - KZGChallengesGadget, - }, - nonnative::affine::NonNativeAffineVar, - CF1, - }, - traits::{WitnessOps, WitnessVarOps}, - }, - frontend::FCircuit, - transcript::Transcript, - Curve, Error, -}; - -/// In-circuit representation of the Witness associated to the CommittedInstance. -#[derive(Debug, Clone)] -pub struct WitnessVar { - pub E: Vec>, - pub rE: FpVar, - pub W: Vec>, - pub rW: FpVar, -} - -impl AllocVar, CF1> for WitnessVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let E: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; - let rE = - FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; - - let W: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; - let rW = - FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?; - - Ok(Self { E, rE, W, rW }) - }) - } -} - -impl WitnessVarOps for WitnessVar { - fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { - vec![(&self.W, self.rW.clone()), (&self.E, self.rE.clone())] - } -} - -pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< - C1, - C2, - CommittedInstance, - CommittedInstance, - Witness, - R1CS>, - R1CSMatricesVar, FpVar>>, - DeciderNovaGadget, ->; - -/// returns an instance of the DeciderEthCircuit from the given Nova struct -impl< - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - const H: bool, - > TryFrom> for DeciderEthCircuit -{ - type Error = Error; - - fn try_from(nova: Nova) -> Result { - let mut transcript = PoseidonSponge::new_with_pp_hash(&nova.poseidon_config, nova.pp_hash); - - // compute the U_{i+1}, W_{i+1} - let (W_i1, U_i1, cmT, r_bits) = NIFS::, H>::prove( - &nova.cs_pp, - &nova.r1cs.clone(), - &mut transcript, - &nova.W_i, - &nova.U_i, - &nova.w_i, - &nova.u_i, - )?; - let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - // compute the KZG challenges used as inputs in the circuit - let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); - - // get KZG evals - let kzg_evaluations = W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) - .collect::, _>>()?; - - Ok(Self { - _avar: PhantomData, - arith: nova.r1cs, - cf_arith: nova.cf_r1cs, - cf_pedersen_params: nova.cf_cs_pp, - poseidon_config: nova.poseidon_config, - pp_hash: nova.pp_hash, - i: nova.i, - z_0: nova.z_0, - z_i: nova.z_i, - U_i: nova.U_i, - W_i: nova.W_i, - u_i: nova.u_i, - w_i: nova.w_i, - U_i1, - W_i1, - proof: cmT, - randomness: r_Fr, - cf_U_i: nova.cf_U_i, - cf_W_i: nova.cf_W_i, - kzg_challenges, - kzg_evaluations, - }) - } -} - -pub struct DeciderNovaGadget; - -impl - DeciderEnabledNIFS, CommittedInstance, Witness, R1CS>> - for DeciderNovaGadget -{ - type ProofDummyCfg = (); - type Proof = C; - type RandomnessDummyCfg = (); - type Randomness = CF1; - - fn fold_field_elements_gadget( - _arith: &R1CS>, - transcript: &mut PoseidonSpongeVar>, - U: CommittedInstanceVar, - U_vec: Vec>>, - u: CommittedInstanceVar, - proof: C, - _randomness: CF1, - ) -> Result, SynthesisError> { - let cs = U.u.cs(); - let cmT = NonNativeAffineVar::new_input(cs.clone(), || Ok(proof))?; - let (new_U, _) = NIFSGadget::verify(transcript, U, U_vec, u, Some(cmT))?; - Ok(new_U) - } - - fn fold_group_elements_native( - U_commitments: &[C], - u_commitments: &[C], - cmT: Option, - r: Self::Randomness, - ) -> Result, Error> { - let cmT = cmT.ok_or(Error::Empty)?; - let U_cmW = U_commitments[0]; - let U_cmE = U_commitments[1]; - let u_cmW = u_commitments[0]; - let u_cmE = u_commitments[1]; - if !u_cmE.is_zero() { - return Err(Error::NotIncomingCommittedInstance); - } - let cmW = U_cmW + u_cmW.mul(r); - let cmE = U_cmE + cmT.mul(r); - Ok(vec![cmW, cmE]) - } -} - -#[cfg(test)] -pub mod tests { - use ark_pallas::{Fr, Projective}; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - use ark_vesta::Projective as Projective2; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::PreprocessorParam; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::FoldingScheme; - - #[test] - fn test_decider_circuit() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - type N = Nova< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - false, - >; - - let prep_param = PreprocessorParam::< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - false, - >::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &prep_param)?; - - // generate a Nova instance and do a step of it - let mut nova = N::init(&nova_params, F_circuit, z_0.clone())?; - nova.prove_step(&mut rng, (), None)?; - let ivc_proof = nova.ivc_proof(); - N::verify(nova_params.1, ivc_proof)?; - - // load the DeciderEthCircuit from the generated Nova instance - let decider_circuit = DeciderEthCircuit::::try_from(nova)?; - - let cs = ConstraintSystem::::new_ref(); - - // generate the constraints and check that are satisfied by the inputs - decider_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs deleted file mode 100644 index 817d0d321..000000000 --- a/folding-schemes/src/folding/nova/mod.rs +++ /dev/null @@ -1,1167 +0,0 @@ -/// Implements the scheme described in [Nova](https://eprint.iacr.org/2021/370.pdf) and -/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf). -/// -/// The structure of the Nova code is the following: -/// - NIFS implementation for Nova (nifs.rs), Mova (mova.rs), Ova (ova.rs) -/// - IVC and the Decider (offchain Decider & onchain Decider) implementations for Nova -use ark_crypto_primitives::sponge::{ - poseidon::{PoseidonConfig, PoseidonSponge}, - Absorb, -}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{alloc::AllocVar, prelude::Boolean, GR1CSVar}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Valid}; -use ark_std::{cmp::max, fmt::Debug, rand::RngCore, One, UniformRand, Zero}; - -use crate::arith::{ - r1cs::{extract_r1cs, extract_w_x, R1CS}, - Arith, ArithRelation, -}; -use crate::commitment::CommitmentScheme; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::{ - circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCommittedInstance, CycleFoldConfig, - CycleFoldWitness, - }, - CF1, - }, - traits::Dummy, -}; -use crate::frontend::FCircuit; -use crate::transcript::{poseidon::poseidon_canonical_config, Transcript}; -use crate::utils::{pp_hash, vec::is_zero_vec}; -use crate::{Curve, Error, FoldingScheme}; -use decider_eth_circuit::WitnessVar; - -pub mod circuits; -pub mod traits; -pub mod zk; - -// NIFS related: -pub mod nifs; - -use circuits::AugmentedFCircuit; -use nifs::{nova::NIFS, nova_circuits::CommittedInstanceVar, NIFSTrait}; - -// offchain decider -pub mod decider; -pub mod decider_circuits; -// onchain decider -pub mod decider_eth; -pub mod decider_eth_circuit; - -use super::{ - circuits::{cyclefold::CycleFoldCircuit, CF2}, - traits::{CommittedInstanceOps, Inputize, WitnessOps}, -}; - -/// Configuration for Nova's CycleFold circuit -pub struct NovaCycleFoldConfig { - r: Vec, - points: Vec, -} - -impl Default for NovaCycleFoldConfig { - fn default() -> Self { - Self { - r: vec![false; NOVA_N_BITS_RO], - points: vec![C::zero(); 2], - } - } -} - -impl CycleFoldConfig for NovaCycleFoldConfig { - const RANDOMNESS_BIT_LENGTH: usize = NOVA_N_BITS_RO; - // Number of points to be folded in the CycleFold circuit, in Nova's case, this is a fixed - // amount: - // 2 points to be folded. - const N_INPUT_POINTS: usize = 2; - const N_UNIQUE_RANDOMNESSES: usize = 1; - - fn alloc_points(&self, cs: ConstraintSystemRef>) -> Result, SynthesisError> { - let points = Vec::new_witness(cs.clone(), || Ok(self.points.clone()))?; - for point in &points { - Self::mark_point_as_public(point)?; - } - Ok(points) - } - - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError> { - let one = &CF1::::one().into_bigint().to_bits_le()[..NOVA_N_BITS_RO]; - let one_var = Vec::new_constant(cs.clone(), one)?; - let r_var = Vec::new_witness(cs.clone(), || Ok(self.r.clone()))?; - Self::mark_randomness_as_public(&r_var)?; - Ok(vec![one_var, r_var]) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct CommittedInstance { - pub cmE: C, - pub u: C::ScalarField, - pub cmW: C, - pub x: Vec, -} - -impl Dummy for CommittedInstance { - fn dummy(io_len: usize) -> Self { - Self { - cmE: C::zero(), - u: CF1::::zero(), - cmW: C::zero(), - x: vec![CF1::::zero(); io_len], - } - } -} - -impl Dummy<&R1CS>> for CommittedInstance { - fn dummy(r1cs: &R1CS>) -> Self { - Self::dummy(r1cs.n_public_inputs()) - } -} - -impl Absorb for CommittedInstance { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.u.to_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - self.cmE.to_native_sponge_field_elements(dest); - self.cmW.to_native_sponge_field_elements(dest); - } -} - -impl CommittedInstanceOps for CommittedInstance { - type Var = CommittedInstanceVar; - - fn get_commitments(&self) -> Vec { - vec![self.cmW, self.cmE] - } - - fn is_incoming(&self) -> bool { - self.cmE == C::zero() && self.u == One::one() - } -} - -impl Inputize> for CommittedInstance { - /// Returns the internal representation in the same order as how the value - /// is allocated in `CommittedInstanceVar::new_input`. - fn inputize(&self) -> Vec> { - [ - &[self.u][..], - &self.x, - &self.cmE.inputize_nonnative(), - &self.cmW.inputize_nonnative(), - ] - .concat() - } -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Witness { - pub E: Vec, - pub rE: C::ScalarField, - pub W: Vec, - pub rW: C::ScalarField, -} - -impl Witness { - pub fn new(w: Vec, e_len: usize, mut rng: impl RngCore) -> Self { - let (rW, rE) = if H { - ( - C::ScalarField::rand(&mut rng), - C::ScalarField::rand(&mut rng), - ) - } else { - (C::ScalarField::zero(), C::ScalarField::zero()) - }; - - Self { - E: vec![C::ScalarField::zero(); e_len], - rE, - W: w, - rW, - } - } - - pub fn commit, const HC: bool>( - &self, - params: &CS::ProverParams, - x: Vec, - ) -> Result, Error> { - let mut cmE = C::zero(); - if !is_zero_vec::(&self.E) { - cmE = CS::commit(params, &self.E, &self.rE)?; - } - let cmW = CS::commit(params, &self.W, &self.rW)?; - Ok(CommittedInstance { - cmE, - u: C::ScalarField::one(), - cmW, - x, - }) - } -} - -impl Dummy<&R1CS>> for Witness { - fn dummy(r1cs: &R1CS>) -> Self { - Self { - E: vec![C::ScalarField::zero(); r1cs.n_constraints()], - rE: C::ScalarField::zero(), - W: vec![C::ScalarField::zero(); r1cs.n_witnesses()], - rW: C::ScalarField::zero(), - } - } -} - -impl WitnessOps for Witness { - type Var = WitnessVar; - - fn get_openings(&self) -> Vec<(&[C::ScalarField], C::ScalarField)> { - vec![(&self.W, self.rW), (&self.E, self.rE)] - } -} - -#[derive(Debug, Clone)] -pub struct PreprocessorParam -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - pub poseidon_config: PoseidonConfig, - pub F: FC, - // cs params if not provided, will be generated at the preprocess method - pub cs_pp: Option, - pub cs_vp: Option, - pub cf_cs_pp: Option, - pub cf_cs_vp: Option, -} - -impl PreprocessorParam -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - pub fn new(poseidon_config: PoseidonConfig, F: FC) -> Self { - Self { - poseidon_config, - F, - cs_pp: None, - cs_vp: None, - cf_cs_pp: None, - cf_cs_vp: None, - } - } -} - -/// Proving parameters for Nova-based IVC -#[derive(Debug, Clone)] -pub struct ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// Proving parameters of the underlying commitment scheme over C1 - pub cs_pp: CS1::ProverParams, - /// Proving parameters of the underlying commitment scheme over C2 - pub cf_cs_pp: CS2::ProverParams, -} - -impl Valid for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.poseidon_config.full_rounds.check()?; - self.poseidon_config.partial_rounds.check()?; - self.poseidon_config.alpha.check()?; - self.poseidon_config.ark.check()?; - self.poseidon_config.mds.check()?; - self.poseidon_config.rate.check()?; - self.poseidon_config.capacity.check()?; - self.cs_pp.check()?; - self.cf_cs_pp.check()?; - Ok(()) - } -} -impl CanonicalSerialize for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.cs_pp.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_pp.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.cs_pp.serialized_size(compress) + self.cf_cs_pp.serialized_size(compress) - } -} -impl CanonicalDeserialize for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - ) -> Result { - let cs_pp = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_pp = CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - Ok(ProverParams { - poseidon_config: poseidon_canonical_config::(), - cs_pp, - cf_cs_pp, - }) - } -} - -/// Verification parameters for Nova-based IVC -#[derive(Debug, Clone)] -pub struct VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// R1CS of the Augmented step circuit - pub r1cs: R1CS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - /// Verification parameters of the underlying commitment scheme over C1 - pub cs_vp: CS1::VerifierParams, - /// Verification parameters of the underlying commitment scheme over C2 - pub cf_cs_vp: CS2::VerifierParams, -} - -impl Valid for VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.cs_vp.check()?; - self.cf_cs_vp.check()?; - Ok(()) - } -} -impl CanonicalSerialize for VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.cs_vp.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_vp.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.cs_vp.serialized_size(compress) + self.cf_cs_vp.serialized_size(compress) - } -} - -impl VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// returns the hash of the public parameters of Nova - pub fn pp_hash(&self) -> Result { - pp_hash::( - &self.r1cs, - &self.cf_r1cs, - &self.cs_vp, - &self.cf_cs_vp, - &self.poseidon_config, - ) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] -pub struct IVCProof -where - C1: Curve, - C2: Curve, -{ - // current step of the IVC - pub i: C1::ScalarField, - // initial state - pub z_0: Vec, - // current state - pub z_i: Vec, - // running instance - pub W_i: Witness, - pub U_i: CommittedInstance, - // incoming instance - pub w_i: Witness, - pub u_i: CommittedInstance, - // CycleFold instances - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -/// Implements Nova+CycleFold's IVC, described in [Nova](https://eprint.iacr.org/2021/370.pdf) and -/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf), following the FoldingScheme trait -/// The `H` const generic specifies whether the homorphic commitment scheme is blinding -#[derive(Clone, Debug)] -pub struct Nova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// R1CS of the Augmented Function circuit - pub r1cs: R1CS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - pub poseidon_config: PoseidonConfig, - /// CommitmentScheme::ProverParams over C1 - pub cs_pp: CS1::ProverParams, - /// CycleFold CommitmentScheme::ProverParams, over C2 - pub cf_cs_pp: CS2::ProverParams, - /// F circuit, the circuit that is being folded - pub F: FC, - /// public params hash - pub pp_hash: C1::ScalarField, - pub i: C1::ScalarField, - /// initial state - pub z_0: Vec, - /// current i-th state - pub z_i: Vec, - /// Nova instances - pub w_i: Witness, - pub u_i: CommittedInstance, - pub W_i: Witness, - pub U_i: CommittedInstance, - - /// CycleFold running instance - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -impl FoldingScheme - for Nova -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - C1: Curve, -{ - type PreprocessorParam = PreprocessorParam; - type ProverParam = ProverParams; - type VerifierParam = VerifierParams; - type RunningInstance = (CommittedInstance, Witness); - type IncomingInstance = (CommittedInstance, Witness); - type MultiCommittedInstanceWithWitness = (); - type CFInstance = (CycleFoldCommittedInstance, CycleFoldWitness); - type IVCProof = IVCProof; - - fn pp_deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - _fc_params: FC::Params, // FCircuit params - ) -> Result { - Ok(Self::ProverParam::deserialize_with_mode( - reader, compress, validate, - )?) - } - fn vp_deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, - ) -> Result { - let poseidon_config = poseidon_canonical_config::(); - - // generate the r1cs & cf_r1cs needed for the VerifierParams. In this way we avoid needing - // to serialize them, saving significant space in the VerifierParams serialized size. - - // main circuit R1CS: - let f_circuit = FC::new(fc_params)?; - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - let augmented_F_circuit = - AugmentedFCircuit::::empty(&poseidon_config, f_circuit.clone()); - augmented_F_circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - - // CycleFold circuit R1CS - let cs2 = ConstraintSystem::::new_ref(); - cs2.set_mode(SynthesisMode::Setup); - let cf_circuit = CycleFoldCircuit::<_, NovaCycleFoldConfig>::default(); - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - - let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - - Ok(Self::VerifierParam { - poseidon_config, - r1cs, - cf_r1cs, - cs_vp, - cf_cs_vp, - }) - } - - fn preprocess( - mut rng: impl RngCore, - prep_param: &Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - let (r1cs, cf_r1cs) = - get_r1cs::(&prep_param.poseidon_config, prep_param.F.clone())?; - - // if cs params exist, use them, if not, generate new ones - let (cs_pp, cs_vp) = match (&prep_param.cs_pp, &prep_param.cs_vp) { - (Some(cs_pp), Some(cs_vp)) => (cs_pp.clone(), cs_vp.clone()), - _ => CS1::setup( - &mut rng, - // `CS1` is for committing to Nova's witness vector `w` and - // error term `e`, where the length of `e` is the number of - // constraints, so we set `len` to the maximum of `e` and `w`'s - // lengths. - max(r1cs.n_constraints(), r1cs.n_witnesses()), - )?, - }; - let (cf_cs_pp, cf_cs_vp) = match (&prep_param.cf_cs_pp, &prep_param.cf_cs_vp) { - (Some(cf_cs_pp), Some(cf_cs_vp)) => (cf_cs_pp.clone(), cf_cs_vp.clone()), - _ => CS2::setup( - &mut rng, - // `CS2` is for committing to CycleFold's witness vector `w` and - // error term `e`, where the length of `e` is the number of - // constraints, so we set `len` to the maximum of `e` and `w`'s - // lengths. - max(cf_r1cs.n_constraints(), cf_r1cs.n_witnesses()), - )?, - }; - - let prover_params = ProverParams:: { - poseidon_config: prep_param.poseidon_config.clone(), - cs_pp: cs_pp.clone(), - cf_cs_pp: cf_cs_pp.clone(), - }; - let verifier_params = VerifierParams:: { - poseidon_config: prep_param.poseidon_config.clone(), - r1cs, - cf_r1cs, - cs_vp, - cf_cs_vp, - }; - - Ok((prover_params, verifier_params)) - } - - /// Initializes the Nova+CycleFold's IVC for the given parameters and initial state `z_0`. - fn init( - params: &(Self::ProverParam, Self::VerifierParam), - F: FC, - z_0: Vec, - ) -> Result { - let (pp, vp) = params; - - // prepare the circuit to obtain its R1CS - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - let cs2 = ConstraintSystem::::new_ref(); - cs2.set_mode(SynthesisMode::Setup); - - let augmented_F_circuit = - AugmentedFCircuit::::empty(&pp.poseidon_config, F.clone()); - let cf_circuit = CycleFoldCircuit::<_, NovaCycleFoldConfig>::default(); - - augmented_F_circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - - // compute the public params hash - let pp_hash = vp.pp_hash()?; - - // setup the dummy instances - let (W_dummy, U_dummy) = r1cs.dummy_witness_instance(); - let (w_dummy, u_dummy) = r1cs.dummy_witness_instance(); - let (cf_W_dummy, cf_U_dummy) = cf_r1cs.dummy_witness_instance(); - - // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the - // R1CS that we're working with. - Ok(Self { - r1cs, - cf_r1cs, - poseidon_config: pp.poseidon_config.clone(), - cs_pp: pp.cs_pp.clone(), - cf_cs_pp: pp.cf_cs_pp.clone(), - F, - pp_hash, - i: C1::ScalarField::zero(), - z_0: z_0.clone(), - z_i: z_0, - w_i: w_dummy, - u_i: u_dummy, - W_i: W_dummy, - U_i: U_dummy, - // cyclefold running instance - cf_W_i: cf_W_dummy, - cf_U_i: cf_U_dummy, - }) - } - - /// Implements IVC.P of Nova+CycleFold - fn prove_step( - &mut self, - mut rng: impl RngCore, - external_inputs: FC::ExternalInputs, - // Nova does not support multi-instances folding (by design) - _other_instances: Option, - ) -> Result<(), Error> { - // ensure that commitments are blinding if user has specified so. - if H && self.i >= C1::ScalarField::one() { - let blinding_commitments = if self.i == C1::ScalarField::one() { - // blinding values of the running instances are zero at the first iteration - vec![self.w_i.rW, self.w_i.rE] - } else { - vec![self.w_i.rW, self.w_i.rE, self.W_i.rW, self.W_i.rE] - }; - if blinding_commitments.contains(&C1::ScalarField::zero()) { - return Err(Error::IncorrectBlinding( - H, - format!("{blinding_commitments:?}"), - )); - } - } - // `sponge` is for digest computation. - let sponge = PoseidonSponge::::new_with_pp_hash( - &self.poseidon_config, - self.pp_hash, - ); - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - let augmented_F_circuit: AugmentedFCircuit; - - // Nova does not support (by design) multi-instances folding - if _other_instances.is_some() { - return Err(Error::NoMultiInstances); - } - - if self.z_i.len() != self.F.state_len() { - return Err(Error::NotSameLength( - "z_i.len()".to_string(), - self.z_i.len(), - "F.state_len()".to_string(), - self.F.state_len(), - )); - } - - if self.i > C1::ScalarField::from_le_bytes_mod_order(&usize::MAX.to_le_bytes()) { - return Err(Error::MaxStep); - } - - let i_usize; - - #[cfg(target_pointer_width = "64")] - { - let mut i_bytes: [u8; 8] = [0; 8]; - i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..8]); - i_usize = usize::from_le_bytes(i_bytes); - } - - #[cfg(target_pointer_width = "32")] - { - let mut i_bytes: [u8; 4] = [0; 4]; - i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..4]); - i_usize = usize::from_le_bytes(i_bytes); - } - - // fold Nova instances - let (W_i1, U_i1, cmT, r_bits): (Witness, CommittedInstance, C1, Vec) = - NIFS::, H>::prove( - &self.cs_pp, - &self.r1cs, - &mut transcript, - &self.W_i, - &self.U_i, - &self.w_i, - &self.u_i, - )?; - - if self.i == C1::ScalarField::zero() { - // base case - augmented_F_circuit = AugmentedFCircuit:: { - poseidon_config: self.poseidon_config.clone(), - pp_hash: Some(self.pp_hash), - i: Some(C1::ScalarField::zero()), // = i=0 - i_usize: Some(0), - z_0: Some(self.z_0.clone()), // = z_i - z_i: Some(self.z_i.clone()), - external_inputs: Some(external_inputs.clone()), - u_i_cmW: Some(self.u_i.cmW), // = dummy - U_i: Some(self.U_i.clone()), // = dummy - U_i1_cmE: Some(U_i1.cmE), - U_i1_cmW: Some(U_i1.cmW), - cmT: Some(cmT), - F: self.F.clone(), - cf1_u_i_cmW: None, - cf2_u_i_cmW: None, - cf_U_i: None, - cf1_cmT: None, - cf2_cmT: None, - }; - - #[cfg(test)] - { - let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - let expected = - NIFS::, H>::fold_committed_instances( - r_Fr, &self.U_i, &self.u_i, &cmT, - ); - assert_eq!(U_i1, expected); - } - } else { - // CycleFold part: - let (cfW_w_i, cfW_u_i) = NovaCycleFoldConfig { - r: r_bits.clone(), - points: vec![self.U_i.clone().cmW, self.u_i.clone().cmW], - } - .build_circuit() - .generate_incoming_instance_witness::<_, CS2, H>(&self.cf_cs_pp, &mut rng)?; - let (cfE_w_i, cfE_u_i) = NovaCycleFoldConfig { - r: r_bits.clone(), - points: vec![self.U_i.clone().cmE, cmT], - } - .build_circuit() - .generate_incoming_instance_witness::<_, CS2, H>(&self.cf_cs_pp, &mut rng)?; - - let (cf_W_i1, cf_U_i1, cf_cmTs) = CycleFoldAugmentationGadget::fold_native::<_, CS2, H>( - &mut transcript, - &self.cf_r1cs, - &self.cf_cs_pp, - self.cf_W_i.clone(), - self.cf_U_i.clone(), - vec![cfW_w_i, cfE_w_i], - vec![cfW_u_i.clone(), cfE_u_i.clone()], - )?; - - augmented_F_circuit = AugmentedFCircuit:: { - poseidon_config: self.poseidon_config.clone(), - pp_hash: Some(self.pp_hash), - i: Some(self.i), - i_usize: Some(i_usize), - z_0: Some(self.z_0.clone()), - z_i: Some(self.z_i.clone()), - external_inputs: Some(external_inputs.clone()), - u_i_cmW: Some(self.u_i.cmW), - U_i: Some(self.U_i.clone()), - U_i1_cmE: Some(U_i1.cmE), - U_i1_cmW: Some(U_i1.cmW), - cmT: Some(cmT), - F: self.F.clone(), - // cyclefold values - cf1_u_i_cmW: Some(cfW_u_i.cmW), - cf2_u_i_cmW: Some(cfE_u_i.cmW), - cf_U_i: Some(self.cf_U_i.clone()), - cf1_cmT: Some(cf_cmTs[0]), - cf2_cmT: Some(cf_cmTs[1]), - }; - - self.cf_W_i = cf_W_i1; - self.cf_U_i = cf_U_i1; - } - - let cs = ConstraintSystem::::new_ref(); - - let z_i1 = augmented_F_circuit - .compute_next_state(cs.clone())? - .value()?; - - #[cfg(test)] - assert!(cs.is_satisfied()?); - - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let (w_i1, x_i1) = extract_w_x::(&cs); - - #[cfg(test)] - if x_i1.len() != 2 { - return Err(Error::NotExpectedLength(x_i1.len(), 2)); - } - - // set values for next iteration - self.i += C1::ScalarField::one(); - self.z_i = z_i1; - self.w_i = Witness::::new::(w_i1, self.r1cs.n_constraints(), &mut rng); - self.u_i = self.w_i.commit::(&self.cs_pp, x_i1)?; - self.W_i = W_i1; - self.U_i = U_i1; - - #[cfg(test)] - { - self.u_i.check_incoming()?; - self.r1cs.check_relation(&self.w_i, &self.u_i)?; - self.r1cs.check_relation(&self.W_i, &self.U_i)?; - } - - Ok(()) - } - - fn state(&self) -> Vec { - self.z_i.clone() - } - - fn ivc_proof(&self) -> Self::IVCProof { - Self::IVCProof { - i: self.i, - z_0: self.z_0.clone(), - z_i: self.z_i.clone(), - W_i: self.W_i.clone(), - U_i: self.U_i.clone(), - w_i: self.w_i.clone(), - u_i: self.u_i.clone(), - cf_W_i: self.cf_W_i.clone(), - cf_U_i: self.cf_U_i.clone(), - } - } - - fn from_ivc_proof( - ivc_proof: IVCProof, - fcircuit_params: FC::Params, - params: (Self::ProverParam, Self::VerifierParam), - ) -> Result { - let IVCProof { - i, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - let (pp, vp) = params; - - let f_circuit = FC::new(fcircuit_params)?; - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - let cs2 = ConstraintSystem::::new_ref(); - cs2.set_mode(SynthesisMode::Setup); - let augmented_F_circuit = - AugmentedFCircuit::::empty(&pp.poseidon_config, f_circuit.clone()); - let cf_circuit = CycleFoldCircuit::<_, NovaCycleFoldConfig>::default(); - - augmented_F_circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - - Ok(Self { - r1cs, - cf_r1cs, - poseidon_config: pp.poseidon_config, - cs_pp: pp.cs_pp, - cf_cs_pp: pp.cf_cs_pp, - F: f_circuit, - pp_hash: vp.pp_hash()?, - i, - z_0, - z_i, - w_i, - u_i, - W_i, - U_i, - cf_W_i, - cf_U_i, - }) - } - - /// Implements IVC.V of Nov.clone()a+CycleFold. Notice that this method does not include the - /// commitments verification, which is done in the Decider. - fn verify(vp: Self::VerifierParam, ivc_proof: Self::IVCProof) -> Result<(), Error> { - let Self::IVCProof { - i: num_steps, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - - let sponge = - PoseidonSponge::::new_with_pp_hash(&vp.poseidon_config, vp.pp_hash()?); - - if num_steps == C1::ScalarField::zero() { - if z_0 != z_i { - return Err(Error::IVCVerificationFail); - } - return Ok(()); - } - - if u_i.x.len() != 2 || U_i.x.len() != 2 { - return Err(Error::IVCVerificationFail); - } - - // check that u_i's output points to the running instance - // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, num_steps, &z_0, &z_i); - if expected_u_i_x != u_i.x[0] { - return Err(Error::IVCVerificationFail); - } - // u_i.X[1] == H(cf_U_i) - let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&sponge); - if expected_cf_u_i_x != u_i.x[1] { - return Err(Error::IVCVerificationFail); - } - - // check R1CS satisfiability, which is equivalent to checking if `u_i` - // is an incoming instance and if `w_i` and `u_i` satisfy RelaxedR1CS - u_i.check_incoming()?; - vp.r1cs.check_relation(&w_i, &u_i)?; - // check RelaxedR1CS satisfiability - vp.r1cs.check_relation(&W_i, &U_i)?; - - // check CycleFold RelaxedR1CS satisfiability - vp.cf_r1cs.check_relation(&cf_W_i, &cf_U_i)?; - - Ok(()) - } -} - -/// helper method to get the r1cs from the ConstraintSynthesizer -pub fn get_r1cs_from_cs( - circuit: impl ConstraintSynthesizer, -) -> Result, Error> { - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - Ok(r1cs) -} - -/// helper method to get the R1CS for both the AugmentedFCircuit and the CycleFold circuit -#[allow(clippy::type_complexity)] -pub fn get_r1cs( - poseidon_config: &PoseidonConfig, - F_circuit: FC, -) -> Result<(R1CS, R1CS), Error> -where - C1: Curve, - C2: Curve, - FC: FCircuit, - C1: Curve, -{ - let augmented_F_circuit = AugmentedFCircuit::::empty(poseidon_config, F_circuit); - let cf_circuit = CycleFoldCircuit::<_, NovaCycleFoldConfig>::default(); - let r1cs = get_r1cs_from_cs::(augmented_F_circuit)?; - let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; - Ok((r1cs, cf_r1cs)) -} - -#[cfg(test)] -pub mod tests { - use crate::commitment::kzg::KZG; - use ark_bn254::{Bn254, Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - - /// This test tests the Nova+CycleFold IVC, and by consequence it is also testing the - /// AugmentedFCircuit - #[test] - fn test_ivc() -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - - // run the test using Pedersen commitments on both sides of the curve cycle - let _ = test_ivc_opt::, Pedersen, false>( - poseidon_config.clone(), - F_circuit, - 3, - )?; - - let _ = test_ivc_opt::, Pedersen, true>( - poseidon_config.clone(), - F_circuit, - 3, - )?; - - // run the test using KZG for the commitments on the main curve, and Pedersen for the - // commitments on the secondary curve - let _ = test_ivc_opt::, Pedersen, false>( - poseidon_config, - F_circuit, - 3, - )?; - Ok(()) - } - - // test_ivc allowing to choose the CommitmentSchemes - #[allow(clippy::type_complexity)] - pub(crate) fn test_ivc_opt< - CS1: CommitmentScheme, - CS2: CommitmentScheme, - const H: bool, - >( - poseidon_config: PoseidonConfig, - F_circuit: CubicFCircuit, - num_steps: usize, - ) -> Result< - ( - Vec, - Nova, CS1, CS2, H>, - ), - Error, - > { - let mut rng = ark_std::test_rng(); - - let prep_param = - PreprocessorParam::, CS1, CS2, H> { - poseidon_config, - F: F_circuit, - cs_pp: None, - cs_vp: None, - cf_cs_pp: None, - cf_cs_vp: None, - }; - let nova_params = - Nova::, CS1, CS2, H>::preprocess( - &mut rng, - &prep_param, - )?; - - let z_0 = vec![Fr::from(3_u32)]; - let mut nova = Nova::, CS1, CS2, H>::init( - &nova_params, - F_circuit, - z_0.clone(), - )?; - - for _ in 0..num_steps { - nova.prove_step(&mut rng, (), None)?; - } - assert_eq!(Fr::from(num_steps as u32), nova.i); - - // serialize the Nova Prover & Verifier params. These params are the trusted setup of the commitment schemes used - let mut nova_pp_serialized = vec![]; - nova_params - .0 - .serialize_compressed(&mut nova_pp_serialized)?; - let mut nova_vp_serialized = vec![]; - nova_params - .1 - .serialize_compressed(&mut nova_vp_serialized)?; - - // deserialize the Nova params - let _nova_pp_deserialized = - ProverParams::::deserialize_compressed( - &mut nova_pp_serialized.as_slice(), - )?; - let nova_vp_deserialized = Nova::< - Projective, - Projective2, - CubicFCircuit, - CS1, - CS2, - H, - >::vp_deserialize_with_mode( - &mut nova_vp_serialized.as_slice(), - ark_serialize::Compress::Yes, - ark_serialize::Validate::Yes, - (), // fcircuit_params - )?; - - let ivc_proof = nova.ivc_proof(); - - // serialize IVCProof - let mut ivc_proof_serialized = vec![]; - assert!(ivc_proof - .serialize_compressed(&mut ivc_proof_serialized) - .is_ok()); - // deserialize IVCProof - let ivc_proof_deserialized = - , CS1, CS2, H> as FoldingScheme< - Projective, - Projective2, - CubicFCircuit, - >>::IVCProof::deserialize_compressed(ivc_proof_serialized.as_slice())?; - - // verify the deserialized IVCProof with the deserialized VerifierParams - Nova::, CS1, CS2, H>::verify( - nova_vp_deserialized, // Nova's verifier params - ivc_proof_deserialized, - )?; - Ok((z_0, nova)) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/mod.rs b/folding-schemes/src/folding/nova/nifs/mod.rs deleted file mode 100644 index 652649a4e..000000000 --- a/folding-schemes/src/folding/nova/nifs/mod.rs +++ /dev/null @@ -1,322 +0,0 @@ -/// This module defines the traits related to the NIFS (Non-Interactive Folding Scheme). -/// - NIFSTrait, which implements the NIFS interface -/// - NIFSGadget, which implements the NIFS in-circuit -/// -/// Both traits implemented by the various Nova variants schemes; ie. -/// - [Nova](https://eprint.iacr.org/2021/370.pdf) -/// - [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) -/// - [Mova](https://eprint.iacr.org/2024/1220.pdf) -use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, Absorb, CryptographicSponge}; -use ark_r1cs_std::{alloc::AllocVar, boolean::Boolean, fields::fp::FpVar}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::fmt::Debug; -use ark_std::rand::RngCore; - -use crate::arith::r1cs::R1CS; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::CF1; -use crate::folding::traits::{CommittedInstanceOps, CommittedInstanceVarOps}; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::{Curve, Error}; - -pub mod mova; -pub mod nova; -pub mod nova_circuits; -pub mod ova; -pub mod ova_circuits; -pub mod pointvsline; - -/// Defines the NIFS (Non-Interactive Folding Scheme) trait, initially defined in -/// [Nova](https://eprint.iacr.org/2021/370.pdf), and it's variants -/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) and -/// [Mova](https://eprint.iacr.org/2024/1220.pdf). -/// `H` specifies whether the NIFS will use a blinding factor. -pub trait NIFSTrait< - C: Curve, - CS: CommitmentScheme, - T: Transcript, - const H: bool = false, -> -{ - type CommittedInstance: Debug + Clone + Absorb; // + CommittedInstanceOps; - type Witness: Debug + Clone; - type ProverAux: Debug + Clone; // Prover's aux params. eg. in Nova is T - type Proof: Debug + Clone; // proof. eg. in Nova is cmT - - fn new_witness(w: Vec, e_len: usize, rng: impl RngCore) -> Self::Witness; - - fn new_instance( - rng: impl RngCore, - params: &CS::ProverParams, - w: &Self::Witness, - x: Vec, - aux: Vec, // t_or_e in Ova, empty for Nova - ) -> Result; - - fn fold_witness( - r: C::ScalarField, - W: &Self::Witness, // running witness - w: &Self::Witness, // incoming witness - aux: &Self::ProverAux, - ) -> Result; - - /// NIFS.P. Returns a tuple containing the folded Witness, the folded CommittedInstance, and - /// the used challenge `r` as a vector of bits, so that it can be reused in other methods. - #[allow(clippy::type_complexity)] - #[allow(clippy::too_many_arguments)] - fn prove( - cs_prover_params: &CS::ProverParams, - r1cs: &R1CS, - transcript: &mut T, - W_i: &Self::Witness, // running witness - U_i: &Self::CommittedInstance, // running committed instance - w_i: &Self::Witness, // incoming witness - u_i: &Self::CommittedInstance, // incoming committed instance - ) -> Result< - ( - Self::Witness, - Self::CommittedInstance, - Self::Proof, - Vec, - ), - Error, - >; - - /// NIFS.V. Returns the folded CommittedInstance and the used challenge `r` as a vector of - /// bits, so that it can be reused in other methods. - fn verify( - transcript: &mut T, - U_i: &Self::CommittedInstance, - u_i: &Self::CommittedInstance, - proof: &Self::Proof, - ) -> Result<(Self::CommittedInstance, Vec), Error>; -} - -/// Defines the NIFS (Non-Interactive Folding Scheme) Gadget trait, which specifies the in-circuit -/// logic of the NIFS.Verify defined in [Nova](https://eprint.iacr.org/2021/370.pdf) and it's -/// variants [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) and -/// [Mova](https://eprint.iacr.org/2024/1220.pdf). -pub trait NIFSGadgetTrait, S>> { - type CommittedInstance: Debug + Clone + Absorb + CommittedInstanceOps; - type CommittedInstanceVar: Debug - + Clone - + AbsorbGadget - + AllocVar> - + CommittedInstanceVarOps; - type Proof: Debug + Clone; - type ProofVar: Debug + Clone + AllocVar>; - - /// Implements the constraints for NIFS.V for u and x, since cm(E) and cm(W) are delegated to - /// the CycleFold circuit. - #[allow(clippy::type_complexity)] - fn verify( - transcript: &mut T, - U_i: Self::CommittedInstanceVar, - // U_i_vec is passed to reuse the already computed U_i_vec from previous methods - U_i_vec: Vec>>, - u_i: Self::CommittedInstanceVar, - proof: Option, - ) -> Result<(Self::CommittedInstanceVar, Vec>>), SynthesisError>; -} - -/// These tests are the generic tests so that in the tests of Nova, Mova, Ova, we just need to -/// instantiate these tests to test both the NIFSTrait and NIFSGadgetTrait implementations for each -/// of the schemes. -#[cfg(test)] -pub mod tests { - use ark_crypto_primitives::sponge::{ - constraints::AbsorbGadget, - poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, - Absorb, - }; - use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{cmp::max, test_rng, UniformRand}; - - use super::NIFSTrait; - use super::*; - use crate::arith::{ - r1cs::tests::{get_test_r1cs, get_test_z}, - Arith, - }; - use crate::commitment::pedersen::Pedersen; - use crate::folding::traits::{CommittedInstanceOps, CommittedInstanceVarOps}; - use crate::transcript::poseidon::poseidon_canonical_config; - - /// Test method used to test the different implementations of the NIFSTrait (ie. Nova, Mova, - /// Ova). Runs a loop using the NIFS trait, and returns the last Witness and CommittedInstance - /// so that their relation can be checked. - pub(crate) fn test_nifs_opt< - N: NIFSTrait, PoseidonSponge>, - >() -> Result<(N::Witness, N::CommittedInstance), Error> { - let r1cs: R1CS = get_test_r1cs(); - - let mut rng = ark_std::test_rng(); - let (pedersen_params, _) = - Pedersen::::setup(&mut rng, max(r1cs.n_constraints(), r1cs.n_witnesses()))?; - - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::rand(&mut rng); - let mut transcript_p = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - let mut transcript_v = transcript_p.clone(); - - // prepare the running instance - let z = get_test_z(3); - let (w, x) = r1cs.split_z(&z); - let mut W_i = N::new_witness(w.clone(), r1cs.n_constraints(), test_rng()); - let mut U_i = N::new_instance(&mut rng, &pedersen_params, &W_i, x, vec![])?; - - let num_iters = 10; - for i in 0..num_iters { - // prepare the incoming instance - let incoming_instance_z = get_test_z(i + 4); - let (w, x) = r1cs.split_z(&incoming_instance_z); - let w_i = N::new_witness(w.clone(), r1cs.n_constraints(), test_rng()); - let u_i = N::new_instance(&mut rng, &pedersen_params, &w_i, x, vec![])?; - - // NIFS.P - let (folded_witness, _, proof, _) = N::prove( - &pedersen_params, - &r1cs, - &mut transcript_p, - &W_i, - &U_i, - &w_i, - &u_i, - )?; - - // NIFS.V - let (folded_committed_instance, _) = N::verify(&mut transcript_v, &U_i, &u_i, &proof)?; - - // set running_instance for next loop iteration - W_i = folded_witness; - U_i = folded_committed_instance; - } - - Ok((W_i, U_i)) - } - - /// Test method used to test the different implementations of the NIFSGadgetTrait (ie. Nova, - /// Mova, Ova). It returns the last Witness and CommittedInstance so that it can be checked at - /// the parent test that their values match. - pub(crate) fn test_nifs_gadget_opt( - ci: Vec, - proof: NG::Proof, - ) -> Result<(NG::CommittedInstance, NG::CommittedInstanceVar), Error> - where - N: NIFSTrait, PoseidonSponge>, - NG: NIFSGadgetTrait< - Projective, - PoseidonSponge, - PoseidonSpongeVar, - CommittedInstance = N::CommittedInstance, // constrain that N::CI==NG::CI - Proof = N::Proof, // constrain that N::Proof==NG::Proof - >, - { - let mut rng = ark_std::test_rng(); - - let (U_i, u_i) = (ci[0].clone(), ci[1].clone()); - let pp_hash = Fr::rand(&mut rng); - let poseidon_config = poseidon_canonical_config::(); - let mut transcript = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - let (ci3, _) = N::verify(&mut transcript, &U_i, &u_i, &proof)?; - - let cs = ConstraintSystem::::new_ref(); - - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let mut transcriptVar = - PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - let ci1Var = NG::CommittedInstanceVar::new_witness(cs.clone(), || Ok(U_i.clone()))?; - let ci2Var = NG::CommittedInstanceVar::new_witness(cs.clone(), || Ok(u_i.clone()))?; - let proofVar = NG::ProofVar::new_witness(cs.clone(), || Ok(proof))?; - - let ci1Var_vec = ci1Var.to_sponge_field_elements()?; - let (out, _) = NG::verify( - &mut transcriptVar, - ci1Var.clone(), - ci1Var_vec, - ci2Var.clone(), - Some(proofVar.clone()), - )?; - assert!(cs.is_satisfied()?); - - // return the NIFS.V and the NIFSGadget.V obtained values, so that they are checked at the - // parent test - Ok((ci3, out)) - } - - /// test that checks the native CommittedInstance.to_sponge_{bytes,field_elements} - /// vs the R1CS constraints version - pub(crate) fn test_committed_instance_to_sponge_preimage_opt( - ci: N::CommittedInstance, - ) -> Result<(), Error> - where - N: NIFSTrait, PoseidonSponge>, - NG: NIFSGadgetTrait< - Projective, - PoseidonSponge, - PoseidonSpongeVar, - CommittedInstance = N::CommittedInstance, // constrain that N::CI==NG::CI - >, - { - let bytes = ci.to_sponge_bytes_as_vec(); - let field_elements = ci.to_sponge_field_elements_as_vec(); - - let cs = ConstraintSystem::::new_ref(); - - let ciVar = NG::CommittedInstanceVar::new_witness(cs.clone(), || Ok(ci.clone()))?; - let bytes_var = ciVar.to_sponge_bytes()?; - let field_elements_var = ciVar.to_sponge_field_elements()?; - - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(bytes_var.value()?, bytes); - assert_eq!(field_elements_var.value()?, field_elements); - Ok(()) - } - - pub(crate) fn test_committed_instance_hash_opt( - ci: NG::CommittedInstance, - ) -> Result<(), Error> - where - N: NIFSTrait, PoseidonSponge>, - NG: NIFSGadgetTrait< - Projective, - PoseidonSponge, - PoseidonSpongeVar, - CommittedInstance = N::CommittedInstance, // constrain that N::CI==NG::CI - >, - N::CommittedInstance: CommittedInstanceOps, - { - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for test - let sponge = PoseidonSponge::::new_with_pp_hash(&poseidon_config, pp_hash); - - let i = Fr::from(3_u32); - let z_0 = vec![Fr::from(3_u32)]; - let z_i = vec![Fr::from(3_u32)]; - - // compute the CommittedInstance hash natively - let h = ci.hash(&sponge, i, &z_0, &z_i); - - let cs = ConstraintSystem::::new_ref(); - - let pp_hashVar = FpVar::::new_witness(cs.clone(), || Ok(pp_hash))?; - let iVar = FpVar::::new_witness(cs.clone(), || Ok(i))?; - let z_0Var = Vec::>::new_witness(cs.clone(), || Ok(z_0.clone()))?; - let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i.clone()))?; - let ciVar = NG::CommittedInstanceVar::new_witness(cs.clone(), || Ok(ci.clone()))?; - - let sponge = PoseidonSpongeVar::::new_with_pp_hash(&poseidon_config, &pp_hashVar)?; - - // compute the CommittedInstance hash in-circuit - let (hVar, _) = ciVar.hash(&sponge, &iVar, &z_0Var, &z_iVar)?; - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(hVar.value()?, h); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/mova.rs b/folding-schemes/src/folding/nova/nifs/mova.rs deleted file mode 100644 index 06c511be4..000000000 --- a/folding-schemes/src/folding/nova/nifs/mova.rs +++ /dev/null @@ -1,391 +0,0 @@ -/// This module contains the implementation the NIFSTrait for the -/// [Mova](https://eprint.iacr.org/2024/1220.pdf) NIFS (Non-Interactive Folding Scheme). -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::PrimeField; -use ark_poly::Polynomial; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{log2, marker::PhantomData, rand::RngCore, One, UniformRand, Zero}; - -use super::{ - nova::NIFS as NovaNIFS, - pointvsline::{PointVsLine, PointVsLineProof, PointvsLineEvaluationClaim}, - NIFSTrait, -}; -use crate::arith::{r1cs::R1CS, Arith, ArithRelation}; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::CF1; -use crate::folding::traits::Dummy; -use crate::transcript::Transcript; -use crate::utils::{ - mle::dense_vec_to_dense_mle, - vec::{is_zero_vec, vec_add, vec_scalar_mul}, -}; -use crate::{Curve, Error}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct CommittedInstance { - // Random evaluation point for the E - pub rE: Vec, - // mleE is the evaluation of the MLE of E at r_E - pub mleE: C::ScalarField, - pub u: C::ScalarField, - pub cmW: C, - pub x: Vec, -} - -impl Absorb for CommittedInstance { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.u.to_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - self.rE.to_sponge_field_elements(dest); - self.mleE.to_sponge_field_elements(dest); - self.cmW.to_native_sponge_field_elements(dest); - } -} - -impl Dummy for CommittedInstance { - fn dummy(io_len: usize) -> Self { - Self { - rE: vec![C::ScalarField::zero(); io_len], - mleE: C::ScalarField::zero(), - u: C::ScalarField::zero(), - cmW: C::zero(), - x: vec![C::ScalarField::zero(); io_len], - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Witness { - pub E: Vec, - pub W: Vec, - pub rW: C::ScalarField, -} - -impl Dummy<&R1CS> for Witness { - fn dummy(r1cs: &R1CS) -> Self { - Self { - E: vec![C::ScalarField::zero(); r1cs.n_constraints()], - W: vec![C::ScalarField::zero(); r1cs.n_witnesses()], - rW: C::ScalarField::zero(), - } - } -} - -impl Witness { - pub fn new(w: Vec, e_len: usize, mut rng: impl RngCore) -> Self { - let rW = if H { - C::ScalarField::rand(&mut rng) - } else { - C::ScalarField::zero() - }; - - Self { - E: vec![C::ScalarField::zero(); e_len], - W: w, - rW, - } - } - - pub fn commit, const H: bool>( - &self, - params: &CS::ProverParams, - x: Vec, - rE: Vec, - ) -> Result, Error> { - let mut mleE = C::ScalarField::zero(); - if !is_zero_vec::(&self.E) { - let E = dense_vec_to_dense_mle(log2(self.E.len()) as usize, &self.E); - mleE = E.evaluate(&rE); - } - let cmW = CS::commit(params, &self.W, &self.rW)?; - Ok(CommittedInstance { - rE, - mleE, - u: C::ScalarField::one(), - cmW, - x, - }) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof { - pub h_proof: PointVsLineProof, - pub mleE1_prime: C::ScalarField, - pub mleE2_prime: C::ScalarField, - pub mleT: C::ScalarField, - pub rE_prime: Vec, -} - -/// Implements the Non-Interactive Folding Scheme described in section 4 of -/// [Mova](https://eprint.iacr.org/2024/1220.pdf). -/// `H` specifies whether the NIFS will use a blinding factor -pub struct NIFS< - C: Curve, - CS: CommitmentScheme, - T: Transcript, - const H: bool = false, -> { - _c: PhantomData, - _cp: PhantomData, - _ct: PhantomData, -} - -impl, T: Transcript, const H: bool> - NIFSTrait for NIFS -{ - type CommittedInstance = CommittedInstance; - type Witness = Witness; - type ProverAux = Vec; // T in Mova's notation - type Proof = Proof; - - fn new_witness(w: Vec, e_len: usize, rng: impl RngCore) -> Self::Witness { - Witness::new::(w, e_len, rng) - } - - fn new_instance( - mut rng: impl RngCore, - params: &CS::ProverParams, - W: &Self::Witness, - x: Vec, - aux: Vec, // = r_E - ) -> Result { - let mut rE = aux.clone(); - if is_zero_vec(&rE) { - // means that we're in a fresh instance, so generate random value - rE = (0..log2(W.E.len())) - .map(|_| C::ScalarField::rand(&mut rng)) - .collect(); - } - - W.commit::(params, x, rE) - } - - // Protocol 7 - point 3 (16) - fn fold_witness( - a: C::ScalarField, - W_i: &Witness, - w_i: &Witness, - aux: &Vec, // T in Mova's notation - ) -> Result, Error> { - let a2 = a * a; - let E: Vec = vec_add( - &vec_add(&W_i.E, &vec_scalar_mul(aux, &a))?, - &vec_scalar_mul(&w_i.E, &a2), - )?; - let W: Vec = W_i - .W - .iter() - .zip(&w_i.W) - .map(|(i1, i2)| *i1 + (a * i2)) - .collect(); - - let rW = W_i.rW + a * w_i.rW; - Ok(Witness:: { E, W, rW }) - } - - /// [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 4. Protocol 8 - /// Returns a proof for the pt-vs-line operations along with the folded committed instance - /// instances and witness - #[allow(clippy::type_complexity)] - fn prove( - _cs_prover_params: &CS::ProverParams, // not used in Mova since we don't commit to T - r1cs: &R1CS, - transcript: &mut T, - W_i: &Witness, - U_i: &CommittedInstance, - w_i: &Witness, - u_i: &CommittedInstance, - ) -> Result< - ( - Self::Witness, - Self::CommittedInstance, - Self::Proof, - Vec, - ), - Error, - > { - // Protocol 5 is pre-processing - transcript.absorb(U_i); - transcript.absorb(u_i); - - // Protocol 6 - let ( - h_proof, - PointvsLineEvaluationClaim { - mleE1_prime, - mleE2_prime, - rE_prime, - }, - ) = PointVsLine::::prove(transcript, U_i, u_i, W_i, w_i)?; - - // Protocol 7 - - transcript.absorb(&mleE1_prime); - transcript.absorb(&mleE2_prime); - - // compute the cross terms - let z1: Vec = [vec![U_i.u], U_i.x.to_vec(), W_i.W.to_vec()].concat(); - let z2: Vec = [vec![u_i.u], u_i.x.to_vec(), w_i.W.to_vec()].concat(); - let T = NovaNIFS::::compute_T(r1cs, U_i.u, u_i.u, &z1, &z2, &W_i.E, &w_i.E)?; - - let n_vars: usize = log2(W_i.E.len()) as usize; - if log2(T.len()) as usize != n_vars { - return Err(Error::NotExpectedLength(T.len(), n_vars)); - } - - let mleT = dense_vec_to_dense_mle(n_vars, &T); - let mleT_evaluated = mleT.evaluate(&rE_prime); - - transcript.absorb(&mleT_evaluated); - - let alpha: C::ScalarField = transcript.get_challenge(); - - let ci = Self::fold_committed_instance( - alpha, - U_i, - u_i, - &rE_prime, - &mleE1_prime, - &mleE2_prime, - &mleT_evaluated, - )?; - let w = Self::fold_witness(alpha, W_i, w_i, &T)?; - - let proof = Self::Proof { - h_proof, - mleE1_prime, - mleE2_prime, - mleT: mleT_evaluated, - rE_prime, - }; - Ok(( - w, - ci, - proof, - vec![], // r_bits, returned to be passed as inputs to the circuit, not used at the - // current impl status - )) - } - - /// [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 4. It verifies the results from the proof - /// Both the folding and the pt-vs-line proof - /// returns the folded committed instance - fn verify( - transcript: &mut T, - U_i: &CommittedInstance, - u_i: &CommittedInstance, - proof: &Proof, - ) -> Result<(Self::CommittedInstance, Vec), Error> { - transcript.absorb(U_i); - transcript.absorb(u_i); - let rE_prime = PointVsLine::::verify( - transcript, - U_i, - u_i, - &proof.h_proof, - &proof.mleE1_prime, - &proof.mleE2_prime, - &proof.rE_prime, - )?; - - transcript.absorb(&proof.mleE1_prime); - transcript.absorb(&proof.mleE2_prime); - transcript.absorb(&proof.mleT); - - let alpha: C::ScalarField = transcript.get_challenge(); - - Ok(( - Self::fold_committed_instance( - alpha, - U_i, - u_i, - &rE_prime, - &proof.mleE1_prime, - &proof.mleE2_prime, - &proof.mleT, - )?, - vec![], - )) - } -} - -impl, T: Transcript, const H: bool> - NIFS -{ - // Protocol 7 - point 3 (15) - fn fold_committed_instance( - a: C::ScalarField, - U_i: &CommittedInstance, - u_i: &CommittedInstance, - rE_prime: &[C::ScalarField], - mleE1_prime: &C::ScalarField, - mleE2_prime: &C::ScalarField, - mleT: &C::ScalarField, - ) -> Result, Error> { - let a2 = a * a; - let mleE = *mleE1_prime + a * mleT + a2 * mleE2_prime; - let u = U_i.u + a * u_i.u; - let cmW = U_i.cmW + u_i.cmW.mul(a); - let x = U_i - .x - .iter() - .zip(&u_i.x) - .map(|(i1, i2)| *i1 + (a * i2)) - .collect::>(); - - Ok(CommittedInstance:: { - rE: rE_prime.to_vec(), - mleE, - u, - cmW, - x, - }) - } -} - -impl ArithRelation, CommittedInstance> for R1CS> { - type Evaluation = Vec>; - - fn eval_relation( - &self, - w: &Witness, - u: &CommittedInstance, - ) -> Result { - self.eval_at_z(&[&[u.u][..], &u.x, &w.W].concat()) - } - - fn check_evaluation( - w: &Witness, - _u: &CommittedInstance, - e: Self::Evaluation, - ) -> Result<(), Error> { - (w.E == e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - - use crate::arith::{r1cs::tests::get_test_r1cs, ArithRelation}; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::nifs::tests::test_nifs_opt; - - #[test] - fn test_nifs_mova() -> Result<(), Error> { - let (W, U) = test_nifs_opt::, PoseidonSponge>>()?; - - // check the last folded instance relation - let r1cs = get_test_r1cs(); - r1cs.check_relation(&W, &U)?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/nova.rs b/folding-schemes/src/folding/nova/nifs/nova.rs deleted file mode 100644 index c4f4274c6..000000000 --- a/folding-schemes/src/folding/nova/nifs/nova.rs +++ /dev/null @@ -1,290 +0,0 @@ -/// This module contains the implementation the NIFSTrait for the -/// [Nova](https://eprint.iacr.org/2021/370.pdf) NIFS (Non-Interactive Folding Scheme). -use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, Absorb, CryptographicSponge}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::rand::RngCore; -use ark_std::Zero; -use std::marker::PhantomData; - -use super::NIFSTrait; -use crate::arith::r1cs::R1CS; -use crate::commitment::CommitmentScheme; -use crate::constants::NOVA_N_BITS_RO; -use crate::folding::circuits::{ - cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, - nonnative::affine::NonNativeAffineVar, - CF1, -}; -use crate::folding::nova::{CommittedInstance, Witness}; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::utils::vec::{hadamard, mat_vec_mul, vec_add, vec_scalar_mul, vec_sub}; -use crate::{Curve, Error}; - -/// ChallengeGadget computes the RO challenge used for the Nova instances NIFS, it contains a -/// rust-native and an in-circuit compatible versions. -pub struct ChallengeGadget { - _c: PhantomData, - _ci: PhantomData, -} -impl ChallengeGadget { - pub fn get_challenge_native>( - transcript: &mut T, - U_i: &CI, - u_i: &CI, - cmT: Option<&C>, - ) -> Vec { - transcript.absorb(&U_i); - transcript.absorb(&u_i); - // in the Nova case we absorb the cmT, in Ova case we don't since it is not used. - if let Some(cmT_value) = cmT { - transcript.absorb_nonnative(cmT_value); - } - transcript.squeeze_bits(NOVA_N_BITS_RO) - } - - // compatible with the native get_challenge_native - pub fn get_challenge_gadget< - S: CryptographicSponge, - T: TranscriptVar, S>, - CIVar: AbsorbGadget>, - >( - transcript: &mut T, - U_i_vec: Vec>>, // apready processed input, so we don't have to recompute these values - u_i: CIVar, - cmT: Option>, - ) -> Result>, SynthesisError> { - transcript.absorb(&U_i_vec)?; - transcript.absorb(&u_i)?; - // in the Nova case we absorb the cmT, in Ova case we don't since it is not used. - if let Some(cmT_value) = cmT { - transcript.absorb_nonnative(&cmT_value)?; - } - transcript.squeeze_bits(NOVA_N_BITS_RO) - } -} - -/// Implements the Non-Interactive Folding Scheme described in section 4 of -/// [Nova](https://eprint.iacr.org/2021/370.pdf). -/// `H` specifies whether the NIFS will use a blinding factor -pub struct NIFS< - C: Curve, - CS: CommitmentScheme, - T: Transcript, - const H: bool = false, -> { - _c: PhantomData, - _cp: PhantomData, - _t: PhantomData, -} - -impl, T: Transcript, const H: bool> - NIFSTrait for NIFS -{ - type CommittedInstance = CommittedInstance; - type Witness = Witness; - type ProverAux = Vec; - type Proof = C; - - fn new_witness(w: Vec, e_len: usize, rng: impl RngCore) -> Self::Witness { - Witness::new::(w, e_len, rng) - } - - fn new_instance( - _rng: impl RngCore, - params: &CS::ProverParams, - W: &Self::Witness, - x: Vec, - _aux: Vec, - ) -> Result { - W.commit::(params, x) - } - - fn fold_witness( - r: C::ScalarField, - W_i: &Self::Witness, - w_i: &Self::Witness, - aux: &Self::ProverAux, // T in Nova's notation - ) -> Result { - let r2 = r * r; - let E: Vec = vec_add( - &vec_add(&W_i.E, &vec_scalar_mul(aux, &r))?, // aux is Nova's T - &vec_scalar_mul(&w_i.E, &r2), - )?; - // use r_T=0 since we don't need hiding property for cm(T) - let rT = C::ScalarField::zero(); - let rE = W_i.rE + r * rT + r2 * w_i.rE; - let W: Vec = W_i - .W - .iter() - .zip(&w_i.W) - .map(|(a, b)| *a + (r * b)) - .collect(); - - let rW = W_i.rW + r * w_i.rW; - Ok(Self::Witness { E, rE, W, rW }) - } - - fn prove( - cs_prover_params: &CS::ProverParams, - r1cs: &R1CS, - transcript: &mut T, - W_i: &Self::Witness, - U_i: &Self::CommittedInstance, - w_i: &Self::Witness, - u_i: &Self::CommittedInstance, - ) -> Result< - ( - Self::Witness, - Self::CommittedInstance, - Self::Proof, - Vec, - ), - Error, - > { - // compute the cross terms - let z1: Vec = [vec![U_i.u], U_i.x.to_vec(), W_i.W.to_vec()].concat(); - let z2: Vec = [vec![u_i.u], u_i.x.to_vec(), w_i.W.to_vec()].concat(); - let T = Self::compute_T(r1cs, U_i.u, u_i.u, &z1, &z2, &W_i.E, &w_i.E)?; - - // use r_T=0 since we don't need hiding property for cm(T) - let cmT = CS::commit(cs_prover_params, &T, &C::ScalarField::zero())?; - - let r_bits = ChallengeGadget::::get_challenge_native( - transcript, - U_i, - u_i, - Some(&cmT), - ); - let r_Fr = C::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - let w = Self::fold_witness(r_Fr, W_i, w_i, &T)?; - - let ci = Self::fold_committed_instances(r_Fr, U_i, u_i, &cmT); - - Ok((w, ci, cmT, r_bits)) - } - - fn verify( - transcript: &mut T, - U_i: &Self::CommittedInstance, - u_i: &Self::CommittedInstance, - cmT: &C, // Proof - ) -> Result<(Self::CommittedInstance, Vec), Error> { - let r_bits = ChallengeGadget::::get_challenge_native( - transcript, - U_i, - u_i, - Some(cmT), - ); - let r = C::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - Ok((Self::fold_committed_instances(r, U_i, u_i, cmT), r_bits)) - } -} - -impl, T: Transcript, const H: bool> - NIFS -{ - /// compute_T: compute cross-terms T. We use the approach described in - /// [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. - pub fn compute_T( - r1cs: &R1CS, - u1: C::ScalarField, - u2: C::ScalarField, - z1: &[C::ScalarField], - z2: &[C::ScalarField], - E1: &[C::ScalarField], - E2: &[C::ScalarField], - ) -> Result, Error> { - let z = vec_add(z1, z2)?; - - // this is parallelizable (for the future) - let Az = mat_vec_mul(&r1cs.A, &z)?; - let Bz = mat_vec_mul(&r1cs.B, &z)?; - let Cz = mat_vec_mul(&r1cs.C, &z)?; - let u = u1 + u2; - let uCz = vec_scalar_mul(&Cz, &u); - let AzBz = hadamard(&Az, &Bz)?; - let lhs = vec_sub(&AzBz, &uCz)?; - vec_sub(&vec_sub(&lhs, E1)?, E2) - } - - pub fn compute_cyclefold_cmT( - cs_prover_params: &CS::ProverParams, - r1cs: &R1CS, // R1CS over C2.Fr=C1.Fq (here C=C2) - w1: &CycleFoldWitness, - ci1: &CycleFoldCommittedInstance, - w2: &CycleFoldWitness, - ci2: &CycleFoldCommittedInstance, - ) -> Result<(Vec, C), Error> { - let z1: Vec = [vec![ci1.u], ci1.x.to_vec(), w1.W.to_vec()].concat(); - let z2: Vec = [vec![ci2.u], ci2.x.to_vec(), w2.W.to_vec()].concat(); - - // compute cross terms - let T = Self::compute_T(r1cs, ci1.u, ci2.u, &z1, &z2, &w1.E, &w2.E)?; - // use r_T=0 since we don't need hiding property for cm(T) - let cmT = CS::commit(cs_prover_params, &T, &C::ScalarField::zero())?; - Ok((T, cmT)) - } - - /// folds two committed instances with the given r and cmT. This method is used by - /// Nova::verify, but also by Nova::prove and the CycleFoldNIFS::verify. - pub fn fold_committed_instances( - r: C::ScalarField, - U_i: &CommittedInstance, - u_i: &CommittedInstance, - cmT: &C, - ) -> CommittedInstance { - let r2 = r * r; - let cmE = U_i.cmE + cmT.mul(r) + u_i.cmE.mul(r2); - let u = U_i.u + r * u_i.u; - let cmW = U_i.cmW + u_i.cmW.mul(r); - let x = U_i - .x - .iter() - .zip(&u_i.x) - .map(|(a, b)| *a + (r * b)) - .collect::>(); - - CommittedInstance { cmE, u, cmW, x } - } - - pub fn prove_commitments( - tr: &mut impl Transcript, - cs_prover_params: &CS::ProverParams, - w: &Witness, - ci: &CommittedInstance, - T: Vec, - cmT: &C, - ) -> Result<[CS::Proof; 3], Error> { - let cmE_proof = CS::prove(cs_prover_params, tr, &ci.cmE, &w.E, &w.rE, None)?; - let cmW_proof = CS::prove(cs_prover_params, tr, &ci.cmW, &w.W, &w.rW, None)?; - let cmT_proof = CS::prove(cs_prover_params, tr, cmT, &T, &C::ScalarField::zero(), None)?; // cm(T) is committed with rT=0 - Ok([cmE_proof, cmW_proof, cmT_proof]) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - - use crate::arith::{r1cs::tests::get_test_r1cs, ArithRelation}; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::nifs::tests::test_nifs_opt; - - #[test] - fn test_nifs_nova() -> Result<(), Error> { - let (W, U) = test_nifs_opt::, PoseidonSponge>>()?; - - // check the last folded instance relation - let r1cs = get_test_r1cs(); - r1cs.check_relation(&W, &U)?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/nova_circuits.rs b/folding-schemes/src/folding/nova/nifs/nova_circuits.rs deleted file mode 100644 index 0ac5e28b2..000000000 --- a/folding-schemes/src/folding/nova/nifs/nova_circuits.rs +++ /dev/null @@ -1,232 +0,0 @@ -/// contains [Nova](https://eprint.iacr.org/2021/370.pdf) NIFS related circuits -use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, CryptographicSponge}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - uint8::UInt8, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{fmt::Debug, Zero}; -use core::{borrow::Borrow, marker::PhantomData}; - -use super::NIFSGadgetTrait; -use crate::folding::traits::CommittedInstanceVarOps; -use crate::transcript::TranscriptVar; -use crate::{ - folding::circuits::{ - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, - }, - Curve, -}; -use crate::{folding::nova::CommittedInstance, transcript::AbsorbNonNativeGadget}; - -use super::nova::ChallengeGadget; - -/// CommittedInstanceVar contains the u, x, cmE and cmW values which are folded on the main Nova -/// constraints field (E1::Fr, where E1 is the main curve). The peculiarity is that cmE and cmW are -/// represented non-natively over the constraint field. -#[derive(Debug, Clone)] -pub struct CommittedInstanceVar { - pub u: FpVar, - pub x: Vec>, - pub cmE: NonNativeAffineVar, - pub cmW: NonNativeAffineVar, -} - -impl AllocVar, CF1> for CommittedInstanceVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let u = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; - let x: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; - - let cmE = - NonNativeAffineVar::::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; - let cmW = - NonNativeAffineVar::::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; - - Ok(Self { u, x, cmE, cmW }) - }) - } -} - -impl AbsorbGadget for CommittedInstanceVar { - fn to_sponge_bytes(&self) -> Result>, SynthesisError> { - FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) - } - - fn to_sponge_field_elements(&self) -> Result>, SynthesisError> { - Ok([ - vec![self.u.clone()], - self.x.clone(), - self.cmE.to_native_sponge_field_elements()?, - self.cmW.to_native_sponge_field_elements()?, - ] - .concat()) - } -} - -impl CommittedInstanceVarOps for CommittedInstanceVar { - type PointVar = NonNativeAffineVar; - - fn get_commitments(&self) -> Vec { - vec![self.cmW.clone(), self.cmE.clone()] - } - - fn get_public_inputs(&self) -> &[FpVar>] { - &self.x - } - - fn enforce_incoming(&self) -> Result<(), SynthesisError> { - let zero = NonNativeUintVar::new_constant(ConstraintSystemRef::None, CF2::::zero())?; - self.cmE.x.enforce_equal_unaligned(&zero)?; - self.cmE.y.enforce_equal_unaligned(&zero)?; - self.u.enforce_equal(&FpVar::one()) - } - - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { - self.u.enforce_equal(&other.u)?; - self.x.enforce_equal(&other.x) - } -} - -/// Implements the circuit that does the checks of the Non-Interactive Folding Scheme Verifier -/// described in section 4 of [Nova](https://eprint.iacr.org/2021/370.pdf), where the cmE & cmW checks are -/// delegated to the NIFSCycleFoldGadget. -pub struct NIFSGadget, S>> { - _c: PhantomData, - _s: PhantomData, - _t: PhantomData, -} - -impl NIFSGadgetTrait for NIFSGadget -where - C: Curve, - S: CryptographicSponge, - T: TranscriptVar, S>, -{ - type CommittedInstance = CommittedInstance; - type CommittedInstanceVar = CommittedInstanceVar; - type Proof = C; - type ProofVar = NonNativeAffineVar; - - fn verify( - transcript: &mut T, - U_i: Self::CommittedInstanceVar, - // U_i_vec is passed to reuse the already computed U_i_vec from previous methods - U_i_vec: Vec>>, - u_i: Self::CommittedInstanceVar, - cmT: Option, - ) -> Result<(Self::CommittedInstanceVar, Vec>>), SynthesisError> { - let r_bits = ChallengeGadget::>::get_challenge_gadget( - transcript, - U_i_vec, - u_i.clone(), - cmT.clone(), - )?; - let r = Boolean::le_bits_to_fp(&r_bits)?; - - Ok(( - Self::CommittedInstanceVar { - cmE: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, - cmW: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, - // ci3.u = U_i.u + r * u_i.u - u: U_i.u + &r * u_i.u, - // ci3.x = U_i.x + r * u_i.x - x: U_i - .x - .iter() - .zip(u_i.x) - .map(|(a, b)| a + &r * &b) - .collect::>>>(), - }, - r_bits, - )) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::constraints::PoseidonSpongeVar; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::GR1CSVar; - use ark_std::UniformRand; - - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::nifs::{ - nova::NIFS, - tests::{ - test_committed_instance_hash_opt, test_committed_instance_to_sponge_preimage_opt, - test_nifs_gadget_opt, - }, - }; - use crate::Error; - - #[test] - fn test_nifs_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - // prepare the committed instances to test in-circuit - let ci: Vec> = (0..2) - .into_iter() - .map(|_| CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }) - .collect(); - let cmT = Projective::rand(&mut rng); - - let (ci_out, ciVar_out) = test_nifs_gadget_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci, cmT)?; - assert_eq!(ciVar_out.u.value()?, ci_out.u); - assert_eq!(ciVar_out.x.value()?, ci_out.x); - Ok(()) - } - - #[test] - fn test_committed_instance_to_sponge_preimage() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let ci = CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }; - - test_committed_instance_to_sponge_preimage_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci)?; - Ok(()) - } - - #[test] - fn test_committed_instance_hash() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let ci = CommittedInstance:: { - cmE: Projective::rand(&mut rng), - u: Fr::rand(&mut rng), - cmW: Projective::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - }; - test_committed_instance_hash_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci)?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/ova.rs b/folding-schemes/src/folding/nova/nifs/ova.rs deleted file mode 100644 index 1f8d9ef0b..000000000 --- a/folding-schemes/src/folding/nova/nifs/ova.rs +++ /dev/null @@ -1,301 +0,0 @@ -/// This module contains the implementation the NIFSTrait for the -/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) NIFS (Non-Interactive Folding Scheme). -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::{BigInteger, PrimeField}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::fmt::Debug; -use ark_std::rand::RngCore; -use ark_std::{One, UniformRand, Zero}; -use std::marker::PhantomData; - -use super::nova::ChallengeGadget; -use super::ova_circuits::CommittedInstanceVar; -use super::NIFSTrait; -use crate::arith::{r1cs::R1CS, Arith}; -use crate::commitment::CommitmentScheme; -use crate::folding::traits::{CommittedInstanceOps, Inputize}; -use crate::folding::{circuits::CF1, traits::Dummy}; -use crate::transcript::Transcript; -use crate::utils::vec::{hadamard, mat_vec_mul, vec_scalar_mul, vec_sub}; -use crate::{Curve, Error}; - -/// A CommittedInstance in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) is represented by `W` or -/// `W'`. It is the result of the commitment to a vector that contains the witness `w` concatenated -/// with `t` or `e` + the public inputs `x` and a relaxation factor `u`. (Notice that in the Ova -/// document `u` is denoted as `mu`, in this implementation we use `u` so it follows the original -/// Nova notation, so code is easier to follow). -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct CommittedInstance { - pub u: C::ScalarField, // in the Ova document is denoted as `mu` - pub x: Vec, - pub cmWE: C, -} - -impl Absorb for CommittedInstance { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.u.to_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - self.cmWE.to_native_sponge_field_elements(dest); - } -} - -impl CommittedInstanceOps for CommittedInstance { - type Var = CommittedInstanceVar; - - fn get_commitments(&self) -> Vec { - vec![self.cmWE] - } - - fn is_incoming(&self) -> bool { - self.u == One::one() - } -} - -impl Inputize> for CommittedInstance { - /// Returns the internal representation in the same order as how the value - /// is allocated in `CommittedInstanceVar::new_input`. - fn inputize(&self) -> Vec> { - [&[self.u][..], &self.x, &self.cmWE.inputize_nonnative()].concat() - } -} - -/// A Witness in Ova is represented by `w`. It also contains a blinder which can or not be used -/// when committing to the witness itself. -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Witness { - pub w: Vec, - pub rW: C::ScalarField, -} - -impl Witness { - /// Generates a new `Witness` instance from a given witness vector. - /// If `H = true`, then we assume we want to blind it at commitment time, - /// hence sampling `rW` from the randomness passed. - pub fn new(w: Vec, mut rng: impl RngCore) -> Self { - Self { - w, - rW: if H { - C::ScalarField::rand(&mut rng) - } else { - C::ScalarField::zero() - }, - } - } - - /// Given `x` (public inputs) and `t` or `e` (which we always concatenate in Ova) and the - /// public inputs `x`, generates a [`CommittedInstance`] as a result which will or not be - /// blinded depending on how the const generic `HC` is set up. - pub fn commit, const HC: bool>( - &self, - params: &CS::ProverParams, - x: Vec, - t_or_e: Vec, - ) -> Result, Error> { - let cmWE = CS::commit(params, &[self.w.clone(), t_or_e].concat(), &self.rW)?; - Ok(CommittedInstance { - u: C::ScalarField::one(), - cmWE, - x, - }) - } -} - -impl Dummy<&R1CS>> for Witness { - fn dummy(r1cs: &R1CS>) -> Self { - Self { - w: vec![C::ScalarField::zero(); r1cs.n_witnesses()], - rW: C::ScalarField::zero(), - } - } -} - -/// Implements the NIFS (Non-Interactive Folding Scheme) trait for Ova. -pub struct NIFS< - C: Curve, - CS: CommitmentScheme, - T: Transcript, - const H: bool = false, -> { - _c: PhantomData, - _cp: PhantomData, - _t: PhantomData, -} - -impl, T: Transcript, const H: bool> - NIFSTrait for NIFS -{ - type CommittedInstance = CommittedInstance; - type Witness = Witness; - type ProverAux = (); - // Proof is unused, but set to C::ScalarField so that the NIFSGadgetTrait abstraction can - // define the ProofsVar implementing the AllocVar from Proof - type Proof = C::ScalarField; - - fn new_witness(w: Vec, _e_len: usize, rng: impl RngCore) -> Self::Witness { - Witness::new::(w, rng) - } - - fn new_instance( - _rng: impl RngCore, - params: &CS::ProverParams, - W: &Self::Witness, - x: Vec, - aux: Vec, // t_or_e - ) -> Result { - W.commit::(params, x, aux) - } - - fn fold_witness( - r: C::ScalarField, // in Ova's hackmd denoted as `alpha` - W_i: &Self::Witness, - w_i: &Self::Witness, - _aux: &Self::ProverAux, - ) -> Result { - let w: Vec = W_i - .w - .iter() - .zip(&w_i.w) - .map(|(a, b)| *a + (r * b)) - .collect(); - - let rW = W_i.rW + r * w_i.rW; - Ok(Self::Witness { w, rW }) - } - - fn prove( - _cs_prover_params: &CS::ProverParams, - _r1cs: &R1CS, - transcript: &mut T, - W_i: &Self::Witness, - U_i: &Self::CommittedInstance, - w_i: &Self::Witness, - u_i: &Self::CommittedInstance, - ) -> Result< - ( - Self::Witness, - Self::CommittedInstance, - Self::Proof, - Vec, - ), - Error, - > { - let mut transcript_v = transcript.clone(); - - let r_bits = ChallengeGadget::::get_challenge_native( - transcript, U_i, u_i, None, // cmT not used in Ova - ); - let r_Fr = C::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - let w = Self::fold_witness(r_Fr, W_i, w_i, &())?; - - let proof = C::ScalarField::zero(); - let (ci, _r_bits_v) = Self::verify(&mut transcript_v, U_i, u_i, &proof)?; - #[cfg(test)] - assert_eq!(_r_bits_v, r_bits); - - Ok((w, ci, proof, r_bits)) - } - - fn verify( - transcript: &mut T, - U_i: &Self::CommittedInstance, - u_i: &Self::CommittedInstance, - _proof: &Self::Proof, // unused in Ova - ) -> Result<(Self::CommittedInstance, Vec), Error> { - let r_bits = ChallengeGadget::::get_challenge_native( - transcript, U_i, u_i, None, // cmT not used in Ova - ); - let r = C::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - // recall that r=alpha, and u=mu between Nova and Ova respectively - let u = U_i.u + r; // u_i.u is always 1 in Ova as we just can do IVC (not PCD). - let cmWE = U_i.cmWE + u_i.cmWE.mul(r); - let x = U_i - .x - .iter() - .zip(&u_i.x) - .map(|(a, b)| *a + (r * b)) - .collect::>(); - - Ok((Self::CommittedInstance { cmWE, u, x }, r_bits)) - } -} - -/// Computes the E parameter (error terms) for the given R1CS and the instance's z and u. This -/// method is used by the verifier to obtain E in order to check the RelaxedR1CS relation. -pub fn compute_E( - r1cs: &R1CS, - z: &[C::ScalarField], - u: C::ScalarField, -) -> Result, Error> { - let (A, B, C) = (r1cs.A.clone(), r1cs.B.clone(), r1cs.C.clone()); - - // this is parallelizable (for the future) - let Az = mat_vec_mul(&A, z)?; - let Bz = mat_vec_mul(&B, z)?; - let Cz = mat_vec_mul(&C, z)?; - - let Az_Bz = hadamard(&Az, &Bz)?; - let uCz = vec_scalar_mul(&Cz, &u); - - vec_sub(&Az_Bz, &uCz) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_pallas::{Fr, Projective}; - - use crate::arith::{r1cs::tests::get_test_r1cs, ArithRelation}; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::nifs::tests::test_nifs_opt; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - - // Simple auxiliary structure mainly used to help pass a witness for which we can check - // easily an R1CS relation. - // Notice that checking it requires us to have `E` as per [`ArithRelation`] trait definition. - // But since we don't hold `E` nor `e` within the NIFS, we create this structure to pass - // `e` such that the check can be done. - #[derive(Debug, Clone)] - pub(crate) struct TestingWitness { - pub(crate) w: Vec, - pub(crate) e: Vec, - } - impl ArithRelation, CommittedInstance> for R1CS> { - type Evaluation = Vec>; - - fn eval_relation( - &self, - w: &TestingWitness, - u: &CommittedInstance, - ) -> Result { - self.eval_at_z(&[&[u.u], u.x.as_slice(), &w.w].concat()) - } - - fn check_evaluation( - w: &TestingWitness, - _u: &CommittedInstance, - e: Self::Evaluation, - ) -> Result<(), Error> { - (w.e == e).then_some(()).ok_or(Error::NotSatisfied) - } - } - - #[test] - fn test_nifs_ova() -> Result<(), Error> { - let (W, U) = test_nifs_opt::, PoseidonSponge>>()?; - - // check the last folded instance relation - let r1cs = get_test_r1cs(); - let z: Vec = [&[U.u][..], &U.x, &W.w].concat(); - let e = compute_E::(&r1cs, &z, U.u)?; - r1cs.check_relation(&TestingWitness:: { e, w: W.w.clone() }, &U)?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/ova_circuits.rs b/folding-schemes/src/folding/nova/nifs/ova_circuits.rs deleted file mode 100644 index 1e5a2b6b0..000000000 --- a/folding-schemes/src/folding/nova/nifs/ova_circuits.rs +++ /dev/null @@ -1,215 +0,0 @@ -/// contains [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) NIFS related circuits -use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, CryptographicSponge}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - uint8::UInt8, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::fmt::Debug; -use core::{borrow::Borrow, marker::PhantomData}; - -use super::ova::CommittedInstance; -use super::NIFSGadgetTrait; -use crate::folding::traits::CommittedInstanceVarOps; -use crate::transcript::TranscriptVar; -use crate::{ - folding::circuits::{nonnative::affine::NonNativeAffineVar, CF1}, - transcript::AbsorbNonNativeGadget, -}; - -use crate::folding::nova::nifs::nova::ChallengeGadget; -use crate::Curve; - -#[derive(Debug, Clone)] -pub struct CommittedInstanceVar { - pub u: FpVar, - pub x: Vec>, - pub cmWE: NonNativeAffineVar, -} - -impl AllocVar, CF1> for CommittedInstanceVar { - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let u = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; - let x: Vec> = - Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; - - let cmWE = - NonNativeAffineVar::::new_variable(cs.clone(), || Ok(val.borrow().cmWE), mode)?; - - Ok(Self { u, x, cmWE }) - }) - } -} - -impl AbsorbGadget for CommittedInstanceVar { - fn to_sponge_bytes(&self) -> Result>, SynthesisError> { - FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) - } - - fn to_sponge_field_elements(&self) -> Result>, SynthesisError> { - Ok([ - vec![self.u.clone()], - self.x.clone(), - self.cmWE.to_native_sponge_field_elements()?, - ] - .concat()) - } -} - -impl CommittedInstanceVarOps for CommittedInstanceVar { - type PointVar = NonNativeAffineVar; - - fn get_commitments(&self) -> Vec { - vec![self.cmWE.clone()] - } - - fn get_public_inputs(&self) -> &[FpVar>] { - &self.x - } - - fn enforce_incoming(&self) -> Result<(), SynthesisError> { - self.u.enforce_equal(&FpVar::one()) - } - - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { - self.u.enforce_equal(&other.u)?; - self.x.enforce_equal(&other.x) - } -} - -/// Implements the circuit that does the checks of the Non-Interactive Folding Scheme Verifier -/// described of the Ova variant, where the cmWE check is delegated to the NIFSCycleFoldGadget. -pub struct NIFSGadget, S>> { - _c: PhantomData, - _s: PhantomData, - _t: PhantomData, -} - -impl NIFSGadgetTrait for NIFSGadget -where - C: Curve, - S: CryptographicSponge, - T: TranscriptVar, S>, -{ - type CommittedInstance = CommittedInstance; - type CommittedInstanceVar = CommittedInstanceVar; - type Proof = C::ScalarField; - type ProofVar = FpVar; // unused - - fn verify( - transcript: &mut T, - U_i: Self::CommittedInstanceVar, - // U_i_vec is passed to reuse the already computed U_i_vec from previous methods - U_i_vec: Vec>>, - u_i: Self::CommittedInstanceVar, - _proof: Option, - ) -> Result<(Self::CommittedInstanceVar, Vec>>), SynthesisError> { - let r_bits = ChallengeGadget::>::get_challenge_gadget( - transcript, - U_i_vec, - u_i.clone(), - None, - )?; - let r = Boolean::le_bits_to_fp(&r_bits)?; - - Ok(( - Self::CommittedInstanceVar { - cmWE: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, - // ci3.u = U_i.u + r * u_i.u (u_i.u is always 1 in Ova) - u: U_i.u + &r, - // ci3.x = U_i.x + r * u_i.x - x: U_i - .x - .iter() - .zip(u_i.x) - .map(|(a, b)| a + &r * &b) - .collect::>>>(), - }, - r_bits, - )) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::constraints::PoseidonSpongeVar; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::GR1CSVar; - use ark_std::UniformRand; - use ark_std::Zero; - - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::nifs::{ - ova::NIFS, - tests::{ - test_committed_instance_hash_opt, test_committed_instance_to_sponge_preimage_opt, - test_nifs_gadget_opt, - }, - }; - use crate::Error; - - #[test] - fn test_nifs_gadget() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - // prepare the committed instances to test in-circuit - let ci: Vec> = (0..2) - .into_iter() - .map(|_| CommittedInstance:: { - u: Fr::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - cmWE: Projective::rand(&mut rng), - }) - .collect(); - - let (ci_out, ciVar_out) = test_nifs_gadget_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci, Fr::zero())?; - assert_eq!(ciVar_out.u.value()?, ci_out.u); - assert_eq!(ciVar_out.x.value()?, ci_out.x); - Ok(()) - } - - #[test] - fn test_committed_instance_to_sponge_preimage() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let ci = CommittedInstance:: { - u: Fr::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - cmWE: Projective::rand(&mut rng), - }; - - test_committed_instance_to_sponge_preimage_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci)?; - Ok(()) - } - - #[test] - fn test_committed_instance_hash() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let ci = CommittedInstance:: { - u: Fr::rand(&mut rng), - x: vec![Fr::rand(&mut rng); 1], - cmWE: Projective::rand(&mut rng), - }; - test_committed_instance_hash_opt::< - NIFS, PoseidonSponge>, - NIFSGadget, PoseidonSpongeVar>, - >(ci)?; - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/nifs/pointvsline.rs b/folding-schemes/src/folding/nova/nifs/pointvsline.rs deleted file mode 100644 index 735880ea7..000000000 --- a/folding-schemes/src/folding/nova/nifs/pointvsline.rs +++ /dev/null @@ -1,357 +0,0 @@ -use ark_ff::{One, PrimeField}; -use ark_poly::univariate::DensePolynomial; -use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{log2, Zero}; - -use super::mova::{CommittedInstance, Witness}; -use crate::transcript::Transcript; -use crate::utils::mle::dense_vec_to_dense_mle; -use crate::{Curve, Error}; - -/// Implements the Points vs Line as described in -/// [Mova](https://eprint.iacr.org/2024/1220.pdf) and Section 4.5.2 from Thaler’s book -/// Claim from step 3 protocol 6 -pub struct PointvsLineEvaluationClaim { - pub mleE1_prime: C::ScalarField, - pub mleE2_prime: C::ScalarField, - pub rE_prime: Vec, -} -/// Proof from step 1 protocol 6 -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct PointVsLineProof { - pub h1: DensePolynomial, - pub h2: DensePolynomial, -} - -#[derive(Clone, Debug, Default)] -pub struct PointVsLine> { - _phantom_C: std::marker::PhantomData, - _phantom_T: std::marker::PhantomData, -} - -/// Protocol 6 from Mova -impl> PointVsLine { - pub fn prove( - transcript: &mut T, - ci1: &CommittedInstance, - ci2: &CommittedInstance, - w1: &Witness, - w2: &Witness, - ) -> Result<(PointVsLineProof, PointvsLineEvaluationClaim), Error> { - let n_vars: usize = log2(w1.E.len()) as usize; - - let mleE1 = dense_vec_to_dense_mle(n_vars, &w1.E); - let mleE2 = dense_vec_to_dense_mle(n_vars, &w2.E); - - // We have l(0) = r1, l(1) = r2 so we know that l(x) = r1 + x(r2-r1) that's why we need r2-r1 - let r2_sub_r1: Vec<::ScalarField> = ci1 - .rE - .iter() - .zip(&ci2.rE) - .map(|(&r1, r2)| *r2 - r1) - .collect(); - - let h1 = compute_h(&mleE1, &ci1.rE, &r2_sub_r1)?; - let h2 = compute_h(&mleE2, &ci1.rE, &r2_sub_r1)?; - - transcript.absorb(&h1.coeffs()); - transcript.absorb(&h2.coeffs()); - - let beta_scalar = C::ScalarField::from_le_bytes_mod_order(b"beta"); - transcript.absorb(&beta_scalar); - let beta = transcript.get_challenge(); - - let mleE1_prime = h1.evaluate(&beta); - let mleE2_prime = h2.evaluate(&beta); - - let rE_prime = compute_l(&ci1.rE, &r2_sub_r1, beta)?; - - Ok(( - PointVsLineProof { h1, h2 }, - PointvsLineEvaluationClaim { - mleE1_prime, - mleE2_prime, - rE_prime, - }, - )) - } - - pub fn verify( - transcript: &mut T, - ci1: &CommittedInstance, - ci2: &CommittedInstance, - proof: &PointVsLineProof, - mleE1_prime: &::ScalarField, - mleE2_prime: &::ScalarField, - rE_prime_p: &[::ScalarField], // the rE_prime of the prover - ) -> Result< - Vec<::ScalarField>, // rE=rE1'=rE2'. - Error, - > { - if proof.h1.evaluate(&C::ScalarField::zero()) != ci1.mleE { - return Err(Error::NotEqual); - } - - if proof.h2.evaluate(&C::ScalarField::one()) != ci2.mleE { - return Err(Error::NotEqual); - } - - transcript.absorb(&proof.h1.coeffs()); - transcript.absorb(&proof.h2.coeffs()); - - let beta_scalar = C::ScalarField::from_le_bytes_mod_order(b"beta"); - transcript.absorb(&beta_scalar); - let beta = transcript.get_challenge(); - - if *mleE1_prime != proof.h1.evaluate(&beta) { - return Err(Error::NotEqual); - } - - if *mleE2_prime != proof.h2.evaluate(&beta) { - return Err(Error::NotEqual); - } - - let r2_sub_r1: Vec<::ScalarField> = ci1 - .rE - .iter() - .zip(&ci2.rE) - .map(|(&r1, r2)| *r2 - r1) - .collect(); - let rE_prime = compute_l(&ci1.rE, &r2_sub_r1, beta)?; - if rE_prime != rE_prime_p { - return Err(Error::NotEqual); - } - - Ok(rE_prime) - } -} - -fn compute_h( - mle: &DenseMultilinearExtension, - r1: &[F], - r2_sub_r1: &[F], -) -> Result, Error> { - let n_vars: usize = mle.num_vars; - if r1.len() != r2_sub_r1.len() || r1.len() != n_vars { - return Err(Error::NotEqual); - } - - // Initialize the polynomial vector from the evaluations in the multilinear extension. - // Each evaluation is turned into a constant polynomial. - let mut poly: Vec> = mle - .evaluations - .iter() - .map(|&x| DensePolynomial::from_coefficients_slice(&[x])) - .collect(); - - for (i, (&r1_i, &r2_sub_r1_i)) in r1.iter().zip(r2_sub_r1.iter()).enumerate().take(n_vars) { - // Create a linear polynomial r(X) = r1_i + (r2_sub_r1_i) * X (basically l) - let r = DensePolynomial::from_coefficients_slice(&[r1_i, r2_sub_r1_i]); - let half_len = 1 << (n_vars - i - 1); - - for b in 0..half_len { - let left = &poly[b << 1]; - let right = &poly[(b << 1) + 1]; - poly[b] = left + &(&r * &(right - left)); - } - } - - // After the loop, we should be left with a single polynomial, so return it. - Ok(poly.swap_remove(0)) -} - -fn compute_l(r1: &[F], r2_sub_r1: &[F], x: F) -> Result, Error> { - if r1.len() != r2_sub_r1.len() { - return Err(Error::NotEqual); - } - - // we have l(x) = r1 + x(r2-r1) so return the result - Ok(r1 - .iter() - .zip(r2_sub_r1) - .map(|(&r1, &r1_sub_r0)| r1 + x * r1_sub_r0) - .collect()) -} - -#[cfg(test)] -mod tests { - use super::{compute_h, compute_l, PointVsLine}; - use crate::commitment::pedersen::Pedersen; - use crate::commitment::CommitmentScheme; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::Error; - use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial}; - use ark_std::{log2, UniformRand}; - - use crate::folding::nova::nifs::mova::Witness; - - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_crypto_primitives::sponge::CryptographicSponge; - use ark_ff::Zero; - use ark_pallas::{Fq, Fr, Projective}; - - #[test] - fn test_compute_h() -> Result<(), Error> { - let mle = DenseMultilinearExtension::from_evaluations_slice(1, &[Fq::from(1), Fq::from(2)]); - let r0 = [Fq::from(5)]; - let r1 = [Fq::from(6)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - - let result = compute_h(&mle, &r0, &r1_sub_r0)?; - assert_eq!( - result, - DenseUVPolynomial::from_coefficients_slice(&[Fq::from(6), Fq::from(1)]) - ); - - let mle = DenseMultilinearExtension::from_evaluations_slice(1, &[Fq::from(1), Fq::from(2)]); - let r0 = [Fq::from(4)]; - let r1 = [Fq::from(7)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - - let result = compute_h(&mle, &r0, &r1_sub_r0)?; - assert_eq!( - result, - DenseUVPolynomial::from_coefficients_slice(&[Fq::from(5), Fq::from(3)]) - ); - - let mle = DenseMultilinearExtension::from_evaluations_slice( - 2, - &[Fq::from(1), Fq::from(2), Fq::from(3), Fq::from(4)], - ); - let r0 = [Fq::from(5), Fq::from(4)]; - let r1 = [Fq::from(2), Fq::from(7)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - - let result = compute_h(&mle, &r0, &r1_sub_r0)?; - assert_eq!( - result, - DenseUVPolynomial::from_coefficients_slice(&[Fq::from(14), Fq::from(3)]) - ); - let mle = DenseMultilinearExtension::from_evaluations_slice( - 3, - &[ - Fq::from(1), - Fq::from(2), - Fq::from(3), - Fq::from(4), - Fq::from(5), - Fq::from(6), - Fq::from(7), - Fq::from(8), - ], - ); - let r0 = [Fq::from(1), Fq::from(2), Fq::from(3)]; - let r1 = [Fq::from(5), Fq::from(6), Fq::from(7)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - - let result = compute_h(&mle, &r0, &r1_sub_r0)?; - assert_eq!( - result, - DenseUVPolynomial::from_coefficients_slice(&[Fq::from(18), Fq::from(28)]) - ); - Ok(()) - } - - #[test] - fn test_compute_h_errors() { - let mle = DenseMultilinearExtension::from_evaluations_slice(1, &[Fq::from(1), Fq::from(2)]); - let r0 = [Fq::from(5)]; - let r1_sub_r0 = []; - let result = compute_h(&mle, &r0, &r1_sub_r0); - assert!(result.is_err()); - - let mle = DenseMultilinearExtension::from_evaluations_slice( - 2, - &[Fq::from(1), Fq::from(2), Fq::from(1), Fq::from(2)], - ); - let r0 = [Fq::from(4)]; - let r1 = [Fq::from(7)]; - let r1_sub_r0: Vec = r1.iter().zip(&r0).map(|(&x, y)| x - y).collect(); - - let result = compute_h(&mle, &r0, &r1_sub_r0); - assert!(result.is_err()) - } - - #[test] - fn test_compute_l() -> Result<(), Error> { - // Test with simple non-zero values - let r1 = vec![Fq::from(1), Fq::from(2), Fq::from(3)]; - let r2_sub_r1 = vec![Fq::from(4), Fq::from(5), Fq::from(6)]; - let x = Fq::from(2); - - let expected = vec![ - Fq::from(1) + Fq::from(2) * Fq::from(4), - Fq::from(2) + Fq::from(2) * Fq::from(5), - Fq::from(3) + Fq::from(2) * Fq::from(6), - ]; - - let result = compute_l(&r1, &r2_sub_r1, x)?; - assert_eq!(result, expected); - Ok(()) - } - - #[test] - fn test_evaluations_R1CS() -> Result<(), Error> { - // Basic test with no zero error term to ensure that the folding is correct. - // This test mainly focuses on if the evaluation of h0 and h1 are correct. - let mut rng = ark_std::test_rng(); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, 4)?; - let poseidon_config = poseidon_canonical_config::(); - let mut transcript_p = PoseidonSponge::::new(&poseidon_config); - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - - let W_i = Witness { - E: vec![Fr::from(25), Fr::from(50), Fr::from(0), Fr::from(0)], - W: vec![Fr::from(35), Fr::from(9), Fr::from(27), Fr::from(30)], - rW: Fr::zero(), - }; - let rE = (0..log2(W_i.E.len())).map(|_| Fr::rand(&mut rng)).collect(); - // x is not important - let x = vec![Fr::from(35), Fr::from(9), Fr::from(27), Fr::from(30)]; - let U_i = - Witness::commit::, false>(&W_i, &pedersen_params, x.clone(), rE)?; - - let w_i = Witness { - E: vec![Fr::from(75), Fr::from(100), Fr::from(0), Fr::from(0)], - W: vec![Fr::from(35), Fr::from(9), Fr::from(27), Fr::from(30)], - rW: Fr::zero(), - }; - let rE = (0..log2(W_i.E.len())).map(|_| Fr::rand(&mut rng)).collect(); - let u_i = Witness::commit::, false>(&w_i, &pedersen_params, x, rE)?; - - let (proof, claim) = PointVsLine::prove(&mut transcript_p, &U_i, &u_i, &W_i, &w_i)?; - - let result = PointVsLine::verify( - &mut transcript_v, - &U_i, - &u_i, - &proof, - &claim.mleE1_prime, - &claim.mleE2_prime, - &claim.rE_prime, - ); - - assert!(result.is_ok(), "Verification failed"); - // Check if the re_prime is the same - let re_verified = result.unwrap(); - assert!(re_verified == claim.rE_prime); - let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - - // Pass the wrong committed instance which should result in a wrong evaluation in h returning an error - let result = PointVsLine::verify( - &mut transcript_v, - &U_i, - &U_i, - &proof, - &claim.mleE1_prime, - &claim.mleE2_prime, - &claim.rE_prime, - ); - - assert!(result.is_err(), "Verification was okay when it should fail"); - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/nova/traits.rs b/folding-schemes/src/folding/nova/traits.rs deleted file mode 100644 index b187d7ab9..000000000 --- a/folding-schemes/src/folding/nova/traits.rs +++ /dev/null @@ -1,125 +0,0 @@ -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::SynthesisError; -use ark_std::{rand::RngCore, UniformRand}; - -use super::decider_eth_circuit::WitnessVar; -use super::nifs::nova_circuits::CommittedInstanceVar; -use super::{CommittedInstance, Witness}; -use crate::arith::{ - r1cs::{circuits::R1CSMatricesVar, R1CS}, - Arith, ArithRelation, ArithRelationGadget, ArithSampler, -}; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::CF1; -use crate::utils::gadgets::{EquivalenceGadget, VectorGadget}; -use crate::{Curve, Error}; - -/// Implements [`ArithRelation`] for R1CS, where the witness is of type -/// [`Witness`], and the committed instance is of type [`CommittedInstance`]. -/// -/// Due to the error terms `Witness.E` and `CommittedInstance.u`, R1CS here is -/// considered as a relaxed R1CS. -/// -/// One may wonder why we do not provide distinct structs for R1CS and relaxed -/// R1CS. -/// This is because both plain R1CS and relaxed R1CS have the same structure: -/// they are both represented by three matrices. -/// What makes them different is the error terms, which are not part of the R1CS -/// struct, but are part of the witness and committed instance. -/// -/// As a follow-up, one may further ask why not providing a trait for relaxed -/// R1CS and implement it for the [`R1CS`] struct, where the relaxed R1CS trait -/// has methods for relaxed satisfiability check, while the [`ArithRelation`] -/// trait that [`R1CS`] implements has methods for plain satisfiability check. -/// However, it would be more ideal if we have a single method that can smartly -/// choose the type of satisfiability check, which would make the code more -/// generic and easier to maintain. -/// -/// This is achieved thanks to the new design of the [`ArithRelation`] trait, -/// where we can implement the trait for the same constraint system with -/// different types of witnesses and committed instances. -/// For R1CS, whether it is relaxed or not is now determined by the types of `W` -/// and `U`: the satisfiability check is relaxed if `W` and `U` are defined by -/// folding schemes, and plain if they are vectors of field elements. -impl ArithRelation, CommittedInstance> for R1CS> { - type Evaluation = Vec>; - - fn eval_relation( - &self, - w: &Witness, - u: &CommittedInstance, - ) -> Result { - self.eval_at_z(&[&[u.u][..], &u.x, &w.W].concat()) - } - - fn check_evaluation( - w: &Witness, - _u: &CommittedInstance, - e: Self::Evaluation, - ) -> Result<(), Error> { - (w.E == e).then_some(()).ok_or(Error::NotSatisfied) - } -} - -impl ArithSampler, CommittedInstance> for R1CS> { - fn sample_witness_instance>( - &self, - params: &CS::ProverParams, - mut rng: impl RngCore, - ) -> Result<(Witness, CommittedInstance), Error> { - // Implements sampling a (committed) RelaxedR1CS - // See construction 5 in https://eprint.iacr.org/2023/573.pdf - let u = C::ScalarField::rand(&mut rng); - let rE = C::ScalarField::rand(&mut rng); - let rW = C::ScalarField::rand(&mut rng); - - let W = (0..self.n_witnesses()) - .map(|_| C::ScalarField::rand(&mut rng)) - .collect(); - let x = (0..self.n_public_inputs()) - .map(|_| C::ScalarField::rand(&mut rng)) - .collect::>(); - let mut z = vec![u]; - z.extend(&x); - z.extend(&W); - - let E = self.eval_at_z(&z)?; - - let witness = Witness { E, rE, W, rW }; - let mut cm_witness = witness.commit::(params, x)?; - - // witness.commit() sets u to 1, we set it to the sampled u value - cm_witness.u = u; - - debug_assert!( - self.check_relation(&witness, &cm_witness).is_ok(), - "Sampled a non satisfiable relaxed R1CS, sampled u: {}, computed E: {:?}", - u, - witness.E - ); - - Ok((witness, cm_witness)) - } -} - -impl ArithRelationGadget, CommittedInstanceVar> - for R1CSMatricesVar> -{ - type Evaluation = (Vec>, Vec>); - - fn eval_relation( - &self, - w: &WitnessVar, - u: &CommittedInstanceVar, - ) -> Result { - self.eval_at_z(&[&[u.u.clone()][..], &u.x, &w.W].concat()) - } - - fn enforce_evaluation( - w: &WitnessVar, - _u: &CommittedInstanceVar, - (AzBz, uCz): Self::Evaluation, - ) -> Result<(), SynthesisError> { - EquivalenceGadget::::enforce_equivalent(&AzBz[..], &uCz.add(&w.E)?[..]) - } -} diff --git a/folding-schemes/src/folding/nova/zk.rs b/folding-schemes/src/folding/nova/zk.rs deleted file mode 100644 index b6ecd0f6a..000000000 --- a/folding-schemes/src/folding/nova/zk.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Implements Nova's zero-knowledge layer, as described in https://eprint.iacr.org/2023/573.pdf. -//! -//! Remark: this zk layer implementation only covers a subset of the use cases: -//! -//! We identify 3 interesting places to use the nova zk-layer: one before all the folding pipeline -//! (Use-case-1), one at the end of the folding pipeline right before the final Decider SNARK -//! proof (Use-case-2), and a third one for cases where compressed SNARK proofs are not needed, and -//! just IVC proofs (bigger than SNARK proofs) suffice (Use-case-3): -//! -//! * Use-case-1: at the beginning of the folding pipeline, right when the user has their original -//! instance prior to be folded into the running instance, the user can fold it with the -//! random-satisfying-instance to then have a blinded instance that can be sent to a server that -//! will fold it with the running instance. -//! -//! --> In this one, the user could externalize all the IVC folding and also the Decider final -//! proof generation to a server. -//! -//! * Use-case-2: at the end of all the IVC folding steps (after n iterations of nova.prove_step), -//! to 'blind' the IVC proof so then it can be sent to a server that will generate the final -//! decider SNARK proof. -//! -//! --> In this one, the user could offload the Decider final proof generation to a server. -//! -//! * Use-case-3: the user does not care about the Decider (final compressed SNARK proof), and -//! wants to generate a zk-proof of the IVC state to an IVC verifier (without any SNARK proof -//! involved). In this use-case, the zk is only added at the last IVCProof. Note that this proof -//! will be much bigger and expensive to verify than a Decider SNARK proof. -//! -//! The current implementation covers the Use-case-3. -//! Use-case-1 can be achieved directly by a simpler version of the zk IVC scheme skipping steps -//! and implemented directly at the app level by folding the original instance with a randomized -//! instance (steps 2,3,4 from section D.4 of the [HyperNova](https://eprint.iacr.org/2023/573.pdf) -//! paper). -//! And the Use-case-2 would require a modified version of the Decider circuits. -use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; -use ark_std::{rand::RngCore, One, Zero}; - -use super::{ - nifs::{nova::NIFS, NIFSTrait}, - CommittedInstance, Nova, Witness, -}; -use crate::{ - arith::{r1cs::R1CS, ArithRelation, ArithSampler}, - commitment::CommitmentScheme, - folding::traits::CommittedInstanceOps, - frontend::FCircuit, - transcript::Transcript, - Curve, Error, -}; - -pub struct RandomizedIVCProof { - pub U_i: CommittedInstance, - pub u_i: CommittedInstance, - pub U_r: CommittedInstance, - pub pi: C1, // proof = cmT - pub pi_prime: C1, // proof' = cmT' - pub W_i_prime: Witness, - pub cf_U_i: CommittedInstance, - pub cf_W_i: Witness, -} - -impl RandomizedIVCProof { - /// Compute a zero-knowledge proof of a Nova IVC proof - /// It implements the prover of appendix D.4.in https://eprint.iacr.org/2023/573.pdf - /// For further details on why folding is hiding, see lemma 9 - pub fn new< - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - >( - nova: &Nova, - mut rng: impl RngCore, - ) -> Result, Error> { - let mut transcript = PoseidonSponge::::new_with_pp_hash( - &nova.poseidon_config, - nova.pp_hash, - ); - - // I. Compute proof for 'regular' instances - // 1. Fold the instance-witness pairs (U_i, W_i) with (u_i, w_i) - let (W_f, U_f, cmT, _) = NIFS::, true>::prove( - &nova.cs_pp, - &nova.r1cs, - &mut transcript, - &nova.w_i, - &nova.u_i, - &nova.W_i, - &nova.U_i, - )?; - - // 2. Sample a satisfying relaxed R1CS instance-witness pair (W_r, U_r) - let (W_r, U_r) = nova - .r1cs - .sample_witness_instance::(&nova.cs_pp, &mut rng)?; - - // 3. Fold the instance-witness pair (U_f, W_f) with (U_r, W_r) - let (W_i_prime, _, cmT_i_prime, _) = - NIFS::, true>::prove( - &nova.cs_pp, - &nova.r1cs, - &mut transcript, - &W_f, - &U_f, - &W_r, - &U_r, - )?; - - Ok(RandomizedIVCProof { - U_i: nova.U_i.clone(), - u_i: nova.u_i.clone(), - U_r, - pi: cmT, - pi_prime: cmT_i_prime, - W_i_prime, - cf_U_i: nova.cf_U_i.clone(), - cf_W_i: nova.cf_W_i.clone(), - }) - } - - /// Verify a zero-knowledge proof of a Nova IVC proof - /// It implements the verifier of appendix D.4. in https://eprint.iacr.org/2023/573.pdf - #[allow(clippy::too_many_arguments)] - pub fn verify, CS2: CommitmentScheme>( - r1cs: &R1CS, - cf_r1cs: &R1CS, - pp_hash: C1::ScalarField, - poseidon_config: &PoseidonConfig, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - proof: &RandomizedIVCProof, - ) -> Result<(), Error> - where - C1: Curve, - { - // Handles case where i=0 - if i == C1::ScalarField::zero() { - if z_0 == z_i { - return Ok(()); - } else { - return Err(Error::zkIVCVerificationFail); - } - } - - // 1. Check that u_i.x is correct - including the cyclefold running instance - // a. Check length - if proof.u_i.x.len() != 2 { - return Err(Error::IVCVerificationFail); - } - - // b. Check computed hashes are correct - let sponge = PoseidonSponge::::new_with_pp_hash(poseidon_config, pp_hash); - let mut transcript = sponge.clone(); - let expected_u_i_x = proof.U_i.hash(&sponge, i, &z_0, &z_i); - if expected_u_i_x != proof.u_i.x[0] { - return Err(Error::zkIVCVerificationFail); - } - - let expected_cf_u_i_x = proof.cf_U_i.hash_cyclefold(&sponge); - if expected_cf_u_i_x != proof.u_i.x[1] { - return Err(Error::IVCVerificationFail); - } - - // 2. Check that u_i values are correct - if !proof.u_i.cmE.is_zero() || proof.u_i.u != C1::ScalarField::one() { - return Err(Error::zkIVCVerificationFail); - } - - // 3. Obtain the U_f folded instance - let (U_f, _) = NIFS::, true>::verify( - &mut transcript, - &proof.u_i, - &proof.U_i, - &proof.pi, - )?; - - // 4. Obtain the U^{\prime}_i folded instance - let (U_i_prime, _) = NIFS::, true>::verify( - &mut transcript, - &U_f, - &proof.U_r, - &proof.pi_prime, - )?; - - // 5. Check that W^{\prime}_i is a satisfying witness - r1cs.check_relation(&proof.W_i_prime, &U_i_prime)?; - - // 6. Check that the cyclefold instance-witness pair satisfies the cyclefold relaxed r1cs - cf_r1cs.check_relation(&proof.cf_W_i, &proof.cf_U_i)?; - - Ok(()) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::tests::test_ivc_opt; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - use rand::rngs::OsRng; - - // Tests zk proof generation and verification for a valid nova IVC proof - #[test] - fn test_zk_nova_ivc() -> Result<(), Error> { - let mut rng = OsRng; - let poseidon_config = poseidon_canonical_config::(); - let F_circuit = CubicFCircuit::::new(())?; - let (_, nova) = test_ivc_opt::< - Pedersen, - Pedersen, - true, - >(poseidon_config.clone(), F_circuit, 3)?; - - let proof = RandomizedIVCProof::new(&nova, &mut rng)?; - let verify = - RandomizedIVCProof::verify::, Pedersen>( - &nova.r1cs, - &nova.cf_r1cs, - nova.pp_hash, - &nova.poseidon_config, - nova.i, - nova.z_0, - nova.z_i, - &proof, - ); - assert!(verify.is_ok()); - Ok(()) - } - - #[test] - fn test_zk_nova_when_i_is_zero() -> Result<(), Error> { - let mut rng = OsRng; - let poseidon_config = poseidon_canonical_config::(); - let F_circuit = CubicFCircuit::::new(())?; - let (_, nova) = test_ivc_opt::< - Pedersen, - Pedersen, - true, - >(poseidon_config.clone(), F_circuit, 0)?; - - let proof = RandomizedIVCProof::new(&nova, &mut rng)?; - let verify = - RandomizedIVCProof::verify::, Pedersen>( - &nova.r1cs, - &nova.cf_r1cs, - nova.pp_hash, - &nova.poseidon_config, - nova.i, - nova.z_0, - nova.z_i, - &proof, - ); - assert!(verify.is_ok()); - Ok(()) - } - - #[test] - fn test_zk_nova_verification_fails_with_wrong_running_instance() -> Result<(), Error> { - let mut rng = OsRng; - let poseidon_config = poseidon_canonical_config::(); - let F_circuit = CubicFCircuit::::new(())?; - let (_, nova) = test_ivc_opt::< - Pedersen, - Pedersen, - true, - >(poseidon_config.clone(), F_circuit, 3)?; - let (_, sampled_committed_instance) = nova - .r1cs - .sample_witness_instance::>(&nova.cs_pp, rng)?; - - // proof verification fails with incorrect running instance - let mut nova_with_incorrect_running_instance = nova.clone(); - nova_with_incorrect_running_instance.U_i = sampled_committed_instance; - let incorrect_proof = - RandomizedIVCProof::new(&nova_with_incorrect_running_instance, &mut rng)?; - let verify = - RandomizedIVCProof::verify::, Pedersen>( - &nova_with_incorrect_running_instance.r1cs, - &nova_with_incorrect_running_instance.cf_r1cs, - nova_with_incorrect_running_instance.pp_hash, - &nova_with_incorrect_running_instance.poseidon_config, - nova_with_incorrect_running_instance.i, - nova_with_incorrect_running_instance.z_0, - nova_with_incorrect_running_instance.z_i, - &incorrect_proof, - ); - assert!(verify.is_err()); - Ok(()) - } - - #[test] - fn test_zk_nova_verification_fails_with_wrong_running_witness() -> Result<(), Error> { - let mut rng = OsRng; - let poseidon_config = poseidon_canonical_config::(); - let F_circuit = CubicFCircuit::::new(())?; - let (_, nova) = test_ivc_opt::< - Pedersen, - Pedersen, - true, - >(poseidon_config.clone(), F_circuit, 3)?; - let (sampled_committed_witness, _) = nova - .r1cs - .sample_witness_instance::>(&nova.cs_pp, rng)?; - - // proof generation fails with incorrect running witness - let mut nova_with_incorrect_running_witness = nova.clone(); - nova_with_incorrect_running_witness.W_i = sampled_committed_witness; - let incorrect_proof = - RandomizedIVCProof::new(&nova_with_incorrect_running_witness, &mut rng)?; - let verify = - RandomizedIVCProof::verify::, Pedersen>( - &nova_with_incorrect_running_witness.r1cs, - &nova_with_incorrect_running_witness.cf_r1cs, - nova_with_incorrect_running_witness.pp_hash, - &nova_with_incorrect_running_witness.poseidon_config, - nova_with_incorrect_running_witness.i, - nova_with_incorrect_running_witness.z_0, - nova_with_incorrect_running_witness.z_i, - &incorrect_proof, - ); - assert!(verify.is_err()); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/circuits.rs b/folding-schemes/src/folding/protogalaxy/circuits.rs deleted file mode 100644 index ae178e2fb..000000000 --- a/folding-schemes/src/folding/protogalaxy/circuits.rs +++ /dev/null @@ -1,451 +0,0 @@ -use ark_crypto_primitives::sponge::{ - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, - CryptographicSponge, -}; -use ark_poly::{univariate::DensePolynomial, EvaluationDomain, GeneralEvaluationDomain}; -use ark_r1cs_std::{ - alloc::AllocVar, - convert::ToBitsGadget, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - poly::polynomial::univariate::dense::DensePolynomialVar, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::{fmt::Debug, Zero}; - -use super::{ - folding::lagrange_polys, - utils::{all_powers_var, betas_star_var, exponential_powers_var}, - CommittedInstance, CommittedInstanceVar, ProtoGalaxyCycleFoldConfig, -}; -use crate::{ - folding::{ - circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCommittedInstance, - CycleFoldCommittedInstanceVar, CycleFoldConfig, - }, - nonnative::affine::NonNativeAffineVar, - CF1, - }, - traits::{CommittedInstanceVarOps, Dummy}, - }, - frontend::FCircuit, - transcript::TranscriptVar, - utils::gadgets::VectorGadget, - Curve, -}; - -pub struct FoldingGadget {} - -impl FoldingGadget { - #[allow(clippy::type_complexity)] - pub fn fold_committed_instance( - transcript: &mut impl TranscriptVar, - // running instance - instance: &CommittedInstanceVar, - // incoming instances - vec_instances: &[CommittedInstanceVar], - // polys from P - F_coeffs: Vec>, - K_coeffs: Vec>, - ) -> Result<(CommittedInstanceVar, Vec>), SynthesisError> { - let t = instance.betas.len(); - - // absorb the committed instances - transcript.absorb(instance)?; - transcript.absorb(&vec_instances)?; - - let delta = transcript.get_challenge()?; - let deltas = exponential_powers_var(delta, t); - - transcript.absorb(&F_coeffs)?; - - let alpha = transcript.get_challenge()?; - let alphas = all_powers_var(alpha.clone(), t); - - // F(alpha) = e + \sum_t F_i * alpha^i - let mut F_alpha = instance.e.clone(); - for (i, F_i) in F_coeffs.iter().skip(1).enumerate() { - F_alpha += F_i * &alphas[i + 1]; - } - - let betas_star = betas_star_var(&instance.betas, &deltas, &alpha); - - let k = vec_instances.len(); - let H = - GeneralEvaluationDomain::new(k + 1).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; - let L_X = lagrange_polys(H) - .into_iter() - .map(|poly| { - DensePolynomialVar::from_coefficients_vec( - poly.coeffs - .into_iter() - .map(FpVar::constant) - .collect::>(), - ) - }) - .collect::>(); - let Z_X = DensePolynomialVar::from_coefficients_vec( - DensePolynomial::from(H.vanishing_polynomial()) - .coeffs - .into_iter() - .map(FpVar::constant) - .collect::>(), - ); - let K_X = DensePolynomialVar { coeffs: K_coeffs }; - - transcript.absorb(&K_X.coeffs)?; - - let gamma = transcript.get_challenge()?; - - let L_X_evals = L_X - .iter() - .take(k + 1) - .map(|L| L.evaluate(&gamma)) - .collect::, _>>()?; - - let e_star = F_alpha * &L_X_evals[0] + Z_X.evaluate(&gamma)? * K_X.evaluate(&gamma)?; - - let mut x_star = instance.x.mul_scalar(&L_X_evals[0])?; - for i in 0..k { - x_star = x_star.add(&vec_instances[i].x.mul_scalar(&L_X_evals[i + 1])?)?; - } - - // return the folded instance - Ok(( - CommittedInstanceVar { - betas: betas_star, - // phi will be computed in CycleFold - phi: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, - e: e_star, - x: x_star, - }, - L_X_evals, - )) - } -} - -pub struct AugmentationGadget; - -impl AugmentationGadget { - #[allow(clippy::type_complexity)] - pub fn prepare_and_fold_primary( - transcript: &mut impl TranscriptVar, S>, - U: CommittedInstanceVar, - u_phis: Vec>, - u_xs: Vec>>>, - new_U_phi: NonNativeAffineVar, - F_coeffs: Vec>>, - K_coeffs: Vec>>, - ) -> Result<(CommittedInstanceVar, Vec>>), SynthesisError> { - assert_eq!(u_phis.len(), u_xs.len()); - - // Prepare the incoming instances. - // For each instance `u`, we have `u.betas = []`, `u.e = 0`. - let us = u_phis - .into_iter() - .zip(u_xs) - .map(|(phi, x)| CommittedInstanceVar { - phi, - betas: vec![], - e: FpVar::zero(), - x, - }) - .collect::>(); - - // Fold the incoming instances `us` into the running instance `U`. - let (mut U, L_X_evals) = - FoldingGadget::fold_committed_instance(transcript, &U, &us, F_coeffs, K_coeffs)?; - // Notice that FoldingGadget::fold_committed_instance does not fold phi. - // We set `U.phi` to unconstrained witnesses `U_phi` here, whose - // correctness will be checked on the other curve. - U.phi = new_U_phi; - - Ok((U, L_X_evals)) - } -} - -/// `AugmentedFCircuit` enhances the original step function `F`, so that it can -/// be used in recursive arguments such as IVC. -/// -/// The method for converting `F` to `AugmentedFCircuit` (`F'`) is defined in -/// [Nova](https://eprint.iacr.org/2021/370.pdf), where `AugmentedFCircuit` not -/// only invokes `F`, but also adds additional constraints for verifying the -/// correct folding of primary instances (i.e., the instances over `C1`). -/// In the paper, the primary instances are Nova's `CommittedInstance`, but we -/// extend this method to support using ProtoGalaxy's `CommittedInstance` as -/// primary instances. -/// -/// Furthermore, to reduce circuit size over `C2`, we implement the constraints -/// defined in [CycleFold](https://eprint.iacr.org/2023/1192.pdf). These extra -/// constraints verify the correct folding of CycleFold instances. -#[derive(Debug, Clone)] -pub struct AugmentedFCircuit>> { - pub(super) poseidon_config: PoseidonConfig>, - pub(super) pp_hash: CF1, - pub(super) i: CF1, - pub(super) i_usize: usize, - pub(super) z_0: Vec>, - pub(super) z_i: Vec>, - pub(super) external_inputs: FC::ExternalInputs, - pub(super) F: FC, // F circuit - pub(super) u_i_phi: C1, - pub(super) U_i: CommittedInstance, - pub(super) U_i1_phi: C1, - pub(super) F_coeffs: Vec>, - pub(super) K_coeffs: Vec>, - - pub(super) cf_u_i_cmW: C2, // input - pub(super) cf_U_i: CycleFoldCommittedInstance, // input - pub(super) cf_cmT: C2, -} - -impl>> AugmentedFCircuit { - pub fn empty( - poseidon_config: &PoseidonConfig>, - F_circuit: FC, - t: usize, - d: usize, - k: usize, - ) -> Self { - let u_dummy = CommittedInstance::dummy((2, t)); - let cf_u_dummy = - CycleFoldCommittedInstance::dummy(ProtoGalaxyCycleFoldConfig::::IO_LEN); - - Self { - poseidon_config: poseidon_config.clone(), - pp_hash: CF1::::zero(), - i: CF1::::zero(), - i_usize: 0, - z_0: vec![CF1::::zero(); F_circuit.state_len()], - z_i: vec![CF1::::zero(); F_circuit.state_len()], - external_inputs: FC::ExternalInputs::default(), - u_i_phi: C1::zero(), - U_i: u_dummy, - U_i1_phi: C1::zero(), - F_coeffs: vec![CF1::::zero(); t], - K_coeffs: vec![CF1::::zero(); d * k + 1], - F: F_circuit, - // cyclefold values - cf_u_i_cmW: C2::zero(), - cf_U_i: cf_u_dummy, - cf_cmT: C2::zero(), - } - } -} - -impl AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - pub fn compute_next_state( - self, - cs: ConstraintSystemRef>, - ) -> Result>>, SynthesisError> { - let pp_hash = FpVar::>::new_witness(cs.clone(), || Ok(self.pp_hash))?; - let i = FpVar::>::new_witness(cs.clone(), || Ok(self.i))?; - let z_0 = Vec::>>::new_witness(cs.clone(), || Ok(self.z_0))?; - let z_i = Vec::>>::new_witness(cs.clone(), || Ok(self.z_i))?; - let external_inputs = - FC::ExternalInputsVar::new_witness(cs.clone(), || Ok(self.external_inputs))?; - - let u_dummy = CommittedInstance::::dummy((2, self.U_i.betas.len())); - let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.U_i))?; - let u_i_phi = NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.u_i_phi))?; - let U_i1_phi = NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.U_i1_phi))?; - - let cf_u_dummy = - CycleFoldCommittedInstance::dummy(ProtoGalaxyCycleFoldConfig::::IO_LEN); - let cf_U_i = - CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.cf_U_i))?; - let cf_cmT = C2::Var::new_witness(cs.clone(), || Ok(self.cf_cmT))?; - - let F_coeffs = Vec::new_witness(cs.clone(), || Ok(self.F_coeffs))?; - let K_coeffs = Vec::new_witness(cs.clone(), || Ok(self.K_coeffs))?; - - // `sponge` is for digest computation. - let sponge = PoseidonSpongeVar::new_with_pp_hash(&self.poseidon_config, &pp_hash)?; - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - let is_basecase = i.is_zero()?; - - // Primary Part - // P.1. Compute u_i.x - // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash(&sponge, &i, &z_0, &z_i)?; - // u_i.x[1] = H(cf_U_i) - let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge)?; - - // P.2. Prepare incoming primary instances - // P.3. Fold incoming primary instances into the running instance - let (U_i1, r) = AugmentationGadget::prepare_and_fold_primary( - &mut transcript, - U_i.clone(), - vec![u_i_phi.clone()], - vec![vec![u_i_x, cf_u_i_x]], - U_i1_phi, - F_coeffs, - K_coeffs, - )?; - - // P.4.a compute and check the first output of F' - - // get z_{i+1} from the F circuit - let z_i1 = - self.F - .generate_step_constraints(cs.clone(), self.i_usize, z_i, external_inputs)?; - - // Base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{\bot}) - // Non-base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{i+1}) - let (u_i1_x, _) = - U_i1.clone() - .hash(&sponge, &(i + FpVar::>::one()), &z_0, &z_i1)?; - let (u_i1_x_base, _) = CommittedInstanceVar::new_constant(cs.clone(), u_dummy)?.hash( - &sponge, - &FpVar::>::one(), - &z_0, - &z_i1, - )?; - let x = is_basecase.select(&u_i1_x_base, &u_i1_x)?; - // This line "converts" `x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `x`. - // While comparing `x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `x` computed in-circuit. - FpVar::new_input(cs.clone(), || x.value())?.enforce_equal(&x)?; - - // CycleFold part - // C.1. Compute `cf_u_i.x` - // C.2. Construct `cf_u_i` - let cf_u_i = CycleFoldCommittedInstanceVar::new_incoming_from_components( - // `cf_u_i.cmW` is provided by the prover as witness. - C2::Var::new_witness(cs.clone(), || Ok(self.cf_u_i_cmW))?, - // To construct `cf_u_i.x`, we need to provide the randomness `r` as - // well as the `phi` component in committed instances `U_i`, `u_i`, - // and `U_{i+1}`. - // Note that the randomness `r` is converted to `r_0, r_1 / r_0` due - // to how `ProtoGalaxyCycleFoldConfig::alloc_randomnesses` creates - // randomness in the CycleFold circuit. - &[ - r[0].to_bits_le()?, - r[1].mul_by_inverse(&r[0])?.to_bits_le()?, - ] - .concat(), - vec![U_i.phi, u_i_phi, U_i1.phi], - )?; - - // C.2. Prepare incoming CycleFold instances - // C.3. Fold incoming CycleFold instances into the running instance - let cf_U_i1 = CycleFoldAugmentationGadget::fold_gadget( - &mut transcript, - cf_U_i, - vec![cf_u_i], - vec![cf_cmT], - )?; - - // Back to Primary Part - // P.4.b compute and check the second output of F' - // Base case: u_{i+1}.x[1] == H(cf_U_{\bot}) - // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) - let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge)?; - let (cf_u_i1_x_base, _) = - CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? - .hash(&sponge)?; - let cf_x = is_basecase.select(&cf_u_i1_x_base, &cf_u_i1_x)?; - // This line "converts" `cf_x` from a witness to a public input. - // Instead of directly modifying the constraint system, we explicitly - // allocate a public input and enforce that its value is indeed `cf_x`. - // While comparing `cf_x` with itself seems redundant, this is necessary - // because: - // - `.value()` allows an honest prover to extract public inputs without - // computing them outside the circuit. - // - `.enforce_equal()` prevents a malicious prover from claiming wrong - // public inputs that are not the honest `cf_x` computed in-circuit. - FpVar::new_input(cs.clone(), || cf_x.value())?.enforce_equal(&cf_x)?; - - Ok(z_i1) - } -} - -impl ConstraintSynthesizer> for AugmentedFCircuit -where - C1: Curve, - C2: Curve, - FC: FCircuit>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - self.compute_next_state(cs).map(|_| ()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::{ - arith::r1cs::tests::get_test_r1cs, - folding::protogalaxy::folding::{tests::prepare_inputs, Folding}, - transcript::{poseidon::poseidon_canonical_config, Transcript}, - Error, - }; - - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_relations::gr1cs::ConstraintSystem; - - #[test] - fn test_folding_gadget() -> Result<(), Error> { - let k = 7; - let (witness, instance, witnesses, instances) = prepare_inputs(k)?; - let r1cs = get_test_r1cs::(); - - // init Prover & Verifier's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for testing - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - let mut transcript_v = transcript_p.clone(); - - let (_, _, proof, _) = Folding::::prove( - &mut transcript_p, - &r1cs, - &instance, - &witness, - &instances, - &witnesses, - )?; - - let folded_instance = - Folding::::verify(&mut transcript_v, &instance, &instances, proof.clone())?; - - let cs = ConstraintSystem::new_ref(); - let pp_hash_var = FpVar::new_witness(cs.clone(), || Ok(pp_hash))?; - let mut transcript_var = - PoseidonSpongeVar::new_with_pp_hash(&poseidon_config, &pp_hash_var)?; - let instance_var = CommittedInstanceVar::new_witness(cs.clone(), || Ok(instance))?; - let instances_var = Vec::new_witness(cs.clone(), || Ok(instances))?; - let F_coeffs_var = Vec::new_witness(cs.clone(), || Ok(proof.F_coeffs))?; - let K_coeffs_var = Vec::new_witness(cs.clone(), || Ok(proof.K_coeffs))?; - - let (folded_instance_var, _) = FoldingGadget::fold_committed_instance( - &mut transcript_var, - &instance_var, - &instances_var, - F_coeffs_var, - K_coeffs_var, - )?; - assert_eq!(folded_instance.betas, folded_instance_var.betas.value()?); - assert_eq!(folded_instance.e, folded_instance_var.e.value()?); - assert_eq!(folded_instance.x, folded_instance_var.x.value()?); - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/constants.rs b/folding-schemes/src/folding/protogalaxy/constants.rs deleted file mode 100644 index cadbf103d..000000000 --- a/folding-schemes/src/folding/protogalaxy/constants.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// `RUNNING` indicates that the committed instance is a running instance. -pub const RUNNING: bool = true; -/// `INCOMING` indicates that the committed instance is an incoming instance. -pub const INCOMING: bool = false; diff --git a/folding-schemes/src/folding/protogalaxy/decider_eth.rs b/folding-schemes/src/folding/protogalaxy/decider_eth.rs deleted file mode 100644 index 54613e246..000000000 --- a/folding-schemes/src/folding/protogalaxy/decider_eth.rs +++ /dev/null @@ -1,492 +0,0 @@ -/// This file implements the Protogalaxy's onchain (Ethereum's EVM) decider. For non-ethereum use cases, -/// the Decider from decider.rs file will be more efficient. -/// More details can be found at the documentation page: -/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_snark::SNARK; -use ark_std::{ - log2, - marker::PhantomData, - rand::{CryptoRng, RngCore}, - One, Zero, -}; - -pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::decider_eth_circuit::DeciderProtoGalaxyGadget; -use super::ProtoGalaxy; -use crate::arith::Arith; -use crate::folding::traits::{InputizeNonNative, WitnessOps}; -use crate::folding::{circuits::decider::DeciderEnabledNIFS, traits::Dummy}; -use crate::frontend::FCircuit; -use crate::Error; -use crate::{ - commitment::{kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme}, - Curve, -}; -use crate::{Decider as DeciderTrait, FoldingScheme}; - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof -where - C: Curve, - CS: CommitmentScheme, - S: SNARK, -{ - snark_proof: S::Proof, - kzg_proofs: [CS::Proof; 1], - L_X_evals: Vec, - // the KZG challenges are provided by the prover, but in-circuit they are checked to match - // the in-circuit computed computed ones. - kzg_challenges: [C::ScalarField; 1], -} - -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct VerifierParam -where - C1: Curve, - CS_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, - S_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, -{ - pub pp_hash: C1::ScalarField, - pub snark_vp: S_VerifyingKey, - pub cs_vp: CS_VerifyingKey, -} - -/// Onchain Decider, for ethereum use cases -#[derive(Clone, Debug)] -pub struct Decider { - _c1: PhantomData, - _c2: PhantomData, - _fc: PhantomData, - _cs1: PhantomData, - _cs2: PhantomData, - _s: PhantomData, - _fs: PhantomData, -} - -impl DeciderTrait - for Decider -where - C1: Curve, - C2: Curve, - FC: FCircuit, - // CS1 is a KZG commitment, where challenge is C1::Fr elem - CS1: CommitmentScheme< - C1, - ProverChallenge = C1::ScalarField, - Challenge = C1::ScalarField, - Proof = KZGProof, - >, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - S: SNARK, - FS: FoldingScheme, - // constrain FS into ProtoGalaxy, since this is a Decider specifically for ProtoGalaxy - ProtoGalaxy: From, - crate::folding::protogalaxy::ProverParams: - From<>::ProverParam>, - crate::folding::protogalaxy::VerifierParams: - From<>::VerifierParam>, -{ - type PreprocessorParam = ((FS::ProverParam, FS::VerifierParam), usize); - type ProverParam = (S::ProvingKey, CS1::ProverParams); - type Proof = Proof; - type VerifierParam = VerifierParam; - type PublicInput = Vec; - type CommittedInstance = Vec; - - fn preprocess( - mut rng: impl RngCore + CryptoRng, - ((pp, vp), state_len): Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - // get the FoldingScheme prover & verifier params from ProtoGalaxy - let protogalaxy_pp: as FoldingScheme< - C1, - C2, - FC, - >>::ProverParam = pp.into(); - let protogalaxy_vp: as FoldingScheme< - C1, - C2, - FC, - >>::VerifierParam = vp.into(); - let pp_hash = protogalaxy_vp.pp_hash()?; - - // We fix `k`, the number of incoming instances, to 1, because - // multi-instances folding is not supported yet. - // TODO: Support multi-instances folding and make `k` a constant generic parameter (as in - // HyperNova). Tracking issue: - // https://github.com/privacy-scaling-explorations/sonobe/issues/82 - let k = 1; - let d = protogalaxy_vp.r1cs.degree(); - let t = log2(protogalaxy_vp.r1cs.n_constraints()) as usize; - - let circuit = DeciderEthCircuit::::dummy(( - protogalaxy_vp.r1cs, - protogalaxy_vp.cf_r1cs, - protogalaxy_pp.cf_cs_params, - protogalaxy_pp.poseidon_config, - (t, d, k), - k + 1, // `k + 1` is the length of `L_X_evals` - state_len, - 1, // ProtoGalaxy's running CommittedInstance contains 1 commitment - )); - - // get the Groth16 specific setup for the circuit - let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng) - .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; - - let pp = (g16_pk, protogalaxy_pp.cs_params); - let vp = Self::VerifierParam { - pp_hash, - snark_vp: g16_vk, - cs_vp: protogalaxy_vp.cs_vp, - }; - Ok((pp, vp)) - } - - fn prove( - mut rng: impl RngCore + CryptoRng, - pp: Self::ProverParam, - folding_scheme: FS, - ) -> Result { - let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - - let circuit = DeciderEthCircuit::::try_from(ProtoGalaxy::from(folding_scheme))?; - - let L_X_evals = circuit.randomness.clone(); - - // get the challenges that have been already computed when preparing the circuit inputs in - // the above `try_from` call - let kzg_challenges = circuit.kzg_challenges.clone(); - - // generate KZG proofs - let kzg_proofs = circuit - .W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| { - CS1::prove_with_challenge(&cs_pk, c, v, &C1::ScalarField::zero(), None) - }) - .collect::, _>>()?; - - let snark_proof = - S::prove(&snark_pk, circuit, &mut rng).map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self::Proof { - snark_proof, - L_X_evals, - kzg_proofs: kzg_proofs.try_into().map_err(|_| { - Error::ConversionError( - "Vec<_>".to_string(), - "[_; 1]".to_string(), - "variable name: kzg_proofs".to_string(), - ) - })?, - kzg_challenges: kzg_challenges.try_into().map_err(|_| { - Error::ConversionError( - "Vec<_>".to_string(), - "[_; 1]".to_string(), - "variable name: kzg_challenges".to_string(), - ) - })?, - }) - } - - fn verify( - vp: Self::VerifierParam, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - // we don't use the instances at the verifier level, since we check them in-circuit - running_commitments: &Self::CommittedInstance, - incoming_commitments: &Self::CommittedInstance, - proof: &Self::Proof, - ) -> Result { - if i <= C1::ScalarField::one() { - return Err(Error::NotEnoughSteps); - } - - let Self::VerifierParam { - pp_hash, - snark_vp, - cs_vp, - } = vp; - - // 6.2. Fold the commitments - let U_final_commitments = DeciderProtoGalaxyGadget::fold_group_elements_native( - running_commitments, - incoming_commitments, - None, - proof.L_X_evals.clone(), - )?; - - let public_input = [ - &[pp_hash, i][..], - &z_0, - &z_i, - &U_final_commitments.inputize_nonnative(), - &proof.kzg_challenges, - &proof.kzg_proofs.iter().map(|p| p.eval).collect::>(), - &proof.L_X_evals, - ] - .concat(); - - let snark_v = S::verify(&snark_vp, &public_input, &proof.snark_proof) - .map_err(|e| Error::Other(e.to_string()))?; - if !snark_v { - return Err(Error::SNARKVerificationFail); - } - - // 7.3. Verify the KZG proofs - for ((cm, &c), pi) in U_final_commitments - .iter() - .zip(&proof.kzg_challenges) - .zip(&proof.kzg_proofs) - { - // we're at the Ethereum EVM case, so the CS1 is KZG commitments - CS1::verify_with_challenge(&cs_vp, c, cm, pi)?; - } - - Ok(true) - } -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::Bn254; - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_groth16::Groth16; - use ark_grumpkin::Projective as Projective2; - use std::time::Instant; - - use super::*; - use crate::commitment::kzg::KZG; - use crate::commitment::pedersen::Pedersen; - use crate::folding::protogalaxy::ProverParams; - use crate::folding::traits::CommittedInstanceOps; - use crate::frontend::utils::CubicFCircuit; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::Error; - - #[test] - fn test_decider() -> Result<(), Error> { - // use ProtoGalaxy as FoldingScheme - type PG = ProtoGalaxy< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - PG, // here we define the FoldingScheme to use - >; - - let mut rng = rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let preprocessor_param = (poseidon_config, F_circuit); - let protogalaxy_params = PG::preprocess(&mut rng, &preprocessor_param)?; - - let start = Instant::now(); - let mut protogalaxy = PG::init(&protogalaxy_params, F_circuit, z_0.clone())?; - println!("ProtoGalaxy initialized, {:?}", start.elapsed()); - protogalaxy.prove_step(&mut rng, (), None)?; - protogalaxy.prove_step(&mut rng, (), None)?; // do a 2nd step - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = - D::preprocess(&mut rng, (protogalaxy_params, F_circuit.state_len()))?; - - // decider proof generation - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, protogalaxy.clone())?; - println!("Decider prove, {:?}", start.elapsed()); - - // decider proof verification - let start = Instant::now(); - let verified = D::verify( - decider_vp.clone(), - protogalaxy.i, - protogalaxy.z_0.clone(), - protogalaxy.z_i.clone(), - &protogalaxy.U_i.get_commitments(), - &protogalaxy.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); - - // decider proof verification using the deserialized data - let verified = D::verify( - decider_vp, - protogalaxy.i, - protogalaxy.z_0, - protogalaxy.z_i, - &protogalaxy.U_i.get_commitments(), - &protogalaxy.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - Ok(()) - } - - // Test to check the serialization and deserialization of diverse Decider related parameters. - // This test is the same test as `test_decider` but it serializes values and then uses the - // deserialized values to continue the checks. - #[test] - fn test_decider_serialization() -> Result<(), Error> { - // use ProtoGalaxy as FoldingScheme - type PG = ProtoGalaxy< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - >; - type D = Decider< - Projective, - Projective2, - CubicFCircuit, - KZG<'static, Bn254>, - Pedersen, - Groth16, // here we define the Snark to use in the decider - PG, // here we define the FoldingScheme to use - >; - - let mut rng = rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - let preprocessor_param = (poseidon_config, F_circuit); - let protogalaxy_params = PG::preprocess(&mut rng, &preprocessor_param)?; - - // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = D::preprocess( - &mut rng, - (protogalaxy_params.clone(), F_circuit.state_len()), - )?; - - // serialize the Nova params. These params are the trusted setup of the commitment schemes used - // (ie. KZG & Pedersen in this case) - let mut protogalaxy_pp_serialized = vec![]; - protogalaxy_params - .0 - .serialize_compressed(&mut protogalaxy_pp_serialized)?; - let mut protogalaxy_vp_serialized = vec![]; - protogalaxy_params - .1 - .serialize_compressed(&mut protogalaxy_vp_serialized)?; - // deserialize the Nova params. This would be done by the client reading from a file - let protogalaxy_pp_deserialized = ProverParams::< - Projective, - Projective2, - KZG<'static, Bn254>, - Pedersen, - >::deserialize_compressed( - &mut protogalaxy_pp_serialized.as_slice() - )?; - let protogalaxy_vp_deserialized = , - >>::vp_deserialize_with_mode( - &mut protogalaxy_vp_serialized.as_slice(), - ark_serialize::Compress::Yes, - ark_serialize::Validate::Yes, - (), // fcircuit_params - )?; - - // initialize protogalaxy again, but from the deserialized parameters - let protogalaxy_params = (protogalaxy_pp_deserialized, protogalaxy_vp_deserialized); - let mut protogalaxy = PG::init(&protogalaxy_params, F_circuit, z_0)?; - - let start = Instant::now(); - protogalaxy.prove_step(&mut rng, (), None)?; - println!("prove_step, {:?}", start.elapsed()); - protogalaxy.prove_step(&mut rng, (), None)?; // do a 2nd step - - // decider proof generation - let start = Instant::now(); - let proof = D::prove(rng, decider_pp, protogalaxy.clone())?; - println!("Decider prove, {:?}", start.elapsed()); - - // decider proof verification - let start = Instant::now(); - let verified = D::verify( - decider_vp.clone(), - protogalaxy.i, - protogalaxy.z_0.clone(), - protogalaxy.z_i.clone(), - &protogalaxy.U_i.get_commitments(), - &protogalaxy.u_i.get_commitments(), - &proof, - )?; - assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); - - // The rest of this test will serialize the data and deserialize it back, and use it to - // verify the proof: - - // serialize the verifier_params, proof and public inputs - let mut decider_vp_serialized = vec![]; - decider_vp.serialize_compressed(&mut decider_vp_serialized)?; - let mut proof_serialized = vec![]; - proof.serialize_compressed(&mut proof_serialized)?; - // serialize the public inputs in a single packet - let mut public_inputs_serialized = vec![]; - protogalaxy - .i - .serialize_compressed(&mut public_inputs_serialized)?; - protogalaxy - .z_0 - .serialize_compressed(&mut public_inputs_serialized)?; - protogalaxy - .z_i - .serialize_compressed(&mut public_inputs_serialized)?; - - // deserialize back the verifier_params, proof and public inputs - let decider_vp_deserialized = - VerifierParam::< - Projective, - as CommitmentScheme>::VerifierParams, - as SNARK>::VerifyingKey, - >::deserialize_compressed(&mut decider_vp_serialized.as_slice())?; - let proof_deserialized = - Proof::, Groth16>::deserialize_compressed( - &mut proof_serialized.as_slice(), - )?; - - // deserialize the public inputs from the single packet 'public_inputs_serialized' - let mut reader = public_inputs_serialized.as_slice(); - let i_deserialized = Fr::deserialize_compressed(&mut reader)?; - let z_0_deserialized = Vec::::deserialize_compressed(&mut reader)?; - let z_i_deserialized = Vec::::deserialize_compressed(&mut reader)?; - - // decider proof verification using the deserialized data - let verified = D::verify( - decider_vp_deserialized, - i_deserialized, - z_0_deserialized, - z_i_deserialized, - &protogalaxy.U_i.get_commitments(), - &protogalaxy.u_i.get_commitments(), - &proof_deserialized, - )?; - assert!(verified); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs b/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs deleted file mode 100644 index 874325307..000000000 --- a/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs +++ /dev/null @@ -1,239 +0,0 @@ -/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, -/// other more efficient approaches can be used. -use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - eq::EqGadget, - fields::fp::FpVar, - GR1CSVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, marker::PhantomData}; - -use crate::{ - arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, - commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, - folding::{ - circuits::{ - decider::{ - on_chain::GenericOnchainDeciderCircuit, DeciderEnabledNIFS, EvalGadget, - KZGChallengesGadget, - }, - CF1, - }, - traits::{WitnessOps, WitnessVarOps}, - }, - frontend::FCircuit, - transcript::Transcript, - Curve, Error, -}; - -use super::{ - circuits::FoldingGadget, - constants::{INCOMING, RUNNING}, - folding::{Folding, ProtoGalaxyProof}, - CommittedInstance, CommittedInstanceVar, ProtoGalaxy, Witness, -}; - -/// In-circuit representation of the Witness associated to the CommittedInstance. -#[derive(Debug, Clone)] -pub struct WitnessVar { - pub W: Vec>, - pub rW: FpVar, -} - -impl AllocVar, F> for WitnessVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().w.to_vec()), mode)?; - let rW = FpVar::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; - - Ok(Self { W, rW }) - }) - } -} - -impl WitnessVarOps for WitnessVar { - fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { - vec![(&self.W, self.rW.clone())] - } -} - -pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< - C1, - C2, - CommittedInstance, - CommittedInstance, - Witness>, - R1CS>, - R1CSMatricesVar, FpVar>>, - DeciderProtoGalaxyGadget, ->; - -/// returns an instance of the DeciderEthCircuit from the given ProtoGalaxy struct -impl< - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider - CS2: CommitmentScheme>, - > TryFrom> for DeciderEthCircuit -{ - type Error = Error; - - fn try_from(protogalaxy: ProtoGalaxy) -> Result { - let mut transcript = - PoseidonSponge::new_with_pp_hash(&protogalaxy.poseidon_config, protogalaxy.pp_hash); - - let (U_i1, W_i1, proof, aux) = Folding::prove( - &mut transcript, - &protogalaxy.r1cs, - &protogalaxy.U_i, - &protogalaxy.W_i, - &[protogalaxy.u_i.clone()], - &[protogalaxy.w_i.clone()], - )?; - - // compute the KZG challenges used as inputs in the circuit - let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); - - // get KZG evals - let kzg_evaluations = W_i1 - .get_openings() - .iter() - .zip(&kzg_challenges) - .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) - .collect::, _>>()?; - - Ok(Self { - _avar: PhantomData, - arith: protogalaxy.r1cs, - cf_arith: protogalaxy.cf_r1cs, - cf_pedersen_params: protogalaxy.cf_cs_params, - poseidon_config: protogalaxy.poseidon_config, - pp_hash: protogalaxy.pp_hash, - i: protogalaxy.i, - z_0: protogalaxy.z_0, - z_i: protogalaxy.z_i, - U_i: protogalaxy.U_i, - W_i: protogalaxy.W_i, - u_i: protogalaxy.u_i, - w_i: protogalaxy.w_i, - U_i1, - W_i1, - proof, - randomness: aux.L_X_evals, - cf_U_i: protogalaxy.cf_U_i, - cf_W_i: protogalaxy.cf_W_i, - kzg_challenges, - kzg_evaluations, - }) - } -} - -pub struct DeciderProtoGalaxyGadget; - -impl - DeciderEnabledNIFS< - C, - CommittedInstance, - CommittedInstance, - Witness>, - R1CS>, - > for DeciderProtoGalaxyGadget -{ - type Proof = ProtoGalaxyProof>; - type ProofDummyCfg = (usize, usize, usize); - type Randomness = Vec>; - type RandomnessDummyCfg = usize; - - fn fold_field_elements_gadget( - _arith: &R1CS>, - transcript: &mut PoseidonSpongeVar>, - U: CommittedInstanceVar, - _U_vec: Vec>>, - u: CommittedInstanceVar, - proof: Self::Proof, - randomness: Self::Randomness, - ) -> Result, SynthesisError> { - let cs = U.e.cs(); - let F_coeffs = Vec::new_witness(cs.clone(), || Ok(&proof.F_coeffs[..]))?; - let K_coeffs = Vec::new_witness(cs.clone(), || Ok(&proof.K_coeffs[..]))?; - let randomness = Vec::new_input(cs.clone(), || Ok(randomness))?; - - let (U_next, L_X_evals) = - FoldingGadget::fold_committed_instance(transcript, &U, &[u], F_coeffs, K_coeffs)?; - L_X_evals.enforce_equal(&randomness)?; - - Ok(U_next) - } - - fn fold_group_elements_native( - U_commitments: &[C], - u_commitments: &[C], - _: Option, - L_X_evals: Self::Randomness, - ) -> Result, Error> { - let U_phi = U_commitments[0]; - let u_phi = u_commitments[0]; - Ok(vec![U_phi * L_X_evals[0] + u_phi * L_X_evals[1]]) - } -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - - use super::*; - use crate::commitment::pedersen::Pedersen; - use crate::folding::protogalaxy::ProtoGalaxy; - use crate::frontend::{utils::CubicFCircuit, FCircuit}; - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::FoldingScheme; - - #[test] - fn test_decider_circuit() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - let z_0 = vec![Fr::from(3_u32)]; - - type PG = ProtoGalaxy< - Projective, - Projective2, - CubicFCircuit, - Pedersen, - Pedersen, - >; - let pg_params = PG::preprocess(&mut rng, &(poseidon_config, F_circuit))?; - - // generate a Nova instance and do a step of it - let mut protogalaxy = PG::init(&pg_params, F_circuit, z_0.clone())?; - protogalaxy.prove_step(&mut rng, (), None)?; - - let ivc_proof = protogalaxy.ivc_proof(); - PG::verify(pg_params.1, ivc_proof)?; - - // load the DeciderEthCircuit from the generated Nova instance - let decider_circuit = DeciderEthCircuit::::try_from(protogalaxy)?; - - let cs = ConstraintSystem::::new_ref(); - - // generate the constraints and check that are satisfied by the inputs - decider_circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - dbg!(cs.num_constraints()); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/folding.rs b/folding-schemes/src/folding/protogalaxy/folding.rs deleted file mode 100644 index e8a018889..000000000 --- a/folding-schemes/src/folding/protogalaxy/folding.rs +++ /dev/null @@ -1,573 +0,0 @@ -/// Implements the scheme described in [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) -use ark_ff::PrimeField; -use ark_poly::{ - univariate::{DensePolynomial, SparsePolynomial}, - DenseUVPolynomial, EvaluationDomain, Evaluations, GeneralEvaluationDomain, Polynomial, -}; -use ark_std::{cfg_into_iter, log2, One, Zero}; -use rayon::prelude::*; -use std::marker::PhantomData; - -use super::utils::{all_powers, betas_star, exponential_powers, pow_i}; -use super::ProtoGalaxyError; -use super::{CommittedInstance, Witness}; - -use crate::transcript::Transcript; -use crate::utils::vec::*; -use crate::Error; -use crate::{arith::r1cs::R1CS, Curve}; -use crate::{arith::Arith, folding::traits::Dummy}; - -#[derive(Debug, Clone)] -pub struct ProtoGalaxyProof { - pub F_coeffs: Vec, - pub K_coeffs: Vec, -} - -impl Dummy<(usize, usize, usize)> for ProtoGalaxyProof { - fn dummy((t, d, k): (usize, usize, usize)) -> Self { - Self { - F_coeffs: vec![F::zero(); t], - K_coeffs: vec![F::zero(); d * k + 1], - } - } -} - -#[derive(Debug, Clone)] -pub struct ProtoGalaxyAux { - pub L_X_evals: Vec, - pub phi_stars: Vec, -} - -#[derive(Clone, Debug)] -/// Implements the protocol described in section 4 of -/// [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) -pub struct Folding { - _phantom: PhantomData, -} -impl Folding { - #![allow(clippy::type_complexity)] - /// implements the non-interactive Prover from the folding scheme described in section 4 - pub fn prove( - transcript: &mut impl Transcript, - r1cs: &R1CS, - // running instance - instance: &CommittedInstance, - w: &Witness, - // incoming instances - vec_instances: &[CommittedInstance], - vec_w: &[Witness], - ) -> Result< - ( - CommittedInstance, - Witness, - ProtoGalaxyProof, - ProtoGalaxyAux, - ), - Error, - > { - if vec_instances.len() != vec_w.len() { - return Err(Error::NotSameLength( - "vec_instances.len()".to_string(), - vec_instances.len(), - "vec_w.len()".to_string(), - vec_w.len(), - )); - } - let d = r1cs.degree(); - let k = vec_instances.len(); - let t = instance.betas.len(); - let n = r1cs.n_variables(); - let m = r1cs.n_constraints(); - - let z = [vec![C::ScalarField::one()], instance.x.clone(), w.w.clone()].concat(); - - if z.len() != n { - return Err(Error::NotSameLength( - "z.len()".to_string(), - z.len(), - "number of variables in R1CS".to_string(), // hardcoded to R1CS - n, - )); - } - if log2(m) as usize != t { - return Err(Error::NotSameLength( - "log2(number of constraints in R1CS)".to_string(), - log2(m) as usize, - "instance.betas.len()".to_string(), - t, - )); - } - if !(k + 1).is_power_of_two() { - return Err(Error::ProtoGalaxy(ProtoGalaxyError::WrongNumInstances(k))); - } - - // absorb the committed instances - transcript.absorb(instance); - transcript.absorb(&vec_instances); - - let delta = transcript.get_challenge(); - let deltas = exponential_powers(delta, t); - - let mut f_z = r1cs.eval_at_z(&z)?; - if f_z.len() != m { - return Err(Error::NotSameLength( - "number of constraints in R1CS".to_string(), - m, - "f_z.len()".to_string(), - f_z.len(), - )); - } - f_z.resize(1 << t, C::ScalarField::zero()); - - // F(X) - let F_X: SparsePolynomial = - calc_f_from_btree(&f_z, &instance.betas, &deltas).expect("Error calculating F[x]"); - let F_X_dense = DensePolynomial::from(F_X.clone()); - let mut F_coeffs = F_X_dense.coeffs; - F_coeffs.resize(t, C::ScalarField::zero()); - transcript.absorb(&F_coeffs); - - let alpha = transcript.get_challenge(); - - // eval F(alpha) - let F_alpha = F_X.evaluate(&alpha); - - // betas* - let betas_star = betas_star(&instance.betas, &deltas, alpha); - - // sanity check: check that the new randomized instance (the original instance but with - // 'refreshed' randomness) satisfies the relation. - #[cfg(test)] - { - use crate::arith::ArithRelation; - r1cs.check_relation( - w, - &CommittedInstance::<_, true> { - phi: instance.phi, - betas: betas_star.clone(), - e: F_alpha, - x: instance.x.clone(), - }, - )?; - } - - let zs: Vec> = std::iter::once(z.clone()) - .chain( - vec_w - .iter() - .zip(vec_instances) - .map(|(wj, uj)| { - let zj = [vec![C::ScalarField::one()], uj.x.clone(), wj.w.clone()].concat(); - if zj.len() != n { - return Err(Error::NotSameLength( - "zj.len()".to_string(), - zj.len(), - "number of variables in R1CS".to_string(), - n, - )); - } - Ok(zj) - }) - .collect::>, Error>>()?, - ) - .collect::>>(); - - let H = - GeneralEvaluationDomain::::new(k + 1).ok_or(Error::NewDomainFail)?; - let G_domain = GeneralEvaluationDomain::::new((d * k) + 1) - .ok_or(Error::NewDomainFail)?; - let L_X: Vec> = lagrange_polys(H); - - // K(X) computation in a naive way, next iterations will compute K(X) as described in Claim - // 4.5 of the paper. - let mut G_evals: Vec = vec![C::ScalarField::zero(); G_domain.size()]; - for (hi, h) in G_domain.elements().enumerate() { - // each iteration evaluates G(h) - // inner = L_0(x) * z + \sum_k L_i(x) * z_j - let mut inner: Vec = vec![C::ScalarField::zero(); zs[0].len()]; - for (z, L) in zs.iter().zip(&L_X) { - // Li_z_h = (Li(X)*zj)(h) = Li(h) * zj - let Lh = L.evaluate(&h); - for (j, zj) in z.iter().enumerate() { - inner[j] += Lh * zj; - } - } - let f_ev = r1cs.eval_at_z(&inner)?; - - G_evals[hi] = cfg_into_iter!(f_ev) - .enumerate() - .map(|(i, f_ev_i)| pow_i(i, &betas_star) * f_ev_i) - .sum(); - } - let G_X: DensePolynomial = - Evaluations::::from_vec_and_domain(G_evals, G_domain).interpolate(); - let Z_X: DensePolynomial = H.vanishing_polynomial().into(); - // K(X) = (G(X) - F(alpha)*L_0(X)) / Z(X) - // Notice that L0(X)*F(a) will be 0 in the native case (the instance of the first folding - // iteration case). - let L0_e = &L_X[0] * F_alpha; - let G_L0e = &G_X - &L0_e; - // Pending optimization: move division by Z_X to the prev loop - let (K_X, remainder) = G_L0e.divide_by_vanishing_poly(H); - if !remainder.is_zero() { - return Err(Error::ProtoGalaxy(ProtoGalaxyError::RemainderNotZero)); - } - - let mut K_coeffs = K_X.coeffs.clone(); - K_coeffs.resize(d * k + 1, C::ScalarField::zero()); - transcript.absorb(&K_coeffs); - - let gamma = transcript.get_challenge(); - - let L_X_evals = L_X - .iter() - .take(k + 1) - .map(|L| L.evaluate(&gamma)) - .collect::>(); - - let mut phi_stars = vec![]; - - let e_star = F_alpha * L_X_evals[0] + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); - let mut w_star = vec_scalar_mul(&w.w, &L_X_evals[0]); - let mut r_w_star = w.r_w * L_X_evals[0]; - let mut phi_star = instance.phi * L_X_evals[0]; - let mut x_star = vec_scalar_mul(&instance.x, &L_X_evals[0]); - for i in 0..k { - w_star = vec_add(&w_star, &vec_scalar_mul(&vec_w[i].w, &L_X_evals[i + 1]))?; - r_w_star += vec_w[i].r_w * L_X_evals[i + 1]; - phi_stars.push(phi_star); // Push before updating. We don't need the last one - phi_star += vec_instances[i].phi * L_X_evals[i + 1]; - x_star = vec_add( - &x_star, - &vec_scalar_mul(&vec_instances[i].x, &L_X_evals[i + 1]), - )?; - } - - Ok(( - CommittedInstance { - betas: betas_star, - phi: phi_star, - e: e_star, - x: x_star, - }, - Witness { - w: w_star, - r_w: r_w_star, - }, - ProtoGalaxyProof { F_coeffs, K_coeffs }, - ProtoGalaxyAux { - L_X_evals, - phi_stars, - }, - )) - } - - /// implements the non-interactive Verifier from the folding scheme described in section 4 - pub fn verify( - transcript: &mut impl Transcript, - // running instance - instance: &CommittedInstance, - // incoming instances - vec_instances: &[CommittedInstance], - // polys from P - proof: ProtoGalaxyProof, - ) -> Result, Error> { - let t = instance.betas.len(); - - // absorb the committed instances - transcript.absorb(instance); - transcript.absorb(&vec_instances); - - let delta = transcript.get_challenge(); - let deltas = exponential_powers(delta, t); - - transcript.absorb(&proof.F_coeffs); - - let alpha = transcript.get_challenge(); - let alphas = all_powers(alpha, t); - - // F(alpha) = e + \sum_t F_i * alpha^i - let mut F_alpha = instance.e; - for (i, F_i) in proof.F_coeffs.iter().skip(1).enumerate() { - F_alpha += *F_i * alphas[i + 1]; - } - - let betas_star = betas_star(&instance.betas, &deltas, alpha); - - transcript.absorb(&proof.K_coeffs); - - let k = vec_instances.len(); - let H = - GeneralEvaluationDomain::::new(k + 1).ok_or(Error::NewDomainFail)?; - let L_X: Vec> = lagrange_polys(H); - let Z_X: DensePolynomial = H.vanishing_polynomial().into(); - let K_X: DensePolynomial = - DensePolynomial::::from_coefficients_vec(proof.K_coeffs); - - let gamma = transcript.get_challenge(); - - let L_X_evals = L_X - .iter() - .take(k + 1) - .map(|L| L.evaluate(&gamma)) - .collect::>(); - - let e_star = F_alpha * L_X_evals[0] + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); - - let mut phi_star = instance.phi * L_X_evals[0]; - let mut x_star = vec_scalar_mul(&instance.x, &L_X_evals[0]); - for i in 0..k { - phi_star += vec_instances[i].phi * L_X_evals[i + 1]; - x_star = vec_add( - &x_star, - &vec_scalar_mul(&vec_instances[i].x, &L_X_evals[i + 1]), - )?; - } - - // return the folded instance - Ok(CommittedInstance { - betas: betas_star, - phi: phi_star, - e: e_star, - x: x_star, - }) - } -} - -/// calculates F[x] using the optimized binary-tree technique -/// described in Claim 4.4 -/// of [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) -fn calc_f_from_btree( - fw: &[F], - betas: &[F], - deltas: &[F], -) -> Result, Error> { - let fw_len = fw.len(); - let betas_len = betas.len(); - let deltas_len = deltas.len(); - - // ensure our binary tree is full - if !fw_len.is_power_of_two() { - return Err(Error::ProtoGalaxy(ProtoGalaxyError::BTreeNotFull(fw_len))); - } - - if betas_len != deltas_len { - return Err(Error::ProtoGalaxy(ProtoGalaxyError::WrongLenBetas( - betas_len, deltas_len, - ))); - } - - let mut layers: Vec>> = Vec::new(); - let leaves: Vec> = fw - .iter() - .copied() - .map(|e| SparsePolynomial::::from_coefficients_slice(&[(0, e)])) - .collect(); - layers.push(leaves.to_vec()); - let mut currentNodes = leaves.clone(); - while currentNodes.len() > 1 { - let index = layers.len(); - layers.push(vec![]); - for (i, ni) in currentNodes.iter().enumerate().step_by(2) { - let left = ni.clone(); - let right = SparsePolynomial::::from_coefficients_vec(vec![ - (0, betas[layers.len() - 2]), - (1, deltas[layers.len() - 2]), - ]) - .mul(¤tNodes[i + 1]); - - layers[index].push(left + right); - } - currentNodes = layers[index].clone(); - } - let root_index = layers.len() - 1; - Ok(layers[root_index][0].clone()) -} - -// lagrange_polys method from caulk: https://github.com/caulk-crypto/caulk/tree/8210b51fb8a9eef4335505d1695c44ddc7bf8170/src/multi/setup.rs#L300 -pub fn lagrange_polys( - domain_n: GeneralEvaluationDomain, -) -> Vec> { - let mut lagrange_polynomials: Vec> = Vec::new(); - for i in 0..domain_n.size() { - let evals: Vec = cfg_into_iter!(0..domain_n.size()) - .map(|k| if k == i { F::one() } else { F::zero() }) - .collect(); - lagrange_polynomials.push(Evaluations::from_vec_and_domain(evals, domain_n).interpolate()); - } - lagrange_polynomials -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_pallas::{Fr, Projective}; - use ark_std::{rand::Rng, UniformRand}; - - use crate::arith::r1cs::tests::{get_test_r1cs, get_test_z_split}; - use crate::arith::ArithRelation; - use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; - use crate::transcript::poseidon::poseidon_canonical_config; - - #[test] - fn test_pow_i() { - let mut rng = ark_std::test_rng(); - let t = 4; - let n = 16; - let beta = Fr::rand(&mut rng); - let betas = exponential_powers(beta, t); - let not_betas = all_powers(beta, n); - - #[allow(clippy::needless_range_loop)] - for i in 0..n { - assert_eq!(pow_i(i, &betas), not_betas[i]); - } - } - - // k represents the number of instances to be fold, apart from the running instance - #[allow(clippy::type_complexity)] - pub fn prepare_inputs( - k: usize, - ) -> Result< - ( - Witness, - CommittedInstance, - Vec>, - Vec>, - ), - Error, - > { - let mut rng = ark_std::test_rng(); - - let (_, x, w) = get_test_z_split::(rng.gen::() as usize); - - let (pedersen_params, _) = Pedersen::::setup(&mut rng, w.len())?; - - let t = log2(get_test_r1cs::().n_constraints()) as usize; - - let beta = C::ScalarField::rand(&mut rng); - let betas = exponential_powers(beta, t); - - let witness = Witness:: { - w, - r_w: C::ScalarField::zero(), - }; - let phi = Pedersen::::commit(&pedersen_params, &witness.w, &witness.r_w)?; - let instance = CommittedInstance:: { - phi, - betas: betas.clone(), - e: C::ScalarField::zero(), - x, - }; - // same for the other instances - let mut witnesses: Vec> = Vec::new(); - let mut instances: Vec> = Vec::new(); - #[allow(clippy::needless_range_loop)] - for _ in 0..k { - let (_, x_i, w_i) = get_test_z_split::(rng.gen::() as usize); - let witness_i = Witness:: { - w: w_i, - r_w: C::ScalarField::zero(), - }; - let phi_i = Pedersen::::commit(&pedersen_params, &witness_i.w, &witness_i.r_w)?; - let instance_i = CommittedInstance:: { - phi: phi_i, - betas: vec![], - e: C::ScalarField::zero(), - x: x_i, - }; - witnesses.push(witness_i); - instances.push(instance_i); - } - - Ok((witness, instance, witnesses, instances)) - } - - #[test] - fn test_fold() -> Result<(), Error> { - let k = 7; - let (witness, instance, witnesses, instances) = prepare_inputs(k)?; - let r1cs = get_test_r1cs::(); - - // init Prover & Verifier's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for testing - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - let mut transcript_v = transcript_p.clone(); - - let (folded_instance, folded_witness, proof, _) = Folding::::prove( - &mut transcript_p, - &r1cs, - &instance, - &witness, - &instances, - &witnesses, - )?; - - // verifier - let folded_instance_v = - Folding::::verify(&mut transcript_v, &instance, &instances, proof)?; - - // check that prover & verifier folded instances are the same values - assert_eq!(folded_instance.phi, folded_instance_v.phi); - assert_eq!(folded_instance.betas, folded_instance_v.betas); - assert_eq!(folded_instance.e, folded_instance_v.e); - assert!(!folded_instance.e.is_zero()); - - // check that the folded instance satisfies the relation - r1cs.check_relation(&folded_witness, &folded_instance)?; - Ok(()) - } - - #[test] - fn test_fold_various_iterations() -> Result<(), Error> { - let r1cs = get_test_r1cs::(); - - // init Prover & Verifier's transcript - let poseidon_config = poseidon_canonical_config::(); - let pp_hash = Fr::from(42u32); // only for testing - let mut transcript_p = PoseidonSponge::new_with_pp_hash(&poseidon_config, pp_hash); - let mut transcript_v = transcript_p.clone(); - - let (mut running_witness, mut running_instance, _, _) = prepare_inputs(0)?; - - // fold k instances on each of num_iters iterations - let k = 7; - let num_iters = 10; - for _ in 0..num_iters { - // generate the instances to be fold - let (_, _, witnesses, instances) = prepare_inputs(k)?; - - let (folded_instance, folded_witness, proof, _) = Folding::::prove( - &mut transcript_p, - &r1cs, - &running_instance, - &running_witness, - &instances, - &witnesses, - )?; - - // verifier - let folded_instance_v = Folding::::verify( - &mut transcript_v, - &running_instance, - &instances, - proof, - )?; - - // check that prover & verifier folded instances are the same values - assert_eq!(folded_instance, folded_instance_v); - - assert!(!folded_instance.e.is_zero()); - - // check that the folded instance satisfies the relation - r1cs.check_relation(&folded_witness, &folded_instance)?; - - running_witness = folded_witness; - running_instance = folded_instance; - } - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/mod.rs b/folding-schemes/src/folding/protogalaxy/mod.rs deleted file mode 100644 index 6887e0cf8..000000000 --- a/folding-schemes/src/folding/protogalaxy/mod.rs +++ /dev/null @@ -1,1187 +0,0 @@ -/// Implements the scheme described in [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) -use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - prelude::Boolean, - GR1CSVar, -}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, Namespace, SynthesisError, - SynthesisMode, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Valid}; -use ark_std::{borrow::Borrow, cmp::max, fmt::Debug, log2, rand::RngCore, One, Zero}; -use constants::{INCOMING, RUNNING}; -use num_bigint::BigUint; - -use crate::{ - arith::{ - r1cs::{extract_r1cs, extract_w_x, R1CS}, - Arith, ArithRelation, - }, - commitment::CommitmentScheme, - folding::circuits::{ - cyclefold::{ - CycleFoldAugmentationGadget, CycleFoldCommittedInstance, CycleFoldConfig, - CycleFoldWitness, - }, - nonnative::affine::NonNativeAffineVar, - CF1, - }, - frontend::{utils::DummyCircuit, FCircuit}, - transcript::{poseidon::poseidon_canonical_config, Transcript}, - utils::pp_hash, - Curve, Error, FoldingScheme, -}; - -pub mod circuits; -pub mod constants; -pub mod decider_eth; -pub mod decider_eth_circuit; -pub mod folding; -pub mod traits; -pub(crate) mod utils; - -use circuits::AugmentedFCircuit; -use folding::Folding; - -use super::{ - circuits::{cyclefold::CycleFoldCircuit, CF2}, - traits::{ - CommittedInstanceOps, CommittedInstanceVarOps, Dummy, Inputize, WitnessOps, WitnessVarOps, - }, -}; - -/// Configuration for ProtoGalaxy's CycleFold circuit -pub struct ProtoGalaxyCycleFoldConfig { - rs: Vec>, - points: Vec, -} - -impl Default for ProtoGalaxyCycleFoldConfig { - fn default() -> Self { - Self { - rs: vec![CF1::::one(); 2], - points: vec![C::zero(); 2], - } - } -} - -impl CycleFoldConfig for ProtoGalaxyCycleFoldConfig { - const RANDOMNESS_BIT_LENGTH: usize = C::ScalarField::MODULUS_BIT_SIZE as usize; - const N_UNIQUE_RANDOMNESSES: usize = 2; - const N_INPUT_POINTS: usize = 2; - - fn alloc_points(&self, cs: ConstraintSystemRef>) -> Result, SynthesisError> { - let points = Vec::new_witness(cs.clone(), || Ok(self.points.clone()))?; - for point in &points { - Self::mark_point_as_public(point)?; - } - Ok(points) - } - - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError> { - let rs = vec![self.rs[0]] - .into_iter() - .chain(self.rs.windows(2).map(|r| r[1] / r[0])) - .map(|r| { - let mut bits = r.into_bigint().to_bits_le(); - bits.resize(CF1::::MODULUS_BIT_SIZE as usize, false); - Vec::new_witness(cs.clone(), || Ok(bits)) - }) - .collect::, _>>()?; - Self::mark_randomness_as_public(&rs.concat())?; - Ok(rs) - } -} - -/// The committed instance of ProtoGalaxy. -/// -/// We use `TYPE` to distinguish between incoming and running instances, as -/// they have slightly different structures (e.g., length of `betas`) and -/// behaviors (e.g., in satisfiability checks). -#[derive(Clone, Debug, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)] -pub struct CommittedInstance { - phi: C, - betas: Vec, - e: C::ScalarField, - x: Vec, -} - -impl Dummy<(usize, usize)> for CommittedInstance { - fn dummy((io_len, t): (usize, usize)) -> Self { - if TYPE == INCOMING { - assert_eq!(t, 0); - } - Self { - phi: C::zero(), - betas: vec![Zero::zero(); t], - e: Zero::zero(), - x: vec![Zero::zero(); io_len], - } - } -} - -impl Dummy<&R1CS>> for CommittedInstance { - fn dummy(r1cs: &R1CS>) -> Self { - let t = if TYPE == RUNNING { - log2(r1cs.n_constraints()) as usize - } else { - 0 - }; - Self::dummy((r1cs.n_public_inputs(), t)) - } -} - -impl CommittedInstanceOps for CommittedInstance { - type Var = CommittedInstanceVar; - - fn get_commitments(&self) -> Vec { - vec![self.phi] - } - - fn is_incoming(&self) -> bool { - TYPE == INCOMING - } -} - -impl Inputize> for CommittedInstance { - /// Returns the internal representation in the same order as how the value - /// is allocated in `CommittedInstanceVar::new_input`. - fn inputize(&self) -> Vec> { - [ - &self.phi.inputize_nonnative(), - &self.betas, - &[self.e][..], - &self.x, - ] - .concat() - } -} - -#[derive(Clone, Debug)] -pub struct CommittedInstanceVar { - phi: NonNativeAffineVar, - betas: Vec>, - e: FpVar, - x: Vec>, -} - -impl AllocVar, C::ScalarField> - for CommittedInstanceVar -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|u| { - let cs = cs.into(); - - let u = u.borrow(); - - Ok(Self { - phi: NonNativeAffineVar::new_variable(cs.clone(), || Ok(u.phi), mode)?, - betas: Vec::new_variable(cs.clone(), || Ok(u.betas.clone()), mode)?, - e: if TYPE == RUNNING { - FpVar::new_variable(cs.clone(), || Ok(u.e), mode)? - } else { - FpVar::zero() - }, - x: Vec::new_variable(cs.clone(), || Ok(u.x.clone()), mode)?, - }) - }) - } -} - -impl GR1CSVar for CommittedInstanceVar { - type Value = CommittedInstance; - - fn cs(&self) -> ConstraintSystemRef { - self.phi - .cs() - .or(self.betas.cs()) - .or(self.e.cs()) - .or(self.x.cs()) - } - - fn value(&self) -> Result { - Ok(CommittedInstance { - phi: self.phi.value()?, - betas: self - .betas - .iter() - .map(|v| v.value()) - .collect::>()?, - e: self.e.value()?, - x: self.x.iter().map(|v| v.value()).collect::>()?, - }) - } -} - -impl CommittedInstanceVarOps for CommittedInstanceVar { - type PointVar = NonNativeAffineVar; - - fn get_commitments(&self) -> Vec { - vec![self.phi.clone()] - } - - fn get_public_inputs(&self) -> &[FpVar>] { - &self.x - } - - fn enforce_incoming(&self) -> Result<(), SynthesisError> { - // We don't need to check if `self` is an incoming instance in-circuit, - // because incoming instances and running instances already have - // different types of `e` (constant vs witness) when we allocate them - // in-circuit. - (TYPE == INCOMING) - .then_some(()) - .ok_or(SynthesisError::Unsatisfiable) - } - - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { - self.betas.enforce_equal(&other.betas)?; - self.e.enforce_equal(&other.e)?; - self.x.enforce_equal(&other.x) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Witness { - w: Vec, - r_w: F, -} - -impl Witness { - pub fn new(w: Vec) -> Self { - // note: at the current version, we don't use the blinding factors and we set them to 0 - // always. - // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 - Self { w, r_w: F::zero() } - } - - pub fn commit, C: Curve>( - &self, - params: &CS::ProverParams, - x: Vec, - ) -> Result, crate::Error> { - let phi = CS::commit(params, &self.w, &self.r_w)?; - Ok(CommittedInstance:: { - phi, - x, - e: F::zero(), - betas: vec![], - }) - } -} - -impl Dummy<&R1CS> for Witness { - fn dummy(r1cs: &R1CS) -> Self { - Self { - w: vec![F::zero(); r1cs.n_witnesses()], - r_w: F::zero(), - } - } -} - -impl WitnessOps for Witness { - type Var = WitnessVar; - - fn get_openings(&self) -> Vec<(&[F], F)> { - vec![(&self.w, self.r_w)] - } -} - -/// In-circuit representation of the Witness associated to the CommittedInstance. -#[derive(Debug, Clone)] -pub struct WitnessVar { - pub W: Vec>, - pub rW: FpVar, -} - -impl AllocVar, F> for WitnessVar { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().w.to_vec()), mode)?; - let rW = FpVar::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; - - Ok(Self { W, rW }) - }) - } -} - -impl WitnessVarOps for WitnessVar { - fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { - vec![(&self.W, self.rW.clone())] - } -} - -#[derive(Debug, thiserror::Error, PartialEq)] -pub enum ProtoGalaxyError { - #[error("The remainder from G(X)-F(α)*L_0(X)) / Z(X) should be zero")] - RemainderNotZero, - #[error("Could not divide by vanishing polynomial")] - CouldNotDivideByVanishing, - #[error("The number of incoming instances + 1 should be a power of two, current number of instances: {0}")] - WrongNumInstances(usize), - #[error("The number of incoming items should be a power of two, current number of coefficients: {0}")] - BTreeNotFull(usize), - #[error("The lengths of β and δ do not equal: |β| = {0}, |δ|={0}")] - WrongLenBetas(usize, usize), -} - -/// Proving parameters for ProtoGalaxy-based IVC -#[derive(Debug, Clone)] -pub struct ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// Proving parameters of the underlying commitment scheme over C1 - pub cs_params: CS1::ProverParams, - /// Proving parameters of the underlying commitment scheme over C2 - pub cf_cs_params: CS2::ProverParams, -} -impl CanonicalSerialize for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.cs_params.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_params.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.cs_params.serialized_size(compress) + self.cf_cs_params.serialized_size(compress) - } -} -impl Valid for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.poseidon_config.full_rounds.check()?; - self.poseidon_config.partial_rounds.check()?; - self.poseidon_config.alpha.check()?; - self.poseidon_config.ark.check()?; - self.poseidon_config.mds.check()?; - self.poseidon_config.rate.check()?; - self.poseidon_config.capacity.check()?; - self.cs_params.check()?; - self.cf_cs_params.check()?; - Ok(()) - } -} -impl CanonicalDeserialize for ProverParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - ) -> Result { - let cs_params = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_params = - CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; - Ok(ProverParams { - poseidon_config: poseidon_canonical_config::(), - cs_params, - cf_cs_params, - }) - } -} - -/// Verification parameters for ProtoGalaxy-based IVC -#[derive(Debug, Clone)] -pub struct VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// Poseidon sponge configuration - pub poseidon_config: PoseidonConfig, - /// R1CS of the Augmented step circuit - pub r1cs: R1CS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - /// Verification parameters of the underlying commitment scheme over C1 - pub cs_vp: CS1::VerifierParams, - /// Verification parameters of the underlying commitment scheme over C2 - pub cf_cs_vp: CS2::VerifierParams, -} - -impl Valid for VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn check(&self) -> Result<(), ark_serialize::SerializationError> { - self.cs_vp.check()?; - self.cf_cs_vp.check()?; - Ok(()) - } -} -impl CanonicalSerialize for VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - fn serialize_with_mode( - &self, - mut writer: W, - compress: ark_serialize::Compress, - ) -> Result<(), ark_serialize::SerializationError> { - self.cs_vp.serialize_with_mode(&mut writer, compress)?; - self.cf_cs_vp.serialize_with_mode(&mut writer, compress) - } - - fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { - self.cs_vp.serialized_size(compress) + self.cf_cs_vp.serialized_size(compress) - } -} - -impl VerifierParams -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// returns the hash of the public parameters of ProtoGalaxy - pub fn pp_hash(&self) -> Result { - // TODO: support hiding commitments in ProtoGalaxy. For now, `H` is set to false. Tracking - // issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 - pp_hash::( - &self.r1cs, - &self.cf_r1cs, - &self.cs_vp, - &self.cf_cs_vp, - &self.poseidon_config, - ) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] -pub struct IVCProof { - pub i: C1::ScalarField, - pub z_0: Vec, - pub z_i: Vec, - pub W_i: Witness, - pub U_i: CommittedInstance, - pub w_i: Witness, - pub u_i: CommittedInstance, - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -/// Implements ProtoGalaxy+CycleFold's IVC, described in [ProtoGalaxy] and -/// [CycleFold], following the FoldingScheme trait -/// -/// [ProtoGalaxy]: https://eprint.iacr.org/2023/1106.pdf -/// [CycleFold]: https://eprint.iacr.org/2023/1192.pdf -#[derive(Clone, Debug)] -pub struct ProtoGalaxy -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// R1CS of the Augmented Function circuit - pub r1cs: R1CS, - /// R1CS of the CycleFold circuit - pub cf_r1cs: R1CS, - pub poseidon_config: PoseidonConfig, - /// CommitmentScheme::ProverParams over C1 - pub cs_params: CS1::ProverParams, - /// CycleFold CommitmentScheme::ProverParams, over C2 - pub cf_cs_params: CS2::ProverParams, - /// F circuit, the circuit that is being folded - pub F: FC, - /// public params hash - pub pp_hash: C1::ScalarField, - pub i: C1::ScalarField, - /// initial state - pub z_0: Vec, - /// current i-th state - pub z_i: Vec, - /// ProtoGalaxy instances - pub w_i: Witness, - pub u_i: CommittedInstance, - pub W_i: Witness, - pub U_i: CommittedInstance, - - /// CycleFold running instance - pub cf_W_i: CycleFoldWitness, - pub cf_U_i: CycleFoldCommittedInstance, -} - -impl ProtoGalaxy -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - /// This method computes the parameter `t` in ProtoGalaxy for folding `F'`, - /// the augmented circuit of `F` - fn compute_t( - poseidon_config: &PoseidonConfig>, - F: &FC, - d: usize, - k: usize, - ) -> Result { - // In ProtoGalaxy, prover and verifier are parameterized by `t = log(n)` - // where `n` is the number of constraints in the circuit (known as the - // mapping `f` in the paper). - // For IVC, `f` is the augmented circuit `F'`, which not only includes - // the original computation `F`, but also the in-circuit verifier of - // ProtoGalaxy. - // Therefore, `t` depends on the size of `F'`, but the size of `F'` in - // turn depends on `t`. - // To address this circular dependency, we first find `t_lower_bound`, - // the lower bound of `t`. Then we incrementally increase `t` and build - // the circuit `F'` with `t` as ProtoGalaxy's parameter, until `t` is - // the smallest integer that equals the logarithm of the number of - // constraints. - - // For `t_lower_bound`, we configure `F'` with `t = 1` and compute log2 - // of the size of `F'`. - let state_len = F.state_len(); - - // `F'` includes `F` and `ProtoGalaxy.V`, where `F` might be costly. - // Observing that the cost of `F` is constant with respect to `t`, we - // separately compute `step_constraints`, the size of `F`. - // Later, we only need to re-run the rest of `F'` with updated `t` to - // get the size of `F'`. - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - F.generate_step_constraints( - cs.clone(), - 0, - Vec::new_witness(cs.clone(), || Ok(vec![Zero::zero(); state_len]))?, - FC::ExternalInputsVar::new_witness(cs.clone(), || Ok(FC::ExternalInputs::default()))?, - )?; - let step_constraints = cs.num_constraints(); - - // Create a dummy circuit with the same state length and external inputs - // length as `F`, which replaces `F` in the augmented circuit `F'`. - let dummy_circuit: DummyCircuit = FCircuit::::new(state_len)?; - - // Compute `augmentation_constraints`, the size of `F'` without `F`. - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - AugmentedFCircuit::::empty( - poseidon_config, - dummy_circuit.clone(), - 1, - d, - k, - ) - .generate_constraints(cs.clone())?; - let augmentation_constraints = cs.num_constraints(); - - // The sum of `step_constraints` and `augmentation_constraints` is the - // size of `F'` with `t = 1`, and hence the actual `t` should have lower - // bound `log2(step_constraints + augmentation_constraints)`. - let t_lower_bound = log2(step_constraints + augmentation_constraints) as usize; - // Optimization: we in fact only need to try two values of `t`. - // This is because increasing `t` will only slightly affect the size of - // `F'` (more specifically, the size of `F'` will never be doubled). - // Thus, `t_lower_bound` (the log2 size of `F'` with `t = 1`) is very - // close to the actual `t` (either `t` or `t - 1`). - let t_upper_bound = t_lower_bound + 1; - - for t in t_lower_bound..=t_upper_bound { - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - AugmentedFCircuit::::empty( - poseidon_config, - dummy_circuit.clone(), - t, - d, - k, - ) - .generate_constraints(cs.clone())?; - let augmentation_constraints = cs.num_constraints(); - if t == log2(step_constraints + augmentation_constraints) as usize { - return Ok(t); - } - } - unreachable!() - } -} - -impl FoldingScheme for ProtoGalaxy -where - C1: Curve, - C2: Curve, - FC: FCircuit, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - type PreprocessorParam = (PoseidonConfig>, FC); - type ProverParam = ProverParams; - type VerifierParam = VerifierParams; - type RunningInstance = (CommittedInstance, Witness); - type IncomingInstance = (CommittedInstance, Witness); - type MultiCommittedInstanceWithWitness = - (CommittedInstance, Witness); - type CFInstance = (CycleFoldCommittedInstance, CycleFoldWitness); - type IVCProof = IVCProof; - - fn pp_deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - _fc_params: FC::Params, // FCircuit params - ) -> Result { - Ok(Self::ProverParam::deserialize_with_mode( - reader, compress, validate, - )?) - } - - fn vp_deserialize_with_mode( - mut reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, - ) -> Result { - let poseidon_config = poseidon_canonical_config::(); - - // generate the r1cs & cf_r1cs needed for the VerifierParams. In this way we avoid needing - // to serialize them, saving significant space in the VerifierParams serialized size. - - let f_circuit = FC::new(fc_params)?; - let k = 1; - let d = R1CS::>::empty().degree(); - let t = Self::compute_t(&poseidon_config, &f_circuit, d, k)?; - - // main circuit R1CS: - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - let augmented_F_circuit = - AugmentedFCircuit::::empty(&poseidon_config, f_circuit.clone(), t, d, k); - augmented_F_circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - - // CycleFold circuit R1CS - let cs2 = ConstraintSystem::::new_ref(); - cs2.set_mode(SynthesisMode::Setup); - let cf_circuit = CycleFoldCircuit::<_, ProtoGalaxyCycleFoldConfig>::default(); - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - - let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; - - Ok(Self::VerifierParam { - poseidon_config, - r1cs, - cf_r1cs, - cs_vp, - cf_cs_vp, - }) - } - - fn preprocess( - mut rng: impl RngCore, - (poseidon_config, F): &Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - // We fix `k`, the number of incoming instances, to 1, because - // multi-instances folding is not supported yet. - // TODO: Support multi-instances folding and make `k` a constant generic parameter (as in - // HyperNova). Tracking issue: - // https://github.com/privacy-scaling-explorations/sonobe/issues/82 - let k = 1; - let d = R1CS::>::empty().degree(); - let t = Self::compute_t(poseidon_config, F, d, k)?; - - // prepare the circuit to obtain its R1CS - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(SynthesisMode::Setup); - let cs2 = ConstraintSystem::::new_ref(); - cs2.set_mode(SynthesisMode::Setup); - - let augmented_F_circuit = - AugmentedFCircuit::::empty(poseidon_config, F.clone(), t, d, k); - let cf_circuit = CycleFoldCircuit::<_, ProtoGalaxyCycleFoldConfig>::default(); - - augmented_F_circuit.generate_constraints(cs.clone())?; - cs.finalize(); - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs)?; - - cf_circuit.generate_constraints(cs2.clone())?; - cs2.finalize(); - let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2)?; - - // `CS1` is for committing to ProtoGalaxy's witness vector `w`, so we - // set `len` to the number of witnesses in `r1cs`. - let (cs_pp, cs_vp) = CS1::setup(&mut rng, r1cs.n_witnesses())?; - // `CS2` is for committing to CycleFold's witness vector `w` and error - // term `e`, where the length of `e` is the number of constraints, so we - // set `len` to the maximum of `e` and `w`'s lengths. - let (cf_cs_pp, cf_cs_vp) = CS2::setup( - &mut rng, - max(cf_r1cs.n_constraints(), cf_r1cs.n_witnesses()), - )?; - - Ok(( - Self::ProverParam { - poseidon_config: poseidon_config.clone(), - cs_params: cs_pp, - cf_cs_params: cf_cs_pp, - }, - Self::VerifierParam { - poseidon_config: poseidon_config.clone(), - r1cs, - cf_r1cs, - cs_vp, - cf_cs_vp, - }, - )) - } - - /// Initializes the ProtoGalaxy+CycleFold's IVC for the given parameters and - /// initial state `z_0`. - fn init( - (pp, vp): &(Self::ProverParam, Self::VerifierParam), - F: FC, - z_0: Vec, - ) -> Result { - // compute the public params hash - let pp_hash = vp.pp_hash()?; - - // setup the dummy instances - let (w_dummy, u_dummy) = vp.r1cs.dummy_witness_instance(); - let (W_dummy, U_dummy) = vp.r1cs.dummy_witness_instance(); - let (cf_W_dummy, cf_U_dummy) = vp.cf_r1cs.dummy_witness_instance(); - - // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the - // R1CS that we're working with. - Ok(Self { - r1cs: vp.r1cs.clone(), - cf_r1cs: vp.cf_r1cs.clone(), - poseidon_config: pp.poseidon_config.clone(), - cs_params: pp.cs_params.clone(), - cf_cs_params: pp.cf_cs_params.clone(), - F, - pp_hash, - i: C1::ScalarField::zero(), - z_0: z_0.clone(), - z_i: z_0, - w_i: w_dummy, - u_i: u_dummy, - W_i: W_dummy, - U_i: U_dummy, - // cyclefold running instance - cf_W_i: cf_W_dummy, - cf_U_i: cf_U_dummy, - }) - } - - /// Implements IVC.P of ProtoGalaxy+CycleFold - fn prove_step( - &mut self, - mut rng: impl RngCore, - external_inputs: FC::ExternalInputs, - _other_instances: Option, - ) -> Result<(), Error> { - // Multi-instances folding is not supported yet. - if _other_instances.is_some() { - return Err(Error::NoMultiInstances); - } - // We fix `k`, the number of incoming instances, to 1, because - // multi-instances folding is not supported yet. - // TODO: Support multi-instances folding and make `k` a constant generic parameter (as in - // HyperNova). Tracking issue: - // https://github.com/privacy-scaling-explorations/sonobe/issues/82 - let k = 1; - let d = self.r1cs.degree(); - - // `sponge` is for digest computation. - let sponge = PoseidonSponge::::new_with_pp_hash( - &self.poseidon_config, - self.pp_hash, - ); - // `transcript` is for challenge generation. - let mut transcript_prover = sponge.clone(); - - let mut augmented_F_circuit: AugmentedFCircuit; - - if self.z_i.len() != self.F.state_len() { - return Err(Error::NotSameLength( - "z_i.len()".to_string(), - self.z_i.len(), - "F.state_len()".to_string(), - self.F.state_len(), - )); - } - - let i_bn: BigUint = self.i.into(); - let i_usize: usize = i_bn.try_into().map_err(|_| Error::MaxStep)?; - - if self.i.is_zero() { - augmented_F_circuit = AugmentedFCircuit::empty( - &self.poseidon_config, - self.F.clone(), - self.U_i.betas.len(), - d, - k, - ); - augmented_F_circuit.pp_hash = self.pp_hash; - augmented_F_circuit.z_0.clone_from(&self.z_0); - augmented_F_circuit.z_i.clone_from(&self.z_i); - augmented_F_circuit - .external_inputs - .clone_from(&external_inputs); - - // There is no need to update `self.U_i` etc. as they are unchanged. - } else { - // Primary part: - // Compute `U_{i+1}` by folding `u_i` into `U_i`. - let (U_i1, W_i1, proof, aux) = Folding::prove( - &mut transcript_prover, - &self.r1cs, - &self.U_i, - &self.W_i, - &[self.u_i.clone()], - &[self.w_i.clone()], - )?; - - // CycleFold part: - // Create cyclefold circuit for enforcing: - // U_i.phi * L_evals[0] + u_i.phi * L_evals[1] = U_i1.phi - let (cf_w_i, cf_u_i) = ProtoGalaxyCycleFoldConfig { - rs: aux.L_X_evals, - points: vec![self.U_i.phi, self.u_i.phi], - } - .build_circuit() - .generate_incoming_instance_witness::<_, CS2, false>(&self.cf_cs_params, &mut rng)?; - - // fold cf_U_i + cf_u_i -> folded running instance cf_U_i1 - let (cf_W_i1, cf_U_i1, cf_cmTs) = - CycleFoldAugmentationGadget::fold_native::<_, CS2, false>( - &mut transcript_prover, - &self.cf_r1cs, - &self.cf_cs_params, - self.cf_W_i.clone(), - self.cf_U_i.clone(), - vec![cf_w_i], - vec![cf_u_i.clone()], - )?; - - augmented_F_circuit = AugmentedFCircuit { - poseidon_config: self.poseidon_config.clone(), - pp_hash: self.pp_hash, - i: self.i, - i_usize, - z_0: self.z_0.clone(), - z_i: self.z_i.clone(), - external_inputs: external_inputs.clone(), - u_i_phi: self.u_i.phi, - U_i: self.U_i.clone(), - U_i1_phi: U_i1.phi, - F_coeffs: proof.F_coeffs.clone(), - K_coeffs: proof.K_coeffs.clone(), - F: self.F.clone(), - // cyclefold values - cf_u_i_cmW: cf_u_i.cmW, - cf_U_i: self.cf_U_i.clone(), - cf_cmT: cf_cmTs[0], - }; - - #[cfg(test)] - { - let mut transcript_verifier = sponge.clone(); - assert_eq!( - Folding::verify( - &mut transcript_verifier, - &self.U_i, - &[self.u_i.clone()], - proof - )?, - U_i1 - ); - } - - self.W_i = W_i1; - self.U_i = U_i1; - self.cf_W_i = cf_W_i1; - self.cf_U_i = cf_U_i1; - } - - let cs = ConstraintSystem::::new_ref(); - - let z_i1 = augmented_F_circuit - .compute_next_state(cs.clone())? - .value()?; - - #[cfg(test)] - assert!(cs.is_satisfied()?); - - let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let (w_i1, x_i1) = extract_w_x::(&cs); - - #[cfg(test)] - if x_i1.len() != 2 { - return Err(Error::NotExpectedLength(x_i1.len(), 2)); - } - - // set values for next iteration - self.i += C1::ScalarField::one(); - self.z_i = z_i1; - self.w_i = Witness::new(w_i1); - self.u_i = self.w_i.commit::(&self.cs_params, x_i1)?; - - #[cfg(test)] - { - self.u_i.check_incoming()?; - self.r1cs.check_relation(&self.w_i, &self.u_i)?; - self.r1cs.check_relation(&self.W_i, &self.U_i)?; - } - - Ok(()) - } - - fn state(&self) -> Vec { - self.z_i.clone() - } - - fn ivc_proof(&self) -> Self::IVCProof { - Self::IVCProof { - i: self.i, - z_0: self.z_0.clone(), - z_i: self.z_i.clone(), - W_i: self.W_i.clone(), - U_i: self.U_i.clone(), - w_i: self.w_i.clone(), - u_i: self.u_i.clone(), - cf_W_i: self.cf_W_i.clone(), - cf_U_i: self.cf_U_i.clone(), - } - } - - fn from_ivc_proof( - ivc_proof: Self::IVCProof, - fcircuit_params: FC::Params, - params: (Self::ProverParam, Self::VerifierParam), - ) -> Result { - let IVCProof { - i, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - let (pp, vp) = params; - - let f_circuit = FC::new(fcircuit_params)?; - - Ok(Self { - r1cs: vp.r1cs.clone(), - cf_r1cs: vp.cf_r1cs.clone(), - poseidon_config: pp.poseidon_config, - cs_params: pp.cs_params, - cf_cs_params: pp.cf_cs_params, - F: f_circuit, - pp_hash: vp.pp_hash()?, - i, - z_0, - z_i, - w_i, - u_i, - W_i, - U_i, - cf_W_i, - cf_U_i, - }) - } - - /// Implements IVC.V of ProtoGalaxy+CycleFold - fn verify(vp: Self::VerifierParam, ivc_proof: Self::IVCProof) -> Result<(), Error> { - let Self::IVCProof { - i: num_steps, - z_0, - z_i, - W_i, - U_i, - w_i, - u_i, - cf_W_i, - cf_U_i, - } = ivc_proof; - - let sponge = PoseidonSponge::new_with_pp_hash(&vp.poseidon_config, vp.pp_hash()?); - - if u_i.x.len() != 2 || U_i.x.len() != 2 { - return Err(Error::IVCVerificationFail); - } - - // check that u_i's output points to the running instance - // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, num_steps, &z_0, &z_i); - if expected_u_i_x != u_i.x[0] { - return Err(Error::IVCVerificationFail); - } - // u_i.X[1] == H(cf_U_i) - let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&sponge); - if expected_cf_u_i_x != u_i.x[1] { - return Err(Error::IVCVerificationFail); - } - - // check R1CS satisfiability, which is equivalent to checking if `u_i` - // is an incoming instance and if `w_i` and `u_i` satisfy RelaxedR1CS - u_i.check_incoming()?; - vp.r1cs.check_relation(&w_i, &u_i)?; - // check RelaxedR1CS satisfiability - vp.r1cs.check_relation(&W_i, &U_i)?; - - // check CycleFold RelaxedR1CS satisfiability - vp.cf_r1cs.check_relation(&cf_W_i, &cf_U_i)?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use ark_bn254::{Bn254, Fr, G1Projective as Projective}; - use ark_grumpkin::Projective as Projective2; - use ark_std::test_rng; - use rayon::prelude::*; - - use crate::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - frontend::utils::CubicFCircuit, - transcript::poseidon::poseidon_canonical_config, - }; - - /// This test tests the ProtoGalaxy+CycleFold IVC, and by consequence it is - /// also testing the AugmentedFCircuit - #[test] - fn test_ivc() -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - - let F_circuit = CubicFCircuit::::new(())?; - - // run the test using Pedersen commitments on both sides of the curve cycle - let _ = test_ivc_opt::, Pedersen>( - poseidon_config.clone(), - F_circuit, - )?; - // run the test using KZG for the commitments on the main curve, and Pedersen for the - // commitments on the secondary curve - let _ = test_ivc_opt::, Pedersen>(poseidon_config, F_circuit)?; - Ok(()) - } - - // test_ivc allowing to choose the CommitmentSchemes - fn test_ivc_opt, CS2: CommitmentScheme>( - poseidon_config: PoseidonConfig, - F_circuit: CubicFCircuit, - ) -> Result<(), Error> { - type PG = ProtoGalaxy, CS1, CS2>; - - let params = PG::::preprocess(&mut test_rng(), &(poseidon_config, F_circuit))?; - - let z_0 = vec![Fr::from(3_u32)]; - let mut protogalaxy = PG::init(¶ms, F_circuit, z_0.clone())?; - - let num_steps: usize = 3; - for _ in 0..num_steps { - protogalaxy.prove_step(&mut test_rng(), (), None)?; - } - assert_eq!(Fr::from(num_steps as u32), protogalaxy.i); - - let ivc_proof = protogalaxy.ivc_proof(); - PG::::verify(params.1, ivc_proof)?; - Ok(()) - } - - #[ignore] - #[test] - fn test_t_bounds() -> Result<(), Error> { - let d = R1CS::::empty().degree(); - let k = 1; - - let poseidon_config = poseidon_canonical_config::(); - for state_len in [1, 10, 100] { - let dummy_circuit: DummyCircuit = FCircuit::::new(state_len)?; - - let costs: Vec = (1..32) - .into_par_iter() - .map(|t| { - let cs = ConstraintSystem::::new_ref(); - AugmentedFCircuit::::empty( - &poseidon_config, - dummy_circuit.clone(), - t, - d, - k, - ) - .generate_constraints(cs.clone())?; - Ok(cs.num_constraints()) - }) - .collect::, Error>>()?; - - for t_lower_bound in log2(costs[0]) as usize..32 { - let num_constraints = (1 << t_lower_bound) - costs[0] + costs[t_lower_bound - 1]; - let t = log2(num_constraints) as usize; - assert!(t == t_lower_bound || t == t_lower_bound + 1); - } - } - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/traits.rs b/folding-schemes/src/folding/protogalaxy/traits.rs deleted file mode 100644 index a98eb982f..000000000 --- a/folding-schemes/src/folding/protogalaxy/traits.rs +++ /dev/null @@ -1,178 +0,0 @@ -use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, Absorb}; -use ark_ff::PrimeField; -use ark_r1cs_std::{ - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - uint8::UInt8, -}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::{cfg_into_iter, log2, One}; -use rayon::prelude::*; - -use super::{ - constants::RUNNING, - utils::{pow_i, pow_i_var}, - CommittedInstance, CommittedInstanceVar, Witness, WitnessVar, -}; -use crate::{ - arith::{ - r1cs::{circuits::R1CSMatricesVar, R1CS}, - ArithRelation, ArithRelationGadget, - }, - folding::circuits::CF1, - transcript::AbsorbNonNativeGadget, - utils::vec::is_zero_vec, - Curve, Error, -}; - -// Implements the trait for absorbing ProtoGalaxy's CommittedInstance. -impl Absorb for CommittedInstance { - fn to_sponge_bytes(&self, dest: &mut Vec) { - C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - self.phi.to_native_sponge_field_elements(dest); - self.betas.to_sponge_field_elements(dest); - self.e.to_sponge_field_elements(dest); - self.x.to_sponge_field_elements(dest); - } -} - -// Implements the trait for absorbing ProtoGalaxy's CommittedInstanceVar in-circuit. -impl AbsorbGadget for CommittedInstanceVar { - fn to_sponge_bytes(&self) -> Result>, SynthesisError> { - FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) - } - - fn to_sponge_field_elements(&self) -> Result>, SynthesisError> { - Ok([ - self.phi.to_native_sponge_field_elements()?, - self.betas.to_sponge_field_elements()?, - self.e.to_sponge_field_elements()?, - self.x.to_sponge_field_elements()?, - ] - .concat()) - } -} - -/// Implements [`ArithRelation`] for R1CS, where the witness is of type -/// [`Witness`], and the committed instance is of type [`CommittedInstance`]. -/// -/// Due to the error term `CommittedInstance.e`, R1CS here is considered as a -/// relaxed R1CS. -/// -/// See `nova/traits.rs` for the rationale behind the design. -impl ArithRelation>, CommittedInstance> - for R1CS> -{ - type Evaluation = Vec>; - - fn eval_relation( - &self, - w: &Witness>, - u: &CommittedInstance, - ) -> Result { - self.eval_at_z(&[&[C::ScalarField::one()][..], &u.x, &w.w].concat()) - } - - fn check_evaluation( - _w: &Witness, - u: &CommittedInstance, - e: Vec, - ) -> Result<(), Error> { - let ok = if TYPE == RUNNING { - if u.betas.len() != log2(e.len()) as usize { - return Err(Error::NotSameLength( - "instance.betas.len()".to_string(), - u.betas.len(), - "log2(e.len())".to_string(), - log2(e.len()) as usize, - )); - } - - u.e == cfg_into_iter!(e) - .enumerate() - .map(|(i, e_i)| pow_i(i, &u.betas) * e_i) - .sum::>() - } else { - is_zero_vec(&e) - }; - ok.then_some(()).ok_or(Error::NotSatisfied) - } -} - -/// Unlike its native counterpart, we only need to support running instances in -/// circuit, as the decider circuit only checks running instance satisfiability. -impl ArithRelationGadget>, CommittedInstanceVar> - for R1CSMatricesVar, FpVar>> -{ - type Evaluation = (Vec>>, Vec>>); - - fn eval_relation( - &self, - w: &WitnessVar>, - u: &CommittedInstanceVar, - ) -> Result { - self.eval_at_z(&[&[FpVar::one()][..], &u.x, &w.W].concat()) - } - - fn enforce_evaluation( - _w: &WitnessVar, - u: &CommittedInstanceVar, - (AzBz, uCz): Self::Evaluation, - ) -> Result<(), SynthesisError> { - let mut e = vec![]; - for (i, (l, r)) in AzBz.iter().zip(uCz).enumerate() { - e.push(pow_i_var(i, &u.betas) * (l - r)); - } - // Call `sum` on a vector instead of computing the sum in the above loop - // to avoid stack overflow (the cause of this is similar to issue #80 - // https://github.com/privacy-scaling-explorations/sonobe/issues/80) - e.iter().sum::>().enforce_equal(&u.e) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_bn254::{Fr, G1Projective as Projective}; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::UniformRand; - use rand::Rng; - - /// test that checks the native CommittedInstance.to_sponge_{bytes,field_elements} - /// vs the R1CS constraints version - #[test] - pub fn test_committed_instance_to_sponge_preimage() -> Result<(), Error> { - let mut rng = ark_std::test_rng(); - - let t = rng.gen::() as usize; - let io_len = rng.gen::() as usize; - - let ci = CommittedInstance:: { - phi: Projective::rand(&mut rng), - betas: (0..t).map(|_| Fr::rand(&mut rng)).collect(), - e: Fr::rand(&mut rng), - x: (0..io_len).map(|_| Fr::rand(&mut rng)).collect(), - }; - - let bytes = ci.to_sponge_bytes_as_vec(); - let field_elements = ci.to_sponge_field_elements_as_vec(); - - let cs = ConstraintSystem::::new_ref(); - - let ciVar = - CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci.clone()))?; - let bytes_var = ciVar.to_sponge_bytes()?; - let field_elements_var = ciVar.to_sponge_field_elements()?; - - assert!(cs.is_satisfied()?); - - // check that the natively computed and in-circuit computed hashes match - assert_eq!(bytes_var.value()?, bytes); - assert_eq!(field_elements_var.value()?, field_elements); - Ok(()) - } -} diff --git a/folding-schemes/src/folding/protogalaxy/utils.rs b/folding-schemes/src/folding/protogalaxy/utils.rs deleted file mode 100644 index d89a5ed23..000000000 --- a/folding-schemes/src/folding/protogalaxy/utils.rs +++ /dev/null @@ -1,204 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; -use num_integer::Integer; - -/// Returns (b, b^2, b^4, ..., b^{2^{t-1}}) -pub fn exponential_powers(b: F, t: usize) -> Vec { - let mut r = vec![F::zero(); t]; - r[0] = b; - for i in 1..t { - r[i] = r[i - 1].square(); - } - r -} - -/// The in-circuit version of `exponential_powers` -pub fn exponential_powers_var(b: FpVar, t: usize) -> Vec> { - let mut r = vec![FpVar::zero(); t]; - r[0] = b; - for i in 1..t { - r[i] = &r[i - 1] * &r[i - 1]; - } - r -} - -/// Returns (a, a^2, a^3, ..., a^{n-1}) -pub fn all_powers(a: F, n: usize) -> Vec { - let mut r = vec![F::zero(); n]; - for (i, r_i) in r.iter_mut().enumerate() { - *r_i = a.pow([i as u64]); - } - r -} - -/// The in-circuit version of `all_powers` -pub fn all_powers_var(a: FpVar, n: usize) -> Vec> { - if n == 0 { - return vec![]; - } - let mut r = vec![FpVar::zero(); n]; - r[0] = FpVar::one(); - for i in 1..n { - r[i] = &r[i - 1] * &a; - } - r -} - -/// returns a vector containing βᵢ* = βᵢ + α ⋅ δᵢ -pub fn betas_star(betas: &[F], deltas: &[F], alpha: F) -> Vec { - betas - .iter() - .zip( - deltas - .iter() - .map(|delta_i| alpha * delta_i) - .collect::>(), - ) - .map(|(beta_i, delta_i_alpha)| *beta_i + delta_i_alpha) - .collect() -} - -/// The in-circuit version of `betas_star` -pub fn betas_star_var( - betas: &[FpVar], - deltas: &[FpVar], - alpha: &FpVar, -) -> Vec> { - betas - .iter() - .zip(deltas) - .map(|(beta_i, delta_i)| beta_i + alpha * delta_i) - .collect::>>() -} - -/// Returns the product of selected elements in `betas`. -/// For every index `j`, whether `betas[j]` is selected is determined by the -/// `j`-th bit in the binary (little endian) representation of `i`. -/// -/// If `betas = (β, β^2, β^4, ..., β^{2^{t-1}})`, then the result is equal to -/// `β^i`. -pub fn pow_i(mut i: usize, betas: &[F]) -> F { - let mut j = 0; - let mut r = F::one(); - while i > 0 { - if i.is_odd() { - r *= betas[j]; - } - i >>= 1; - j += 1; - } - r -} - -/// The in-circuit version of `pow_i` -#[allow(dead_code)] // Will remove this once we have the decider circuit for Protogalaxy -pub fn pow_i_var(mut i: usize, betas: &[FpVar]) -> FpVar { - let mut j = 0; - let mut r = FieldVar::one(); - while i > 0 { - if i.is_odd() { - r *= &betas[j]; - } - i >>= 1; - j += 1; - } - r -} - -#[cfg(test)] -mod tests { - - use ark_bn254::Fr; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand}; - use rand::Rng; - - use super::*; - use crate::Error; - - #[test] - fn test_exponential_powers() -> Result<(), Error> { - let rng = &mut test_rng(); - - for t in 1..10 { - let cs = ConstraintSystem::::new_ref(); - - let b = Fr::rand(rng); - let b_var = FpVar::new_witness(cs.clone(), || Ok(b))?; - - let r = exponential_powers(b, t); - let r_var = exponential_powers_var(b_var, t); - - assert_eq!(r, r_var.value()?); - assert!(cs.is_satisfied()?); - } - - Ok(()) - } - - #[test] - fn test_all_powers() -> Result<(), Error> { - let rng = &mut test_rng(); - - for n in 1..10 { - let cs = ConstraintSystem::::new_ref(); - - let a = Fr::rand(rng); - let a_var = FpVar::new_witness(cs.clone(), || Ok(a))?; - - let r = all_powers(a, n); - let r_var = all_powers_var(a_var, n); - - assert_eq!(r, r_var.value()?); - assert!(cs.is_satisfied()?); - } - - Ok(()) - } - - #[test] - fn test_betas_star() -> Result<(), Error> { - let rng = &mut test_rng(); - - for t in 1..10 { - let cs = ConstraintSystem::::new_ref(); - - let betas = (0..t).map(|_| Fr::rand(rng)).collect::>(); - let deltas = (0..t).map(|_| Fr::rand(rng)).collect::>(); - let alpha = Fr::rand(rng); - - let betas_var = Vec::new_witness(cs.clone(), || Ok(betas.clone()))?; - let deltas_var = Vec::new_witness(cs.clone(), || Ok(deltas.clone()))?; - let alpha_var = FpVar::new_witness(cs.clone(), || Ok(alpha))?; - - let r = betas_star(&betas, &deltas, alpha); - let r_var = betas_star_var(&betas_var, &deltas_var, &alpha_var); - assert_eq!(r, r_var.value()?); - assert!(cs.is_satisfied()?); - } - - Ok(()) - } - - #[test] - fn test_pow_i() -> Result<(), Error> { - let rng = &mut test_rng(); - - for t in 1..10 { - let cs = ConstraintSystem::::new_ref(); - - let betas = (0..t).map(|_| Fr::rand(rng)).collect::>(); - let i = rng.gen_range(0..(1 << t)); - - let betas_var = Vec::new_witness(cs.clone(), || Ok(betas.clone()))?; - - let r = pow_i(i, &betas); - let r_var = pow_i_var(i, &betas_var); - assert_eq!(r, r_var.value()?); - assert!(cs.is_satisfied()?); - } - - Ok(()) - } -} diff --git a/folding-schemes/src/folding/traits.rs b/folding-schemes/src/folding/traits.rs deleted file mode 100644 index b2e8e864d..000000000 --- a/folding-schemes/src/folding/traits.rs +++ /dev/null @@ -1,174 +0,0 @@ -use ark_crypto_primitives::sponge::{ - constraints::{AbsorbGadget, CryptographicSpongeVar}, - poseidon::constraints::PoseidonSpongeVar, - Absorb, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; -use ark_relations::gr1cs::SynthesisError; - -use crate::{ - transcript::{AbsorbNonNativeGadget, Transcript}, - Curve, Error, -}; - -use super::circuits::CF1; - -pub trait CommittedInstanceOps: Inputize> { - /// The in-circuit representation of the committed instance. - type Var: AllocVar> + CommittedInstanceVarOps; - /// `hash` implements the committed instance hash compatible with the - /// in-circuit implementation from `CommittedInstanceVarOps::hash`. - /// - /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and - /// `U_i` is the committed instance `self`. - fn hash>>( - &self, - sponge: &T, - i: CF1, - z_0: &[CF1], - z_i: &[CF1], - ) -> CF1 - where - Self: Sized + Absorb, - { - let mut sponge = sponge.clone(); - sponge.absorb(&i); - sponge.absorb(&z_0); - sponge.absorb(&z_i); - sponge.absorb(&self); - sponge.squeeze_field_elements(1)[0] - } - - /// Returns the commitments contained in the committed instance. - fn get_commitments(&self) -> Vec; - - /// Returns `true` if the committed instance is an incoming instance, and - /// `false` if it is a running instance. - fn is_incoming(&self) -> bool; - - /// Checks if the committed instance is an incoming instance. - fn check_incoming(&self) -> Result<(), Error> { - self.is_incoming() - .then_some(()) - .ok_or(Error::NotIncomingCommittedInstance) - } -} - -pub trait CommittedInstanceVarOps { - type PointVar: AbsorbNonNativeGadget>; - /// `hash` implements the in-circuit committed instance hash compatible with - /// the native implementation from `CommittedInstanceOps::hash`. - /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and - /// `U_i` is the committed instance `self`. - /// - /// Additionally it returns the in-circuit representation of the committed - /// instance `self` as a vector of field elements, so they can be reused in - /// other gadgets avoiding recalculating (reconstraining) them. - #[allow(clippy::type_complexity)] - fn hash( - &self, - sponge: &PoseidonSpongeVar>, - i: &FpVar>, - z_0: &[FpVar>], - z_i: &[FpVar>], - ) -> Result<(FpVar>, Vec>>), SynthesisError> - where - Self: AbsorbGadget>, - { - let mut sponge = sponge.clone(); - let U_vec = self.to_sponge_field_elements()?; - sponge.absorb(&i)?; - sponge.absorb(&z_0)?; - sponge.absorb(&z_i)?; - sponge.absorb(&U_vec)?; - Ok(( - // `unwrap` is safe because the sponge is guaranteed to return a single element - sponge.squeeze_field_elements(1)?.pop().unwrap(), - U_vec, - )) - } - - /// Returns the commitments contained in the committed instance. - fn get_commitments(&self) -> Vec; - - /// Returns the public inputs contained in the committed instance. - fn get_public_inputs(&self) -> &[FpVar>]; - - /// Generates constraints to enforce that the committed instance is an - /// incoming instance. - fn enforce_incoming(&self) -> Result<(), SynthesisError>; - - /// Generates constraints to enforce that the committed instance `self` is - /// partially equal to another committed instance `other`. - /// Here, only field elements are compared, while commitments (points) are - /// not. - fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError>; -} - -pub trait WitnessOps { - /// The in-circuit representation of the witness. - type Var: AllocVar + WitnessVarOps; - - /// Returns the openings (i.e., the values being committed to and the - /// randomness) contained in the witness. - fn get_openings(&self) -> Vec<(&[F], F)>; -} - -pub trait WitnessVarOps { - /// Returns the openings (i.e., the values being committed to and the - /// randomness) contained in the witness. - fn get_openings(&self) -> Vec<(&[FpVar], FpVar)>; -} - -pub trait Dummy { - fn dummy(cfg: Cfg) -> Self; -} - -impl Dummy for Vec { - fn dummy(cfg: usize) -> Self { - vec![Default::default(); cfg] - } -} - -impl Dummy<()> for T { - fn dummy(_: ()) -> Self { - Default::default() - } -} - -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *natively* in the -/// circuit. -/// -/// This is useful for the verifier to compute the public inputs. -pub trait Inputize { - fn inputize(&self) -> Vec; -} - -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *non-natively* in -/// the circuit. -/// -/// This is useful for the verifier to compute the public inputs. -/// -/// Note that we require this trait because we need to distinguish between some -/// data types that are represented both natively and non-natively in-circuit -/// (e.g., field elements can have type `FpVar` and `NonNativeUintVar`). -pub trait InputizeNonNative { - fn inputize_nonnative(&self) -> Vec; -} - -impl> Inputize for [T] { - fn inputize(&self) -> Vec { - self.iter().flat_map(Inputize::::inputize).collect() - } -} - -impl> InputizeNonNative for [T] { - fn inputize_nonnative(&self) -> Vec { - self.iter() - .flat_map(InputizeNonNative::::inputize_nonnative) - .collect() - } -} diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs deleted file mode 100644 index f19e14055..000000000 --- a/folding-schemes/src/frontend/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::Error; -use ark_ff::PrimeField; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use ark_std::fmt::Debug; - -pub mod utils; - -/// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. -/// inside the agmented F' function). -/// The parameter z_i denotes the current state, and z_{i+1} denotes the next state after applying -/// the step. -/// Note that the external inputs for the specific circuit are defined at the implementation of -/// both `FCircuit::ExternalInputs` and `FCircuit::ExternalInputsVar`, where the `Default` trait -/// implementation for the `ExternalInputs` returns the initialized data structure (ie. if the type -/// contains a vector, it is initialized at the expected length). -pub trait FCircuit: Clone + Debug { - type Params: Debug; - type ExternalInputs: Clone + Default + Debug; - type ExternalInputsVar: Clone + Debug + AllocVar; - - /// returns a new FCircuit instance - fn new(params: Self::Params) -> Result; - - /// returns the number of elements in the state of the FCircuit, which corresponds to the - /// FCircuit inputs. - fn state_len(&self) -> usize; - - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - // this method uses self, so that each FCircuit implementation (and different frontends) - // can hold a state if needed to store data to generate the constraints. - &self, - cs: ConstraintSystemRef, - i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputsVar, // inputs that are not part of the state - ) -> Result>, SynthesisError>; -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_bn254::Fr; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - - use utils::{custom_step_native, CubicFCircuit, CustomFCircuit, WrapperCircuit}; - - #[test] - fn test_testfcircuit() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let F_circuit = CubicFCircuit::::new(())?; - - let wrapper_circuit = WrapperCircuit::> { - FC: F_circuit, - z_i: Some(vec![Fr::from(3_u32)]), - z_i1: Some(vec![Fr::from(35_u32)]), - }; - wrapper_circuit.generate_constraints(cs.clone())?; - assert_eq!(cs.num_constraints(), 3); - Ok(()) - } - - #[test] - fn test_customtestfcircuit() -> Result<(), Error> { - let cs = ConstraintSystem::::new_ref(); - let n_constraints = 1000; - let custom_circuit = CustomFCircuit::::new(n_constraints)?; - let z_i = vec![Fr::from(5_u32)]; - let wrapper_circuit = WrapperCircuit::> { - FC: custom_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(custom_step_native(z_i, n_constraints)), - }; - wrapper_circuit.generate_constraints(cs.clone())?; - assert_eq!(cs.num_constraints(), n_constraints); - Ok(()) - } -} diff --git a/folding-schemes/src/frontend/utils.rs b/folding-schemes/src/frontend/utils.rs deleted file mode 100644 index 6298f76a1..000000000 --- a/folding-schemes/src/frontend/utils.rs +++ /dev/null @@ -1,159 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::AllocVar, - fields::{fp::FpVar, FieldVar}, -}; -use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::marker::PhantomData; -use ark_std::{fmt::Debug, Zero}; - -use super::FCircuit; -use crate::Error; - -/// DummyCircuit is a circuit that has dummy state whose length is specified in the `state_len` -/// parameter, without any constraints. -#[derive(Clone, Debug)] -pub struct DummyCircuit { - state_len: usize, -} -impl FCircuit for DummyCircuit { - type Params = usize; - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(state_len: Self::Params) -> Result { - Ok(Self { state_len }) - } - fn state_len(&self) -> usize { - self.state_len - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - _z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - Vec::new_witness(cs.clone(), || Ok(vec![Zero::zero(); self.state_len])) - } -} - -/// CubicFCircuit is a struct that implements the FCircuit trait, for the R1CS example circuit -/// from https://www.vitalik.ca/general/2016/12/10/qap.html, which checks `x^3 + x + 5 = y`. -/// `z_i` is used as `x`, and `z_{i+1}` is used as `y`, and at the next step, `z_{i+1}` will be -/// assigned to `z_i`, and a new `z+{i+1}` will be computted. -#[cfg(test)] -#[derive(Clone, Copy, Debug)] -pub struct CubicFCircuit { - _f: PhantomData, -} - -#[cfg(test)] -impl FCircuit for CubicFCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 1 - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - let z_i = z_i[0].clone(); - - Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) - } -} - -/// Native implementation of `CubicFCircuit` -#[cfg(test)] -pub fn cubic_step_native(z_i: Vec) -> Vec { - let z = z_i[0]; - vec![z * z * z + z + F::from(5)] -} - -/// CustomFCircuit is a circuit that has the number of constraints specified in the -/// `n_constraints` parameter. Note that the generated circuit will have very sparse matrices. -#[derive(Clone, Copy, Debug)] -pub struct CustomFCircuit { - _f: PhantomData, - pub n_constraints: usize, -} - -impl FCircuit for CustomFCircuit { - type Params = usize; - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(params: Self::Params) -> Result { - Ok(Self { - _f: PhantomData, - n_constraints: params, - }) - } - fn state_len(&self) -> usize { - 1 - } - fn generate_step_constraints( - &self, - _cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let mut z_i1 = z_i[0].clone(); - for _ in 0..self.n_constraints - 1 { - z_i1 = z_i1.square()?; - } - - Ok(vec![z_i1]) - } -} - -/// Native implementation of `CubicFCircuit` -#[cfg(test)] -pub fn custom_step_native(z_i: Vec, n_constraints: usize) -> Vec { - let mut z_i1 = z_i[0]; - for _ in 0..n_constraints - 1 { - z_i1 = z_i1.square(); - } - vec![z_i1] -} - -/// WrapperCircuit is a circuit that wraps any circuit that implements the FCircuit trait. This -/// is used to test the `FCircuit.generate_step_constraints` method. This is a similar wrapping -/// than the one done in the `AugmentedFCircuit`, but without adding all the extra constraints -/// of the AugmentedF circuit logic, in order to run lighter tests when we're not interested in -/// the AugmentedF logic but in the wrapping of the circuits. -pub struct WrapperCircuit> { - pub FC: FC, // F circuit - pub z_i: Option>, - pub z_i1: Option>, -} - -impl> ConstraintSynthesizer for WrapperCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let z_i = - Vec::>::new_witness(cs.clone(), || Ok(self.z_i.unwrap_or(vec![F::zero()])))?; - let z_i1 = - Vec::>::new_input(cs.clone(), || Ok(self.z_i1.unwrap_or(vec![F::zero()])))?; - let external_inputs = - FC::ExternalInputsVar::new_input(cs.clone(), || Ok(FC::ExternalInputs::default()))?; - let computed_z_i1 = - self.FC - .generate_step_constraints(cs.clone(), 0, z_i.clone(), external_inputs)?; - - use ark_r1cs_std::eq::EqGadget; - computed_z_i1.enforce_equal(&z_i1)?; - Ok(()) - } -} diff --git a/folding-schemes/src/lib.rs b/folding-schemes/src/lib.rs deleted file mode 100644 index 65f90ec9c..000000000 --- a/folding-schemes/src/lib.rs +++ /dev/null @@ -1,316 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] - -use ark_crypto_primitives::sponge::Absorb; -use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig}, - CurveGroup, -}; -use ark_ff::{Fp, FpConfig, PrimeField}; -use ark_r1cs_std::{ - fields::{fp::FpVar, FieldVar}, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{ - fmt::Debug, - rand::{CryptoRng, RngCore}, -}; -use thiserror::Error; - -use crate::folding::traits::{Inputize, InputizeNonNative}; -use crate::frontend::FCircuit; -use crate::transcript::AbsorbNonNative; - -pub mod arith; -pub mod commitment; -pub mod constants; -pub mod folding; -pub mod frontend; -pub mod transcript; -pub mod utils; - -#[derive(Debug, Error)] -pub enum Error { - // Wrappers on top of other errors - #[error("ark_relations::gr1cs::SynthesisError")] - SynthesisError(#[from] ark_relations::gr1cs::SynthesisError), - #[error("ark_serialize::SerializationError")] - SerializationError(#[from] ark_serialize::SerializationError), - #[error("ark_poly_commit::Error")] - PolyCommitError(#[from] ark_poly_commit::Error), - #[error("crate::utils::espresso::virtual_polynomial::ArithErrors")] - ArithError(#[from] utils::espresso::virtual_polynomial::ArithErrors), - #[error(transparent)] - ProtoGalaxy(folding::protogalaxy::ProtoGalaxyError), - #[error("std::io::Error")] - IOError(#[from] std::io::Error), - - // Relation errors - #[error("Relation not satisfied")] - NotSatisfied, - #[error("SNARK setup failed: {0}")] - SNARKSetupFail(String), - #[error("SNARK verification failed")] - SNARKVerificationFail, - #[error("IVC verification failed")] - IVCVerificationFail, - #[error("zkIVC verification failed")] - zkIVCVerificationFail, - #[error("Committed instance is expected to be an incoming (fresh) instance")] - NotIncomingCommittedInstance, - #[error("R1CS instance is expected to not be relaxed")] - R1CSUnrelaxedFail, - #[error("Could not find the inner ConstraintSystem")] - NoInnerConstraintSystem, - #[error("Sum-check prove failed: {0}")] - SumCheckProveError(String), - #[error("Sum-check verify failed: {0}")] - SumCheckVerifyError(String), - - // Comparators errors - #[error("Not equal")] - NotEqual, - #[error("Vectors should have the same length ({0}: {1}, {2}: {3})")] - NotSameLength(String, usize, String, usize), - #[error("Vector's length ({0}) is not the expected ({1})")] - NotExpectedLength(usize, usize), - #[error("Vector ({0}) length ({1}) is not a power of two")] - NotPowerOfTwo(String, usize), - #[error("Can not be empty")] - Empty, - #[error("Value out of bounds")] - OutOfBounds, - #[error("Could not construct the Evaluation Domain")] - NewDomainFail, - #[error("The number of folded steps must be greater than 1")] - NotEnoughSteps, - #[error("Evaluation failed")] - EvaluationFail, - #[error("{0} can not be zero")] - CantBeZero(String), - - // Commitment errors - #[error("Pedersen parameters length is not sufficient (generators.len={0} < vector.len={1} unsatisfied)")] - PedersenParamsLen(usize, usize), - #[error("Blinding factor not 0 for Commitment without hiding")] - BlindingNotZero, - #[error("Blinding factors incorrect, blinding is set to {0} but blinding values are {1}")] - IncorrectBlinding(bool, String), - #[error("Commitment verification failed")] - CommitmentVerificationFail, - - // Polynomial IOP errors, from https://github.com/EspressoSystems/hyperplonk/blob/main/subroutines/src/poly_iop/errors.rs - #[error("Invalid Polynomial IOP Prover: {0}")] - InvalidPolyIOPProver(String), - #[error("Invalid Polynomial IOP Verifier: {0}")] - InvalidPolyIOPVerifier(String), - #[error("Invalid Polynomial IOP Proof: {0}")] - InvalidPolyIOPProof(String), - #[error("Invalid Polynomial IOP Parameters: {0}")] - InvalidPolyIOPParameters(String), - - // Other - #[error("{0}")] - Other(String), - #[error("Randomness for blinding not found")] - MissingRandomness, - #[error("Missing value: {0}")] - MissingValue(String), - #[error("Feature '{0}' not supported yet")] - NotSupportedYet(String), - #[error("Feature '{0}' is not supported and it will not be")] - NotSupported(String), - #[error("max i-th step reached (usize limit reached)")] - MaxStep, - #[error("Witness calculation error: {0}")] - WitnessCalculationError(String), - #[error("Failed to convert {0} into {1}: {2}")] - ConversionError(String, String, String), - #[error("Failed to serde: {0}")] - JSONSerdeError(String), - #[error("Multi instances folding not supported in this scheme")] - NoMultiInstances, - #[error("Missing 'other' instances, since this is a multi-instances folding scheme. Expected number of instances, mu:{0}, nu:{1}")] - MissingOtherInstances(usize, usize), -} - -/// FoldingScheme defines trait that is implemented by the diverse folding schemes. It is defined -/// over a cycle of curves (C1, C2), where: -/// - C1 is the main curve, which ScalarField we use as our F for all the field operations -/// - C2 is the auxiliary curve, which we use for the commitments, whose BaseField (for point -/// coordinates) are in the C1::ScalarField. -/// -/// In other words, C1.Fq == C2.Fr, and C1.Fr == C2.Fq. -pub trait FoldingScheme< - C1: Curve, - C2: Curve, - FC: FCircuit, ->: Clone + Debug -{ - type PreprocessorParam: Debug + Clone; - type ProverParam: Debug + Clone + CanonicalSerialize; - type VerifierParam: Debug + Clone + CanonicalSerialize; - type RunningInstance: Debug; // contains the CommittedInstance + Witness - type IncomingInstance: Debug; // contains the CommittedInstance + Witness - type MultiCommittedInstanceWithWitness: Debug; // type used for the extra instances in the multi-instance folding setting - type CFInstance: Debug; // CycleFold CommittedInstance & Witness - type IVCProof: PartialEq + Eq + Clone + Debug + CanonicalSerialize + CanonicalDeserialize; - - /// deserialize Self::ProverParam and recover the not serialized data that is recomputed on the - /// fly to save serialized bytes. - /// Internally it generates the r1cs/ccs & cf_r1cs needed for the VerifierParams. In this way - /// we avoid needing to serialize them, saving significant space in the VerifierParams - /// serialized size. - fn pp_deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, // FCircuit params - ) -> Result; - - /// deserialize Self::VerifierParam and recover the not serialized data that is recomputed on - /// the fly to save serialized bytes. - /// Internally it generates the r1cs/ccs & cf_r1cs needed for the VerifierParams. In this way - /// we avoid needing to serialize them, saving significant space in the VerifierParams - /// serialized size. - fn vp_deserialize_with_mode( - reader: R, - compress: ark_serialize::Compress, - validate: ark_serialize::Validate, - fc_params: FC::Params, // FCircuit params - ) -> Result; - - fn preprocess( - rng: impl RngCore, - prep_param: &Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error>; - - fn init( - params: &(Self::ProverParam, Self::VerifierParam), - step_circuit: FC, - z_0: Vec, // initial state - ) -> Result; - - fn prove_step( - &mut self, - rng: impl RngCore, - external_inputs: FC::ExternalInputs, - other_instances: Option, - ) -> Result<(), Error>; - - /// returns the state at the current step - fn state(&self) -> Vec; - - /// returns the last IVC state proof, which can be verified in the `verify` method - fn ivc_proof(&self) -> Self::IVCProof; - - /// constructs the FoldingScheme instance from the given IVCProof, ProverParams, VerifierParams - /// and PoseidonConfig. - /// This method is useful for when the IVCProof is sent between different parties, so that they - /// can continue iterating the IVC from the received IVCProof. - fn from_ivc_proof( - ivc_proof: Self::IVCProof, - fcircuit_params: FC::Params, - params: (Self::ProverParam, Self::VerifierParam), - ) -> Result; - - fn verify(vp: Self::VerifierParam, ivc_proof: Self::IVCProof) -> Result<(), Error>; -} - -/// Trait with auxiliary methods for multi-folding schemes (ie. HyperNova, ProtoGalaxy, etc), -/// allowing to create new instances for the multifold. -pub trait MultiFolding< - C1: Curve, - C2: Curve, - FC: FCircuit, ->: Clone + Debug -{ - type RunningInstance: Debug; - type IncomingInstance: Debug; - type MultiInstance: Debug; - - /// Creates a new RunningInstance for the given state, to be folded in the multi-folding step. - fn new_running_instance( - &self, - rng: impl RngCore, - state: Vec, - external_inputs: FC::ExternalInputs, - ) -> Result; - - /// Creates a new IncomingInstance for the given state, to be folded in the multi-folding step. - fn new_incoming_instance( - &self, - rng: impl RngCore, - state: Vec, - external_inputs: FC::ExternalInputs, - ) -> Result; -} - -pub trait Decider< - C1: Curve, - C2: Curve, - FC: FCircuit, - FS: FoldingScheme, -> -{ - type PreprocessorParam: Debug; - type ProverParam: Clone; - type Proof; - type VerifierParam; - type PublicInput: Debug; - type CommittedInstance: Clone + Debug; - - fn preprocess( - rng: impl RngCore + CryptoRng, - prep_param: Self::PreprocessorParam, - ) -> Result<(Self::ProverParam, Self::VerifierParam), Error>; - - fn prove( - rng: impl RngCore + CryptoRng, - pp: Self::ProverParam, - folding_scheme: FS, - ) -> Result; - - fn verify( - vp: Self::VerifierParam, - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - running_instance: &Self::CommittedInstance, - incoming_instance: &Self::CommittedInstance, - proof: &Self::Proof, - // returns `Result` to differentiate between an error occurred while performing - // the verification steps, and the verification logic of the scheme not passing. - ) -> Result; -} - -/// `Field` trait is a wrapper around `PrimeField` that also includes the -/// necessary bounds for the field to be used conveniently in folding schemes. -pub trait Field: - PrimeField + Absorb + AbsorbNonNative + Inputize -{ - /// The in-circuit variable type for this field. - type Var: FieldVar; -} - -impl, const N: usize> Field for Fp { - type Var = FpVar; -} - -/// `Curve` trait is a wrapper around `CurveGroup` that also includes the -/// necessary bounds for the curve to be used conveniently in folding schemes. -pub trait Curve: - CurveGroup - + AbsorbNonNative - + Inputize - + InputizeNonNative -{ - /// The in-circuit variable type for this curve. - type Var: CurveVar; -} - -impl> Curve for Projective

{ - type Var = ProjectiveVar>; -} diff --git a/folding-schemes/src/transcript/mod.rs b/folding-schemes/src/transcript/mod.rs deleted file mode 100644 index 608201f8e..000000000 --- a/folding-schemes/src/transcript/mod.rs +++ /dev/null @@ -1,139 +0,0 @@ -use ark_crypto_primitives::sponge::{constraints::CryptographicSpongeVar, CryptographicSponge}; -use ark_ec::CurveGroup; -use ark_ff::PrimeField; -use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; - -pub mod poseidon; - -/// An interface for objects that can be absorbed by a `Transcript`. -/// -/// Matches `Absorb` in `ark-crypto-primitives`. -pub trait AbsorbNonNative { - /// Converts the object into field elements that can be absorbed by a `Transcript`. - /// Append the list to `dest` - fn to_native_sponge_field_elements(&self, dest: &mut Vec); - - /// Converts the object into field elements that can be absorbed by a `Transcript`. - /// Return the list as `Vec` - fn to_native_sponge_field_elements_as_vec(&self) -> Vec { - let mut result = Vec::new(); - self.to_native_sponge_field_elements(&mut result); - result - } -} - -/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field -/// is `F`. -/// -/// Matches `AbsorbGadget` in `ark-crypto-primitives`. -pub trait AbsorbNonNativeGadget { - /// Converts the object into field elements that can be absorbed by a `TranscriptVar`. - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError>; -} - -impl AbsorbNonNative for [T] { - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - for t in self.iter() { - t.to_native_sponge_field_elements(dest); - } - } -} - -impl> AbsorbNonNativeGadget for &T { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - T::to_native_sponge_field_elements(self) - } -} - -impl> AbsorbNonNativeGadget for [T] { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - let mut result = Vec::new(); - for t in self.iter() { - result.extend(t.to_native_sponge_field_elements()?); - } - Ok(result) - } -} - -pub trait Transcript: CryptographicSponge { - /// `new_with_pp_hash` creates a new transcript / sponge with the given - /// hash of the public parameters. - fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self; - - /// `absorb_point` is for absorbing points whose `BaseField` is the field of - /// the sponge, i.e., the type `C` of these points should satisfy - /// `C::BaseField = F`. - /// - /// If the sponge field `F` is `C::ScalarField`, call `absorb_nonnative` - /// instead. - fn absorb_point>(&mut self, v: &C); - /// `absorb_nonnative` is for structs that contain non-native (field or - /// group) elements, including: - /// - /// - A field element of type `T: PrimeField` that will be absorbed into a - /// sponge that operates in another field `F != T`. - /// - A group element of type `C: CurveGroup` that will be absorbed into a - /// sponge that operates in another field `F != C::BaseField`, e.g., - /// `F = C::ScalarField`. - /// - A `CommittedInstance` on the secondary curve (used for CycleFold) that - /// will be absorbed into a sponge that operates in the (scalar field of - /// the) primary curve. - /// - /// Note that although a `CommittedInstance` for `AugmentedFCircuit` on - /// the primary curve also contains non-native elements, we still regard - /// it as native, because the sponge is on the same curve. - fn absorb_nonnative(&mut self, v: &V); - - fn get_challenge(&mut self) -> F; - /// get_challenge_nbits returns a field element of size nbits - fn get_challenge_nbits(&mut self, nbits: usize) -> Vec; - fn get_challenges(&mut self, n: usize) -> Vec; -} - -pub trait TranscriptVar: - CryptographicSpongeVar -{ - /// `new_with_pp_hash` creates a new transcript / sponge with the given - /// hash of the public parameters. - fn new_with_pp_hash( - config: &Self::Parameters, - pp_hash: &FpVar, - ) -> Result; - - /// `absorb_point` is for absorbing points whose `BaseField` is the field of - /// the sponge, i.e., the type `C` of these points should satisfy - /// `C::BaseField = F`. - /// - /// If the sponge field `F` is `C::ScalarField`, call `absorb_nonnative` - /// instead. - fn absorb_point, GC: CurveVar>( - &mut self, - v: &GC, - ) -> Result<(), SynthesisError>; - /// `absorb_nonnative` is for structs that contain non-native (field or - /// group) elements, including: - /// - /// - A field element of type `T: PrimeField` that will be absorbed into a - /// sponge that operates in another field `F != T`. - /// - A group element of type `C: CurveGroup` that will be absorbed into a - /// sponge that operates in another field `F != C::BaseField`, e.g., - /// `F = C::ScalarField`. - /// - A `CommittedInstance` on the secondary curve (used for CycleFold) that - /// will be absorbed into a sponge that operates in the (scalar field of - /// the) primary curve. - /// - /// Note that although a `CommittedInstance` for `AugmentedFCircuit` on - /// the primary curve also contains non-native elements, we still regard - /// it as native, because the sponge is on the same curve. - fn absorb_nonnative>( - &mut self, - v: &V, - ) -> Result<(), SynthesisError>; - - fn get_challenge(&mut self) -> Result, SynthesisError>; - /// returns the bit representation of the challenge, we use its output in-circuit for the - /// `GC.scalar_mul_le` method. - fn get_challenge_nbits(&mut self, nbits: usize) -> Result>, SynthesisError>; - fn get_challenges(&mut self, n: usize) -> Result>, SynthesisError>; -} diff --git a/folding-schemes/src/transcript/poseidon.rs b/folding-schemes/src/transcript/poseidon.rs deleted file mode 100644 index 1d8fcc13d..000000000 --- a/folding-schemes/src/transcript/poseidon.rs +++ /dev/null @@ -1,294 +0,0 @@ -use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, - poseidon::{ - constraints::PoseidonSpongeVar, find_poseidon_ark_and_mds, PoseidonConfig, PoseidonSponge, - }, - Absorb, CryptographicSponge, -}; -use ark_ec::{AffineRepr, CurveGroup}; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; - -use super::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar}; - -impl Transcript for PoseidonSponge { - fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self { - let mut sponge = Self::new(config); - sponge.absorb(&pp_hash); - sponge - } - - // Compatible with the in-circuit `TranscriptVar::absorb_point` - fn absorb_point>(&mut self, p: &C) { - let (x, y) = p.into_affine().xy().unwrap_or_default(); - self.absorb(&x); - self.absorb(&y); - } - fn absorb_nonnative(&mut self, v: &V) { - self.absorb(&v.to_native_sponge_field_elements_as_vec::()); - } - fn get_challenge(&mut self) -> F { - let c = self.squeeze_field_elements(1); - self.absorb(&c[0]); - c[0] - } - fn get_challenge_nbits(&mut self, nbits: usize) -> Vec { - let bits = self.squeeze_bits(nbits); - self.absorb(&F::from(F::BigInt::from_bits_le(&bits))); - bits - } - fn get_challenges(&mut self, n: usize) -> Vec { - let c = self.squeeze_field_elements(n); - self.absorb(&c); - c - } -} - -impl TranscriptVar> for PoseidonSpongeVar { - fn new_with_pp_hash( - config: &Self::Parameters, - pp_hash: &FpVar, - ) -> Result { - let mut sponge = Self::new(ConstraintSystemRef::None, config); - sponge.absorb(&pp_hash)?; - Ok(sponge) - } - - fn absorb_point, GC: CurveVar>( - &mut self, - v: &GC, - ) -> Result<(), SynthesisError> { - let mut vec = v.to_constraint_field()?; - // The last element in the vector tells whether the point is infinity, - // but we can in fact avoid absorbing it without loss of soundness. - // This is because the `to_constraint_field` method internally invokes - // [`ProjectiveVar::to_afine`](https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/groups/curves/short_weierstrass/mod.rs#L160-L195), - // which guarantees that an infinity point is represented as `(0, 0)`, - // but the y-coordinate of a non-infinity point is never 0 (for why, see - // https://crypto.stackexchange.com/a/108242 ). - vec.pop(); - self.absorb(&vec) - } - fn absorb_nonnative>( - &mut self, - v: &V, - ) -> Result<(), SynthesisError> { - self.absorb(&v.to_native_sponge_field_elements()?) - } - fn get_challenge(&mut self) -> Result, SynthesisError> { - let c = self.squeeze_field_elements(1)?; - self.absorb(&c[0])?; - Ok(c[0].clone()) - } - - /// returns the bit representation of the challenge, we use its output in-circuit for the - /// `GC.scalar_mul_le` method. - fn get_challenge_nbits(&mut self, nbits: usize) -> Result>, SynthesisError> { - let bits = self.squeeze_bits(nbits)?; - self.absorb(&Boolean::le_bits_to_fp(&bits)?)?; - Ok(bits) - } - fn get_challenges(&mut self, n: usize) -> Result>, SynthesisError> { - let c = self.squeeze_field_elements(n)?; - self.absorb(&c)?; - Ok(c) - } -} - -/// This Poseidon configuration generator produces a Poseidon configuration with custom parameters -pub fn poseidon_custom_config( - full_rounds: usize, - partial_rounds: usize, - alpha: u64, - rate: usize, - capacity: usize, -) -> PoseidonConfig { - let (ark, mds) = find_poseidon_ark_and_mds::( - F::MODULUS_BIT_SIZE as u64, - rate, - full_rounds as u64, - partial_rounds as u64, - 0, - ); - - PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, rate, capacity) -} - -/// This Poseidon configuration generator agrees with Circom's Poseidon(4) in the case of BN254's scalar field -pub fn poseidon_canonical_config() -> PoseidonConfig { - // 120 bit security target as in - // https://eprint.iacr.org/2019/458.pdf - // t = rate + 1 - - let full_rounds = 8; - let partial_rounds = 60; - let alpha = 5; - let rate = 4; - - poseidon_custom_config(full_rounds, partial_rounds, alpha, rate, 1) -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; - use ark_ec::PrimeGroup; - use ark_ff::UniformRand; - use ark_r1cs_std::{ - alloc::AllocVar, groups::curves::short_weierstrass::ProjectiveVar, GR1CSVar, - }; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::test_rng; - - use super::*; - use crate::folding::circuits::nonnative::affine::NonNativeAffineVar; - use crate::Error; - - // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 - #[test] - fn check_against_circom_poseidon() -> Result<(), Error> { - use ark_bn254::Fr; - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use std::str::FromStr; - - let config = poseidon_canonical_config::(); - let mut poseidon_sponge: PoseidonSponge<_> = CryptographicSponge::new(&config); - let v: Vec = vec!["1", "2", "3", "4"] - .into_iter() - .map(|x| { - Fr::from_str(x).map_err(|_| { - Error::ConversionError("str".to_string(), "Fr".to_string(), x.to_string()) - }) - }) - .collect::, Error>>()?; - poseidon_sponge.absorb(&v); - poseidon_sponge.squeeze_field_elements::(1); - assert!( - poseidon_sponge.state[0] - == Fr::from_str( - "18821383157269793795438455681495246036402687001665670618754263018637548127333" - ) - .map_err(|_| { - Error::ConversionError( - "str".to_string(), - "Fr".to_string(), - "hardcoded string".to_string(), - ) - })? - ); - Ok(()) - } - - #[test] - fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Error> { - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); - let rng = &mut test_rng(); - - let p = G1::rand(rng); - tr.absorb_point(&p); - let c = tr.get_challenge(); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let p_var = ProjectiveVar::>::new_witness( - ConstraintSystem::::new_ref(), - || Ok(p), - )?; - tr_var.absorb_point(&p_var)?; - let c_var = tr_var.get_challenge()?; - - // assert that native & gadget transcripts return the same challenge - assert_eq!(c, c_var.value()?); - Ok(()) - } - - #[test] - fn test_transcript_and_transcriptvar_absorb_nonnative_point() -> Result<(), Error> { - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); - let rng = &mut test_rng(); - - let p = G1::rand(rng); - tr.absorb_nonnative(&p); - let c = tr.get_challenge(); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let p_var = - NonNativeAffineVar::::new_witness(ConstraintSystem::::new_ref(), || Ok(p))?; - tr_var.absorb_nonnative(&p_var)?; - let c_var = tr_var.get_challenge()?; - - // assert that native & gadget transcripts return the same challenge - assert_eq!(c, c_var.value()?); - Ok(()) - } - - #[test] - fn test_transcript_and_transcriptvar_get_challenge() -> Result<(), Error> { - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); - tr.absorb(&Fr::from(42_u32)); - let c = tr.get_challenge(); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; - tr_var.absorb(&v)?; - let c_var = tr_var.get_challenge()?; - - // assert that native & gadget transcripts return the same challenge - assert_eq!(c, c_var.value()?); - Ok(()) - } - - #[test] - fn test_transcript_and_transcriptvar_nbits() -> Result<(), Error> { - let nbits = crate::constants::NOVA_N_BITS_RO; - - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); - tr.absorb(&Fq::from(42_u32)); - - // get challenge from native transcript - let c_bits = tr.get_challenge_nbits(nbits); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; - tr_var.absorb(&v)?; - - // get challenge from circuit transcript - let c_var = tr_var.get_challenge_nbits(nbits)?; - - let P = G1::generator(); - let PVar = GVar::new_witness(cs.clone(), || Ok(P))?; - - // multiply point P by the challenge in different formats, to ensure that we get the same - // result natively and in-circuit - - // native c*P - let c_Fr = Fr::from_bigint(BigInteger::from_bits_le(&c_bits)).ok_or(Error::OutOfBounds)?; - let cP_native = P * c_Fr; - - // native c*P using mul_bits_be (notice the .rev to convert the LE to BE) - let cP_native_bits = P.mul_bits_be(c_bits.into_iter().rev()); - - // in-circuit c*P using scalar_mul_le - let cPVar = PVar.scalar_mul_le(c_var.iter())?; - - // check that they are equal - assert_eq!(cP_native.into_affine(), cPVar.value()?.into_affine()); - assert_eq!(cP_native_bits.into_affine(), cPVar.value()?.into_affine()); - Ok(()) - } -} diff --git a/folding-schemes/src/utils/espresso/mod.rs b/folding-schemes/src/utils/espresso/mod.rs deleted file mode 100644 index 8c11fd081..000000000 --- a/folding-schemes/src/utils/espresso/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod multilinear_polynomial; -pub mod sum_check; -pub mod virtual_polynomial; diff --git a/folding-schemes/src/utils/espresso/multilinear_polynomial.rs b/folding-schemes/src/utils/espresso/multilinear_polynomial.rs deleted file mode 100644 index da5d39af7..000000000 --- a/folding-schemes/src/utils/espresso/multilinear_polynomial.rs +++ /dev/null @@ -1,200 +0,0 @@ -// code forked from -// https://github.com/EspressoSystems/hyperplonk/blob/main/arithmetic/src/multilinear_polynomial.rs -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -use ark_ff::Field; -#[cfg(feature = "parallel")] -use rayon::prelude::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; - -pub use ark_poly::DenseMultilinearExtension; - -pub fn fix_variables( - poly: &DenseMultilinearExtension, - partial_point: &[F], -) -> DenseMultilinearExtension { - assert!( - partial_point.len() <= poly.num_vars, - "invalid size of partial point" - ); - let nv = poly.num_vars; - let mut poly = poly.evaluations.to_vec(); - let dim = partial_point.len(); - // evaluate single variable of partial point from left to right - for (i, point) in partial_point.iter().enumerate().take(dim) { - poly = fix_one_variable_helper(&poly, nv - i, point); - } - - DenseMultilinearExtension::::from_evaluations_slice(nv - dim, &poly[..(1 << (nv - dim))]) -} - -fn fix_one_variable_helper(data: &[F], nv: usize, point: &F) -> Vec { - let mut res = vec![F::zero(); 1 << (nv - 1)]; - - // evaluate single variable of partial point from left to right - #[cfg(not(feature = "parallel"))] - for i in 0..(1 << (nv - 1)) { - res[i] = data[i << 1] + (data[(i << 1) + 1] - data[i << 1]) * point; - } - - #[cfg(feature = "parallel")] - res.par_iter_mut().enumerate().for_each(|(i, x)| { - *x = data[i << 1] + (data[(i << 1) + 1] - data[i << 1]) * point; - }); - - res -} - -pub fn evaluate_no_par(poly: &DenseMultilinearExtension, point: &[F]) -> F { - assert_eq!(poly.num_vars, point.len()); - fix_variables_no_par(poly, point).evaluations[0] -} - -fn fix_variables_no_par( - poly: &DenseMultilinearExtension, - partial_point: &[F], -) -> DenseMultilinearExtension { - assert!( - partial_point.len() <= poly.num_vars, - "invalid size of partial point" - ); - let nv = poly.num_vars; - let mut poly = poly.evaluations.to_vec(); - let dim = partial_point.len(); - // evaluate single variable of partial point from left to right - for i in 1..dim + 1 { - let r = partial_point[i - 1]; - for b in 0..(1 << (nv - i)) { - poly[b] = poly[b << 1] + (poly[(b << 1) + 1] - poly[b << 1]) * r; - } - } - DenseMultilinearExtension::from_evaluations_slice(nv - dim, &poly[..(1 << (nv - dim))]) -} - -/// Given multilinear polynomial `p(x)` and s `s`, compute `s*p(x)` -pub fn scalar_mul( - poly: &DenseMultilinearExtension, - s: &F, -) -> DenseMultilinearExtension { - DenseMultilinearExtension { - evaluations: poly.evaluations.iter().map(|e| *e * s).collect(), - num_vars: poly.num_vars, - } -} - -/// Test-only methods used in virtual_polynomial.rs -#[cfg(test)] -pub mod tests { - use super::*; - use ark_ff::PrimeField; - use ark_std::rand::RngCore; - use ark_std::{end_timer, start_timer}; - use std::sync::Arc; - - pub fn fix_last_variables( - poly: &DenseMultilinearExtension, - partial_point: &[F], - ) -> DenseMultilinearExtension { - assert!( - partial_point.len() <= poly.num_vars, - "invalid size of partial point" - ); - let nv = poly.num_vars; - let mut poly = poly.evaluations.to_vec(); - let dim = partial_point.len(); - // evaluate single variable of partial point from left to right - for (i, point) in partial_point.iter().rev().enumerate().take(dim) { - poly = fix_last_variable_helper(&poly, nv - i, point); - } - - DenseMultilinearExtension::::from_evaluations_slice(nv - dim, &poly[..(1 << (nv - dim))]) - } - - fn fix_last_variable_helper(data: &[F], nv: usize, point: &F) -> Vec { - let half_len = 1 << (nv - 1); - let mut res = vec![F::zero(); half_len]; - - // evaluate single variable of partial point from left to right - #[cfg(not(feature = "parallel"))] - for b in 0..half_len { - res[b] = data[b] + (data[b + half_len] - data[b]) * point; - } - - #[cfg(feature = "parallel")] - res.par_iter_mut().enumerate().for_each(|(i, x)| { - *x = data[i] + (data[i + half_len] - data[i]) * point; - }); - - res - } - - /// Sample a random list of multilinear polynomials. - /// Returns - /// - the list of polynomials, - /// - its sum of polynomial evaluations over the boolean hypercube. - #[cfg(test)] - pub fn random_mle_list( - nv: usize, - degree: usize, - rng: &mut R, - ) -> (Vec>>, F) { - let start = start_timer!(|| "sample random mle list"); - let mut multiplicands = Vec::with_capacity(degree); - for _ in 0..degree { - multiplicands.push(Vec::with_capacity(1 << nv)) - } - let mut sum = F::zero(); - - for _ in 0..(1 << nv) { - let mut product = F::one(); - - for e in multiplicands.iter_mut() { - let val = F::rand(rng); - e.push(val); - product *= val; - } - sum += product; - } - - let list = multiplicands - .into_iter() - .map(|x| Arc::new(DenseMultilinearExtension::from_evaluations_vec(nv, x))) - .collect(); - - end_timer!(start); - (list, sum) - } - - // Build a randomize list of mle-s whose sum is zero. - #[cfg(test)] - pub fn random_zero_mle_list( - nv: usize, - degree: usize, - rng: &mut R, - ) -> Vec>> { - let start = start_timer!(|| "sample random zero mle list"); - - let mut multiplicands = Vec::with_capacity(degree); - for _ in 0..degree { - multiplicands.push(Vec::with_capacity(1 << nv)) - } - for _ in 0..(1 << nv) { - multiplicands[0].push(F::zero()); - for e in multiplicands.iter_mut().skip(1) { - e.push(F::rand(rng)); - } - } - - let list = multiplicands - .into_iter() - .map(|x| Arc::new(DenseMultilinearExtension::from_evaluations_vec(nv, x))) - .collect(); - - end_timer!(start); - list - } -} diff --git a/folding-schemes/src/utils/espresso/sum_check/mod.rs b/folding-schemes/src/utils/espresso/sum_check/mod.rs deleted file mode 100644 index 2d472250e..000000000 --- a/folding-schemes/src/utils/espresso/sum_check/mod.rs +++ /dev/null @@ -1,259 +0,0 @@ -// code forked from: -// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! This module implements the sum check protocol. - -use crate::{ - transcript::Transcript, - utils::virtual_polynomial::{VPAuxInfo, VirtualPolynomial}, - Error, -}; -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::PrimeField; -use ark_poly::univariate::DensePolynomial; -use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, Polynomial}; -use ark_std::{end_timer, start_timer}; -use std::{fmt::Debug, marker::PhantomData, sync::Arc}; - -use crate::utils::sum_check::structs::IOPProverMessage; -use crate::utils::sum_check::structs::IOPVerifierState; -use structs::{IOPProof, IOPProverState}; - -mod prover; -pub mod structs; -pub mod verifier; - -/// A generic sum-check trait over a curve group -pub trait SumCheck { - type VirtualPolynomial; - type VPAuxInfo; - type MultilinearExtension; - - type SumCheckProof: Clone + Debug + Default + PartialEq; - type SumCheckSubClaim: Clone + Debug + Default + PartialEq; - - /// Extract sum from the proof - fn extract_sum(proof: &Self::SumCheckProof) -> F; - - /// Generate proof of the sum of polynomial over {0,1}^`num_vars` - /// - /// The polynomial is represented in the form of a VirtualPolynomial. - fn prove( - poly: &Self::VirtualPolynomial, - transcript: &mut impl Transcript, - ) -> Result; - - /// Verify the claimed sum using the proof - fn verify( - sum: F, - proof: &Self::SumCheckProof, - aux_info: &Self::VPAuxInfo, - transcript: &mut impl Transcript, - ) -> Result; -} - -/// Trait for sum check protocol prover side APIs. -pub trait SumCheckProver: Sized { - type VirtualPolynomial; - type ProverMessage; - - /// Initialize the prover state to argue for the sum of the input polynomial - /// over {0,1}^`num_vars`. - fn prover_init(polynomial: &Self::VirtualPolynomial) -> Result; - - /// Receive message from verifier, generate prover message, and proceed to - /// next round. - /// - /// Main algorithm used is from section 3.2 of [XZZPS19](https://eprint.iacr.org/2019/317.pdf#subsection.3.2). - fn prove_round_and_update_state( - &mut self, - challenge: &Option, - ) -> Result; -} - -/// Trait for sum check protocol verifier side APIs. -pub trait SumCheckVerifier { - type VPAuxInfo; - type ProverMessage; - type Challenge; - type SumCheckSubClaim; - - /// Initialize the verifier's state. - fn verifier_init(index_info: &Self::VPAuxInfo) -> Self; - - /// Run verifier for the current round, given a prover message. - /// - /// Note that `verify_round_and_update_state` only samples and stores - /// challenges; and update the verifier's state accordingly. The actual - /// verifications are deferred (in batch) to `check_and_generate_subclaim` - /// at the last step. - fn verify_round_and_update_state( - &mut self, - prover_msg: &Self::ProverMessage, - transcript: &mut impl Transcript, - ) -> Result; - - /// This function verifies the deferred checks in the interactive version of - /// the protocol; and generate the subclaim. Returns an error if the - /// proof failed to verify. - /// - /// If the asserted sum is correct, then the multilinear polynomial - /// evaluated at `subclaim.point` will be `subclaim.expected_evaluation`. - /// Otherwise, it is highly unlikely that those two will be equal. - /// Larger field size guarantees smaller soundness error. - fn check_and_generate_subclaim( - &self, - asserted_sum: &F, - ) -> Result; -} - -/// A SumCheckSubClaim is a claim generated by the verifier at the end of -/// verification when it is convinced. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SumCheckSubClaim { - /// the multi-dimensional point that this multilinear extension is evaluated - /// to - pub point: Vec, - /// the expected evaluation - pub expected_evaluation: F, -} - -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] -pub struct IOPSumCheck> { - #[doc(hidden)] - phantom: PhantomData, - #[doc(hidden)] - phantom2: PhantomData, -} - -impl> SumCheck for IOPSumCheck { - type SumCheckProof = IOPProof; - type VirtualPolynomial = VirtualPolynomial; - type VPAuxInfo = VPAuxInfo; - type MultilinearExtension = Arc>; - type SumCheckSubClaim = SumCheckSubClaim; - - fn extract_sum(proof: &Self::SumCheckProof) -> F { - let start = start_timer!(|| "extract sum"); - let poly = DensePolynomial::from_coefficients_vec(proof.proofs[0].coeffs.clone()); - let res = poly.evaluate(&F::ONE) + poly.evaluate(&F::ZERO); - end_timer!(start); - res - } - - fn prove( - poly: &VirtualPolynomial, - transcript: &mut impl Transcript, - ) -> Result, Error> { - transcript.absorb(&F::from(poly.aux_info.num_variables as u64)); - transcript.absorb(&F::from(poly.aux_info.max_degree as u64)); - let mut prover_state: IOPProverState = IOPProverState::prover_init(poly)?; - let mut challenge: Option = None; - let mut prover_msgs: Vec> = - Vec::with_capacity(poly.aux_info.num_variables); - for _ in 0..poly.aux_info.num_variables { - let prover_msg: IOPProverMessage = - IOPProverState::prove_round_and_update_state(&mut prover_state, &challenge)?; - transcript.absorb(&prover_msg.coeffs); - prover_msgs.push(prover_msg); - challenge = Some(transcript.get_challenge()); - } - if let Some(p) = challenge { - prover_state.challenges.push(p) - }; - Ok(IOPProof { - point: prover_state.challenges, - proofs: prover_msgs, - }) - } - - fn verify( - claimed_sum: F, - proof: &IOPProof, - aux_info: &VPAuxInfo, - transcript: &mut impl Transcript, - ) -> Result, Error> { - transcript.absorb(&F::from(aux_info.num_variables as u64)); - transcript.absorb(&F::from(aux_info.max_degree as u64)); - let mut verifier_state = IOPVerifierState::verifier_init(aux_info); - for i in 0..aux_info.num_variables { - let prover_msg = proof.proofs.get(i).expect("proof is incomplete"); - transcript.absorb(&prover_msg.coeffs); - IOPVerifierState::verify_round_and_update_state( - &mut verifier_state, - prover_msg, - transcript, - )?; - } - - IOPVerifierState::check_and_generate_subclaim(&verifier_state, &claimed_sum) - } -} - -#[cfg(test)] -pub mod tests { - use std::sync::Arc; - - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; - use ark_crypto_primitives::sponge::CryptographicSponge; - use ark_ff::Field; - use ark_pallas::Fr; - use ark_poly::DenseMultilinearExtension; - use ark_poly::MultilinearExtension; - use ark_std::{test_rng, Zero}; - - use crate::transcript::poseidon::poseidon_canonical_config; - use crate::utils::sum_check::SumCheck; - use crate::utils::virtual_polynomial::VirtualPolynomial; - use crate::Error; - - use super::IOPSumCheck; - - #[test] - pub fn sumcheck_poseidon() -> Result<(), Error> { - let n_vars = 5; - - let mut rng = test_rng(); - let poly_mle = DenseMultilinearExtension::rand(n_vars, &mut rng); - let virtual_poly = VirtualPolynomial::new_from_mle(&Arc::new(poly_mle), Fr::ONE); - - let _ = sumcheck_poseidon_opt(virtual_poly)?; - - // test with zero poly - let poly_mle = DenseMultilinearExtension::from_evaluations_vec( - n_vars, - vec![Fr::zero(); 2u32.pow(n_vars as u32) as usize], - ); - let virtual_poly = VirtualPolynomial::new_from_mle(&Arc::new(poly_mle), Fr::ONE); - let _ = sumcheck_poseidon_opt(virtual_poly)?; - Ok(()) - } - - fn sumcheck_poseidon_opt(virtual_poly: VirtualPolynomial) -> Result<(), Error> { - let poseidon_config = poseidon_canonical_config::(); - - // sum-check prove - let mut transcript_p: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let sum_check = - IOPSumCheck::>::prove(&virtual_poly, &mut transcript_p)?; - - // sum-check verify - let claimed_sum = IOPSumCheck::>::extract_sum(&sum_check); - let mut transcript_v: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let res_verify = IOPSumCheck::>::verify( - claimed_sum, - &sum_check, - &virtual_poly.aux_info, - &mut transcript_v, - ); - - assert!(res_verify.is_ok()); - Ok(()) - } -} diff --git a/folding-schemes/src/utils/espresso/sum_check/prover.rs b/folding-schemes/src/utils/espresso/sum_check/prover.rs deleted file mode 100644 index d9824b37a..000000000 --- a/folding-schemes/src/utils/espresso/sum_check/prover.rs +++ /dev/null @@ -1,226 +0,0 @@ -// code forked from: -// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! Prover subroutines for a SumCheck protocol. - -use super::SumCheckProver; -use crate::{ - utils::{ - lagrange_poly::compute_lagrange_interpolated_poly, multilinear_polynomial::fix_variables, - virtual_polynomial::VirtualPolynomial, - }, - Error, -}; -use ark_ff::{batch_inversion, PrimeField}; -use ark_poly::DenseMultilinearExtension; -use ark_std::{cfg_into_iter, end_timer, start_timer}; -use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator}; -use std::sync::Arc; - -use super::structs::{IOPProverMessage, IOPProverState}; - -// #[cfg(feature = "parallel")] -use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; - -impl SumCheckProver for IOPProverState { - type VirtualPolynomial = VirtualPolynomial; - type ProverMessage = IOPProverMessage; - - /// Initialize the prover state to argue for the sum of the input polynomial - /// over {0,1}^`num_vars`. - fn prover_init(polynomial: &Self::VirtualPolynomial) -> Result { - let start = start_timer!(|| "sum check prover init"); - if polynomial.aux_info.num_variables == 0 { - return Err(Error::InvalidPolyIOPParameters( - "Attempt to prove a constant.".to_string(), - )); - } - end_timer!(start); - - Ok(Self { - challenges: Vec::with_capacity(polynomial.aux_info.num_variables), - round: 0, - poly: polynomial.clone(), - extrapolation_aux: (1..polynomial.aux_info.max_degree) - .map(|degree| { - let points = (0..1 + degree as u64).map(F::from).collect::>(); - let weights = barycentric_weights(&points); - (points, weights) - }) - .collect(), - }) - } - - /// Receive message from verifier, generate prover message, and proceed to - /// next round. - /// - /// Main algorithm used is from section 3.2 of [XZZPS19](https://eprint.iacr.org/2019/317.pdf#subsection.3.2). - fn prove_round_and_update_state( - &mut self, - challenge: &Option, - ) -> Result { - // let start = - // start_timer!(|| format!("sum check prove {}-th round and update state", - // self.round)); - - if self.round >= self.poly.aux_info.num_variables { - return Err(Error::InvalidPolyIOPProver( - "Prover is not active".to_string(), - )); - } - - // let fix_argument = start_timer!(|| "fix argument"); - - // Step 1: - // fix argument and evaluate f(x) over x_m = r; where r is the challenge - // for the current round, and m is the round number, indexed from 1 - // - // i.e.: - // at round m <= n, for each mle g(x_1, ... x_n) within the flattened_mle - // which has already been evaluated to - // - // g(r_1, ..., r_{m-1}, x_m ... x_n) - // - // eval g over r_m, and mutate g to g(r_1, ... r_m,, x_{m+1}... x_n) - let mut flattened_ml_extensions: Vec> = self - .poly - .flattened_ml_extensions - .par_iter() - .map(|x| x.as_ref().clone()) - .collect(); - - if let Some(chal) = challenge { - if self.round == 0 { - return Err(Error::InvalidPolyIOPProver( - "first round should be prover first.".to_string(), - )); - } - self.challenges.push(*chal); - - let r = self.challenges[self.round - 1]; - // #[cfg(feature = "parallel")] - flattened_ml_extensions - .par_iter_mut() - .for_each(|mle| *mle = fix_variables(mle, &[r])); - // #[cfg(not(feature = "parallel"))] - // flattened_ml_extensions - // .iter_mut() - // .for_each(|mle| *mle = fix_variables(mle, &[r])); - } else if self.round > 0 { - return Err(Error::InvalidPolyIOPProver( - "verifier message is empty".to_string(), - )); - } - // end_timer!(fix_argument); - - self.round += 1; - - let products_list = self.poly.products.clone(); - let mut products_sum = vec![F::ZERO; self.poly.aux_info.max_degree + 1]; - - // Step 2: generate sum for the partial evaluated polynomial: - // f(r_1, ... r_m,, x_{m+1}... x_n) - - products_list.iter().for_each(|(coefficient, products)| { - let mut sum = cfg_into_iter!(0..1 << (self.poly.aux_info.num_variables - self.round)) - .fold( - || { - ( - vec![(F::ZERO, F::ZERO); products.len()], - vec![F::ZERO; products.len() + 1], - ) - }, - |(mut buf, mut acc), b| { - buf.iter_mut() - .zip(products.iter()) - .for_each(|((eval, step), f)| { - let table = &flattened_ml_extensions[*f]; - *eval = table[b << 1]; - *step = table[(b << 1) + 1] - table[b << 1]; - }); - acc[0] += buf.iter().map(|(eval, _)| eval).product::(); - acc[1..].iter_mut().for_each(|acc| { - buf.iter_mut().for_each(|(eval, step)| *eval += step as &_); - *acc += buf.iter().map(|(eval, _)| eval).product::(); - }); - (buf, acc) - }, - ) - .map(|(_, partial)| partial) - .reduce( - || vec![F::ZERO; products.len() + 1], - |mut sum, partial| { - sum.iter_mut() - .zip(partial.iter()) - .for_each(|(sum, partial)| *sum += partial); - sum - }, - ); - sum.iter_mut().for_each(|sum| *sum *= coefficient); - let extraploation = cfg_into_iter!(0..self.poly.aux_info.max_degree - products.len()) - .map(|i| { - let (points, weights) = &self.extrapolation_aux[products.len() - 1]; - let at = F::from((products.len() + 1 + i) as u64); - extrapolate(points, weights, &sum, &at) - }) - .collect::>(); - products_sum - .iter_mut() - .zip(sum.iter().chain(extraploation.iter())) - .for_each(|(products_sum, sum)| *products_sum += sum); - }); - - // update prover's state to the partial evaluated polynomial - self.poly.flattened_ml_extensions = flattened_ml_extensions - .par_iter() - .map(|x| Arc::new(x.clone())) - .collect(); - - let prover_poly = compute_lagrange_interpolated_poly::(&products_sum); - Ok(IOPProverMessage { - coeffs: prover_poly.coeffs, - }) - } -} - -#[allow(clippy::filter_map_bool_then)] -fn barycentric_weights(points: &[F]) -> Vec { - let mut weights = points - .iter() - .enumerate() - .map(|(j, point_j)| { - points - .iter() - .enumerate() - .filter_map(|(i, point_i)| (i != j).then(|| *point_j - point_i)) - .reduce(|acc, value| acc * value) - .unwrap_or_else(F::one) - }) - .collect::>(); - batch_inversion(&mut weights); - weights -} - -fn extrapolate(points: &[F], weights: &[F], evals: &[F], at: &F) -> F { - let (coeffs, sum_inv) = { - let mut coeffs = points.iter().map(|point| *at - point).collect::>(); - batch_inversion(&mut coeffs); - coeffs.iter_mut().zip(weights).for_each(|(coeff, weight)| { - *coeff *= weight; - }); - let sum_inv = coeffs.iter().sum::().inverse().unwrap_or_default(); - (coeffs, sum_inv) - }; - coeffs - .iter() - .zip(evals) - .map(|(coeff, eval)| *coeff * eval) - .sum::() - * sum_inv -} diff --git a/folding-schemes/src/utils/espresso/sum_check/structs.rs b/folding-schemes/src/utils/espresso/sum_check/structs.rs deleted file mode 100644 index de487d93c..000000000 --- a/folding-schemes/src/utils/espresso/sum_check/structs.rs +++ /dev/null @@ -1,59 +0,0 @@ -// code forked from: -// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! This module defines structs that are shared by all sub protocols. - -use crate::utils::virtual_polynomial::VirtualPolynomial; -use ark_ff::PrimeField; -use ark_serialize::CanonicalSerialize; - -/// An IOP proof is a collections of -/// - messages from prover to verifier at each round through the interactive -/// protocol. -/// - a point that is generated by the transcript for evaluation -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IOPProof { - pub point: Vec, - pub proofs: Vec>, -} - -/// A message from the prover to the verifier at a given round -/// is a list of coeffs. -#[derive(Clone, Debug, Default, PartialEq, Eq, CanonicalSerialize)] -pub struct IOPProverMessage { - pub(crate) coeffs: Vec, -} - -/// Prover State of a PolyIOP. -#[derive(Debug)] -pub struct IOPProverState { - /// sampled randomness given by the verifier - pub challenges: Vec, - /// the current round number - pub(crate) round: usize, - /// pointer to the virtual polynomial - pub(crate) poly: VirtualPolynomial, - /// points with precomputed barycentric weights for extrapolating smaller - /// degree uni-polys to `max_degree + 1` evaluations. - #[allow(clippy::type_complexity)] - pub(crate) extrapolation_aux: Vec<(Vec, Vec)>, -} - -/// Verifier State of a PolyIOP, generic over a curve group -#[derive(Debug)] -pub struct IOPVerifierState { - pub(crate) round: usize, - pub(crate) num_vars: usize, - pub(crate) finished: bool, - /// a list storing the univariate polynomial in evaluation form sent by the - /// prover at each round - pub(crate) polynomials_received: Vec>, - /// a list storing the randomness sampled by the verifier at each round - pub(crate) challenges: Vec, -} diff --git a/folding-schemes/src/utils/espresso/sum_check/verifier.rs b/folding-schemes/src/utils/espresso/sum_check/verifier.rs deleted file mode 100644 index 7fb6e9813..000000000 --- a/folding-schemes/src/utils/espresso/sum_check/verifier.rs +++ /dev/null @@ -1,302 +0,0 @@ -// code forked from: -// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! Verifier subroutines for a SumCheck protocol. - -use super::{ - structs::{IOPProverMessage, IOPVerifierState}, - SumCheckSubClaim, SumCheckVerifier, -}; -use crate::{transcript::Transcript, utils::virtual_polynomial::VPAuxInfo, Error}; -use ark_crypto_primitives::sponge::Absorb; -use ark_ff::PrimeField; -use ark_poly::Polynomial; -use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; -use ark_std::{end_timer, start_timer}; - -#[cfg(feature = "parallel")] -use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; - -impl SumCheckVerifier for IOPVerifierState { - type VPAuxInfo = VPAuxInfo; - type ProverMessage = IOPProverMessage; - type Challenge = F; - type SumCheckSubClaim = SumCheckSubClaim; - - /// Initialize the verifier's state. - fn verifier_init(index_info: &Self::VPAuxInfo) -> Self { - let start = start_timer!(|| "sum check verifier init"); - let res = Self { - round: 1, - num_vars: index_info.num_variables, - finished: false, - polynomials_received: Vec::with_capacity(index_info.num_variables), - challenges: Vec::with_capacity(index_info.num_variables), - }; - end_timer!(start); - res - } - - fn verify_round_and_update_state( - &mut self, - prover_msg: & as SumCheckVerifier>::ProverMessage, - transcript: &mut impl Transcript, - ) -> Result< as SumCheckVerifier>::Challenge, Error> { - let start = - start_timer!(|| format!("sum check verify {}-th round and update state", self.round)); - - if self.finished { - return Err(Error::InvalidPolyIOPVerifier( - "Incorrect verifier state: Verifier is already finished.".to_string(), - )); - } - - // In an interactive protocol, the verifier should - // - // 1. check if the received 'P(0) + P(1) = expected`. - // 2. set `expected` to P(r)` - // - // When we turn the protocol to a non-interactive one, it is sufficient to defer - // such checks to `check_and_generate_subclaim` after the last round. - let challenge = transcript.get_challenge(); - self.challenges.push(challenge); - self.polynomials_received.push(prover_msg.coeffs.to_vec()); - - if self.round == self.num_vars { - // accept and close - self.finished = true; - } else { - // proceed to the next round - self.round += 1; - } - - end_timer!(start); - Ok(challenge) - } - - fn check_and_generate_subclaim( - &self, - asserted_sum: &F, - ) -> Result { - let start = start_timer!(|| "sum check check and generate subclaim"); - if !self.finished { - return Err(Error::InvalidPolyIOPVerifier( - "Incorrect verifier state: Verifier has not finished.".to_string(), - )); - } - - if self.polynomials_received.len() != self.num_vars { - return Err(Error::InvalidPolyIOPVerifier( - "insufficient rounds".to_string(), - )); - } - - // the deferred check during the interactive phase: - // 2. set `expected` to P(r)` - #[cfg(feature = "parallel")] - let mut expected_vec = self - .polynomials_received - .clone() - .into_par_iter() - .zip(self.challenges.clone().into_par_iter()) - .map(|(coeffs, challenge)| { - // Removed check on number of evaluations here since verifier receives polynomial in coeffs form - let prover_poly = DensePolynomial::from_coefficients_slice(&coeffs); - prover_poly.evaluate(&challenge) - }) - .collect::>(); - - #[cfg(not(feature = "parallel"))] - let mut expected_vec = self - .polynomials_received - .clone() - .into_iter() - .zip(self.challenges.clone().into_iter()) - .map(|(coeffs, challenge)| { - // Removed check on number of evaluations here since verifier receives polynomial in coeffs form - let prover_poly = DensePolynomial::from_coefficients_slice(&coeffs); - prover_poly.evaluate(&challenge) - }) - .collect::>(); - - // insert the asserted_sum to the first position of the expected vector - expected_vec.insert(0, *asserted_sum); - - for (coeffs, &expected) in self - .polynomials_received - .iter() - .zip(expected_vec.iter()) - .take(self.num_vars) - { - let poly = DensePolynomial::from_coefficients_slice(coeffs); - let eval_at_one: F = poly.iter().sum(); - let eval_at_zero: F = if poly.coeffs.is_empty() { - F::zero() - } else { - poly.coeffs[0] - }; - let eval = eval_at_one + eval_at_zero; - - // the deferred check during the interactive phase: - // 1. check if the received 'P(0) + P(1) = expected`. - if eval != expected { - return Err(Error::InvalidPolyIOPProof( - "Prover message is not consistent with the claim.".to_string(), - )); - } - } - end_timer!(start); - Ok(SumCheckSubClaim { - point: self.challenges.clone(), - // the last expected value (not checked within this function) will be included in the - // subclaim - expected_evaluation: expected_vec[self.num_vars], - }) - } -} - -/// Interpolate a uni-variate degree-`p_i.len()-1` polynomial and evaluate this -/// polynomial at `eval_at`: -/// -/// \sum_{i=0}^len p_i * (\prod_{j!=i} (eval_at - j)/(i-j) ) -/// -/// This implementation is linear in number of inputs in terms of field -/// operations. It also has a quadratic term in primitive operations which is -/// negligible compared to field operations. -/// TODO: The quadratic term can be removed by precomputing the lagrange -/// coefficients. -pub fn interpolate_uni_poly(p_i: &[F], eval_at: F) -> F { - let start = start_timer!(|| "sum check interpolate uni poly opt"); - - let len = p_i.len(); - let mut evals = vec![]; - let mut prod = eval_at; - evals.push(eval_at); - - // `prod = \prod_{j} (eval_at - j)` - for e in 1..len { - let tmp = eval_at - F::from(e as u64); - evals.push(tmp); - prod *= tmp; - } - let mut res = F::zero(); - // we want to compute \prod (j!=i) (i-j) for a given i - // - // we start from the last step, which is - // denom[len-1] = (len-1) * (len-2) *... * 2 * 1 - // the step before that is - // denom[len-2] = (len-2) * (len-3) * ... * 2 * 1 * -1 - // and the step before that is - // denom[len-3] = (len-3) * (len-4) * ... * 2 * 1 * -1 * -2 - // - // i.e., for any i, the one before this will be derived from - // denom[i-1] = denom[i] * (len-i) / i - // - // that is, we only need to store - // - the last denom for i = len-1, and - // - the ratio between current step and fhe last step, which is the product of - // (len-i) / i from all previous steps and we store this product as a fraction - // number to reduce field divisions. - - // We know - // - 2^61 < factorial(20) < 2^62 - // - 2^122 < factorial(33) < 2^123 - // so we will be able to compute the ratio - // - for len <= 20 with i64 - // - for len <= 33 with i128 - // - for len > 33 with BigInt - if p_i.len() <= 20 { - let last_denominator = F::from(u64_factorial(len - 1)); - let mut ratio_numerator = 1i64; - let mut ratio_denominator = 1u64; - - for i in (0..len).rev() { - let ratio_numerator_f = if ratio_numerator < 0 { - -F::from((-ratio_numerator) as u64) - } else { - F::from(ratio_numerator as u64) - }; - - res += p_i[i] * prod * F::from(ratio_denominator) - / (last_denominator * ratio_numerator_f * evals[i]); - - // compute denom for the next step is current_denom * (len-i)/i - if i != 0 { - ratio_numerator *= -(len as i64 - i as i64); - ratio_denominator *= i as u64; - } - } - } else if p_i.len() <= 33 { - let last_denominator = F::from(u128_factorial(len - 1)); - let mut ratio_numerator = 1i128; - let mut ratio_denominator = 1u128; - - for i in (0..len).rev() { - let ratio_numerator_f = if ratio_numerator < 0 { - -F::from((-ratio_numerator) as u128) - } else { - F::from(ratio_numerator as u128) - }; - - res += p_i[i] * prod * F::from(ratio_denominator) - / (last_denominator * ratio_numerator_f * evals[i]); - - // compute denom for the next step is current_denom * (len-i)/i - if i != 0 { - ratio_numerator *= -(len as i128 - i as i128); - ratio_denominator *= i as u128; - } - } - } else { - let mut denom_up = field_factorial::(len - 1); - let mut denom_down = F::one(); - - for i in (0..len).rev() { - res += p_i[i] * prod * denom_down / (denom_up * evals[i]); - - // compute denom for the next step is current_denom * (len-i)/i - if i != 0 { - denom_up *= -F::from((len - i) as u64); - denom_down *= F::from(i as u64); - } - } - } - end_timer!(start); - res -} - -/// compute the factorial(a) = 1 * 2 * ... * a -#[inline] -fn field_factorial(a: usize) -> F { - let mut res = F::one(); - for i in 2..=a { - res *= F::from(i as u64); - } - res -} - -/// compute the factorial(a) = 1 * 2 * ... * a -#[inline] -fn u128_factorial(a: usize) -> u128 { - let mut res = 1u128; - for i in 2..=a { - res *= i as u128; - } - res -} - -/// compute the factorial(a) = 1 * 2 * ... * a -#[inline] -fn u64_factorial(a: usize) -> u64 { - let mut res = 1u64; - for i in 2..=a { - res *= i as u64; - } - res -} diff --git a/folding-schemes/src/utils/espresso/virtual_polynomial.rs b/folding-schemes/src/utils/espresso/virtual_polynomial.rs deleted file mode 100644 index 0d16ae8b0..000000000 --- a/folding-schemes/src/utils/espresso/virtual_polynomial.rs +++ /dev/null @@ -1,546 +0,0 @@ -// code forked from -// https://github.com/privacy-scaling-explorations/multifolding-poc/blob/main/src/espresso/virtual_polynomial.rs -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! This module defines our main mathematical object `VirtualPolynomial`; and -//! various functions associated with it. - -use ark_ff::PrimeField; -use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; -use ark_serialize::CanonicalSerialize; -use ark_std::{end_timer, start_timer}; -use rayon::prelude::*; -use std::{cmp::max, collections::HashMap, marker::PhantomData, ops::Add, sync::Arc}; -use thiserror::Error; - -//-- aritherrors -/// A `enum` specifying the possible failure modes of the arithmetic. -#[derive(Error, Debug)] -pub enum ArithErrors { - #[error("Invalid parameters: {0}")] - InvalidParameters(String), - #[error("Should not arrive to this point")] - ShouldNotArrive, - #[error("An error during (de)serialization: {0}")] - SerializationErrors(ark_serialize::SerializationError), -} - -impl From for ArithErrors { - fn from(e: ark_serialize::SerializationError) -> Self { - Self::SerializationErrors(e) - } -} -//-- aritherrors - -#[rustfmt::skip] -/// A virtual polynomial is a sum of products of multilinear polynomials; -/// where the multilinear polynomials are stored via their multilinear -/// extensions: `(coefficient, DenseMultilinearExtension)` -/// -/// * Number of products n = `polynomial.products.len()`, -/// * Number of multiplicands of ith product m_i = -/// `polynomial.products[i].1.len()`, -/// * Coefficient of ith product c_i = `polynomial.products[i].0` -/// -/// The resulting polynomial is -/// -/// $$ \sum_{i=0}^{n} c_i \cdot \prod_{j=0}^{m_i} P_{ij} $$ -/// -/// Example: -/// f = c0 * f0 * f1 * f2 + c1 * f3 * f4 -/// where f0 ... f4 are multilinear polynomials -/// -/// - `flattened_ml_extensions` stores the multilinear extension representation -/// of f0, f1, f2, f3 and f4 -/// - `products` is `[(c0, [0, 1, 2]), (c1, [3, 4])]` -/// - raw_pointers_lookup_table maps fi to i -/// -#[derive(Clone, Debug, Default, PartialEq)] -pub struct VirtualPolynomial { - /// Aux information about the multilinear polynomial - pub aux_info: VPAuxInfo, - /// list of reference to products (as usize) of multilinear extension - pub products: Vec<(F, Vec)>, - /// Stores multilinear extensions in which product multiplicand can refer - /// to. - pub flattened_ml_extensions: Vec>>, - /// Pointers to the above poly extensions - raw_pointers_lookup_table: HashMap<*const DenseMultilinearExtension, usize>, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, CanonicalSerialize)] -/// Auxiliary information about the multilinear polynomial -pub struct VPAuxInfo { - /// max number of multiplicands in each product - pub max_degree: usize, - /// number of variables of the polynomial - pub num_variables: usize, - /// Associated field - #[doc(hidden)] - pub phantom: PhantomData, -} - -impl Add for &VirtualPolynomial { - type Output = VirtualPolynomial; - fn add(self, other: &VirtualPolynomial) -> Self::Output { - let start = start_timer!(|| "virtual poly add"); - let mut res = self.clone(); - for products in other.products.iter() { - let cur: Vec>> = products - .1 - .iter() - .map(|&x| other.flattened_ml_extensions[x].clone()) - .collect(); - - res.add_mle_list(cur, products.0) - .expect("add product failed"); - } - end_timer!(start); - res - } -} - -// TODO: convert this into a trait -impl VirtualPolynomial { - /// Creates an empty virtual polynomial with `num_variables`. - pub fn new(num_variables: usize) -> Self { - VirtualPolynomial { - aux_info: VPAuxInfo { - max_degree: 0, - num_variables, - phantom: PhantomData, - }, - products: Vec::new(), - flattened_ml_extensions: Vec::new(), - raw_pointers_lookup_table: HashMap::new(), - } - } - - /// Creates a new virtual polynomial from a MLE and its coefficient. - pub fn new_from_mle(mle: &Arc>, coefficient: F) -> Self { - let mle_ptr: *const DenseMultilinearExtension = Arc::as_ptr(mle); - let mut hm = HashMap::new(); - hm.insert(mle_ptr, 0); - - VirtualPolynomial { - aux_info: VPAuxInfo { - // The max degree is the max degree of any individual variable - max_degree: 1, - num_variables: mle.num_vars, - phantom: PhantomData, - }, - // here `0` points to the first polynomial of `flattened_ml_extensions` - products: vec![(coefficient, vec![0])], - flattened_ml_extensions: vec![mle.clone()], - raw_pointers_lookup_table: hm, - } - } - - /// Add a product of list of multilinear extensions to self - /// Returns an error if the list is empty, or the MLE has a different - /// `num_vars` from self. - /// - /// The MLEs will be multiplied together, and then multiplied by the scalar - /// `coefficient`. - pub fn add_mle_list( - &mut self, - mle_list: impl IntoIterator>>, - coefficient: F, - ) -> Result<(), ArithErrors> { - let mle_list: Vec>> = mle_list.into_iter().collect(); - let mut indexed_product = Vec::with_capacity(mle_list.len()); - - if mle_list.is_empty() { - return Err(ArithErrors::InvalidParameters( - "input mle_list is empty".to_string(), - )); - } - - self.aux_info.max_degree = max(self.aux_info.max_degree, mle_list.len()); - - for mle in mle_list { - if mle.num_vars != self.aux_info.num_variables { - return Err(ArithErrors::InvalidParameters(format!( - "product has a multiplicand with wrong number of variables {} vs {}", - mle.num_vars, self.aux_info.num_variables - ))); - } - - let mle_ptr: *const DenseMultilinearExtension = Arc::as_ptr(&mle); - if let Some(index) = self.raw_pointers_lookup_table.get(&mle_ptr) { - indexed_product.push(*index) - } else { - let curr_index = self.flattened_ml_extensions.len(); - self.flattened_ml_extensions.push(mle.clone()); - self.raw_pointers_lookup_table.insert(mle_ptr, curr_index); - indexed_product.push(curr_index); - } - } - self.products.push((coefficient, indexed_product)); - Ok(()) - } - - /// Multiple the current VirtualPolynomial by an MLE: - /// - add the MLE to the MLE list; - /// - multiple each product by MLE and its coefficient. - /// - /// Returns an error if the MLE has a different `num_vars` from self. - pub fn mul_by_mle( - &mut self, - mle: Arc>, - coefficient: F, - ) -> Result<(), ArithErrors> { - let start = start_timer!(|| "mul by mle"); - - if mle.num_vars != self.aux_info.num_variables { - return Err(ArithErrors::InvalidParameters(format!( - "product has a multiplicand with wrong number of variables {} vs {}", - mle.num_vars, self.aux_info.num_variables - ))); - } - - let mle_ptr: *const DenseMultilinearExtension = Arc::as_ptr(&mle); - - // check if this mle already exists in the virtual polynomial - let mle_index = match self.raw_pointers_lookup_table.get(&mle_ptr) { - Some(&p) => p, - None => { - self.raw_pointers_lookup_table - .insert(mle_ptr, self.flattened_ml_extensions.len()); - self.flattened_ml_extensions.push(mle); - self.flattened_ml_extensions.len() - 1 - } - }; - - for (prod_coef, indices) in self.products.iter_mut() { - // - add the MLE to the MLE list; - // - multiple each product by MLE and its coefficient. - indices.push(mle_index); - *prod_coef *= coefficient; - } - - // increase the max degree by one as the MLE has degree 1. - self.aux_info.max_degree += 1; - end_timer!(start); - Ok(()) - } - - /// Given virtual polynomial `p(x)` and scalar `s`, compute `s*p(x)` - pub fn scalar_mul(&mut self, s: &F) { - for (prod_coef, _) in self.products.iter_mut() { - *prod_coef *= s; - } - } - - /// Evaluate the virtual polynomial at point `point`. - /// Returns an error is point.len() does not match `num_variables`. - pub fn evaluate(&self, point: &[F]) -> Result { - let start = start_timer!(|| "evaluation"); - - if self.aux_info.num_variables != point.len() { - return Err(ArithErrors::InvalidParameters(format!( - "wrong number of variables {} vs {}", - self.aux_info.num_variables, - point.len() - ))); - } - - // Evaluate all the MLEs at `point` - let evals: Vec = self - .flattened_ml_extensions - .iter() - .map(|x| x.fix_variables(point)[0]) - .collect(); - - let res = self - .products - .iter() - .map(|(c, p)| *c * p.iter().map(|&i| evals[i]).product::()) - .sum(); - - end_timer!(start); - Ok(res) - } - - // Input poly f(x) and a random vector r, output - // \hat f(x) = \sum_{x_i \in eval_x} f(x_i) eq(x, r) - // where - // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) - // - // This function is used in ZeroCheck. - pub fn build_f_hat(&self, r: &[F]) -> Result { - let start = start_timer!(|| "zero check build hat f"); - - if self.aux_info.num_variables != r.len() { - return Err(ArithErrors::InvalidParameters(format!( - "r.len() is different from number of variables: {} vs {}", - r.len(), - self.aux_info.num_variables - ))); - } - - let eq_x_r = build_eq_x_r(r)?; - let mut res = self.clone(); - res.mul_by_mle(eq_x_r, F::one())?; - - end_timer!(start); - Ok(res) - } -} - -/// Evaluate eq polynomial. -pub fn eq_eval(x: &[F], y: &[F]) -> Result { - if x.len() != y.len() { - return Err(ArithErrors::InvalidParameters( - "x and y have different length".to_string(), - )); - } - let start = start_timer!(|| "eq_eval"); - let mut res = F::one(); - for (&xi, &yi) in x.iter().zip(y.iter()) { - let xi_yi = xi * yi; - res *= xi_yi + xi_yi - xi - yi + F::one(); - } - end_timer!(start); - Ok(res) -} - -/// This function build the eq(x, r) polynomial for any given r. -/// -/// Evaluate -/// eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) -/// over r, which is -/// eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) -pub fn build_eq_x_r( - r: &[F], -) -> Result>, ArithErrors> { - let evals = build_eq_x_r_vec(r)?; - let mle = DenseMultilinearExtension::from_evaluations_vec(r.len(), evals); - - Ok(Arc::new(mle)) -} - -/// This function build the eq(x, r) polynomial for any given r, and output the -/// evaluation of eq(x, r) in its vector form. -/// -/// Evaluate -/// eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) -/// over r, which is -/// eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) -pub fn build_eq_x_r_vec(r: &[F]) -> Result, ArithErrors> { - // we build eq(x,r) from its evaluations - // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars - // for example, with num_vars = 4, x is a binary vector of 4, then - // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) - // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) - // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) - // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) - // .... - // 1 1 1 1 -> r0 * r1 * r2 * r3 - // we will need 2^num_var evaluations - - let mut eval = Vec::new(); - build_eq_x_r_helper(r, &mut eval)?; - - Ok(eval) -} - -/// A helper function to build eq(x, r) recursively. -/// This function takes `r.len()` steps, and for each step it requires a maximum -/// `r.len()-1` multiplications. -fn build_eq_x_r_helper(r: &[F], buf: &mut Vec) -> Result<(), ArithErrors> { - if r.is_empty() { - return Err(ArithErrors::InvalidParameters("r length is 0".to_string())); - } else if r.len() == 1 { - // initializing the buffer with [1-r_0, r_0] - buf.push(F::one() - r[0]); - buf.push(r[0]); - } else { - build_eq_x_r_helper(&r[1..], buf)?; - - // suppose at the previous step we received [b_1, ..., b_k] - // for the current step we will need - // if x_0 = 0: (1-r0) * [b_1, ..., b_k] - // if x_0 = 1: r0 * [b_1, ..., b_k] - // let mut res = vec![]; - // for &b_i in buf.iter() { - // let tmp = r[0] * b_i; - // res.push(b_i - tmp); - // res.push(tmp); - // } - // *buf = res; - - let mut res = vec![F::zero(); buf.len() << 1]; - res.par_iter_mut().enumerate().for_each(|(i, val)| { - let bi = buf[i >> 1]; - let tmp = r[0] * bi; - if i & 1 == 0 { - *val = bi - tmp; - } else { - *val = tmp; - } - }); - *buf = res; - } - - Ok(()) -} - -/// Decompose an integer into a binary vector in little endian. -pub fn bit_decompose(input: u64, num_var: usize) -> Vec { - let mut res = Vec::with_capacity(num_var); - let mut i = input; - for _ in 0..num_var { - res.push(i & 1 == 1); - i >>= 1; - } - res -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils::multilinear_polynomial::tests::random_mle_list; - use crate::Error; - use ark_ff::UniformRand; - use ark_pallas::Fr; - use ark_std::{ - rand::{Rng, RngCore}, - test_rng, - }; - - impl VirtualPolynomial { - /// Sample a random virtual polynomial, return the polynomial and its sum. - fn rand( - nv: usize, - num_multiplicands_range: (usize, usize), - num_products: usize, - rng: &mut R, - ) -> Result<(Self, F), ArithErrors> { - let start = start_timer!(|| "sample random virtual polynomial"); - - let mut sum = F::zero(); - let mut poly = VirtualPolynomial::new(nv); - for _ in 0..num_products { - let num_multiplicands = - rng.gen_range(num_multiplicands_range.0..num_multiplicands_range.1); - let (product, product_sum) = random_mle_list(nv, num_multiplicands, rng); - let coefficient = F::rand(rng); - poly.add_mle_list(product.into_iter(), coefficient)?; - sum += product_sum * coefficient; - } - - end_timer!(start); - Ok((poly, sum)) - } - } - - #[test] - fn test_virtual_polynomial_additions() -> Result<(), ArithErrors> { - let mut rng = test_rng(); - for nv in 2..5 { - for num_products in 2..5 { - let base: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); - - let (a, _a_sum) = - VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; - let (b, _b_sum) = - VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; - let c = &a + &b; - - assert_eq!( - a.evaluate(base.as_ref())? + b.evaluate(base.as_ref())?, - c.evaluate(base.as_ref())? - ); - } - } - - Ok(()) - } - - #[test] - fn test_virtual_polynomial_mul_by_mle() -> Result<(), ArithErrors> { - let mut rng = test_rng(); - for nv in 2..5 { - for num_products in 2..5 { - let base: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); - - let (a, _a_sum) = - VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; - let (b, _b_sum) = random_mle_list(nv, 1, &mut rng); - let b_mle = b[0].clone(); - let coeff = Fr::rand(&mut rng); - let b_vp = VirtualPolynomial::new_from_mle(&b_mle, coeff); - - let mut c = a.clone(); - - c.mul_by_mle(b_mle, coeff)?; - - assert_eq!( - a.evaluate(base.as_ref())? * b_vp.evaluate(base.as_ref())?, - c.evaluate(base.as_ref())? - ); - } - } - - Ok(()) - } - - #[test] - fn test_eq_xr() -> Result<(), Error> { - let mut rng = test_rng(); - for nv in 4..10 { - let r: Vec = (0..nv).map(|_| Fr::rand(&mut rng)).collect(); - let eq_x_r = build_eq_x_r(r.as_ref())?; - let eq_x_r2 = build_eq_x_r_for_test(r.as_ref()); - assert_eq!(eq_x_r, eq_x_r2); - } - Ok(()) - } - - /// Naive method to build eq(x, r). - /// Only used for testing purpose. - // Evaluate - // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) - // over r, which is - // eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) - fn build_eq_x_r_for_test(r: &[F]) -> Arc> { - // we build eq(x,r) from its evaluations - // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars - // for example, with num_vars = 4, x is a binary vector of 4, then - // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) - // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) - // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) - // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) - // .... - // 1 1 1 1 -> r0 * r1 * r2 * r3 - // we will need 2^num_var evaluations - - // First, we build array for {1 - r_i} - let one_minus_r: Vec = r.iter().map(|ri| F::one() - ri).collect(); - - let num_var = r.len(); - let mut eval = vec![]; - - for i in 0..1 << num_var { - let mut current_eval = F::one(); - let bit_sequence = bit_decompose(i, num_var); - - for (&bit, (ri, one_minus_ri)) in - bit_sequence.iter().zip(r.iter().zip(one_minus_r.iter())) - { - current_eval *= if bit { *ri } else { *one_minus_ri }; - } - eval.push(current_eval); - } - - let mle = DenseMultilinearExtension::from_evaluations_vec(num_var, eval); - - Arc::new(mle) - } -} diff --git a/folding-schemes/src/utils/gadgets.rs b/folding-schemes/src/utils/gadgets.rs deleted file mode 100644 index 1433b71cc..000000000 --- a/folding-schemes/src/utils/gadgets.rs +++ /dev/null @@ -1,149 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - GR1CSVar, -}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; -use core::borrow::Borrow; - -use crate::utils::vec::SparseMatrix; - -/// `EquivalenceGadget` enforces that two in-circuit variables are equivalent, -/// where the equivalence relation is parameterized by `M`: -/// - For `FpVar`, it is simply an equality relation, and `M` is unused. -/// - For `NonNativeUintVar`, we consider equivalence as a congruence relation, -/// in terms of modular arithmetic, so `M` specifies the modulus. -pub trait EquivalenceGadget { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError>; -} -impl EquivalenceGadget for FpVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { - self.enforce_equal(other) - } -} -impl> EquivalenceGadget for [T] { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { - self.iter() - .zip(other) - .try_for_each(|(a, b)| a.enforce_equivalent(b)) - } -} - -pub trait MatrixGadget { - fn mul_vector(&self, v: &[FV]) -> Result, SynthesisError>; -} - -pub trait VectorGadget { - fn add(&self, other: &Self) -> Result, SynthesisError>; - - fn mul_scalar(&self, other: &FV) -> Result, SynthesisError>; - - fn hadamard(&self, other: &Self) -> Result, SynthesisError>; -} - -impl VectorGadget> for [FpVar] { - fn add(&self, other: &Self) -> Result>, SynthesisError> { - if self.len() != other.len() { - return Err(SynthesisError::Unsatisfiable); - } - Ok(self.iter().zip(other.iter()).map(|(a, b)| a + b).collect()) - } - - fn mul_scalar(&self, c: &FpVar) -> Result>, SynthesisError> { - Ok(self.iter().map(|a| a * c).collect()) - } - - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { - if self.len() != other.len() { - return Err(SynthesisError::Unsatisfiable); - } - Ok(self.iter().zip(other.iter()).map(|(a, b)| a * b).collect()) - } -} - -#[derive(Debug, Clone)] -pub struct SparseMatrixVar { - pub n_rows: usize, - pub n_cols: usize, - // same format as the native SparseMatrix (which follows ark_relations::gr1cs::Matrix format - pub coeffs: Vec>, -} - -impl> AllocVar, CF> - for SparseMatrixVar -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let mut coeffs: Vec> = Vec::new(); - for row in val.borrow().coeffs.iter() { - let mut rowVar: Vec<(FV, usize)> = Vec::new(); - for &(value, col_i) in row.iter() { - let coeffVar = FV::new_variable(cs.clone(), || Ok(value), mode)?; - rowVar.push((coeffVar, col_i)); - } - coeffs.push(rowVar); - } - - Ok(Self { - n_rows: val.borrow().n_rows, - n_cols: val.borrow().n_cols, - coeffs, - }) - }) - } -} - -impl MatrixGadget> for SparseMatrixVar> { - fn mul_vector(&self, v: &[FpVar]) -> Result>, SynthesisError> { - Ok(self - .coeffs - .iter() - .map(|row| { - let products = row - .iter() - .map(|(value, col_i)| value * &v[*col_i]) - .collect::>(); - if products.is_constant() { - FpVar::constant(products.value().unwrap_or_default().into_iter().sum()) - } else { - products.iter().sum() - } - }) - .collect()) - } -} - -/// Interprets the given vector v as the evaluations of a dense multilinear extension of n_vars, -/// and evaluates it at the given point. This method mimics the behavior of -/// `utils/mle.rs#dense_vec_to_dense_mle` + `DenseMultilinearExtension::evaluate` but in R1CS -/// constraints, since dense multilinear extensions are not supported in ark_r1cs_std. -pub fn eval_mle( - // n_vars indicates the number of variables in the MLE - n_vars: usize, - // v is the vector of the evaluations of the dense multilinear extension (MLE) - v: Vec>, - // point is the point at which we want to evaluate the MLE - point: Vec>, -) -> FpVar { - // pad to 2^n_vars - let mut poly = v; - poly.resize(1 << n_vars, FpVar::zero()); - - for i in 1..n_vars + 1 { - let r = point[i - 1].clone(); - for b in 0..(1 << (n_vars - 1)) { - let left = poly[b << 1].clone(); - let right = poly[(b << 1) + 1].clone(); - poly[b] = left.clone() + r.clone() * (right - left); - } - } - poly[0].clone() -} diff --git a/folding-schemes/src/utils/hypercube.rs b/folding-schemes/src/utils/hypercube.rs deleted file mode 100644 index 673df1a7d..000000000 --- a/folding-schemes/src/utils/hypercube.rs +++ /dev/null @@ -1,77 +0,0 @@ -/// A boolean hypercube structure to create an ergonomic evaluation domain -use crate::utils::virtual_polynomial::bit_decompose; -use ark_ff::PrimeField; - -use std::marker::PhantomData; - -/// A boolean hypercube that returns its points as an iterator -/// If you iterate on it for 3 variables you will get points in little-endian order: -/// 000 -> 100 -> 010 -> 110 -> 001 -> 101 -> 011 -> 111 -#[derive(Debug, Clone)] -pub struct BooleanHypercube { - _f: PhantomData, - n_vars: usize, - current: u64, - max: u64, -} - -impl BooleanHypercube { - pub fn new(n_vars: usize) -> Self { - BooleanHypercube:: { - _f: PhantomData::, - n_vars, - current: 0, - max: 2_u32.pow(n_vars as u32) as u64, - } - } - - /// returns the entry at given i (which is the little-endian bit representation of i) - pub fn at_i(&self, i: usize) -> Vec { - assert!(i < self.max as usize); - let bits = bit_decompose((i) as u64, self.n_vars); - bits.iter().map(|&x| F::from(x)).collect() - } -} - -impl Iterator for BooleanHypercube { - type Item = Vec; - - fn next(&mut self) -> Option { - let bits = bit_decompose(self.current, self.n_vars); - let result: Vec = bits.iter().map(|&x| F::from(x)).collect(); - self.current += 1; - - if self.current > self.max { - return None; - } - - Some(result) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils::vec::tests::to_F_dense_matrix; - use ark_pallas::Fr; - - #[test] - fn test_hypercube() { - let expected_results = to_F_dense_matrix(vec![ - vec![0, 0, 0], - vec![1, 0, 0], - vec![0, 1, 0], - vec![1, 1, 0], - vec![0, 0, 1], - vec![1, 0, 1], - vec![0, 1, 1], - vec![1, 1, 1], - ]); - - let bhc = BooleanHypercube::::new(3); - for (i, point) in bhc.clone().enumerate() { - assert_eq!(point, expected_results[i]); - assert_eq!(point, bhc.at_i(i)); - } - } -} diff --git a/folding-schemes/src/utils/lagrange_poly.rs b/folding-schemes/src/utils/lagrange_poly.rs deleted file mode 100644 index c2237108c..000000000 --- a/folding-schemes/src/utils/lagrange_poly.rs +++ /dev/null @@ -1,120 +0,0 @@ -use ark_ff::PrimeField; -use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; - -/// Computes the lagrange interpolated polynomial from the given points `p_i` -pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePolynomial { - // domain is 0..p_i.len(), to fit `interpolate_uni_poly` from hyperplonk - let domain: Vec = (0..p_i.len()).collect(); - - // compute l(x), common to every basis polynomial - let mut l_x = DensePolynomial::from_coefficients_vec(vec![F::ONE]); - for x_m in domain.clone() { - let prod_m = DensePolynomial::from_coefficients_vec(vec![-F::from(x_m as u64), F::ONE]); - l_x = &l_x * &prod_m; - } - - // compute each w_j - barycentric weights - let mut w_j_vector: Vec = vec![]; - for x_j in domain.clone() { - let mut w_j = F::ONE; - for x_m in domain.clone() { - if x_m != x_j { - let prod = (F::from(x_j as u64) - F::from(x_m as u64)) - .inverse() - .unwrap(); // an inverse always exists since x_j != x_m (!=0) - // hence, we call unwrap() here without checking the Option's content - w_j *= prod; - } - } - w_j_vector.push(w_j); - } - - // compute each polynomial within the sum L(x) - let mut lagrange_poly = DensePolynomial::from_coefficients_vec(vec![F::ZERO]); - for (j, w_j) in w_j_vector.iter().enumerate() { - let x_j = domain[j]; - let y_j = p_i[j]; - // we multiply by l(x) here, otherwise the below division will not work - deg(0)/deg(d) - let poly_numerator = &(&l_x * (*w_j)) * (y_j); - let poly_denominator = - DensePolynomial::from_coefficients_vec(vec![-F::from(x_j as u64), F::ONE]); - let poly = &poly_numerator / &poly_denominator; - lagrange_poly = &lagrange_poly + &poly; - } - - lagrange_poly -} - -#[cfg(test)] -mod tests { - - use crate::utils::espresso::sum_check::verifier::interpolate_uni_poly; - use crate::utils::lagrange_poly::compute_lagrange_interpolated_poly; - use ark_pallas::Fr; - use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; - use ark_std::UniformRand; - - #[test] - fn test_compute_lagrange_interpolated_poly() { - let mut prng = ark_std::test_rng(); - for degree in 1..30 { - let poly = DensePolynomial::::rand(degree, &mut prng); - // range (which is exclusive) is from 0 to degree + 1, since we need degree + 1 evaluations - let evals = (0..(degree + 1)) - .map(|i| poly.evaluate(&Fr::from(i as u64))) - .collect::>(); - let lagrange_poly = compute_lagrange_interpolated_poly(&evals); - for _ in 0..10 { - let query = Fr::rand(&mut prng); - let lagrange_eval = lagrange_poly.evaluate(&query); - let eval = poly.evaluate(&query); - assert_eq!(eval, lagrange_eval); - assert_eq!(lagrange_poly.degree(), poly.degree()); - } - } - } - - #[test] - fn test_interpolation() { - let mut prng = ark_std::test_rng(); - - // test a polynomial with 20 known points, i.e., with degree 19 - let poly = DensePolynomial::::rand(20 - 1, &mut prng); - let evals = (0..20) - .map(|i| poly.evaluate(&Fr::from(i))) - .collect::>(); - let query = Fr::rand(&mut prng); - - assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); - assert_eq!( - compute_lagrange_interpolated_poly(&evals).evaluate(&query), - interpolate_uni_poly(&evals, query) - ); - - // test a polynomial with 33 known points, i.e., with degree 32 - let poly = DensePolynomial::::rand(33 - 1, &mut prng); - let evals = (0..33) - .map(|i| poly.evaluate(&Fr::from(i))) - .collect::>(); - let query = Fr::rand(&mut prng); - - assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); - assert_eq!( - compute_lagrange_interpolated_poly(&evals).evaluate(&query), - interpolate_uni_poly(&evals, query) - ); - - // test a polynomial with 64 known points, i.e., with degree 63 - let poly = DensePolynomial::::rand(64 - 1, &mut prng); - let evals = (0..64) - .map(|i| poly.evaluate(&Fr::from(i))) - .collect::>(); - let query = Fr::rand(&mut prng); - - assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); - assert_eq!( - compute_lagrange_interpolated_poly(&evals).evaluate(&query), - interpolate_uni_poly(&evals, query) - ); - } -} diff --git a/folding-schemes/src/utils/mle.rs b/folding-schemes/src/utils/mle.rs deleted file mode 100644 index e8a638aaf..000000000 --- a/folding-schemes/src/utils/mle.rs +++ /dev/null @@ -1,189 +0,0 @@ -/// Some basic MLE utilities -use ark_ff::PrimeField; -use ark_poly::{DenseMultilinearExtension, SparseMultilinearExtension}; -use ark_std::log2; - -use super::vec::SparseMatrix; - -/// Pad matrix so that its columns and rows are powers of two -pub fn pad_matrix(m: &SparseMatrix) -> SparseMatrix { - let mut r = m.clone(); - r.n_rows = m.n_rows.next_power_of_two(); - r.n_cols = m.n_cols.next_power_of_two(); - r -} - -/// Returns the dense multilinear extension from the given matrix, without modifying the original -/// matrix. -pub fn matrix_to_dense_mle(matrix: SparseMatrix) -> DenseMultilinearExtension { - let n_vars: usize = (log2(matrix.n_rows) + log2(matrix.n_cols)) as usize; // n_vars = s + s' - - // Matrices might need to get padded before turned into an MLE - let padded_matrix = pad_matrix(&matrix); - - // build dense vector representing the sparse padded matrix - let mut v: Vec = vec![F::zero(); padded_matrix.n_rows * padded_matrix.n_cols]; - for (row_i, row) in padded_matrix.coeffs.iter().enumerate() { - for &(value, col_i) in row.iter() { - v[(padded_matrix.n_cols * row_i) + col_i] = value; - } - } - - // convert the dense vector into a mle - vec_to_dense_mle(n_vars, &v) -} - -/// Takes the n_vars and a dense vector and returns its dense MLE. -pub fn vec_to_dense_mle(n_vars: usize, v: &[F]) -> DenseMultilinearExtension { - let mut v_padded = v.to_owned(); - v_padded.resize(1 << n_vars, F::zero()); - DenseMultilinearExtension::::from_evaluations_vec(n_vars, v_padded) -} - -/// Returns the sparse multilinear extension from the given matrix, without modifying the original -/// matrix. -pub fn matrix_to_mle(m: SparseMatrix) -> SparseMultilinearExtension { - let n_rows = m.n_rows.next_power_of_two(); - let n_cols = m.n_cols.next_power_of_two(); - let n_vars: usize = (log2(n_rows) + log2(n_cols)) as usize; // n_vars = s + s' - - // build the sparse vec representing the sparse matrix - let mut v: Vec<(usize, F)> = Vec::new(); - for (i, row) in m.coeffs.iter().enumerate() { - for (val, j) in row.iter() { - v.push((i * n_cols + j, *val)); - } - } - - // convert the dense vector into a mle - vec_to_mle(n_vars, &v) -} - -/// Takes the n_vars and a sparse vector and returns its sparse MLE. -pub fn vec_to_mle(n_vars: usize, v: &[(usize, F)]) -> SparseMultilinearExtension { - SparseMultilinearExtension::::from_evaluations(n_vars, v) -} - -/// Takes the n_vars and a dense vector and returns its dense MLE. -pub fn dense_vec_to_dense_mle( - n_vars: usize, - v: &[F], -) -> DenseMultilinearExtension { - // Pad to 2^n_vars - let mut v_padded = v.to_owned(); - v_padded.resize(1 << n_vars, F::zero()); - DenseMultilinearExtension::::from_evaluations_vec(n_vars, v_padded) -} - -/// Takes the n_vars and a dense vector and returns its sparse MLE. -pub fn dense_vec_to_mle(n_vars: usize, v: &[F]) -> SparseMultilinearExtension { - let v_sparse = v - .iter() - .enumerate() - .map(|(i, v_i)| (i, *v_i)) - .collect::>(); - SparseMultilinearExtension::::from_evaluations(n_vars, &v_sparse) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - arith::ccs::tests::get_test_z, - utils::multilinear_polynomial::fix_variables, - utils::multilinear_polynomial::tests::fix_last_variables, - utils::{hypercube::BooleanHypercube, vec::tests::to_F_matrix}, - }; - use ark_poly::MultilinearExtension; - use ark_std::Zero; - - use ark_pallas::Fr; - - #[test] - fn test_matrix_to_mle() { - let A = to_F_matrix::(vec![ - vec![2, 3, 4, 4], - vec![4, 11, 14, 14], - vec![2, 8, 17, 17], - vec![420, 4, 2, 0], - ]); - - let A_mle = matrix_to_mle(A); - assert_eq!(A_mle.evaluations.len(), 15); // 15 non-zero elements - assert_eq!(A_mle.num_vars, 4); // 4x4 matrix, thus 2bit x 2bit, thus 2^4=16 evals - - let A = to_F_matrix::(vec![ - vec![2, 3, 4, 4, 1], - vec![4, 11, 14, 14, 2], - vec![2, 8, 17, 17, 3], - vec![420, 4, 2, 0, 4], - vec![420, 4, 2, 0, 5], - ]); - let A_mle = matrix_to_mle(A.clone()); - assert_eq!(A_mle.evaluations.len(), 23); // 23 non-zero elements - assert_eq!(A_mle.num_vars, 6); // 5x5 matrix, thus 3bit x 3bit, thus 2^6=64 evals - - // check that the A_mle evaluated over the boolean hypercube equals the matrix A_i_j values - let bhc = BooleanHypercube::new(A_mle.num_vars); - let A_padded = pad_matrix(&A); - let A_padded_dense = A_padded.to_dense(); - for (i, A_row) in A_padded_dense.iter().enumerate() { - for (j, _) in A_row.iter().enumerate() { - let s_i_j = bhc.at_i(i * A_row.len() + j); - assert_eq!(A_mle.fix_variables(&s_i_j)[0], A_padded_dense[i][j]); - } - } - } - - #[test] - fn test_vec_to_mle() { - let z = get_test_z::(3); - let n_vars = 3; - let z_mle = dense_vec_to_mle(n_vars, &z); - - // check that the z_mle evaluated over the boolean hypercube equals the vec z_i values - let bhc = BooleanHypercube::new(z_mle.num_vars); - for (i, z_i) in z.iter().enumerate() { - let s_i = bhc.at_i(i); - assert_eq!(z_mle.fix_variables(&s_i)[0], z_i.clone()); - } - // for the rest of elements of the boolean hypercube, expect it to evaluate to zero - for i in (z.len())..(1 << z_mle.num_vars) { - let s_i = bhc.at_i(i); - assert_eq!(z_mle.fix_variables(&s_i)[0], Fr::zero()); - } - } - - #[test] - fn test_fix_variables() { - let A = to_F_matrix(vec![ - vec![2, 3, 4, 4], - vec![4, 11, 14, 14], - vec![2, 8, 17, 17], - vec![420, 4, 2, 0], - ]); - - let A_mle = matrix_to_dense_mle(A.clone()); - let A = A.to_dense(); - let bhc = BooleanHypercube::new(2); - for (i, y) in bhc.enumerate() { - // First check that the arkworks and espresso funcs match - let expected_fix_left = A_mle.fix_variables(&y); // try arkworks fix_variables - let fix_left = fix_variables(&A_mle, &y); // try espresso fix_variables - assert_eq!(fix_left, expected_fix_left); - - // Check that fixing first variables pins down a column - // i.e. fixing x to 0 will return the first column - // fixing x to 1 will return the second column etc. - let column_i: Vec = A.clone().iter().map(|x| x[i]).collect(); - assert_eq!(fix_left.evaluations, column_i); - - // Now check that fixing last variables pins down a row - // i.e. fixing y to 0 will return the first row - // fixing y to 1 will return the second row etc. - let row_i: Vec = A[i].clone(); - let fix_right = fix_last_variables(&A_mle, &y); - assert_eq!(fix_right.evaluations, row_i); - } - } -} diff --git a/folding-schemes/src/utils/mod.rs b/folding-schemes/src/utils/mod.rs deleted file mode 100644 index ed9c6f58a..000000000 --- a/folding-schemes/src/utils/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::path::Path; -use std::path::PathBuf; - -use ark_crypto_primitives::sponge::poseidon::PoseidonConfig; -use ark_ec::AffineRepr; -use ark_ff::PrimeField; -use ark_serialize::CanonicalSerialize; -use sha3::{Digest, Sha3_256}; - -use crate::arith::ArithSerializer; -use crate::commitment::CommitmentScheme; -use crate::{Curve, Error}; - -pub mod gadgets; -pub mod hypercube; -pub mod lagrange_poly; -pub mod mle; -pub mod vec; - -// expose espresso local modules -pub mod espresso; -pub use crate::utils::espresso::multilinear_polynomial; -pub use crate::utils::espresso::sum_check; -pub use crate::utils::espresso::virtual_polynomial; - -/// For a given x, returns [1, x^1, x^2, ..., x^n-1]; -pub fn powers_of(x: F, n: usize) -> Vec { - let mut c: Vec = vec![F::zero(); n]; - c[0] = F::one(); - for i in 1..n { - c[i] = c[i - 1] * x; - } - c -} - -/// returns the coordinates of a commitment point. This is compatible with the arkworks -/// GC.to_constraint_field()[..2] -pub fn get_cm_coordinates(cm: &C) -> Vec { - let (cm_x, cm_y) = cm.into_affine().xy().unwrap_or_default(); - vec![cm_x, cm_y] -} - -/// returns the hash of the given public parameters of the Folding Scheme -pub fn pp_hash( - arith: &impl ArithSerializer, - cf_arith: &impl ArithSerializer, - cs_vp: &CS1::VerifierParams, - cf_cs_vp: &CS2::VerifierParams, - poseidon_config: &PoseidonConfig, -) -> Result -where - C1: Curve, - C2: Curve, - CS1: CommitmentScheme, - CS2: CommitmentScheme, -{ - let mut hasher = Sha3_256::new(); - - // Fr & Fq modulus bit size - hasher.update(C1::ScalarField::MODULUS_BIT_SIZE.to_le_bytes()); - hasher.update(C2::ScalarField::MODULUS_BIT_SIZE.to_le_bytes()); - // AugmentedFCircuit Arith params - hasher.update(arith.params_to_le_bytes()); - // CycleFold Circuit Arith params - hasher.update(cf_arith.params_to_le_bytes()); - // cs_vp & cf_cs_vp (commitments setup) - let mut cs_vp_bytes = Vec::new(); - cs_vp.serialize_uncompressed(&mut cs_vp_bytes)?; - hasher.update(cs_vp_bytes); - let mut cf_cs_vp_bytes = Vec::new(); - cf_cs_vp.serialize_uncompressed(&mut cf_cs_vp_bytes)?; - hasher.update(cf_cs_vp_bytes); - // poseidon params - let mut poseidon_config_bytes = Vec::new(); - poseidon_config - .full_rounds - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .partial_rounds - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .alpha - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .ark - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .mds - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .rate - .serialize_uncompressed(&mut poseidon_config_bytes)?; - poseidon_config - .capacity - .serialize_uncompressed(&mut poseidon_config_bytes)?; - hasher.update(poseidon_config_bytes); - - let public_params_hash = hasher.finalize(); - Ok(C1::ScalarField::from_le_bytes_mod_order( - &public_params_hash, - )) -} - -/// Tiny utility enum that allows to import circuits and wasm modules from files by passing their path -/// or passing their content already read. -/// -/// This enum implements the [`From`] trait for both [`Path`], [`PathBuf`] and [`Vec`]. -#[derive(Debug, Clone)] -pub enum PathOrBin { - Path(PathBuf), - Bin(Vec), -} - -impl From<&Path> for PathOrBin { - fn from(value: &Path) -> Self { - PathOrBin::Path(value.into()) - } -} - -impl From for PathOrBin { - fn from(value: PathBuf) -> Self { - PathOrBin::Path(value) - } -} - -impl From> for PathOrBin { - fn from(value: Vec) -> Self { - PathOrBin::Bin(value) - } -} diff --git a/folding-schemes/src/utils/vec.rs b/folding-schemes/src/utils/vec.rs deleted file mode 100644 index 9c4938333..000000000 --- a/folding-schemes/src/utils/vec.rs +++ /dev/null @@ -1,252 +0,0 @@ -use ark_ff::PrimeField; -use ark_poly::{ - univariate::DensePolynomial, EvaluationDomain, Evaluations, GeneralEvaluationDomain, -}; -pub use ark_relations::gr1cs::Matrix as R1CSMatrix; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::cfg_iter; -use ark_std::rand::Rng; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; - -use crate::{folding::traits::Dummy, Error}; - -#[derive(Clone, Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct SparseMatrix { - pub n_rows: usize, - pub n_cols: usize, - /// coeffs = R1CSMatrix = Vec>, which contains each row and the F is the value - /// of the coefficient and the usize indicates the column position - pub coeffs: R1CSMatrix, -} - -impl Dummy<(usize, usize)> for SparseMatrix { - fn dummy((n_rows, n_cols): (usize, usize)) -> Self { - Self { - n_rows, - n_cols, - // unnecessary to allocate each row as the matrix is sparse - coeffs: vec![vec![]; n_rows], - } - } -} - -impl SparseMatrix { - pub fn empty() -> Self { - Self::dummy((0, 0)) - } - - pub fn rand(rng: &mut R, n_rows: usize, n_cols: usize) -> Self { - const ZERO_VAL_PROBABILITY: f64 = 0.8f64; - - let dense = (0..n_rows) - .map(|_| { - (0..n_cols) - .map(|_| { - if !rng.gen_bool(ZERO_VAL_PROBABILITY) { - return F::rand(rng); - } - F::zero() - }) - .collect::>() - }) - .collect::>>(); - dense_matrix_to_sparse(dense) - } - pub fn to_dense(&self) -> Vec> { - let mut r: Vec> = vec![vec![F::zero(); self.n_cols]; self.n_rows]; - for (row_i, row) in self.coeffs.iter().enumerate() { - for &(value, col_i) in row.iter() { - r[row_i][col_i] = value; - } - } - r - } -} - -pub fn dense_matrix_to_sparse(m: Vec>) -> SparseMatrix { - let mut r = SparseMatrix:: { - n_rows: m.len(), - n_cols: m[0].len(), - coeffs: Vec::new(), - }; - for m_row in m.iter() { - let mut row: Vec<(F, usize)> = Vec::new(); - for (col_i, value) in m_row.iter().enumerate() { - if !value.is_zero() { - row.push((*value, col_i)); - } - } - r.coeffs.push(row); - } - r -} - -pub fn vec_add(a: &[F], b: &[F]) -> Result, Error> { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a.len()".to_string(), - a.len(), - "b.len()".to_string(), - b.len(), - )); - } - Ok(cfg_iter!(a).zip(b).map(|(x, y)| *x + y).collect()) -} - -pub fn vec_sub(a: &[F], b: &[F]) -> Result, Error> { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a.len()".to_string(), - a.len(), - "b.len()".to_string(), - b.len(), - )); - } - Ok(cfg_iter!(a).zip(b).map(|(x, y)| *x - y).collect()) -} - -pub fn vec_scalar_mul(vec: &[F], c: &F) -> Vec { - cfg_iter!(vec).map(|a| *a * c).collect() -} - -pub fn is_zero_vec(vec: &[F]) -> bool { - cfg_iter!(vec).all(|a| a.is_zero()) -} - -pub fn mat_vec_mul_dense(M: &[Vec], z: &[F]) -> Result, Error> { - if M.is_empty() { - return Err(Error::Empty); - } - if M[0].len() != z.len() { - return Err(Error::NotSameLength( - "M[0].len()".to_string(), - M[0].len(), - "z.len()".to_string(), - z.len(), - )); - } - - Ok(cfg_iter!(M) - .map(|row| row.iter().zip(z).map(|(a, b)| *a * b).sum()) - .collect()) -} - -pub fn mat_vec_mul(M: &SparseMatrix, z: &[F]) -> Result, Error> { - if M.n_cols != z.len() { - return Err(Error::NotSameLength( - "M.n_cols".to_string(), - M.n_cols, - "z.len()".to_string(), - z.len(), - )); - } - Ok(cfg_iter!(M.coeffs) - .map(|row| row.iter().map(|(value, col_i)| *value * z[*col_i]).sum()) - .collect()) -} - -pub fn mat_from_str_mat(str_mat: Vec>) -> Result>, Error> { - str_mat - .into_iter() - .map(|row| { - row.into_iter() - .map(|s| { - F::from_str(s).map_err(|_| Error::Other("Invalid decimal string".to_string())) - }) - .collect() - }) - .collect() -} - -pub fn hadamard(a: &[F], b: &[F]) -> Result, Error> { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a.len()".to_string(), - a.len(), - "b.len()".to_string(), - b.len(), - )); - } - Ok(cfg_iter!(a).zip(b).map(|(a, b)| *a * b).collect()) -} - -/// returns the interpolated polynomial of degree=v.len().next_power_of_two(), which passes through all -/// the given elements of v. -pub fn poly_from_vec(v: Vec) -> Result, Error> { - let D = GeneralEvaluationDomain::::new(v.len()).ok_or(Error::NewDomainFail)?; - Ok(Evaluations::from_vec_and_domain(v, D).interpolate()) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_pallas::Fr; - - pub fn to_F_matrix(M: Vec>) -> SparseMatrix { - dense_matrix_to_sparse(to_F_dense_matrix(M)) - } - pub fn to_F_dense_matrix(M: Vec>) -> Vec> { - M.iter() - .map(|m| m.iter().map(|r| F::from(*r as u64)).collect()) - .collect() - } - pub fn to_F_vec(z: Vec) -> Vec { - z.iter().map(|c| F::from(*c as u64)).collect() - } - - #[test] - fn test_dense_sparse_conversions() { - let A = to_F_dense_matrix::(vec![ - vec![0, 1, 0, 0, 0, 0], - vec![0, 0, 0, 1, 0, 0], - vec![0, 1, 0, 0, 1, 0], - vec![5, 0, 0, 0, 0, 1], - ]); - let A_sparse = dense_matrix_to_sparse(A.clone()); - assert_eq!(A_sparse.to_dense(), A); - } - - // test mat_vec_mul & mat_vec_mul_sparse - #[test] - fn test_mat_vec_mul() -> Result<(), Error> { - let A = to_F_matrix::(vec![ - vec![0, 1, 0, 0, 0, 0], - vec![0, 0, 0, 1, 0, 0], - vec![0, 1, 0, 0, 1, 0], - vec![5, 0, 0, 0, 0, 1], - ]) - .to_dense(); - let z = to_F_vec(vec![1, 3, 35, 9, 27, 30]); - assert_eq!(mat_vec_mul_dense(&A, &z)?, to_F_vec(vec![3, 9, 30, 35])); - assert_eq!( - mat_vec_mul(&dense_matrix_to_sparse(A), &z)?, - to_F_vec(vec![3, 9, 30, 35]) - ); - - let A = to_F_matrix::(vec![vec![2, 3, 4, 5], vec![4, 8, 12, 14], vec![9, 8, 7, 6]]); - let v = to_F_vec(vec![19, 55, 50, 3]); - - assert_eq!( - mat_vec_mul_dense(&A.to_dense(), &v)?, - to_F_vec(vec![418, 1158, 979]) - ); - assert_eq!(mat_vec_mul(&A, &v)?, to_F_vec(vec![418, 1158, 979])); - Ok(()) - } - - #[test] - fn test_hadamard_product() -> Result<(), Error> { - let a = to_F_vec::(vec![1, 2, 3, 4, 5, 6]); - let b = to_F_vec(vec![7, 8, 9, 10, 11, 12]); - assert_eq!(hadamard(&a, &b)?, to_F_vec(vec![7, 16, 27, 40, 55, 72])); - Ok(()) - } - - #[test] - fn test_vec_add() -> Result<(), Error> { - let a: Vec = to_F_vec::(vec![1, 2, 3, 4, 5, 6]); - let b: Vec = to_F_vec(vec![7, 8, 9, 10, 11, 12]); - assert_eq!(vec_add(&a, &b)?, to_F_vec(vec![8, 10, 12, 14, 16, 18])); - Ok(()) - } -} diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 59be59214..000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -1.88.0 diff --git a/solidity-verifiers/Cargo.toml b/solidity-verifiers/Cargo.toml deleted file mode 100644 index eccecef24..000000000 --- a/solidity-verifiers/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "solidity-verifiers" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-ec = { workspace = true } -ark-ff = { workspace = true } -ark-groth16 = { workspace = true } -ark-bn254 = { workspace = true, features = ["r1cs"] } -ark-poly-commit = { workspace = true } -ark-serialize = { workspace = true } -askama = { workspace = true, features = ["config"] } -revm = { workspace = true, features = ["std"] } -rust-crypto = { workspace = true } -num-bigint = { workspace = true } -folding-schemes = { workspace = true } # without 'light-test' enabled - -[dev-dependencies] -ark-ec = { workspace = true, features = ["parallel"] } -ark-ff = { workspace = true, features = ["parallel", "asm"] } -ark-std = { workspace = true, features = ["parallel"] } -ark-crypto-primitives = { workspace = true, features = ["sponge", "parallel"] } -ark-snark = { workspace = true } -ark-relations = { workspace = true } -ark-r1cs-std = { workspace = true, features = ["parallel"] } -ark-grumpkin = { workspace = true, features = ["r1cs"] } -folding-schemes = { workspace = true, features = ["light-test"] } -experimental-frontends = { workspace = true } -noname = { workspace = true } - -[features] -default = ["parallel"] - -parallel = [ - "ark-groth16/parallel", - "ark-poly-commit/parallel", - "folding-schemes/parallel", -] - -[[example]] -name = "full_flow" -path = "../examples/full_flow.rs" - -[[example]] -name = "circom_full_flow" -path = "../examples/circom_full_flow.rs" - -[[example]] -name = "noname_full_flow" -path = "../examples/noname_full_flow.rs" - -[[example]] -name = "noir_full_flow" -path = "../examples/noir_full_flow.rs" - diff --git a/solidity-verifiers/README.md b/solidity-verifiers/README.md deleted file mode 100644 index eb28bfd7d..000000000 --- a/solidity-verifiers/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# `solidity-verifiers` - -This crate implements templating logic to output verifier contracts for `sonobe`-generated decider proofs. -This crate is accompanied by the [cli](https://github.com/privacy-scaling-explorations/sonobe/tree/main/cli) crate, which allows to generate the Solidity contracts from the command line. - -To run the tests it needs [solc](https://docs.soliditylang.org/en/latest/installing-solidity.html) installed. diff --git a/solidity-verifiers/askama.toml b/solidity-verifiers/askama.toml deleted file mode 100644 index c596edf82..000000000 --- a/solidity-verifiers/askama.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[escaper]] -path = "askama::Text" -extensions = ["sol"] \ No newline at end of file diff --git a/solidity-verifiers/src/calldata.rs b/solidity-verifiers/src/calldata.rs deleted file mode 100644 index f3ea66901..000000000 --- a/solidity-verifiers/src/calldata.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::utils::eth::ToEth; -use ark_bn254::Bn254; -use ark_groth16::Groth16; -use crypto::digest::Digest; -use crypto::sha3::Sha3; -use folding_schemes::commitment::kzg::KZG; -use folding_schemes::folding::nova::decider_eth::Proof; -use folding_schemes::folding::nova::CommittedInstance; -use folding_schemes::Error; -use num_bigint::BigUint; - -/// Specifies which API to use for a proof verification in a contract. -#[derive(Copy, Clone, Debug, Default)] -pub enum NovaVerificationMode { - /// Use the `verifyNovaProof` function. - #[default] - Explicit, - /// Use the `verifyOpaqueNovaProof` function. - Opaque, - /// Use the `verifyOpaqueNovaProofWithInputs` function. - OpaqueWithInputs, -} - -/// Formats call data from a vec of bytes to a hashmap -/// Useful for debugging directly on the EVM -/// !! Should follow the contract's function signature, we assume the order of arguments is correct -pub fn get_formatted_calldata(calldata: Vec) -> Vec { - let mut formatted_calldata = vec![]; - for i in (4..calldata.len()).step_by(32) { - let val = BigUint::from_bytes_be(&calldata[i..i + 32]); - formatted_calldata.push(format!("{val}")); - } - formatted_calldata -} - -/// Prepares solidity calldata for calling the NovaDecider contract -pub fn prepare_calldata_for_nova_cyclefold_verifier( - verification_mode: NovaVerificationMode, - i: ark_bn254::Fr, - z_0: Vec, - z_i: Vec, - running_instance: &CommittedInstance, - incoming_instance: &CommittedInstance, - proof: &Proof, Groth16>, -) -> Result, Error> { - let selector = get_function_selector(verification_mode, z_0.len()); - - Ok([ - selector.to_eth(), - i.to_eth(), // i - z_0.to_eth(), // z_0 - z_i.to_eth(), // z_i - running_instance.cmW.to_eth(), - running_instance.cmE.to_eth(), - incoming_instance.cmW.to_eth(), - proof.cmT().to_eth(), // cmT - proof.r().to_eth(), // r - proof.snark_proof().to_eth(), // pA, pB, pC - proof.kzg_challenges().to_eth(), // challenge_W, challenge_E - proof.kzg_proofs()[0].eval.to_eth(), // eval W - proof.kzg_proofs()[1].eval.to_eth(), // eval E - proof.kzg_proofs()[0].proof.to_eth(), // W kzg_proof - proof.kzg_proofs()[1].proof.to_eth(), // E kzg_proof - ] - .concat()) -} - -/// Computes the function selector for the nova cyclefold verifier. -/// It is computed on the fly since it depends on the IVC state length. -fn get_function_selector(mode: NovaVerificationMode, state_len: usize) -> [u8; 4] { - let fn_sig = match mode { - NovaVerificationMode::Explicit => - format!( - "verifyNovaProof(uint256[{}],uint256[4],uint256[2],uint256[3],uint256[2],uint256[2][2],uint256[2],uint256[4],uint256[2][2])", - state_len * 2 + 1 - ), - NovaVerificationMode::Opaque => - format!("verifyOpaqueNovaProof(uint256[{}])", 26 + 2 * state_len), - NovaVerificationMode::OpaqueWithInputs => - format!("verifyOpaqueNovaProofWithInputs(uint256,uint256[{state_len}],uint256[{state_len}],uint256[25])"), - }; - - let mut hasher = Sha3::keccak256(); - hasher.input_str(&fn_sig); - let hash = &mut [0u8; 32]; - hasher.result(hash); - [hash[0], hash[1], hash[2], hash[3]] -} diff --git a/solidity-verifiers/src/evm.rs b/solidity-verifiers/src/evm.rs deleted file mode 100644 index 04441b38c..000000000 --- a/solidity-verifiers/src/evm.rs +++ /dev/null @@ -1,163 +0,0 @@ -pub use revm; -use revm::{ - primitives::{hex, Address, ExecutionResult, Output, TransactTo, TxEnv}, - Evm as EVM, EvmBuilder, InMemoryDB, -}; -use std::{ - fmt::Debug, - fs::{self, create_dir_all, File}, - io::{self, Write}, - path::PathBuf, - process::{Command, Stdio}, - str, -}; - -// from: https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/blob/85cb77b171ce3ee493628007c7a1cfae2ea878e6/examples/separately.rs#L56 -pub fn save_solidity(name: impl AsRef, solidity: &str) { - let curdir = PathBuf::from("."); - let curdir_abs_path = fs::canonicalize(curdir).expect("Failed to get current directory"); - let curdir_abs_path = curdir_abs_path - .to_str() - .expect("Failed to convert path to string"); - let dir_generated = format!("{curdir_abs_path}/generated"); - create_dir_all(dir_generated.clone()).unwrap(); - File::create(format!("{}/{}", dir_generated, name.as_ref())) - .unwrap() - .write_all(solidity.as_bytes()) - .unwrap(); -} - -/// Compile solidity with `--via-ir` flag, then return creation bytecode. -/// -/// # Panics -/// Panics if executable `solc` can not be found, or compilation fails. -pub fn compile_solidity(solidity: impl AsRef<[u8]>, contract_name: &str) -> Vec { - let mut process = match Command::new("solc") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .arg("--bin") - .arg("--optimize") - .arg("-") - .spawn() - { - Ok(process) => process, - Err(err) if err.kind() == io::ErrorKind::NotFound => { - panic!("Command 'solc' not found"); - } - Err(err) => { - panic!("Failed to spawn process with command 'solc':\n{err}"); - } - }; - process - .stdin - .take() - .unwrap() - .write_all(solidity.as_ref()) - .unwrap(); - let output = process.wait_with_output().unwrap(); - let stdout = str::from_utf8(&output.stdout).unwrap(); - if let Some(binary) = find_binary(stdout, contract_name) { - binary - } else { - panic!( - "Compilation fails:\n{}", - str::from_utf8(&output.stderr).unwrap() - ) - } -} - -/// Find binary from `stdout` with given `contract_name`. -/// `contract_name` is provided since `solc` may compile multiple contracts or libraries. -/// hence, we need to find the correct binary. -fn find_binary(stdout: &str, contract_name: &str) -> Option> { - let intro_str = format!("======= :{contract_name} =======\nBinary:\n"); - let start = stdout.find(&intro_str)?; - let end = stdout[start + intro_str.len()..] - .find('\n') - .map(|pos| pos + start + intro_str.len()) - .unwrap_or(stdout.len()); - let binary_section = stdout[start + intro_str.len()..end].trim(); - Some(hex::decode(binary_section).unwrap()) -} - -/// Evm runner. -#[derive(Debug)] -pub struct Evm<'a> { - evm: EVM<'a, (), InMemoryDB>, -} - -impl<'a> Default for Evm<'a> { - fn default() -> Self { - Self { - evm: EvmBuilder::default().with_db(InMemoryDB::default()).build(), - } - } -} - -impl<'a> Evm<'a> { - /// Apply create transaction with given `bytecode` as creation bytecode. - /// Return created `address`. - /// - /// # Panics - /// Panics if execution reverts or halts unexpectedly. - pub fn create(&mut self, bytecode: Vec) -> Address { - let (_, output) = self.transact_success_or_panic(TxEnv { - gas_limit: u64::MAX, - transact_to: TransactTo::Create, - data: bytecode.into(), - ..Default::default() - }); - match output { - Output::Create(_, Some(address)) => address, - _ => unreachable!(), - } - } - - /// Apply call transaction to given `address` with `calldata`. - /// Returns `gas_used` and `return_data`. - /// - /// # Panics - /// Panics if execution reverts or halts unexpectedly. - pub fn call(&mut self, address: Address, calldata: Vec) -> (u64, Vec) { - let (gas_used, output) = self.transact_success_or_panic(TxEnv { - gas_limit: u64::MAX, - transact_to: TransactTo::Call(address), - data: calldata.into(), - ..Default::default() - }); - match output { - Output::Call(output) => (gas_used, output.into()), - _ => unreachable!(), - } - } - - fn transact_success_or_panic(&mut self, tx: TxEnv) -> (u64, Output) { - *self.evm.tx_mut() = tx; - let result = self.evm.transact_commit().unwrap(); - match result { - ExecutionResult::Success { - gas_used, - output, - logs, - .. - } => { - if !logs.is_empty() { - println!("--- logs from {} ---", logs[0].address); - for (log_idx, log) in logs.iter().enumerate() { - println!("log#{log_idx}"); - for (topic_idx, topic) in log.topics().iter().enumerate() { - println!(" topic{topic_idx}: {topic:?}"); - } - } - println!("--- end ---"); - } - (gas_used, output) - } - ExecutionResult::Revert { gas_used, output } => (gas_used, Output::Call(output)), - ExecutionResult::Halt { reason, gas_used } => panic!( - "Transaction halts unexpectedly with gas_used {gas_used} and reason {reason:?}" - ), - } - } -} diff --git a/solidity-verifiers/src/lib.rs b/solidity-verifiers/src/lib.rs deleted file mode 100644 index 2e831eb04..000000000 --- a/solidity-verifiers/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod calldata; -pub mod evm; -pub mod utils; -pub mod verifiers; - -pub use verifiers::*; -pub use verifiers::{ - get_decider_template_for_cyclefold_decider, Groth16VerifierKey, KZG10VerifierKey, - NovaCycleFoldVerifierKey, ProtocolVerifierKey, -}; diff --git a/solidity-verifiers/src/utils/encoding.rs b/solidity-verifiers/src/utils/encoding.rs deleted file mode 100644 index c9f7b486a..000000000 --- a/solidity-verifiers/src/utils/encoding.rs +++ /dev/null @@ -1,43 +0,0 @@ -/// Defines encodings of G1 and G2 elements for use in Solidity templates. -use ark_bn254::{Fq, G1Affine, G2Affine}; -use std::fmt::{self, Display}; - -#[derive(Debug, Default)] -pub struct FqWrapper(pub Fq); - -impl Display for FqWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Default)] -pub struct G1Repr(pub [FqWrapper; 2]); - -impl Display for G1Repr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:#?}", self.0) - } -} - -/// Converts a G1 element to a representation that can be used in Solidity templates. -pub fn g1_to_fq_repr(g1: G1Affine) -> G1Repr { - G1Repr([FqWrapper(g1.x), FqWrapper(g1.y)]) -} - -#[derive(Debug, Default)] -pub struct G2Repr(pub [[FqWrapper; 2]; 2]); - -impl Display for G2Repr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:#?}", self.0) - } -} - -/// Converts a G2 element to a representation that can be used in Solidity templates. -pub fn g2_to_fq_repr(g2: G2Affine) -> G2Repr { - G2Repr([ - [FqWrapper(g2.x.c0), FqWrapper(g2.x.c1)], - [FqWrapper(g2.y.c0), FqWrapper(g2.y.c1)], - ]) -} diff --git a/solidity-verifiers/src/utils/eth.rs b/solidity-verifiers/src/utils/eth.rs deleted file mode 100644 index 765d493f1..000000000 --- a/solidity-verifiers/src/utils/eth.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! This module provides a trait and implementations for converting Rust types -//! to EVM calldata. -use ark_ec::{ - pairing::Pairing, - short_weierstrass::{Affine, Projective, SWCurveConfig}, - AffineRepr, CurveGroup, -}; -use ark_ff::{BigInteger, Fp, Fp2, Fp2Config, FpConfig, PrimeField}; -use ark_groth16::Proof; - -pub trait ToEth { - fn to_eth(&self) -> Vec; -} - -impl ToEth for [T] { - fn to_eth(&self) -> Vec { - self.iter().flat_map(ToEth::to_eth).collect() - } -} - -impl ToEth for u8 { - fn to_eth(&self) -> Vec { - vec![*self] - } -} - -impl, const N: usize> ToEth for Fp { - fn to_eth(&self) -> Vec { - self.into_bigint().to_bytes_be() - } -} - -impl> ToEth for Fp2

{ - fn to_eth(&self) -> Vec { - [self.c1.to_eth(), self.c0.to_eth()].concat() - } -} - -impl> ToEth for Affine

{ - fn to_eth(&self) -> Vec { - // the encoding of the additive identity is [0, 0] on the EVM - let (x, y) = self.xy().unwrap_or_default(); - - [x.to_eth(), y.to_eth()].concat() - } -} - -impl> ToEth for Projective

{ - fn to_eth(&self) -> Vec { - self.into_affine().to_eth() - } -} - -impl> ToEth for Proof { - fn to_eth(&self) -> Vec { - [self.a.to_eth(), self.b.to_eth(), self.c.to_eth()].concat() - } -} diff --git a/solidity-verifiers/src/utils/mod.rs b/solidity-verifiers/src/utils/mod.rs deleted file mode 100644 index cd7c3a716..000000000 --- a/solidity-verifiers/src/utils/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::{GPL3_SDPX_IDENTIFIER, PRAGMA_GROTH16_VERIFIER}; -use askama::Template; - -pub mod encoding; -pub mod eth; - -#[derive(Template)] -#[template(path = "header_template.askama.sol", ext = "sol")] -pub struct HeaderInclusion { - /// SPDX-License-Identifier - pub sdpx: String, - /// The `pragma` statement. - pub pragma_version: String, - /// The template to render alongside the header. - pub template: T, -} - -impl HeaderInclusion { - pub fn builder() -> HeaderInclusionBuilder { - HeaderInclusionBuilder::default() - } -} - -#[derive(Debug)] -pub struct HeaderInclusionBuilder { - /// SPDX-License-Identifier - sdpx: String, - /// The `pragma` statement. - pragma_version: String, - /// The template to render alongside the header. - template: T, -} - -impl Default for HeaderInclusionBuilder { - fn default() -> Self { - Self { - sdpx: GPL3_SDPX_IDENTIFIER.to_string(), - pragma_version: PRAGMA_GROTH16_VERIFIER.to_string(), - template: T::default(), - } - } -} - -impl HeaderInclusionBuilder { - pub fn sdpx>(mut self, sdpx: S) -> Self { - self.sdpx = sdpx.into(); - self - } - - pub fn pragma_version>(mut self, pragma_version: S) -> Self { - self.pragma_version = pragma_version.into(); - self - } - - pub fn template(mut self, template: impl Into) -> Self { - self.template = template.into(); - self - } - - pub fn build(self) -> HeaderInclusion { - HeaderInclusion { - sdpx: self.sdpx, - pragma_version: self.pragma_version, - template: self.template, - } - } -} diff --git a/solidity-verifiers/src/verifiers/g16.rs b/solidity-verifiers/src/verifiers/g16.rs deleted file mode 100644 index ae38e8dde..000000000 --- a/solidity-verifiers/src/verifiers/g16.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::utils::encoding::{g1_to_fq_repr, g2_to_fq_repr}; -use crate::utils::encoding::{G1Repr, G2Repr}; -use crate::utils::HeaderInclusion; -use crate::{ProtocolVerifierKey, GPL3_SDPX_IDENTIFIER}; -use ark_bn254::Bn254; -use ark_groth16::VerifyingKey as ArkVerifyingKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use askama::Template; - -use super::PRAGMA_GROTH16_VERIFIER; - -#[derive(Template, Default)] -#[template(path = "groth16_verifier.askama.sol", ext = "sol")] -pub struct Groth16Verifier { - /// The `alpha * G`, where `G` is the generator of `G1`. - pub vkey_alpha_g1: G1Repr, - /// The `alpha * H`, where `H` is the generator of `G2`. - pub vkey_beta_g2: G2Repr, - /// The `gamma * H`, where `H` is the generator of `G2`. - pub vkey_gamma_g2: G2Repr, - /// The `delta * H`, where `H` is the generator of `G2`. - pub vkey_delta_g2: G2Repr, - /// Length of the `gamma_abc_g1` vector. - pub gamma_abc_len: usize, - /// The `gamma^{-1} * (beta * a_i + alpha * b_i + c_i) * H`, where `H` is the generator of `E::G1`. - pub gamma_abc_g1: Vec, -} - -impl From for Groth16Verifier { - fn from(g16_vk: Groth16VerifierKey) -> Self { - Self { - vkey_alpha_g1: g1_to_fq_repr(g16_vk.0.alpha_g1), - vkey_beta_g2: g2_to_fq_repr(g16_vk.0.beta_g2), - vkey_gamma_g2: g2_to_fq_repr(g16_vk.0.gamma_g2), - vkey_delta_g2: g2_to_fq_repr(g16_vk.0.delta_g2), - gamma_abc_len: g16_vk.0.gamma_abc_g1.len(), - gamma_abc_g1: g16_vk - .0 - .gamma_abc_g1 - .iter() - .copied() - .map(g1_to_fq_repr) - .collect(), - } - } -} - -// Ideally this would be linked to the `Decider` trait in FoldingSchemes. -// For now, this is the easiest as NovaCycleFold isn't clear target from where we can get all it's needed arguments. -#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)] -pub struct Groth16VerifierKey(pub(crate) ArkVerifyingKey); - -impl From> for Groth16VerifierKey { - fn from(value: ArkVerifyingKey) -> Self { - Self(value) - } -} - -impl ProtocolVerifierKey for Groth16VerifierKey { - const PROTOCOL_NAME: &'static str = "Groth16"; - - fn render_as_template(self, pragma: Option) -> Vec { - HeaderInclusion::::builder() - .sdpx(GPL3_SDPX_IDENTIFIER.to_string()) - .pragma_version(pragma.unwrap_or(PRAGMA_GROTH16_VERIFIER.to_string())) - .template(self) - .build() - .render() - .unwrap() - .into_bytes() - } -} - -#[cfg(test)] -mod tests { - use super::Groth16VerifierKey; - use crate::{ - evm::{compile_solidity, save_solidity, Evm}, - ProtocolVerifierKey, - }; - use ark_bn254::{Bn254, Fr}; - use ark_ec::AffineRepr; - use ark_ff::{BigInt, BigInteger, PrimeField}; - use ark_groth16::Groth16; - use ark_snark::SNARK; - use ark_std::rand::{RngCore, SeedableRng}; - use ark_std::test_rng; - use askama::Template; - - use super::Groth16Verifier; - use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN}; - - pub const FUNCTION_SELECTOR_GROTH16_VERIFY_PROOF: [u8; 4] = [0x43, 0x75, 0x3b, 0x4d]; - - #[test] - fn groth16_vk_serde_roundtrip() { - let (_, _, _, _, vk, _) = setup(DEFAULT_SETUP_LEN); - - let g16_vk = Groth16VerifierKey::from(vk); - let mut bytes = vec![]; - g16_vk.serialize_protocol_verifier_key(&mut bytes).unwrap(); - let obtained_g16_vk = - Groth16VerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap(); - - assert_eq!(g16_vk, obtained_g16_vk) - } - - #[test] - fn test_groth16_verifier_accepts_and_rejects_proofs() { - let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); - let (_, _, _, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN); - let g16_vk = Groth16VerifierKey::from(g16_vk); - - let proof = Groth16::::prove(&g16_pk, circuit, &mut rng).unwrap(); - let res = Groth16Verifier::from(g16_vk).render().unwrap(); - save_solidity("groth16_verifier.sol", &res); - let groth16_verifier_bytecode = compile_solidity(&res, "Groth16Verifier"); - let mut evm = Evm::default(); - let verifier_address = evm.create(groth16_verifier_bytecode); - let (a_x, a_y) = proof.a.xy().unwrap(); - let (b_x, b_y) = proof.b.xy().unwrap(); - let (c_x, c_y) = proof.c.xy().unwrap(); - let mut calldata: Vec = [ - &FUNCTION_SELECTOR_GROTH16_VERIFY_PROOF[..], - &a_x.into_bigint().to_bytes_be(), - &a_y.into_bigint().to_bytes_be(), - &b_x.c1.into_bigint().to_bytes_be(), - &b_x.c0.into_bigint().to_bytes_be(), - &b_y.c1.into_bigint().to_bytes_be(), - &b_y.c0.into_bigint().to_bytes_be(), - &c_x.into_bigint().to_bytes_be(), - &c_y.into_bigint().to_bytes_be(), - &BigInt::from(Fr::from(circuit.z)).to_bytes_be(), - ] - .concat(); - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // change calldata to make it invalid - let last_calldata_element = calldata.last_mut().unwrap(); - *last_calldata_element = 0; - let (_, output) = evm.call(verifier_address, calldata); - assert_eq!(*output.last().unwrap(), 0); - } -} diff --git a/solidity-verifiers/src/verifiers/kzg.rs b/solidity-verifiers/src/verifiers/kzg.rs deleted file mode 100644 index 808ac9650..000000000 --- a/solidity-verifiers/src/verifiers/kzg.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::utils::encoding::{g1_to_fq_repr, g2_to_fq_repr}; -use crate::utils::encoding::{G1Repr, G2Repr}; -use crate::utils::HeaderInclusion; -use crate::{ProtocolVerifierKey, MIT_SDPX_IDENTIFIER}; -use ark_bn254::{Bn254, G1Affine}; -use ark_poly_commit::kzg10::VerifierKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use askama::Template; - -use super::PRAGMA_KZG10_VERIFIER; - -#[derive(Template, Default)] -#[template(path = "kzg10_verifier.askama.sol", ext = "sol")] -pub struct KZG10Verifier { - /// The generator of `G1`. - pub(crate) g1: G1Repr, - /// The generator of `G2`. - pub(crate) g2: G2Repr, - /// The verification key - pub(crate) vk: G2Repr, - /// Length of the trusted setup vector. - pub(crate) g1_crs_len: usize, - /// The trusted setup vector. - pub(crate) g1_crs: Vec, -} - -impl From for KZG10Verifier { - fn from(data: KZG10VerifierKey) -> Self { - Self { - g1: g1_to_fq_repr(data.vk.g), - g2: g2_to_fq_repr(data.vk.h), - vk: g2_to_fq_repr(data.vk.beta_h), - g1_crs_len: data.g1_crs_batch_points.len(), - g1_crs: data - .g1_crs_batch_points - .iter() - .map(|g1| g1_to_fq_repr(*g1)) - .collect(), - } - } -} - -#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)] -pub struct KZG10VerifierKey { - pub vk: VerifierKey, - pub g1_crs_batch_points: Vec, -} - -impl From<(VerifierKey, Vec)> for KZG10VerifierKey { - fn from(value: (VerifierKey, Vec)) -> Self { - Self { - vk: value.0, - g1_crs_batch_points: value.1, - } - } -} - -impl ProtocolVerifierKey for KZG10VerifierKey { - const PROTOCOL_NAME: &'static str = "KZG"; - - fn render_as_template(self, pragma: Option) -> Vec { - HeaderInclusion::::builder() - .sdpx(MIT_SDPX_IDENTIFIER.to_string()) - .pragma_version(pragma.unwrap_or(PRAGMA_KZG10_VERIFIER.to_string())) - .template(self) - .build() - .render() - .unwrap() - .into_bytes() - } -} - -#[cfg(test)] -mod tests { - use super::KZG10VerifierKey; - use crate::{ - evm::{compile_solidity, Evm}, - utils::HeaderInclusion, - ProtocolVerifierKey, - }; - use ark_bn254::{Bn254, Fr}; - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_ec::{AffineRepr, CurveGroup}; - use ark_ff::{BigInteger, PrimeField}; - use ark_std::rand::{RngCore, SeedableRng}; - use ark_std::Zero; - use ark_std::{test_rng, UniformRand}; - use askama::Template; - - use folding_schemes::{ - commitment::{kzg::KZG, CommitmentScheme}, - transcript::{poseidon::poseidon_canonical_config, Transcript}, - }; - - use super::KZG10Verifier; - use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN}; - - const FUNCTION_SELECTOR_KZG10_CHECK: [u8; 4] = [0x9e, 0x78, 0xcc, 0xf7]; - - #[test] - fn kzg_vk_serde_roundtrip() { - let (_, pk, vk, _, _, _) = setup(DEFAULT_SETUP_LEN); - - let kzg_vk = KZG10VerifierKey::from((vk, pk.powers_of_g[0..3].to_vec())); - let mut bytes = vec![]; - kzg_vk.serialize_protocol_verifier_key(&mut bytes).unwrap(); - let obtained_kzg_vk = - KZG10VerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap(); - - assert_eq!(kzg_vk, obtained_kzg_vk) - } - - #[test] - fn kzg_verifier_compiles() { - let (_, kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN); - let kzg_vk = KZG10VerifierKey::from((kzg_vk.clone(), kzg_pk.powers_of_g[0..3].to_vec())); - - let res = HeaderInclusion::::builder() - .template(kzg_vk) - .build() - .render() - .unwrap(); - - let kzg_verifier_bytecode = compile_solidity(res, "KZG10Verifier"); - let mut evm = Evm::default(); - _ = evm.create(kzg_verifier_bytecode); - } - - #[test] - fn kzg_verifier_accepts_and_rejects_proofs() { - let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); - let poseidon_config = poseidon_canonical_config::(); - let transcript_p = &mut PoseidonSponge::::new(&poseidon_config); - let transcript_v = &mut PoseidonSponge::::new(&poseidon_config); - - let (_, kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN); - let kzg_vk = KZG10VerifierKey::from((kzg_vk.clone(), kzg_pk.powers_of_g[0..3].to_vec())); - - let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) - .take(DEFAULT_SETUP_LEN) - .collect(); - let cm = KZG::::commit(&kzg_pk, &v, &Fr::zero()).unwrap(); - let proof = KZG::::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap(); - let template = HeaderInclusion::::builder() - .template(kzg_vk) - .build() - .render() - .unwrap(); - - let kzg_verifier_bytecode = compile_solidity(template, "KZG10Verifier"); - let mut evm = Evm::default(); - let verifier_address = evm.create(kzg_verifier_bytecode); - - let (cm_affine, proof_affine) = (cm.into_affine(), proof.proof.into_affine()); - let (x_comm, y_comm) = cm_affine.xy().unwrap(); - let (x_proof, y_proof) = proof_affine.xy().unwrap(); - let y = proof.eval.into_bigint().to_bytes_be(); - - transcript_v.absorb_nonnative(&cm); - let x = transcript_v.get_challenge(); - - let x = x.into_bigint().to_bytes_be(); - let mut calldata: Vec = [ - &FUNCTION_SELECTOR_KZG10_CHECK[..], - &x_comm.into_bigint().to_bytes_be(), - &y_comm.into_bigint().to_bytes_be(), - &x_proof.into_bigint().to_bytes_be(), - &y_proof.into_bigint().to_bytes_be(), - &x, - &y, - ] - .concat(); - - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // change calldata to make it invalid - let last_calldata_element = calldata.last_mut().unwrap(); - *last_calldata_element = 0; - let (_, output) = evm.call(verifier_address, calldata); - assert_eq!(*output.last().unwrap(), 0); - } -} diff --git a/solidity-verifiers/src/verifiers/mod.rs b/solidity-verifiers/src/verifiers/mod.rs deleted file mode 100644 index 332c03a2d..000000000 --- a/solidity-verifiers/src/verifiers/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Solidity templates for the verifier contracts. -//! We use askama for templating and define which variables are required for each template. - -// Pragma statements for verifiers -pub const PRAGMA_GROTH16_VERIFIER: &str = "pragma solidity >=0.7.0 <0.9.0;"; // from snarkjs, avoid changing -pub const PRAGMA_KZG10_VERIFIER: &str = "pragma solidity >=0.8.1 <=0.8.4;"; - -/// Default SDPX License identifier -pub const GPL3_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: GPL-3.0"; -pub const MIT_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: MIT"; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Read, SerializationError, Write}; - -pub mod g16; -pub mod kzg; -pub mod nova_cyclefold; - -pub use g16::Groth16VerifierKey; -pub use kzg::KZG10VerifierKey; -pub use nova_cyclefold::{get_decider_template_for_cyclefold_decider, NovaCycleFoldVerifierKey}; - -pub trait ProtocolVerifierKey: CanonicalDeserialize + CanonicalSerialize { - const PROTOCOL_NAME: &'static str; - - fn serialize_name(&self, writer: &mut W) -> Result<(), SerializationError> { - Self::PROTOCOL_NAME - .to_string() - .serialize_uncompressed(writer) - } - - fn serialize_protocol_verifier_key( - &self, - writer: &mut W, - ) -> Result<(), SerializationError> { - self.serialize_name(writer)?; - self.serialize_compressed(writer) - } - fn deserialize_protocol_verifier_key( - mut reader: R, - ) -> Result { - let name: String = String::deserialize_uncompressed(&mut reader)?; - let data = Self::deserialize_compressed(&mut reader)?; - - if name != Self::PROTOCOL_NAME { - return Err(SerializationError::InvalidData); - } - - Ok(data) - } - - fn render_as_template(self, pragma: Option) -> Vec; -} - -#[cfg(test)] -pub mod tests { - use ark_bn254::{Bn254, Fr, G1Projective as G1}; - use ark_ff::PrimeField; - use ark_groth16::Groth16; - use ark_poly_commit::kzg10::VerifierKey as KZGVerifierKey; - use ark_r1cs_std::alloc::AllocVar; - use ark_r1cs_std::eq::EqGadget; - use ark_r1cs_std::fields::fp::FpVar; - use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; - use ark_snark::CircuitSpecificSetupSNARK; - use ark_std::rand::{RngCore, SeedableRng}; - use ark_std::test_rng; - use std::marker::PhantomData; - - use folding_schemes::commitment::{ - kzg::{ProverKey as KZGProverKey, KZG}, - CommitmentScheme, - }; - - /// Default setup length for testing. - pub const DEFAULT_SETUP_LEN: usize = 5; - - /// Test circuit used to test the Groth16 proof generation - #[derive(Debug, Clone, Copy)] - pub struct TestAddCircuit { - _f: PhantomData, - pub x: u8, - pub y: u8, - pub z: u8, - } - - impl ConstraintSynthesizer for TestAddCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = FpVar::::new_witness(cs.clone(), || Ok(F::from(self.x)))?; - let y = FpVar::::new_witness(cs.clone(), || Ok(F::from(self.y)))?; - let z = FpVar::::new_input(cs.clone(), || Ok(F::from(self.z)))?; - let comp_z = x.clone() + y.clone(); - comp_z.enforce_equal(&z)?; - Ok(()) - } - } - - #[allow(clippy::type_complexity)] - pub fn setup<'a>( - n: usize, - ) -> ( - Fr, // public params hash - KZGProverKey<'a, G1>, - KZGVerifierKey, - ark_groth16::ProvingKey, - ark_groth16::VerifyingKey, - TestAddCircuit, - ) { - let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64()); - let (x, y, z) = (21, 21, 42); - let circuit = TestAddCircuit:: { - _f: PhantomData, - x, - y, - z, - }; - let (g16_pk, g16_vk) = Groth16::::setup(circuit, &mut rng).unwrap(); - - let (kzg_pk, kzg_vk): (KZGProverKey, KZGVerifierKey) = - KZG::::setup(&mut rng, n).unwrap(); - let pp_hash = Fr::from(42u32); // only for test - (pp_hash, kzg_pk, kzg_vk, g16_pk, g16_vk, circuit) - } -} diff --git a/solidity-verifiers/src/verifiers/nova_cyclefold.rs b/solidity-verifiers/src/verifiers/nova_cyclefold.rs deleted file mode 100644 index d592dad6d..000000000 --- a/solidity-verifiers/src/verifiers/nova_cyclefold.rs +++ /dev/null @@ -1,433 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::upper_case_acronyms)] - -use ark_bn254::{Bn254, Fq, Fr, G1Affine, G1Projective}; -use ark_groth16::VerifyingKey as ArkG16VerifierKey; -use ark_poly_commit::kzg10::VerifierKey as ArkKZG10VerifierKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use askama::Template; - -use folding_schemes::folding::circuits::nonnative::uint::NonNativeUintVar; -use folding_schemes::folding::nova::decider_eth::VerifierParam as DeciderVerifierParam; - -use super::g16::Groth16Verifier; -use super::kzg::KZG10Verifier; -use crate::utils::HeaderInclusion; -use crate::{Groth16VerifierKey, KZG10VerifierKey, ProtocolVerifierKey, PRAGMA_GROTH16_VERIFIER}; - -pub fn get_decider_template_for_cyclefold_decider( - nova_cyclefold_vk: NovaCycleFoldVerifierKey, -) -> String { - HeaderInclusion::::builder() - .template(nova_cyclefold_vk) - .build() - .render() - .unwrap() -} - -#[derive(Template, Default)] -#[template(path = "nova_cyclefold_decider.askama.sol", ext = "sol")] -pub struct NovaCycleFoldDecider { - pp_hash: Fr, // public params hash - groth16_verifier: Groth16Verifier, - kzg10_verifier: KZG10Verifier, - // z_len denotes the FCircuit state (z_i) length - z_len: usize, - public_inputs_len: usize, - num_limbs: usize, - bits_per_limb: usize, -} - -impl From for NovaCycleFoldDecider { - fn from(value: NovaCycleFoldVerifierKey) -> Self { - let groth16_verifier = Groth16Verifier::from(value.g16_vk); - let public_inputs_len = groth16_verifier.gamma_abc_len; - let bits_per_limb = NonNativeUintVar::::bits_per_limb(); - Self { - pp_hash: value.pp_hash, - groth16_verifier, - kzg10_verifier: KZG10Verifier::from(value.kzg_vk), - z_len: value.z_len, - public_inputs_len, - num_limbs: (250_f32 / (bits_per_limb as f32)).ceil() as usize, - bits_per_limb, - } - } -} - -#[derive(CanonicalDeserialize, CanonicalSerialize, PartialEq, Debug, Clone)] -pub struct NovaCycleFoldVerifierKey { - pp_hash: Fr, - g16_vk: Groth16VerifierKey, - kzg_vk: KZG10VerifierKey, - z_len: usize, -} - -impl ProtocolVerifierKey for NovaCycleFoldVerifierKey { - const PROTOCOL_NAME: &'static str = "NovaCycleFold"; - - fn render_as_template(self, pragma: Option) -> Vec { - HeaderInclusion::::builder() - .pragma_version(pragma.unwrap_or(PRAGMA_GROTH16_VERIFIER.to_string())) - .template(self) - .build() - .render() - .unwrap() - .into_bytes() - } -} - -impl From<(Fr, Groth16VerifierKey, KZG10VerifierKey, usize)> for NovaCycleFoldVerifierKey { - fn from(value: (Fr, Groth16VerifierKey, KZG10VerifierKey, usize)) -> Self { - Self { - pp_hash: value.0, - g16_vk: value.1, - kzg_vk: value.2, - z_len: value.3, - } - } -} - -// implements From assuming that the 'batchCheck' method from the KZG10 template will not be used -// in the NovaCycleFoldDecider verifier contract -impl - From<( - DeciderVerifierParam, ArkG16VerifierKey>, - usize, - )> for NovaCycleFoldVerifierKey -{ - fn from( - value: ( - DeciderVerifierParam< - G1Projective, - ArkKZG10VerifierKey, - ArkG16VerifierKey, - >, - usize, - ), - ) -> Self { - let decider_vp = value.0; - let g16_vk = Groth16VerifierKey::from(decider_vp.snark_vp); - // pass `Vec::new()` since batchCheck will not be used - let kzg_vk = KZG10VerifierKey::from((decider_vp.cs_vp, Vec::new())); - Self { - pp_hash: decider_vp.pp_hash, - g16_vk, - kzg_vk, - z_len: value.1, - } - } -} - -impl NovaCycleFoldVerifierKey { - pub fn new( - pp_hash: Fr, - vkey_g16: ArkG16VerifierKey, - vkey_kzg: ArkKZG10VerifierKey, - crs_points: Vec, - z_len: usize, - ) -> Self { - Self { - pp_hash, - g16_vk: Groth16VerifierKey::from(vkey_g16), - kzg_vk: KZG10VerifierKey::from((vkey_kzg, crs_points)), - z_len, - } - } -} - -#[cfg(test)] -mod tests { - use ark_bn254::{Bn254, Fr, G1Projective as G1, G1Projective}; - use ark_ff::PrimeField; - use ark_groth16::Groth16; - use ark_grumpkin::Projective as G2; - use ark_r1cs_std::alloc::AllocVar; - use ark_r1cs_std::fields::fp::FpVar; - use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; - use askama::Template; - use std::marker::PhantomData; - use std::time::Instant; - - use super::{DeciderVerifierParam, NovaCycleFoldDecider}; - use crate::calldata::NovaVerificationMode::{Explicit, Opaque, OpaqueWithInputs}; - use crate::calldata::{prepare_calldata_for_nova_cyclefold_verifier, NovaVerificationMode}; - use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN}; - use crate::{ - evm::{compile_solidity, save_solidity, Evm}, - utils::HeaderInclusion, - verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, - NovaCycleFoldVerifierKey, ProtocolVerifierKey, - }; - use folding_schemes::folding::nova::decider_eth::Proof; - use folding_schemes::{ - commitment::{kzg::KZG, pedersen::Pedersen}, - folding::{ - nova::{decider_eth::Decider as DeciderEth, Nova, PreprocessorParam}, - traits::CommittedInstanceOps, - }, - frontend::FCircuit, - transcript::poseidon::poseidon_canonical_config, - Decider, Error, FoldingScheme, - }; - - type NOVA = Nova, Pedersen, false>; - type DECIDER = - DeciderEth, Pedersen, Groth16, NOVA>; - - type FS_PP = as FoldingScheme>::ProverParam; - type FS_VP = as FoldingScheme>::VerifierParam; - type DECIDER_PP = as Decider>>::ProverParam; - type DECIDER_VP = as Decider>>::VerifierParam; - - /// Test circuit to be folded - #[derive(Clone, Copy, Debug)] - pub struct CubicFCircuit { - _f: PhantomData, - } - impl FCircuit for CubicFCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 1 - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - let z_i = z_i[0].clone(); - - Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) - } - } - - /// This is the circuit that we want to fold, it implements the FCircuit trait. The parameter z_i - /// denotes the current state which contains 5 elements, and z_{i+1} denotes the next state which - /// we get by applying the step. - /// In this example we set z_i and z_{i+1} to have five elements, and at each step we do different - /// operations on each of them. - #[derive(Clone, Copy, Debug)] - pub struct MultiInputsFCircuit { - _f: PhantomData, - } - impl FCircuit for MultiInputsFCircuit { - type Params = (); - type ExternalInputs = (); - type ExternalInputsVar = (); - - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 5 - } - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Self::ExternalInputsVar, - ) -> Result>, SynthesisError> { - let four = FpVar::::new_constant(cs.clone(), F::from(4u32))?; - let forty = FpVar::::new_constant(cs.clone(), F::from(40u32))?; - let onehundred = FpVar::::new_constant(cs.clone(), F::from(100u32))?; - let a = z_i[0].clone() + four.clone(); - let b = z_i[1].clone() + forty.clone(); - let c = z_i[2].clone() * four; - let d = z_i[3].clone() * forty; - let e = z_i[4].clone() + onehundred; - - Ok(vec![a, b, c, d, e]) - } - } - - #[test] - fn nova_cyclefold_vk_serde_roundtrip() { - let (pp_hash, _, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); - - let decider_vp = DeciderVerifierParam { - pp_hash, - snark_vp: g16_vk, - cs_vp: kzg_vk, - }; - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, 1)); - - let mut bytes = vec![]; - nova_cyclefold_vk - .serialize_protocol_verifier_key(&mut bytes) - .unwrap(); - let obtained_nova_cyclefold_vk = - NovaCycleFoldVerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap(); - - assert_eq!(nova_cyclefold_vk, obtained_nova_cyclefold_vk) - } - - #[test] - fn nova_cyclefold_decider_template_renders() { - let (pp_hash, _, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); - let decider_vp = DeciderVerifierParam { - pp_hash, - snark_vp: g16_vk, - cs_vp: kzg_vk, - }; - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, 1)); - - let decider_solidity_code = HeaderInclusion::::builder() - .template(nova_cyclefold_vk) - .build(); - - save_solidity("NovaDecider.sol", &decider_solidity_code.render().unwrap()); - } - - /// Initializes Nova parameters and DeciderEth parameters. Only for test purposes. - #[allow(clippy::type_complexity)] - fn init_params>( - ) -> ((FS_PP, FS_VP), (DECIDER_PP, DECIDER_VP)) { - let mut rng = ark_std::rand::rngs::OsRng; - let poseidon_config = poseidon_canonical_config::(); - - let f_circuit = FC::new(()).unwrap(); - let prep_param = - PreprocessorParam::, Pedersen, false>::new( - poseidon_config, - f_circuit.clone(), - ); - let nova_params = NOVA::preprocess(&mut rng, &prep_param).unwrap(); - let decider_params = - DECIDER::::preprocess(&mut rng, (nova_params.clone(), f_circuit.state_len())) - .unwrap(); - - (nova_params, decider_params) - } - - fn interact_with_contract<'a, FC: FCircuit>( - nova_cyclefold_verifier_bytecode: &[u8], - nova: &NOVA, - proof: &Proof, Groth16>, - mode: NovaVerificationMode, - ) { - let mut evm = Evm::default(); - let verifier_address = evm.create(nova_cyclefold_verifier_bytecode.to_vec()); - - let calldata: Vec = prepare_calldata_for_nova_cyclefold_verifier( - mode, - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i, - &nova.u_i, - proof, - ) - .unwrap(); - - let (_, output) = evm.call(verifier_address, calldata.clone()); - assert_eq!(*output.last().unwrap(), 1); - - // change i to make calldata invalid, placed between bytes 4 - 35 - let mut invalid_calldata = calldata.clone(); - invalid_calldata[35] += 1; - let (_, output) = evm.call(verifier_address, invalid_calldata.clone()); - assert_eq!(*output.last().unwrap(), 0); - - // change z_0 to make the EVM check fail, placed between bytes 35 - 67 - let mut invalid_calldata = calldata.clone(); - invalid_calldata[67] += 1; - let (_, output) = evm.call(verifier_address, invalid_calldata.clone()); - assert_eq!(*output.last().unwrap(), 0); - - // change z_i to make the EVM check fail, placed between bytes 68 - 100 - let mut invalid_calldata = calldata.clone(); - invalid_calldata[99] += 1; - let (_, output) = evm.call(verifier_address, invalid_calldata.clone()); - assert_eq!(*output.last().unwrap(), 0); - } - - /// This function allows to define which FCircuit to use for the test, and how many prove_step - /// rounds to perform. - /// Actions performed by this test: - /// - runs the NovaCycleFold folding scheme for the given FCircuit and n_steps times - /// - generates a DeciderEth proof, and executes it through the EVM - /// - modifies the calldata and checks that it does not pass the EVM check - /// - modifies the z_0 and checks that it does not pass the EVM check - #[allow(clippy::type_complexity)] - fn nova_cyclefold_solidity_verifier_opt>( - fs_params: (FS_PP, FS_VP), - decider_params: (DECIDER_PP, DECIDER_VP), - z_0: Vec, - n_steps: usize, - ) { - let (decider_pp, decider_vp) = decider_params; - - let f_circuit = FC::new(()).unwrap(); - - let nova_cyclefold_vk = - NovaCycleFoldVerifierKey::from((decider_vp.clone(), f_circuit.state_len())); - - let mut rng = ark_std::rand::rngs::OsRng; - - let mut nova = NOVA::::init(&fs_params, f_circuit, z_0).unwrap(); - for _ in 0..n_steps { - nova.prove_step(&mut rng, FC::ExternalInputs::default(), None) - .unwrap(); - } - - let start = Instant::now(); - let proof = DECIDER::::prove(rng, decider_pp, nova.clone()).unwrap(); - println!("generated Decider proof: {:?}", start.elapsed()); - - let verified = DECIDER::::verify( - decider_vp, - nova.i, - nova.z_0.clone(), - nova.z_i.clone(), - &nova.U_i.get_commitments(), - &nova.u_i.get_commitments(), - &proof, - ) - .unwrap(); - assert!(verified); - - let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk); - - let nova_cyclefold_verifier_bytecode = - compile_solidity(decider_solidity_code, "NovaDecider"); - - for mode in [Explicit, Opaque, OpaqueWithInputs] { - interact_with_contract(&nova_cyclefold_verifier_bytecode, &nova, &proof, mode); - } - } - - /// Given an `FCircuit` type and initial IVC state `z_0`, this function tests the `NovaCycleFold` - /// verifier with a few different folding steps. - fn nova_cyclefold_solidity_verifier_test>(z_0: Vec) { - let (nova_params, decider_params) = init_params::(); - for num_steps in [2, 3] { - nova_cyclefold_solidity_verifier_opt::( - nova_params.clone(), - decider_params.clone(), - z_0.clone(), - num_steps, - ) - } - } - - #[test] - fn nova_cyclefold_solidity_verifier_single_input() { - nova_cyclefold_solidity_verifier_test::>(vec![Fr::from(3_u32)]); - } - - #[test] - fn nova_cyclefold_solidity_verifier_multi_input() { - nova_cyclefold_solidity_verifier_test::>(vec![Fr::from(1_u32); 5]); - } -} diff --git a/solidity-verifiers/templates/groth16_verifier.askama.sol b/solidity-verifiers/templates/groth16_verifier.askama.sol deleted file mode 100644 index e8e12035c..000000000 --- a/solidity-verifiers/templates/groth16_verifier.askama.sol +++ /dev/null @@ -1,169 +0,0 @@ -/* - Copyright 2021 0KIMS association. - - * `solidity-verifiers` added comment - This file is a template built out of [snarkJS](https://github.com/iden3/snarkjs) groth16 verifier. - See the original ejs template [here](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs) - * - - snarkJS is a free software: you can redistribute it and/or modify it - under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - snarkJS is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public - License for more details. - - You should have received a copy of the GNU General Public License - along with snarkJS. If not, see . -*/ - -contract Groth16Verifier { - // Scalar field size - uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; - // Base field size - uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; - - // Verification Key data - uint256 constant alphax = {{ vkey_alpha_g1.0[0] }}; - uint256 constant alphay = {{ vkey_alpha_g1.0[1] }}; - uint256 constant betax1 = {{ vkey_beta_g2.0[0][1] }}; - uint256 constant betax2 = {{ vkey_beta_g2.0[0][0] }}; - uint256 constant betay1 = {{ vkey_beta_g2.0[1][1] }}; - uint256 constant betay2 = {{ vkey_beta_g2.0[1][0] }}; - uint256 constant gammax1 = {{ vkey_gamma_g2.0[0][1] }}; - uint256 constant gammax2 = {{ vkey_gamma_g2.0[0][0] }}; - uint256 constant gammay1 = {{ vkey_gamma_g2.0[1][1] }}; - uint256 constant gammay2 = {{ vkey_gamma_g2.0[1][0] }}; - uint256 constant deltax1 = {{ vkey_delta_g2.0[0][1] }}; - uint256 constant deltax2 = {{ vkey_delta_g2.0[0][0] }}; - uint256 constant deltay1 = {{ vkey_delta_g2.0[1][1] }}; - uint256 constant deltay2 = {{ vkey_delta_g2.0[1][0] }}; - - {% for (i, point) in gamma_abc_g1.iter().enumerate() %} - uint256 constant IC{{i}}x = {{ point.0[0] }}; - uint256 constant IC{{i}}y = {{ point.0[1] }}; - {% endfor %} - - // Memory data - uint16 constant pVk = 0; - uint16 constant pPairing = 128; - - uint16 constant pLastMem = 896; - - function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[{{ gamma_abc_len - 1 }}] calldata _pubSignals) public view returns (bool) { - assembly { - function checkField(v) { - if iszero(lt(v, r)) { - mstore(0, 0) - return(0, 0x20) - } - } - - // G1 function to multiply a G1 value(x,y) to value in an address - function g1_mulAccC(pR, x, y, s) { - let success - let mIn := mload(0x40) - mstore(mIn, x) - mstore(add(mIn, 32), y) - mstore(add(mIn, 64), s) - - success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) - - if iszero(success) { - mstore(0, 0) - return(0, 0x20) - } - - mstore(add(mIn, 64), mload(pR)) - mstore(add(mIn, 96), mload(add(pR, 32))) - - success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) - - if iszero(success) { - mstore(0, 0) - return(0, 0x20) - } - } - - function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { - let _pPairing := add(pMem, pPairing) - let _pVk := add(pMem, pVk) - - mstore(_pVk, IC0x) - mstore(add(_pVk, 32), IC0y) - - // Compute the linear combination vk_x - {% for (i, _) in gamma_abc_g1.iter().enumerate() %} - {% if loop.first -%} - {%- else -%} - g1_mulAccC(_pVk, IC{{i}}x, IC{{i}}y, calldataload(add(pubSignals, {{(i-1)*32}}))) - {%- endif -%} - {% endfor %} - - // -A - mstore(_pPairing, calldataload(pA)) - mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) - - // B - mstore(add(_pPairing, 64), calldataload(pB)) - mstore(add(_pPairing, 96), calldataload(add(pB, 32))) - mstore(add(_pPairing, 128), calldataload(add(pB, 64))) - mstore(add(_pPairing, 160), calldataload(add(pB, 96))) - - // alpha1 - mstore(add(_pPairing, 192), alphax) - mstore(add(_pPairing, 224), alphay) - - // beta2 - mstore(add(_pPairing, 256), betax1) - mstore(add(_pPairing, 288), betax2) - mstore(add(_pPairing, 320), betay1) - mstore(add(_pPairing, 352), betay2) - - // vk_x - mstore(add(_pPairing, 384), mload(add(pMem, pVk))) - mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) - - - // gamma2 - mstore(add(_pPairing, 448), gammax1) - mstore(add(_pPairing, 480), gammax2) - mstore(add(_pPairing, 512), gammay1) - mstore(add(_pPairing, 544), gammay2) - - // C - mstore(add(_pPairing, 576), calldataload(pC)) - mstore(add(_pPairing, 608), calldataload(add(pC, 32))) - - // delta2 - mstore(add(_pPairing, 640), deltax1) - mstore(add(_pPairing, 672), deltax2) - mstore(add(_pPairing, 704), deltay1) - mstore(add(_pPairing, 736), deltay2) - - - let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) - - isOk := and(success, mload(_pPairing)) - } - - let pMem := mload(0x40) - mstore(0x40, add(pMem, pLastMem)) - - // Validate that all evaluations ∈ F - {% for (i, _) in gamma_abc_g1.iter().enumerate() %} - checkField(calldataload(add(_pubSignals, {{i*32}}))) - {% endfor %} - - // Validate all evaluations - let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) - - mstore(0, isValid) - - return(0, 0x20) - } - } -} diff --git a/solidity-verifiers/templates/header_template.askama.sol b/solidity-verifiers/templates/header_template.askama.sol deleted file mode 100644 index a9f6683be..000000000 --- a/solidity-verifiers/templates/header_template.askama.sol +++ /dev/null @@ -1,4 +0,0 @@ -{{ sdpx }} -{{ pragma_version }} - -{{template}} \ No newline at end of file diff --git a/solidity-verifiers/templates/kzg10_verifier.askama.sol b/solidity-verifiers/templates/kzg10_verifier.askama.sol deleted file mode 100644 index d55802e6e..000000000 --- a/solidity-verifiers/templates/kzg10_verifier.askama.sol +++ /dev/null @@ -1,275 +0,0 @@ -/** - * @author Privacy and Scaling Explorations team - pse.dev - * @dev Contains utility functions for ops in BN254; in G_1 mostly. - * @notice Forked from https://github.com/weijiekoh/libkzg. - * Among others, a few of the changes we did on this fork were: - * - Templating the pragma version - * - Removing type wrappers and use uints instead - * - Performing changes on arg types - * - Update some of the `require` statements - * - Use the bn254 scalar field instead of checking for overflow on the babyjub prime - * - In batch checking, we compute auxiliary polynomials and their commitments at the same time. - */ -contract KZG10Verifier { - - // prime of field F_p over which y^2 = x^3 + 3 is defined - uint256 public constant BN254_PRIME_FIELD = - 21888242871839275222246405745257275088696311157297823662689037894645226208583; - uint256 public constant BN254_SCALAR_FIELD = - 21888242871839275222246405745257275088548364400416034343698204186575808495617; - - /** - * @notice Performs scalar multiplication in G_1. - * @param p G_1 point to multiply - * @param s Scalar to multiply by - * @return r G_1 point p multiplied by scalar s - */ - function mulScalar(uint256[2] memory p, uint256 s) internal view returns (uint256[2] memory r) { - uint256[3] memory input; - input[0] = p[0]; - input[1] = p[1]; - input[2] = s; - bool success; - assembly { - success := staticcall(sub(gas(), 2000), 7, input, 0x60, r, 0x40) - switch success - case 0 { invalid() } - } - require(success, "bn254: scalar mul failed"); - } - - /** - * @notice Negates a point in G_1. - * @param p G_1 point to negate - * @return uint256[2] G_1 point -p - */ - function negate(uint256[2] memory p) internal pure returns (uint256[2] memory) { - if (p[0] == 0 && p[1] == 0) { - return p; - } - return [p[0], BN254_PRIME_FIELD - (p[1] % BN254_PRIME_FIELD)]; - } - - /** - * @notice Adds two points in G_1. - * @param p1 G_1 point 1 - * @param p2 G_1 point 2 - * @return r G_1 point p1 + p2 - */ - function add(uint256[2] memory p1, uint256[2] memory p2) internal view returns (uint256[2] memory r) { - bool success; - uint256[4] memory input = [p1[0], p1[1], p2[0], p2[1]]; - assembly { - success := staticcall(sub(gas(), 2000), 6, input, 0x80, r, 0x40) - switch success - case 0 { invalid() } - } - - require(success, "bn254: point add failed"); - } - - /** - * @notice Computes the pairing check e(p1, p2) * e(p3, p4) == 1 - * @dev Note that G_2 points a*i + b are encoded as two elements of F_p, (a, b) - * @param a_1 G_1 point 1 - * @param a_2 G_2 point 1 - * @param b_1 G_1 point 2 - * @param b_2 G_2 point 2 - * @return result true if pairing check is successful - */ - function pairing(uint256[2] memory a_1, uint256[2][2] memory a_2, uint256[2] memory b_1, uint256[2][2] memory b_2) - internal - view - returns (bool result) - { - uint256[12] memory input = [ - a_1[0], - a_1[1], - a_2[0][1], // imaginary part first - a_2[0][0], - a_2[1][1], // imaginary part first - a_2[1][0], - b_1[0], - b_1[1], - b_2[0][1], // imaginary part first - b_2[0][0], - b_2[1][1], // imaginary part first - b_2[1][0] - ]; - - uint256[1] memory out; - bool success; - - assembly { - success := staticcall(sub(gas(), 2000), 8, input, 0x180, out, 0x20) - switch success - case 0 { invalid() } - } - - require(success, "bn254: pairing failed"); - - return out[0] == 1; - } - - uint256[2] G_1 = [ - {{ g1.0[0] }}, - {{ g1.0[1] }} - ]; - uint256[2][2] G_2 = [ - [ - {{ g2.0[0][0] }}, - {{ g2.0[0][1] }} - ], - [ - {{ g2.0[1][0] }}, - {{ g2.0[1][1] }} - ] - ]; - uint256[2][2] VK = [ - [ - {{ vk.0[0][0] }}, - {{ vk.0[0][1] }} - ], - [ - {{ vk.0[1][0] }}, - {{ vk.0[1][1] }} - ] - ]; - - {% if g1_crs_len>0 %} // only enabled if g1_crs_len>0, for batch_check - uint256[2][{{ g1_crs_len }}] G1_CRS = [ - {%- for (i, point) in g1_crs.iter().enumerate() %} - [ - {{ point.0[0] }}, - {{ point.0[1] }} - {% if loop.last -%} - ] - {%- else -%} - ], - {%- endif -%} - {% endfor -%} - ]; - {%~ endif %} - - /** - * @notice Verifies a single point evaluation proof. Function name follows `ark-poly`. - * @dev To avoid ops in G_2, we slightly tweak how the verification is done. - * @param c G_1 point commitment to polynomial. - * @param pi G_1 point proof. - * @param x Value to prove evaluation of polynomial at. - * @param y Evaluation poly(x). - * @return result Indicates if KZG proof is correct. - */ - function check(uint256[2] calldata c, uint256[2] calldata pi, uint256 x, uint256 y) - public - view - returns (bool result) - { - // - // we want to: - // 1. avoid gas intensive ops in G2 - // 2. format the pairing check in line with what the evm opcode expects. - // - // we can do this by tweaking the KZG check to be: - // - // e(pi, vk - x * g2) = e(c - y * g1, g2) [initial check] - // e(pi, vk - x * g2) * e(c - y * g1, g2)^{-1} = 1 - // e(pi, vk - x * g2) * e(-c + y * g1, g2) = 1 [bilinearity of pairing for all subsequent steps] - // e(pi, vk) * e(pi, -x * g2) * e(-c + y * g1, g2) = 1 - // e(pi, vk) * e(-x * pi, g2) * e(-c + y * g1, g2) = 1 - // e(pi, vk) * e(x * -pi - c + y * g1, g2) = 1 [done] - // |_ rhs_pairing _| - // - uint256[2] memory rhs_pairing = - add(mulScalar(negate(pi), x), add(negate(c), mulScalar(G_1, y))); - return pairing(pi, VK, rhs_pairing, G_2); - } - - function evalPolyAt(uint256[] memory _coefficients, uint256 _index) public pure returns (uint256) { - uint256 m = BN254_SCALAR_FIELD; - uint256 result = 0; - uint256 powerOfX = 1; - - for (uint256 i = 0; i < _coefficients.length; i++) { - uint256 coeff = _coefficients[i]; - assembly { - result := addmod(result, mulmod(powerOfX, coeff, m), m) - powerOfX := mulmod(powerOfX, _index, m) - } - } - return result; - } - - {% if g1_crs_len>0 %} // only enabled if g1_crs_len>0, for batch_check - /** - * @notice Ensures that z(x) == 0 and l(x) == y for all x in x_vals and y in y_vals. It returns the commitment to z(x) and l(x). - * @param z_coeffs coefficients of the zero polynomial z(x) = (x - x_1)(x - x_2)...(x - x_n). - * @param l_coeffs coefficients of the lagrange polynomial l(x). - * @param x_vals x values to evaluate the polynomials at. - * @param y_vals y values to which l(x) should evaluate to. - * @return uint256[2] commitment to z(x). - * @return uint256[2] commitment to l(x). - */ - function checkAndCommitAuxPolys( - uint256[] memory z_coeffs, - uint256[] memory l_coeffs, - uint256[] memory x_vals, - uint256[] memory y_vals - ) public view returns (uint256[2] memory, uint256[2] memory) { - // z(x) is of degree len(x_vals), it is a product of linear polynomials (x - x_i) - // l(x) is of degree len(x_vals) - 1 - uint256[2] memory z_commit; - uint256[2] memory l_commit; - for (uint256 i = 0; i < x_vals.length; i++) { - z_commit = add(z_commit, mulScalar(G1_CRS[i], z_coeffs[i])); // update commitment to z(x) - l_commit = add(l_commit, mulScalar(G1_CRS[i], l_coeffs[i])); // update commitment to l(x) - - uint256 eval_z = evalPolyAt(z_coeffs, x_vals[i]); - uint256 eval_l = evalPolyAt(l_coeffs, x_vals[i]); - - require(eval_z == 0, "checkAndCommitAuxPolys: wrong zero poly"); - require(eval_l == y_vals[i], "checkAndCommitAuxPolys: wrong lagrange poly"); - } - // z(x) has len(x_vals) + 1 coeffs, we add to the commitment the last coeff of z(x) - z_commit = add(z_commit, mulScalar(G1_CRS[z_coeffs.length - 1], z_coeffs[z_coeffs.length - 1])); - - return (z_commit, l_commit); - } - - /** - * @notice Verifies a batch of point evaluation proofs. Function name follows `ark-poly`. - * @dev To avoid ops in G_2, we slightly tweak how the verification is done. - * @param c G1 point commitment to polynomial. - * @param pi G2 point proof. - * @param x_vals Values to prove evaluation of polynomial at. - * @param y_vals Evaluation poly(x). - * @param l_coeffs Coefficients of the lagrange polynomial. - * @param z_coeffs Coefficients of the zero polynomial z(x) = (x - x_1)(x - x_2)...(x - x_n). - * @return result Indicates if KZG proof is correct. - */ - function batchCheck( - uint256[2] calldata c, - uint256[2][2] calldata pi, - uint256[] calldata x_vals, - uint256[] calldata y_vals, - uint256[] calldata l_coeffs, - uint256[] calldata z_coeffs - ) public view returns (bool result) { - // - // we want to: - // 1. avoid gas intensive ops in G2 - // 2. format the pairing check in line with what the evm opcode expects. - // - // we can do this by tweaking the KZG check to be: - // - // e(z(r) * g1, pi) * e(g1, l(r) * g2) = e(c, g2) [initial check] - // e(z(r) * g1, pi) * e(l(r) * g1, g2) * e(c, g2)^{-1} = 1 [bilinearity of pairing] - // e(z(r) * g1, pi) * e(l(r) * g1 - c, g2) = 1 [done] - // - (uint256[2] memory z_commit, uint256[2] memory l_commit) = - checkAndCommitAuxPolys(z_coeffs, l_coeffs, x_vals, y_vals); - uint256[2] memory neg_commit = negate(c); - return pairing(z_commit, pi, add(l_commit, neg_commit), G_2); - } - {%~ endif %} -} diff --git a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol b/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol deleted file mode 100644 index a7f61fbad..000000000 --- a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol +++ /dev/null @@ -1,230 +0,0 @@ -/* - Sonobe's Nova + CycleFold decider verifier. - Joint effort by 0xPARC & PSE. - - More details at https://github.com/privacy-scaling-explorations/sonobe - Usage and design documentation at https://privacy-scaling-explorations.github.io/sonobe-docs/ - - Uses the https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs - Groth16 verifier implementation and a KZG10 Solidity template adapted from - https://github.com/weijiekoh/libkzg. - Additionally we implement the NovaDecider contract, which combines the - Groth16 and KZG10 verifiers to verify the zkSNARK proofs coming from - Nova+CycleFold folding. -*/ - - -/* =============================== */ -/* KZG10 verifier methods */ -{{ kzg10_verifier }} - -/* =============================== */ -/* Groth16 verifier methods */ -{{ groth16_verifier }} - - -/* =============================== */ -/* Nova+CycleFold Decider verifier */ -/** - * @notice Computes the decomposition of a `uint256` into num_limbs limbs of bits_per_limb bits each. - * @dev Compatible with sonobe::folding-schemes::folding::circuits::nonnative::nonnative_field_to_field_elements. - */ -library LimbsDecomposition { - function decompose(uint256 x) internal pure returns (uint256[{{num_limbs}}] memory) { - uint256[{{num_limbs}}] memory limbs; - for (uint8 i = 0; i < {{num_limbs}}; i++) { - limbs[i] = (x >> ({{bits_per_limb}} * i)) & ((1 << {{bits_per_limb}}) - 1); - } - return limbs; - } -} - -/** - * @author PSE & 0xPARC - * @title Interface for the NovaDecider contract hiding proof details. - * @dev This interface enables calling the verifyNovaProof function without exposing the proof details. - */ -interface OpaqueDecider { - /** - * @notice Verifies a Nova+CycleFold proof given initial and final IVC states, number of steps and the rest proof inputs concatenated. - * @dev This function should simply reorganize arguments and pass them to the proper verification function. - */ - function verifyOpaqueNovaProofWithInputs( - uint256 steps, // number of folded steps (i) - uint256[{{ z_len }}] calldata initial_state, // initial IVC state (z0) - uint256[{{ z_len }}] calldata final_state, // IVC state after i steps (zi) - uint256[25] calldata proof // the rest of the decider inputs - ) external view returns (bool); - - /** - * @notice Verifies a Nova+CycleFold proof given all the proof inputs collected in a single array. - * @dev This function should simply reorganize arguments and pass them to the proper verification function. - */ - function verifyOpaqueNovaProof(uint256[{{ 26 + z_len * 2 }}] calldata proof) external view returns (bool); -} - -/** - * @author PSE & 0xPARC - * @title NovaDecider contract, for verifying Nova IVC SNARK proofs. - * @dev This is an askama template which, when templated, features a Groth16 and KZG10 verifiers from which this contract inherits. - */ -contract NovaDecider is Groth16Verifier, KZG10Verifier, OpaqueDecider { - /** - * @notice Computes the linear combination of a and b with r as the coefficient. - * @dev All ops are done mod the BN254 scalar field prime - */ - function rlc(uint256 a, uint256 r, uint256 b) internal pure returns (uint256 result) { - assembly { - result := addmod(a, mulmod(r, b, BN254_SCALAR_FIELD), BN254_SCALAR_FIELD) - } - } - - /** - * @notice Verifies a nova cyclefold proof consisting of two KZG proofs and of a groth16 proof. - * @dev The selector of this function is "dynamic", since it depends on `z_len`. - */ - function verifyNovaProof( - // inputs are grouped to prevent errors due stack too deep - uint256[{{ 1 + z_len * 2 }}] calldata i_z0_zi, // [i, z0, zi] where |z0| == |zi| - uint256[4] calldata U_i_cmW_U_i_cmE, // [U_i_cmW[2], U_i_cmE[2]] - uint256[2] calldata u_i_cmW, // [u_i_cmW[2]] - uint256[3] calldata cmT_r, // [cmT[2], r] - uint256[2] calldata pA, // groth16 - uint256[2][2] calldata pB, // groth16 - uint256[2] calldata pC, // groth16 - uint256[4] calldata challenge_W_challenge_E_kzg_evals, // [challenge_W, challenge_E, eval_W, eval_E] - uint256[2][2] calldata kzg_proof // [proof_W, proof_E] - ) public view returns (bool) { - - require(i_z0_zi[0] >= 2, "Folding: the number of folded steps should be at least 2"); - - // from gamma_abc_len, we subtract 1. - uint256[{{ public_inputs_len - 1 }}] memory public_inputs; - - public_inputs[0] = {{pp_hash}}; - public_inputs[1] = i_z0_zi[0]; - - for (uint i = 0; i < {{ z_len * 2 }}; i++) { - public_inputs[2 + i] = i_z0_zi[1 + i]; - } - - { - // U_i.cmW + r * u_i.cmW - uint256[2] memory mulScalarPoint = super.mulScalar([u_i_cmW[0], u_i_cmW[1]], cmT_r[2]); - uint256[2] memory cmW = super.add([U_i_cmW_U_i_cmE[0], U_i_cmW_U_i_cmE[1]], mulScalarPoint); - - { - uint256[{{num_limbs}}] memory cmW_x_limbs = LimbsDecomposition.decompose(cmW[0]); - uint256[{{num_limbs}}] memory cmW_y_limbs = LimbsDecomposition.decompose(cmW[1]); - - for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 2 }} + k] = cmW_x_limbs[k]; - public_inputs[{{ z_len * 2 + 2 + num_limbs }} + k] = cmW_y_limbs[k]; - } - } - - require(this.check(cmW, kzg_proof[0], challenge_W_challenge_E_kzg_evals[0], challenge_W_challenge_E_kzg_evals[2]), "KZG: verifying proof for challenge W failed"); - } - - { - // U_i.cmE + r * cmT - uint256[2] memory mulScalarPoint = super.mulScalar([cmT_r[0], cmT_r[1]], cmT_r[2]); - uint256[2] memory cmE = super.add([U_i_cmW_U_i_cmE[2], U_i_cmW_U_i_cmE[3]], mulScalarPoint); - - { - uint256[{{num_limbs}}] memory cmE_x_limbs = LimbsDecomposition.decompose(cmE[0]); - uint256[{{num_limbs}}] memory cmE_y_limbs = LimbsDecomposition.decompose(cmE[1]); - - for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 2 + num_limbs * 2 }} + k] = cmE_x_limbs[k]; - public_inputs[{{ z_len * 2 + 2 + num_limbs * 3 }} + k] = cmE_y_limbs[k]; - } - } - - require(this.check(cmE, kzg_proof[1], challenge_W_challenge_E_kzg_evals[1], challenge_W_challenge_E_kzg_evals[3]), "KZG: verifying proof for challenge E failed"); - } - - { - // add challenges - public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 }}] = challenge_W_challenge_E_kzg_evals[0]; - public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 + 1 }}] = challenge_W_challenge_E_kzg_evals[1]; - public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 + 2 }}] = challenge_W_challenge_E_kzg_evals[2]; - public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 + 3 }}] = challenge_W_challenge_E_kzg_evals[3]; - - uint256[{{num_limbs}}] memory cmT_x_limbs; - uint256[{{num_limbs}}] memory cmT_y_limbs; - - cmT_x_limbs = LimbsDecomposition.decompose(cmT_r[0]); - cmT_y_limbs = LimbsDecomposition.decompose(cmT_r[1]); - - for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 }} + 4 + k] = cmT_x_limbs[k]; - public_inputs[{{ z_len * 2 + 2 + num_limbs * 5 }} + 4 + k] = cmT_y_limbs[k]; - } - - bool success_g16 = this.verifyProof(pA, pB, pC, public_inputs); - require(success_g16 == true, "Groth16: verifying proof failed"); - } - - return(true); - } - - /** - * @notice Verifies a Nova+CycleFold proof given initial and final IVC states, number of steps and the rest proof inputs concatenated. - * @dev Simply reorganization of arguments and call to the `verifyNovaProof` function. - */ - function verifyOpaqueNovaProofWithInputs( - uint256 steps, - uint256[{{ z_len }}] calldata initial_state, - uint256[{{ z_len }}] calldata final_state, - uint256[25] calldata proof - ) public override view returns (bool) { - uint256[1 + 2 * {{ z_len }}] memory i_z0_zi; - i_z0_zi[0] = steps; - for (uint256 i = 0; i < {{ z_len }}; i++) { - i_z0_zi[i + 1] = initial_state[i]; - i_z0_zi[i + 1 + {{ z_len }}] = final_state[i]; - } - - uint256[4] memory U_i_cmW_U_i_cmE = [proof[0], proof[1], proof[2], proof[3]]; - uint256[2] memory u_i_cmW = [proof[4], proof[5]]; - uint256[3] memory cmT_r = [proof[6], proof[7], proof[8]]; - uint256[2] memory pA = [proof[9], proof[10]]; - uint256[2][2] memory pB = [[proof[11], proof[12]], [proof[13], proof[14]]]; - uint256[2] memory pC = [proof[15], proof[16]]; - uint256[4] memory challenge_W_challenge_E_kzg_evals = [proof[17], proof[18], proof[19], proof[20]]; - uint256[2][2] memory kzg_proof = [[proof[21], proof[22]], [proof[23], proof[24]]]; - - return this.verifyNovaProof( - i_z0_zi, - U_i_cmW_U_i_cmE, - u_i_cmW, - cmT_r, - pA, - pB, - pC, - challenge_W_challenge_E_kzg_evals, - kzg_proof - ); - } - - /** - * @notice Verifies a Nova+CycleFold proof given all proof inputs concatenated. - * @dev Simply reorganization of arguments and call to the `verifyNovaProof` function. - */ - function verifyOpaqueNovaProof(uint256[{{ 26 + z_len * 2 }}] calldata proof) public override view returns (bool) { - uint256[{{ z_len }}] memory z0; - uint256[{{ z_len }}] memory zi; - for (uint256 i = 0; i < {{ z_len }}; i++) { - z0[i] = proof[i + 1]; - zi[i] = proof[i + 1 + {{ z_len }}]; - } - - uint256[25] memory extracted_proof; - for (uint256 i = 0; i < 25; i++) { - extracted_proof[i] = proof[{{ 1 + 2 * z_len }} + i]; - } - - return this.verifyOpaqueNovaProofWithInputs(proof[0], z0, zi, extracted_proof); - } -}