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/Cargo.toml b/Cargo.toml index e8fe65f39..d1b8c62ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,50 @@ [workspace] members = [ "crates/primitives", - "crates/traits", + "crates/fs", + "crates/ivc", ] resolver = "2" +[workspace.package] +edition = "2024" +license = "MIT" +repository = "https://github.com/privacy-scaling-explorations/sonobe/" +rust-version = "1.85.1" + +[workspace.dependencies] +num-bigint = { version = "0.4.3" } +num-integer = { version = "0.1" } +num-traits = { version = "0.2" } +sha3 = { version = "0.10" } +rayon = { version = "1" } +thiserror = { version = "2.0.16" } +wasm-bindgen-test = { version = "0.3" } + +# Arkworks family +ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives", default-features = false } +ark-ec = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-ff = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-groth16 = { git = "https://github.com/arkworks-rs/groth16", default-features = false } +ark-poly = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-poly-commit = { git = "https://github.com/arkworks-rs/poly-commit", default-features = false } +ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std", default-features = false } +ark-relations = { git = "https://github.com/arkworks-rs/snark", default-features = false } +ark-serialize = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-snark = { git = "https://github.com/arkworks-rs/snark", default-features = false } +ark-std = { git = "https://github.com/arkworks-rs/std", default-features = false } + +# Ark curves +ark-bn254 = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-grumpkin = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-pallas = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-vesta = { git = "https://github.com/arkworks-rs/algebra", default-features = false } + +# Local crates +sonobe-primitives = { path = "crates/primitives", default-features = false } +sonobe-fs = { path = "crates/fs", default-features = false } +sonobe-ivc = { path = "crates/ivc", default-features = false } + [patch.crates-io] # We depend on git versions of arkworks crates, but some of our dependencies # depend on crates.io versions, so we need to override them here to avoid @@ -19,64 +59,9 @@ ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" # Curve crates also need git versions ark-bn254 = { git = "https://github.com/arkworks-rs/algebra" } -ark-grumpkin = { git = "https://github.com/arkworks-rs/algebra" } [patch."https://github.com/arkworks-rs/crypto-primitives"] ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives", rev = "af003fc" } [patch."https://github.com/arkworks-rs/r1cs-std"] -ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch - -[workspace.package] -edition = "2021" -license = "MIT" -repository = "https://github.com/privacy-scaling-explorations/sonobe/" - -[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" } -num-bigint = { version = "0.4.3" } -num-integer = { version = "0.1" } -num-traits = { version = "0.2" } -pprof = { version = "0.13" } -serde = { version = "^1.0.0" } -serde_json = { version = "^1.0.0" } -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", default-features = false } - -# 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 = { git = "https://github.com/arkworks-rs/groth16" } -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 = { git = "https://github.com/arkworks-rs/snark", default-features = false } -ark-serialize = { version = "^0.5.0" } -ark-snark = { git = "https://github.com/arkworks-rs/snark", default-features = false } -ark-std = { version = "^0.5.0", default-features = false } -ark-vesta = { version = "^0.5.0" } - -# Local crates -sonobe-primitives = { path = "crates/primitives", default-features = false } -sonobe-traits = { path = "crates/traits" } \ No newline at end of file +ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch \ No newline at end of file diff --git a/README.md b/README.md index bb9ee6f10..19ca77b10 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Experimental folding 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.

@@ -67,7 +67,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 +87,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 diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml new file mode 100644 index 000000000..772f4e47d --- /dev/null +++ b/crates/fs/Cargo.toml @@ -0,0 +1,37 @@ +[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 } +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..a4238a038 --- /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, Self::Challenge), 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..68d1be5f5 --- /dev/null +++ b/crates/fs/src/definitions/circuits.rs @@ -0,0 +1,60 @@ +//! Traits that define in-circuit gadgets for folding scheme algorithms, mainly +//! for proof verification. + +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{commitments::CommitmentDefGadget, 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<(Self::RU, Self::Challenge), SynthesisError>; +} + +/// [`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; +} diff --git a/crates/fs/src/definitions/errors.rs b/crates/fs/src/definitions/errors.rs new file mode 100644 index 000000000..d721a88dc --- /dev/null +++ b/crates/fs/src/definitions/errors.rs @@ -0,0 +1,49 @@ +//! Error definitions for folding schemes. + +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + arithmetizations::Error as ArithError, commitments::Error as CommitmentError, + sumcheck::Error as SumCheckError, +}; +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::SumCheckError`] indicates an error from the underlying sumcheck + /// protocol. + #[error(transparent)] + SumCheckError(#[from] SumCheckError), + /// [`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..cd431c448 --- /dev/null +++ b/crates/fs/src/definitions/instances.rs @@ -0,0 +1,114 @@ +//! 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}, + traits::Dummy, + transcripts::{Absorbable, AbsorbableVar}, +}; + +use super::utils::TaggedVec; + +/// [`FoldingInstance`] defines the operations that a folding scheme's instance +/// should support. +pub trait FoldingInstance: Clone + Debug + PartialEq + Eq + Absorbable { + /// [`FoldingInstance::N_COMMITMENTS`] defines the number of commitments + /// contained in the instance. + const N_COMMITMENTS: usize; + + /// [`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<&CM::Commitment>; + + /// [`FoldingInstance::public_inputs`] returns the reference to the public + /// inputs contained in the instance. + fn public_inputs(&self) -> &[CM::Scalar]; + + /// [`FoldingInstance::public_inputs_mut`] returns the mutable reference to + /// the public inputs contained in the instance. + fn public_inputs_mut(&mut self) -> &mut [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<&A> for PlainInstance { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_public_inputs()].into() + } +} + +impl FoldingInstance for PlainInstance { + const N_COMMITMENTS: usize = 0; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + self + } + + fn public_inputs_mut(&mut self) -> &mut [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..a71d7f0b7 --- /dev/null +++ b/crates/fs/src/definitions/keys.rs @@ -0,0 +1,25 @@ +//! Traits and abstractions for folding scheme keys. + +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 { + /// [`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; + /// [`DeciderKey::ArithConfig`] is the constraint system configuration + /// associated with the folding scheme. + type ArithConfig: ArithConfig; + + /// [`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 reference to the constraint + /// system configuration. + fn to_arith_config(&self) -> &Self::ArithConfig; +} diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs new file mode 100644 index 000000000..38a737027 --- /dev/null +++ b/crates/fs/src/definitions/mod.rs @@ -0,0 +1,137 @@ +//! 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::{ + arithmetizations::Arith, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget}, + relations::{Relation, WitnessInstanceSampler}, + traits::{Dummy, SonobeField}, +}; + +use self::{ + errors::Error, + instances::{FoldingInstance, FoldingInstanceVar}, + keys::DeciderKey, + witnesses::FoldingWitness, +}; + +/// [`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 + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::RU`] is the type of running instance. + type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::IW`] is the type of incoming witness. + type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::IU`] is the type of incoming instance. + type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + /// [`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::ArithConfig>; + /// [`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 ::Config>; +} + +/// [`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>; + /// [`FoldingSchemeDefGadget::RU`] is the type of in-circuit running + /// instance variable. + type RU: FoldingInstanceVar::RU>; + /// [`FoldingSchemeDefGadget::IU`] is the type of in-circuit incoming + /// instance variable. + type IU: FoldingInstanceVar::IU>; + + /// [`FoldingSchemeDefGadget::VerifierKey`] is the type of in-circuit + /// verifier key variable. + type VerifierKey; + + /// [`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..f4d27acb8 --- /dev/null +++ b/crates/fs/src/definitions/utils.rs @@ -0,0 +1,107 @@ +//! 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}, +}; +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 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..b3655f37d --- /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::{ + commitments::{CommitmentDef, GroupBasedCommitment}, + traits::CF2, +}; + +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..95da4b78b --- /dev/null +++ b/crates/fs/src/definitions/witnesses.rs @@ -0,0 +1,65 @@ +//! 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}, + traits::Dummy, +}; + +use super::utils::TaggedVec; + +/// [`FoldingWitness`] defines the operations that a folding scheme's witness +/// should support. +pub trait FoldingWitness: Debug { + /// [`FoldingWitness::N_OPENINGS`] defines the number of openings contained + /// in the witness. + const N_OPENINGS: usize; + + /// [`FoldingWitness::openings`] returns the reference to all openings + /// contained in the witness, where each opening a tuple of the values being + /// committed to and the randomness used in the commitment. + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)>; +} + +/// [`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<&A> for PlainWitness { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_witnesses()].into() + } +} + +impl FoldingWitness for PlainWitness { + const N_OPENINGS: usize = 0; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![] + } +} + +/// [`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/hypernova/algorithms/key_generator.rs b/crates/fs/src/hypernova/algorithms/key_generator.rs new file mode 100644 index 000000000..7c5c709e9 --- /dev/null +++ b/crates/fs/src/hypernova/algorithms/key_generator.rs @@ -0,0 +1,42 @@ +//! Key generation for HyperNova. + +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig, ccs::CCSVariant}, + commitments::{CommitmentKey, GroupBasedCommitment}, +}; + +use crate::{ + Error, FoldingSchemeKeyGenerator, + hypernova::{HyperNova, HyperNova2}, +}; + +impl FoldingSchemeKeyGenerator + for HyperNova +{ + fn generate_keys(ck: Self::PublicParam, ccs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let ccs = Arc::new(ccs); + if ck.max_scalars_len() < ccs.config().n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the CCS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: ccs, ck }) + } +} + +impl FoldingSchemeKeyGenerator + for HyperNova2 +{ + fn generate_keys(ck: Self::PublicParam, ccs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let ccs = Arc::new(ccs); + if ck.max_scalars_len() < ccs.config().n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the CCS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: ccs, ck }) + } +} diff --git a/crates/fs/src/hypernova/algorithms/mod.rs b/crates/fs/src/hypernova/algorithms/mod.rs new file mode 100644 index 000000000..2b6a65a6e --- /dev/null +++ b/crates/fs/src/hypernova/algorithms/mod.rs @@ -0,0 +1,6 @@ +//! Implementations folding scheme algorithms for HyperNova. + +pub mod key_generator; +pub mod preprocessor; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/hypernova/algorithms/preprocessor.rs b/crates/fs/src/hypernova/algorithms/preprocessor.rs new file mode 100644 index 000000000..f4624318d --- /dev/null +++ b/crates/fs/src/hypernova/algorithms/preprocessor.rs @@ -0,0 +1,27 @@ +//! Preprocessing for HyperNova. + +use ark_std::rand::RngCore; +use sonobe_primitives::{arithmetizations::ccs::CCSVariant, commitments::GroupBasedCommitment}; + +use crate::{ + Error, FoldingSchemePreprocessor, + hypernova::{HyperNova, HyperNova2}, +}; + +impl FoldingSchemePreprocessor + for HyperNova +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} + +impl FoldingSchemePreprocessor + for HyperNova2 +{ + 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/hypernova/algorithms/prover.rs b/crates/fs/src/hypernova/algorithms/prover.rs new file mode 100644 index 000000000..4ee5ba037 --- /dev/null +++ b/crates/fs/src/hypernova/algorithms/prover.rs @@ -0,0 +1,324 @@ +//! Proof generation for HyperNova. + +use ark_ff::One; +use ark_poly::{DenseMultilinearExtension as MLE, MultilinearExtension}; +use ark_std::{borrow::Borrow, rand::RngCore}; +use sonobe_primitives::{ + algebra::ops::{ + bits::FromBits, + pow::Pow, + rlc::{ScalarRLC, SliceRLC}, + }, + arithmetizations::{Arith, ArithConfig, ccs::CCSVariant}, + commitments::GroupBasedCommitment, + sumcheck::{ + SumCheck, + utils::{EqPoly, VPAuxInfo, VirtualPolynomial}, + }, + transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + hypernova::{HyperNova, HyperNova2, HyperNovaKey, NIMFSProof}, +}; + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemeProver for HyperNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &HyperNovaKey, + 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, Self::Challenge), Error> { + let Ws = &Ws.iter().map(|i| i.borrow()).collect::>(); + let Us = &Us.iter().map(|i| i.borrow()).collect::>(); + let ws = &ws.iter().map(|i| i.borrow()).collect::>(); + let us = &us.iter().map(|i| i.borrow()).collect::>(); + + let ccs = &pk.arith; + let d = V::degree(); + let s = ccs.config().log_constraints(); + let t = V::n_matrices(); + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + + // absorb instances to transcript + transcript.add(&Us[..]); + transcript.add(&us[..]); + + // Step 1: Get some challenges + let gamma = transcript.challenge_field_element(); + let beta = transcript.challenge_field_elements(s); + + let gamma_powers = gamma.powers(M * t + N); + let (running_gammas, incoming_gammas) = gamma_powers.split_at(M * t); + + // Compute g(x) + let running_mles = Ws + .iter() + .zip(Us) + .flat_map(|(W, U)| ccs.mles((U.u, &U.x, &W.w).into())); + let incoming_mles = ws + .iter() + .zip(us) + .flat_map(|(w, u)| ccs.mles((One::one(), &u.x, &w.w).into())); + let eq_mles = Us + .iter() + .map(|U| &U.r_x) + .chain([&beta]) + .map(|r| MLE::from_evaluations_vec(s, EqPoly::fix_y_evals(r))); + + let running_products = running_gammas + .iter() + .enumerate() + .map(|(i, &gamma)| (gamma, vec![i, (M + N) * t + i / t])); + let incoming_products = incoming_gammas.iter().enumerate().flat_map(|(k, gamma)| { + S.iter().zip(c).map(move |(S_i, &c_i)| { + ( + c_i * gamma, + S_i.iter() + .map(|j| (M + k) * t + j) + .chain([(M + N) * t + M]) + .collect(), + ) + }) + }); + + let g = VirtualPolynomial { + aux_info: VPAuxInfo { + num_variables: s, + max_degree: d + 1, + }, + flattened_ml_extensions: running_mles.chain(incoming_mles).chain(eq_mles).collect(), + products: running_products.chain(incoming_products).collect(), + }; + + // Step 3: Run the sumcheck prover + // Step 2: dig into the sumcheck and extract r_x_prime + let (sumcheck_proof, r_x_prime, mles) = SumCheck::prove(g, transcript)?; + + // Step 4: compute sigmas and thetas + let sigmas = mles[0..t * M] + .iter() + .map(|mle| mle.fix_variables(&[])[0]) + .collect::>(); + let thetas = mles[t * M..t * (M + N)] + .iter() + .map(|mle| mle.fix_variables(&[])[0]) + .collect::>(); + + // Step 6: Get the folding challenge + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let rho_powers = rho.powers(M + N); + + Ok(( + Self::RW { + w: Ws + .iter() + .map(|w| &w.w[..]) + .chain(ws.iter().map(|w| &w.w[..])) + .slice_rlc(&rho_powers), + r: Ws + .iter() + .map(|w| w.r) + .chain(ws.iter().map(|w| w.r)) + .scalar_rlc(&rho_powers), + }, + Self::RU { + cm: Us + .iter() + .map(|u| u.cm) + .chain(us.iter().map(|u| u.cm)) + .scalar_rlc(&rho_powers), + u: Us + .iter() + .map(|u| u.u) + .chain([CM::Scalar::one(); N]) + .scalar_rlc(&rho_powers), + x: Us + .iter() + .map(|u| &u.x[..]) + .chain(us.iter().map(|u| &u.x[..])) + .slice_rlc(&rho_powers), + r_x: r_x_prime, + v: sigmas + .chunks(t) + .chain(thetas.chunks(t)) + .slice_rlc(&rho_powers), + }, + NIMFSProof { + sc_proof: sumcheck_proof, + sigmas, + thetas, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemeProver for HyperNova2 +{ + #[allow(non_snake_case)] + fn prove( + pk: &HyperNovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; M], + Us: &[impl Borrow; M], + ws: &[impl Borrow; N], + us: &[impl Borrow; N], + mut rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error> { + let Ws = &Ws.iter().map(|i| i.borrow()).collect::>(); + let Us = &Us.iter().map(|i| i.borrow()).collect::>(); + let ws = &ws.iter().map(|i| i.borrow()).collect::>(); + let us = &us.iter().map(|i| i.borrow()).collect::>(); + + let ccs = &pk.arith; + let d = V::degree(); + let s = ccs.config().log_constraints(); + let t = V::n_matrices(); + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + + let mut cms = [CM::Commitment::default(); N]; + let mut rs = [CM::Randomness::default(); N]; + for i in 0..N { + let (cm, r) = CM::commit(&pk.ck, ws[i], &mut rng)?; + cms[i] = cm; + rs[i] = r; + } + + // absorb instances to transcript + transcript.add(&Us[..]); + transcript.add(&us[..]); + transcript.add(&cms[..]); + + // Step 1: Get some challenges + let gamma = transcript.challenge_field_element(); + let beta = transcript.challenge_field_elements(s); + + let gamma_powers = gamma.powers(M * t + N); + let (running_gammas, incoming_gammas) = gamma_powers.split_at(M * t); + + // Compute g(x) + let running_mles = Ws + .iter() + .zip(Us) + .flat_map(|(W, U)| ccs.mles((U.u, &U.x, &W.w).into())); + let incoming_mles = ws + .iter() + .zip(us) + .flat_map(|(w, u)| ccs.mles((One::one(), &u[..], &w[..]).into())); + let eq_mles = Us + .iter() + .map(|U| &U.r_x) + .chain([&beta]) + .map(|r| MLE::from_evaluations_vec(s, EqPoly::fix_y_evals(r))); + + let running_products = running_gammas + .iter() + .enumerate() + .map(|(i, &gamma)| (gamma, vec![i, (M + N) * t + i / t])); + let incoming_products = incoming_gammas.iter().enumerate().flat_map(|(k, gamma)| { + S.iter().zip(c).map(move |(S_i, &c_i)| { + ( + c_i * gamma, + S_i.iter() + .map(|j| (M + k) * t + j) + .chain([(M + N) * t + M]) + .collect(), + ) + }) + }); + + let g = VirtualPolynomial { + aux_info: VPAuxInfo { + num_variables: s, + max_degree: d + 1, + }, + flattened_ml_extensions: running_mles.chain(incoming_mles).chain(eq_mles).collect(), + products: running_products.chain(incoming_products).collect(), + }; + + // Step 3: Run the sumcheck prover + // Step 2: dig into the sumcheck and extract r_x_prime + let (sumcheck_proof, r_x_prime, mles) = SumCheck::prove(g, transcript)?; + + // Step 4: compute sigmas and thetas + let sigmas = mles[0..t * M] + .iter() + .map(|mle| mle.fix_variables(&[])[0]) + .collect::>(); + let thetas = mles[t * M..t * (M + N)] + .iter() + .map(|mle| mle.fix_variables(&[])[0]) + .collect::>(); + + // Step 6: Get the folding challenge + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let rho_powers = rho.powers(M + N); + + Ok(( + Self::RW { + w: Ws + .iter() + .map(|w| &w.w[..]) + .chain(ws.iter().map(|w| &w[..])) + .slice_rlc(&rho_powers), + r: Ws.iter().map(|w| w.r).chain(rs).scalar_rlc(&rho_powers), + }, + Self::RU { + cm: Us + .iter() + .map(|u| u.cm) + .chain(cms.iter().copied()) + .scalar_rlc(&rho_powers), + u: Us + .iter() + .map(|u| u.u) + .chain([CM::Scalar::one(); N]) + .scalar_rlc(&rho_powers), + x: Us + .iter() + .map(|u| &u.x[..]) + .chain(us.iter().map(|u| &u[..])) + .slice_rlc(&rho_powers), + r_x: r_x_prime, + v: sigmas + .chunks(t) + .chain(thetas.chunks(t)) + .slice_rlc(&rho_powers), + }, + ( + cms, + NIMFSProof { + sc_proof: sumcheck_proof, + sigmas, + thetas, + }, + ), + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/hypernova/algorithms/verifier.rs b/crates/fs/src/hypernova/algorithms/verifier.rs new file mode 100644 index 000000000..324e8e4a6 --- /dev/null +++ b/crates/fs/src/hypernova/algorithms/verifier.rs @@ -0,0 +1,244 @@ +//! Proof verification for HyperNova. + +use ark_ff::One; +use ark_std::borrow::Borrow; +use sonobe_primitives::{ + algebra::ops::{ + bits::FromBits, + pow::Pow, + rlc::{ScalarRLC, SliceRLC}, + }, + arithmetizations::ccs::CCSVariant, + commitments::GroupBasedCommitment, + sumcheck::{ + Error as SumCheckError, SumCheck, + utils::{EqPoly, VPAuxInfo}, + }, + transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeVerifier, + hypernova::{HyperNova, HyperNova2}, +}; + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemeVerifier for HyperNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + ) -> Result { + let Us = &Us.iter().map(|i| i.borrow()).collect::>(); + let us = &us.iter().map(|i| i.borrow()).collect::>(); + + let d = V::degree(); + let s = proof.sc_proof.len(); + let t = V::n_matrices(); + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + + // absorb instances to transcript + transcript.add(&Us[..]); + transcript.add(&us[..]); + + // Step 1: Get some challenges + let gamma = transcript.challenge_field_element(); + let beta = transcript.challenge_field_elements(s); + + let gamma_powers = gamma.powers(M * t + N); + + let vp_aux_info = VPAuxInfo { + max_degree: d + 1, + num_variables: s, + }; + + // Step 3: Start verifying the sumcheck + // First, compute the expected sumcheck sum: \sum gamma^j v_j + let sum_v_j_gamma = Us + .iter() + .zip(gamma_powers.chunks(t)) + .flat_map(|(U, gammas)| U.v.iter().zip(gammas).map(|(&v, &g)| v * g)) + .sum(); + + // Verify the interactive part of the sumcheck + // Step 2: Dig into the sumcheck claim and extract the randomness used + let (claimed_eval, r_x_prime) = + SumCheck::verify(sum_v_j_gamma, &proof.sc_proof, &vp_aux_info, transcript)?; + + // Step 5: Finish verifying sumcheck (verify the claim c) + let e_beta = EqPoly::fix_xy_eval(&beta, &r_x_prime); + let c = proof + .sigmas + .chunks(t) + .zip(Us) + .flat_map(|(sigmas, u)| { + let e_lcccs = EqPoly::fix_xy_eval(&u.r_x, &r_x_prime); + sigmas.iter().map(move |sigma_j| e_lcccs * sigma_j) + }) + .chain(proof.thetas.chunks(t).map(|thetas| { + S.iter() + .zip(c) + .map(|(S_i, &c_i)| c_i * S_i.iter().map(|&j| thetas[j]).product::()) + .sum::() + * e_beta + })) + .zip(gamma_powers) + .map(|(val, gamma_i)| val * gamma_i) + .sum::(); + // check that the g(r_x') from the sumcheck proof is equal to the computed c from sigmas&thetas + (c == claimed_eval).then_some(()).ok_or_else(|| { + SumCheckError::IncorrectEvaluation(claimed_eval.to_string(), c.to_string()) + })?; + + // Step 6: Get the folding challenge + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let rho_powers = rho.powers(M + N); + + Ok(Self::RU { + cm: Us + .iter() + .map(|u| u.cm) + .chain(us.iter().map(|u| u.cm)) + .scalar_rlc(&rho_powers), + u: Us + .iter() + .map(|u| u.u) + .chain([CM::Scalar::one(); N]) + .scalar_rlc(&rho_powers), + x: Us + .iter() + .map(|u| &u.x[..]) + .chain(us.iter().map(|u| &u.x[..])) + .slice_rlc(&rho_powers), + r_x: r_x_prime, + v: proof + .sigmas + .chunks(t) + .chain(proof.thetas.chunks(t)) + .slice_rlc(&rho_powers), + }) + } +} + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemeVerifier for HyperNova2 +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + (cms, proof): &Self::Proof, + ) -> Result { + let Us = &Us.iter().map(|i| i.borrow()).collect::>(); + let us = &us.iter().map(|i| i.borrow()).collect::>(); + + let d = V::degree(); + let s = proof.sc_proof.len(); + let t = V::n_matrices(); + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + + // absorb instances to transcript + transcript.add(&Us[..]); + transcript.add(&us[..]); + transcript.add(&cms[..]); + + // Step 1: Get some challenges + let gamma = transcript.challenge_field_element(); + let beta = transcript.challenge_field_elements(s); + + let gamma_powers = gamma.powers(M * t + N); + + let vp_aux_info = VPAuxInfo { + max_degree: d + 1, + num_variables: s, + }; + + // Step 3: Start verifying the sumcheck + // First, compute the expected sumcheck sum: \sum gamma^j v_j + let sum_v_j_gamma = Us + .iter() + .zip(gamma_powers.chunks(t)) + .flat_map(|(U, gammas)| U.v.iter().zip(gammas).map(|(&v, &g)| v * g)) + .sum(); + + // Verify the interactive part of the sumcheck + // Step 2: Dig into the sumcheck claim and extract the randomness used + let (claimed_eval, r_x_prime) = + SumCheck::verify(sum_v_j_gamma, &proof.sc_proof, &vp_aux_info, transcript)?; + + // Step 5: Finish verifying sumcheck (verify the claim c) + let e_beta = EqPoly::fix_xy_eval(&beta, &r_x_prime); + let c = proof + .sigmas + .chunks(t) + .zip(Us) + .flat_map(|(sigmas, u)| { + let e_lcccs = EqPoly::fix_xy_eval(&u.r_x, &r_x_prime); + sigmas.iter().map(move |sigma_j| e_lcccs * sigma_j) + }) + .chain(proof.thetas.chunks(t).map(|thetas| { + S.iter() + .zip(c) + .map(|(S_i, &c_i)| c_i * S_i.iter().map(|&j| thetas[j]).product::()) + .sum::() + * e_beta + })) + .zip(gamma_powers) + .map(|(val, gamma_i)| val * gamma_i) + .sum::(); + // check that the g(r_x') from the sumcheck proof is equal to the computed c from sigmas&thetas + (c == claimed_eval).then_some(()).ok_or_else(|| { + SumCheckError::IncorrectEvaluation(claimed_eval.to_string(), c.to_string()) + })?; + + // Step 6: Get the folding challenge + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let rho_powers = rho.powers(M + N); + + Ok(Self::RU { + cm: Us + .iter() + .map(|u| u.cm) + .chain(cms.iter().copied()) + .scalar_rlc(&rho_powers), + u: Us + .iter() + .map(|u| u.u) + .chain([CM::Scalar::one(); N]) + .scalar_rlc(&rho_powers), + x: Us + .iter() + .map(|u| &u.x[..]) + .chain(us.iter().map(|u| &u[..])) + .slice_rlc(&rho_powers), + r_x: r_x_prime, + v: proof + .sigmas + .chunks(t) + .chain(proof.thetas.chunks(t)) + .slice_rlc(&rho_powers), + }) + } +} diff --git a/crates/fs/src/hypernova/circuits/mod.rs b/crates/fs/src/hypernova/circuits/mod.rs new file mode 100644 index 000000000..9367afad7 --- /dev/null +++ b/crates/fs/src/hypernova/circuits/mod.rs @@ -0,0 +1,3 @@ +//! In-circuit gadgets for HyperNova. + +pub mod verifier; diff --git a/crates/fs/src/hypernova/circuits/verifier.rs b/crates/fs/src/hypernova/circuits/verifier.rs new file mode 100644 index 000000000..38d35c2aa --- /dev/null +++ b/crates/fs/src/hypernova/circuits/verifier.rs @@ -0,0 +1,153 @@ +//! Partial in-circuit verifier implementation for HyperNova. + +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, + prelude::Boolean, +}; +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + algebra::ops::{ + pow::PowGadget, + rlc::{ScalarRLC, SliceRLC}, + }, + arithmetizations::ccs::CCSVariant, + commitments::GroupBasedCommitment, + sumcheck::{ + circuits::SumCheckGadget, + utils::{EqPolyGadget, VPAuxInfo}, + }, + transcripts::TranscriptGadget, +}; + +use crate::{FoldingSchemePartialVerifierGadget, hypernova::HyperNovaGadget}; + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemePartialVerifierGadget for HyperNovaGadget +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + Us: [&Self::RU; M], + us: [&Self::IU; N], + proof: &Self::Proof, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let d = V::degree(); + let s = proof.sc_proof.len(); + let t = V::n_matrices(); + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + + // absorb instances to transcript + transcript.add(&Us[..])?; + transcript.add(&us[..])?; + + // Step 1: Get some challenges + let gamma = transcript.challenge_field_element()?; + let beta = transcript.challenge_field_elements(s)?; + + let gamma_powers = gamma.powers(M * t + N); + + let vp_aux_info = VPAuxInfo { + max_degree: d + 1, + num_variables: s, + }; + + // Step 3: Start verifying the sumcheck + // First, compute the expected sumcheck sum: \sum gamma^j v_j + let mut sum_v_j_gamma = FpVar::zero(); + for (i, U) in Us.iter().enumerate() { + for j in 0..U.v.len() { + sum_v_j_gamma += &U.v[j] * &gamma_powers[i * t + j]; + } + } + + // Verify the interactive part of the sumcheck + // Step 2: Dig into the sumcheck claim and extract the randomness used + let (expected_eval, r_x_prime) = + SumCheckGadget::verify(sum_v_j_gamma, &proof.sc_proof, &vp_aux_info, transcript)?; + + // Step 5: Finish verifying sumcheck (verify the claim c) + let c = { + let e2 = EqPolyGadget::fix_xy_eval(&beta, &r_x_prime); + proof + .sigmas + .chunks(t) + .zip(Us) + .flat_map(|(sigmas, u)| { + let e_lcccs = EqPolyGadget::fix_xy_eval(&u.r_x, &r_x_prime); + sigmas.iter().map(move |sigma_j| &e_lcccs * sigma_j) + }) + .chain(proof.thetas.chunks(t).map(|thetas| { + &e2 * S + .iter() + .zip(c) + .map(|(S_i, &c_i)| { + let mut prod = FpVar::one(); + for &j in S_i { + prod *= &thetas[j]; + } + prod * c_i + }) + .sum::>() + })) + .zip(gamma_powers.iter()) + .map(|(val, gamma_i)| val * gamma_i) + .sum::>() + }; + + // check that the g(r_x') from the sumcheck proof is equal to the computed c from sigmas&thetas + c.enforce_equal(&expected_eval)?; + + // Step 6: Get the folding challenge + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS)?; + let rho = Boolean::le_bits_to_fp(&rho_bits)?; + + let rho_powers = rho.powers(M + N); + + Ok(( + Self::RU { + cm: { + let cms = Us + .iter() + .map(|u| &u.cm) + .chain(us.iter().map(|u| &u.cm)) + .collect::>(); + + AllocVar::new_witness(cms.cs().or(rho_powers.cs()), || { + let cms = cms.value().unwrap_or(vec![Default::default(); M + N]); + let rho_powers = rho_powers + .value() + .unwrap_or(vec![Default::default(); M + N]); + Ok(cms.into_iter().scalar_rlc(&rho_powers)) + })? + }, + u: Us + .iter() + .map(|u| u.u.clone()) + .chain(vec![FpVar::one(); N]) + .scalar_rlc(&rho_powers), + x: Us + .iter() + .map(|u| &u.x[..]) + .chain(us.iter().map(|u| &u.x[..])) + .slice_rlc(&rho_powers), + r_x: r_x_prime, + v: proof + .sigmas + .chunks(t) + .chain(proof.thetas.chunks(t)) + .slice_rlc(&rho_powers), + }, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/hypernova/instances/circuits.rs b/crates/fs/src/hypernova/instances/circuits.rs new file mode 100644 index 000000000..384f1b7e7 --- /dev/null +++ b/crates/fs/src/hypernova/instances/circuits.rs @@ -0,0 +1,247 @@ +//! In-circuit variables for HyperNova instances. +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + fields::fp::FpVar, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; + +use super::{CCCSInstance, LCCCSInstance}; +use crate::FoldingInstanceVar; + +/// [`LCCCSInstanceVar`] defines HyperNova's running instance variable. +#[derive(Clone, Debug, PartialEq)] +pub struct LCCCSInstanceVar { + /// [`LCCCSInstanceVar::cm`] is the witness commitment. + pub cm: CM::CommitmentVar, + /// [`LCCCSInstanceVar::u`] is the constant term. + pub u: CM::ScalarVar, + /// [`LCCCSInstanceVar::x`] is the vector of public inputs (to the circuit). + pub x: Vec, + /// [`LCCCSInstanceVar::r_x`] is the random evaluation point. + pub r_x: Vec, + /// [`LCCCSInstanceVar::v`] is the vector of sums of MLE evaluations defined + /// in Definition 2. + pub v: Vec, +} + +impl AllocVar, CM::ConstraintField> + for LCCCSInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let LCCCSInstance { cm, u, x, r_x, v } = v.borrow(); + Ok(Self { + cm: AllocVar::new_variable(cs.clone(), || Ok(cm), mode)?, + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + r_x: AllocVar::new_variable(cs.clone(), || Ok(&r_x[..]), mode)?, + v: AllocVar::new_variable(cs.clone(), || Ok(&v[..]), mode)?, + }) + } +} + +impl GR1CSVar for LCCCSInstanceVar { + type Value = LCCCSInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm + .cs() + .or(self.u.cs()) + .or(self.x.cs()) + .or(self.r_x.cs()) + .or(self.v.cs()) + } + + fn value(&self) -> Result { + Ok(LCCCSInstance { + cm: self.cm.value()?, + u: self.u.value()?, + x: self.x.value()?, + r_x: self.r_x.value()?, + v: self.v.value()?, + }) + } +} + +impl AbsorbableVar for LCCCSInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.cm.absorb_into(dest)?; + self.u.absorb_into(dest)?; + self.x.absorb_into(dest)?; + self.r_x.absorb_into(dest)?; + self.v.absorb_into(dest) + } +} + +impl CondSelectGadget for LCCCSInstanceVar { + 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); + } + if true_value.r_x.len() != false_value.r_x.len() { + return Err(SynthesisError::Unsatisfiable); + } + if true_value.v.len() != false_value.v.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + cm: cond.select(&true_value.cm, &false_value.cm)?, + u: cond.select(&true_value.u, &false_value.u)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + r_x: true_value + .r_x + .iter() + .zip(&false_value.r_x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + v: true_value + .v + .iter() + .zip(&false_value.v) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for LCCCSInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm] + } + + 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: AllocVar::new_witness(cs.clone(), || Ok(&u.cm))?, + u: AllocVar::new_witness(cs.clone(), || Ok(&u.u))?, + x, + r_x: AllocVar::new_witness(cs.clone(), || Ok(&u.r_x[..]))?, + v: AllocVar::new_witness(cs.clone(), || Ok(&u.v[..]))?, + }) + } +} + +/// [`CCCSInstanceVar`] defines HyperNova's incoming instance variable. +#[derive(Clone, Debug, PartialEq)] +pub struct CCCSInstanceVar { + /// [`CCCSInstanceVar::cm`] is the witness commitment. + pub cm: CM::CommitmentVar, + /// [`CCCSInstanceVar::x`] is the vector of public inputs (to the circuit). + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for CCCSInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let CCCSInstance { cm, x } = v.borrow(); + Ok(Self { + cm: AllocVar::new_variable(cs.clone(), || Ok(cm), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for CCCSInstanceVar { + type Value = CCCSInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm.cs().or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(CCCSInstance { + cm: self.cm.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for CCCSInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.cm.absorb_into(dest)?; + self.x.absorb_into(dest) + } +} + +impl CondSelectGadget for CCCSInstanceVar { + 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: cond.select(&true_value.cm, &false_value.cm)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for CCCSInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm] + } + + 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: AllocVar::new_witness(cs.clone(), || Ok(&u.cm))?, + x, + }) + } +} diff --git a/crates/fs/src/hypernova/instances/mod.rs b/crates/fs/src/hypernova/instances/mod.rs new file mode 100644 index 000000000..5399c6c07 --- /dev/null +++ b/crates/fs/src/hypernova/instances/mod.rs @@ -0,0 +1,112 @@ +//! Definitions of out-of-circuit values and in-circuit variables for HyperNova +//! instances. + +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::{ + ArithConfig, + ccs::{CCSConfig, CCSVariant}, + }, + commitments::CommitmentDef, + traits::Dummy, + transcripts::Absorbable, +}; + +use crate::FoldingInstance; + +pub mod circuits; + +/// [`LCCCSInstance`] defines HyperNova's running instance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LCCCSInstance { + /// [`LCCCSInstance::cm`] is the witness commitment. + pub cm: CM::Commitment, + /// [`LCCCSInstance::u`] is the constant term. + pub u: CM::Scalar, + /// [`LCCCSInstance::x`] is the vector of public inputs (to the circuit). + pub x: Vec, + /// [`LCCCSInstance::r_x`] is the random evaluation point. + pub r_x: Vec, + /// [`LCCCSInstance::v`] is the vector of sums of MLE evaluations defined in + /// Definition 2. + pub v: Vec, +} + +impl FoldingInstance for LCCCSInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&CCSConfig> for LCCCSInstance { + fn dummy(cfg: &CCSConfig) -> Self { + Self { + cm: Default::default(), + u: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + r_x: vec![Default::default(); cfg.log_constraints()], + v: vec![Default::default(); V::n_matrices()], + } + } +} + +impl Absorbable for LCCCSInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.cm.absorb_into(dest); + self.u.absorb_into(dest); + self.x.absorb_into(dest); + self.r_x.absorb_into(dest); + self.v.absorb_into(dest); + } +} + +/// [`CCCSInstance`] defines HyperNova's incoming instance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CCCSInstance { + /// [`CCCSInstance::cm`] is the witness commitment. + pub cm: CM::Commitment, + /// [`CCCSInstance::x`] is the vector of public inputs (to the circuit). + pub x: Vec, +} + +impl FoldingInstance for CCCSInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for CCCSInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + cm: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + } + } +} + +impl Absorbable for CCCSInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.cm.absorb_into(dest); + self.x.absorb_into(dest); + } +} diff --git a/crates/fs/src/hypernova/mod.rs b/crates/fs/src/hypernova/mod.rs new file mode 100644 index 000000000..1e5ea89d9 --- /dev/null +++ b/crates/fs/src/hypernova/mod.rs @@ -0,0 +1,452 @@ +//! This module implements the HyperNova folding scheme, which is introduced in +//! this [paper]. +//! +//! [paper]: https://eprint.iacr.org/2023/573.pdf + +use ark_ff::{Field, PrimeField}; +use ark_poly::MultilinearExtension; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + UniformRand, borrow::Borrow, cfg_iter, marker::PhantomData, rand::RngCore, sync::Arc, +}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + arithmetizations::{ + Arith, ArithConfig, ArithRelation, Error as ArithError, + ccs::{CCS, CCSConfig, CCSVariant}, + r1cs::R1CSConfig, + }, + circuits::{Assignments, AssignmentsOwned}, + commitments::{CommitmentDef, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + traits::Dummy, +}; + +use self::{ + instances::{ + CCCSInstance as IU, LCCCSInstance as RU, + circuits::{CCCSInstanceVar as IUVar, LCCCSInstanceVar as RUVar}, + }, + witnesses::{CCCSWitness as IW, LCCCSWitness as RW}, +}; +use crate::{ + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + PlainInstance as PU, PlainWitness as PW, +}; + +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; + +/// [`HyperNovaKey`] is HyperNova's decider key. +#[derive(Clone)] +pub struct HyperNovaKey { + arith: Arc, + ck: Arc, +} + +impl DeciderKey for HyperNovaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &Self::ArithConfig { + self.arith.config() + } +} + +impl, V: CCSVariant> ArithRelation, RU> + for CCS +{ + type Evaluation = Vec; + + fn eval_relation(&self, w: &RW, u: &RU) -> Result { + let z = Assignments::from((u.u, &u.x, &w.w)); + Ok(self + .mles(z) + .iter() + .map(|mle| mle.fix_variables(&u.r_x)[0]) + .collect()) + } + + fn check_evaluation(_w: &RW, u: &RU, e: Self::Evaluation) -> Result<(), ArithError> { + cfg_iter!(e) + .zip(&u.v) + .all(|(e, v)| e == v) + .then_some(()) + .ok_or(ArithError::UnsatisfiedAssignments( + "Evaluation contains non-zero values".into(), + )) + } +} + +impl Relation, RU> for HyperNovaKey +where + A: ArithRelation, RU>, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + CM::open(&self.ck, &w.w, &w.r, &u.cm)?; + Ok(()) + } +} + +impl Relation, IU> for HyperNovaKey +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, &u.cm)?; + Ok(()) + } +} + +impl Relation, PU> for HyperNovaKey +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 HyperNovaKey { + 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, r) = CM::commit(&self.ck, &w, rng)?; + Ok((IW { w, r }, IU { cm, x })) + } +} + +impl WitnessInstanceSampler, PU> + for HyperNovaKey +{ + 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 HyperNovaKey +where + A: ArithRelation, RU, Evaluation = Vec>, + CM: CommitmentOps, +{ + type Source = (); + type Error = Error; + + #[allow(non_snake_case)] + 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 (cm, r) = CM::commit(&self.ck, &w, &mut rng)?; + + let r_x = (0..cfg.log_constraints()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect(); + + let W = RW { w, r }; + let mut U = RU { + cm, + x, + u, + r_x, + v: vec![], + }; + U.v = self.arith.eval_relation(&W, &U)?; + + Ok((W, U)) + } +} + +/// [`NIMFSProof`] is HyperNova's proof. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NIMFSProof { + /// [`NIMFSProof::sc_proof`] is the sum-check proof. + pub sc_proof: Vec>, + /// [`NIMFSProof::sigmas`] is a vector of claimed internal sums defined + /// in Equation 9 + pub sigmas: Vec, + /// [`NIMFSProof::thetas`] is a vector of claimed internal sums defined + /// in Equation 10 + pub thetas: Vec, +} + +impl Dummy<&CCSConfig> + for NIMFSProof +{ + fn dummy(cfg: &CCSConfig) -> Self { + let s = cfg.log_constraints(); + let d = cfg.degree(); + let t = V::n_matrices(); + Self { + sc_proof: vec![vec![F::zero(); d + 2]; s], + sigmas: vec![F::zero(); t * M], + thetas: vec![F::zero(); t * N], + } + } +} + +/// [`HyperNova`] implements the HyperNova folding scheme for a CCS variant `V`. +pub struct HyperNova { + _t: PhantomData<(CM, V)>, +} + +impl FoldingSchemeDef + for HyperNova +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = CM::Scalar; + type Arith = CCS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = HyperNovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = NIMFSProof; +} + +/// [`HyperNova2`] implements the HyperNova folding scheme for a CCS variant +/// `V`. +/// +/// This design is experimental, following the definition of accumulation +/// schemes where the incoming witnesses and instances are simply plain vectors +/// in the circuit's assignments. +pub struct HyperNova2 { + _t: PhantomData<(CM, V)>, +} + +impl FoldingSchemeDef + for HyperNova2 +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = PW; + type IU = PU; + + type TranscriptField = CM::Scalar; + type Arith = CCS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = HyperNovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = + ([CM::Commitment; N], NIMFSProof); +} + +/// [`NIMFSProofVar`] is the in-circuit variable for [`NIMFSProof`]. +#[derive(Clone)] +pub struct NIMFSProofVar { + /// [`NIMFSProofVar::sc_proof`] is the sum-check proof. + pub sc_proof: Vec>>, + /// [`NIMFSProofVar::sigmas`] is a vector of claimed internal sums defined + /// in Equation 9 + pub sigmas: Vec>, + /// [`NIMFSProofVar::thetas`] is a vector of claimed internal sums defined + /// in Equation 10 + pub thetas: Vec>, +} + +impl AllocVar, F> + for NIMFSProofVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let ns = cs.into(); + let cs = ns.cs(); + + let proof = f()?.borrow().clone(); + + Ok(NIMFSProofVar { + sc_proof: proof + .sc_proof + .iter() + .map(|v| Vec::new_variable(cs.clone(), || Ok(&v[..]), mode)) + .collect::>, SynthesisError>>()?, + sigmas: Vec::new_variable(cs.clone(), || Ok(&proof.sigmas[..]), mode)?, + thetas: Vec::new_variable(cs.clone(), || Ok(&proof.thetas[..]), mode)?, + }) + } +} + +impl GR1CSVar for NIMFSProofVar { + type Value = NIMFSProof; + + fn cs(&self) -> ConstraintSystemRef { + self.sc_proof + .iter() + .fold(ConstraintSystemRef::None, |cs, v| cs.or(v.cs())) + .or(self.sigmas.cs()) + .or(self.thetas.cs()) + } + + fn value(&self) -> Result { + Ok(NIMFSProof { + sc_proof: self + .sc_proof + .iter() + .map(|v| v.value()) + .collect::>, SynthesisError>>()?, + sigmas: self.sigmas.value()?, + thetas: self.thetas.value()?, + }) + } +} + +/// [`HyperNovaGadget`] is the in-circuit gadget for [`HyperNova`]. +pub struct HyperNovaGadget { + _t: PhantomData<(CM, V)>, +} + +impl FoldingSchemeDefGadget + for HyperNovaGadget +{ + type Widget = HyperNova; + + type CM = CM::Gadget2; + type RU = RUVar; + type IU = IUVar; + type VerifierKey = (); + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = NIMFSProofVar; +} + +impl + GroupBasedFoldingSchemePrimaryDef for HyperNova +{ + type Gadget = HyperNovaGadget; +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{ + error::Error, + rand::{Rng, thread_rng}, + }; + use sonobe_primitives::{ + circuits::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_hypernova_opt( + rounds: usize, + mut rng: impl Rng, + ) -> Result<(), Box> { + test_folding_scheme::>, M, N>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, M, N>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, M, N>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, M, N>( + 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_hypernova() -> Result<(), Box> { + let mut rng = thread_rng(); + test_hypernova_opt::<1, 1>(10, &mut rng)?; + test_hypernova_opt::<1, 3>(10, &mut rng)?; + test_hypernova_opt::<3, 1>(10, &mut rng)?; + test_hypernova_opt::<3, 3>(10, &mut rng)?; + test_hypernova_opt::<0, 5>(10, &mut rng)?; + test_hypernova_opt::<5, 0>(10, &mut rng)?; + Ok(()) + } +} diff --git a/crates/fs/src/hypernova/witnesses/circuits.rs b/crates/fs/src/hypernova/witnesses/circuits.rs new file mode 100644 index 000000000..a129b7eea --- /dev/null +++ b/crates/fs/src/hypernova/witnesses/circuits.rs @@ -0,0 +1,95 @@ +//! In-circuit variables for HyperNova 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::{CCCSWitness, LCCCSWitness}; + +/// [`LCCCSWitnessVar`] defines HyperNova's running witness variable. +#[derive(Debug, PartialEq)] +pub struct LCCCSWitnessVar { + /// [`LCCCSWitnessVar::w`] is the witness (to the circuit). + pub w: Vec, + /// [`LCCCSWitnessVar::r`] is the randomness for the witness commitment. + pub r: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for LCCCSWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let LCCCSWitness { w, r } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r: AllocVar::new_variable(cs.clone(), || Ok(r), mode)?, + }) + } +} + +impl GR1CSVar for LCCCSWitnessVar { + type Value = LCCCSWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r.cs()) + } + + fn value(&self) -> Result { + Ok(LCCCSWitness { + w: self.w.value()?, + r: self.r.value()?, + }) + } +} + +/// [`CCCSWitnessVar`] defines HyperNova's incoming witness variable. +#[derive(Debug, PartialEq)] +pub struct CCCSWitnessVar { + /// [`CCCSWitnessVar::w`] is the witness (to the circuit). + pub w: Vec, + /// [`CCCSWitnessVar::r`] is the randomness for the witness commitment. + pub r: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for CCCSWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let CCCSWitness { w, r } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r: AllocVar::new_variable(cs.clone(), || Ok(r), mode)?, + }) + } +} + +impl GR1CSVar for CCCSWitnessVar { + type Value = CCCSWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r.cs()) + } + + fn value(&self) -> Result { + Ok(CCCSWitness { + w: self.w.value()?, + r: self.r.value()?, + }) + } +} diff --git a/crates/fs/src/hypernova/witnesses/mod.rs b/crates/fs/src/hypernova/witnesses/mod.rs new file mode 100644 index 000000000..fbe217614 --- /dev/null +++ b/crates/fs/src/hypernova/witnesses/mod.rs @@ -0,0 +1,60 @@ +//! Definitions of out-of-circuit values and in-circuit variables for HyperNova +//! witnesses. + +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; + +use crate::FoldingWitness; + +pub mod circuits; + +/// [`LCCCSWitness`] defines HyperNova's running witness. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LCCCSWitness { + /// [`LCCCSWitness::w`] is the witness (to the circuit). + pub w: Vec, + /// [`LCCCSWitness::r`] is the randomness for the witness commitment. + pub r: CM::Randomness, +} + +impl FoldingWitness for LCCCSWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r)] + } +} + +impl Dummy<&Cfg> for LCCCSWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r: Default::default(), + } + } +} + +/// [`CCCSWitness`] defines HyperNova's incoming witness. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CCCSWitness { + /// [`CCCSWitness::w`] is the witness (to the circuit). + pub w: Vec, + /// [`CCCSWitness::r`] is the randomness for the witness commitment. + pub r: CM::Randomness, +} + +impl FoldingWitness for CCCSWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r)] + } +} + +impl Dummy<&Cfg> for CCCSWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r: Default::default(), + } + } +} diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs new file mode 100644 index 000000000..b1d5db2f2 --- /dev/null +++ b/crates/fs/src/lib.rs @@ -0,0 +1,136 @@ +#![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. +//! - `witnesses/`: Witness types. + +pub mod definitions; +pub mod hypernova; +pub mod nova; +pub mod ova; + +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 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); + 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..89104470e --- /dev/null +++ b/crates/fs/src/nova/algorithms/key_generator.rs @@ -0,0 +1,45 @@ +//! Key generation for Nova. + +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig}, + commitments::{CommitmentKey, GroupBasedCommitment}, + traits::SonobeField, +}; + +use crate::{ + Error, FoldingSchemeKeyGenerator, + nova::{AbstractNova, AbstractNova2}, +}; + +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 }) + } +} + +impl + FoldingSchemeKeyGenerator for AbstractNova2 +{ + 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..316fbbec6 --- /dev/null +++ b/crates/fs/src/nova/algorithms/preprocessor.rs @@ -0,0 +1,27 @@ +//! Preprocessing for Nova. + +use ark_std::rand::RngCore; +use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; + +use crate::{ + Error, FoldingSchemePreprocessor, + nova::{AbstractNova, AbstractNova2}, +}; + +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) + } +} + +impl + FoldingSchemePreprocessor for AbstractNova2 +{ + 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..de6ec8485 --- /dev/null +++ b/crates/fs/src/nova/algorithms/prover.rs @@ -0,0 +1,208 @@ +//! Proof generation for Nova. + +use ark_ff::One; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, ops::Mul, rand::RngCore}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, circuits::AssignmentsOwned, commitments::GroupBasedCommitment, + traits::SonobeField, transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + nova::{AbstractNova, AbstractNova2, NovaKey}, +}; + +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>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // 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 = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w).zip(&w.w).map(|(a, b)| rho * b + a).collect(), + r_w: W.r_w + w.r_w * rho, + }, + 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(&u.x).map(|(a, b)| rho * b + a).collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +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>, Self::Challenge), Error> { + let (W1, U1) = (W1.borrow(), U1.borrow()); + let (W2, U2) = (W2.borrow(), U2.borrow()); + + // 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 = pk.arith.evaluate_at(AssignmentsOwned::from(( + U1.u + U2.u, + cfg_iter!(U1.x).zip(&U2.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W1.w).zip(&W2.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W1.e) + .zip(&W2.e) + .map(|((a, b), c)| a - b - c) + .collect::>(); + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = { + transcript.add(&U1); + transcript.add(&U2); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + let rho_squared = rho * rho; + + Ok(( + Self::RW { + e: cfg_iter!(W1.e) + .zip(&t) + .zip(&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(&W2.w) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W1.r_w + W2.r_w * rho, + }, + 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(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +impl + FoldingSchemeProver<1, 1> for AbstractNova2 +{ + #[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], + mut rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // 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 = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + let (cm_w, r_w) = CM::commit(&pk.ck, w, &mut rng)?; + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, &mut rng)?; + + let pi = (cm_w, cm_t); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + r_w * rho, + }, + Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + pi, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/nova/algorithms/verifier.rs b/crates/fs/src/nova/algorithms/verifier.rs new file mode 100644 index 000000000..bfc3f89b9 --- /dev/null +++ b/crates/fs/src/nova/algorithms/verifier.rs @@ -0,0 +1,113 @@ +//! Proof verification for Nova. + +use ark_std::{borrow::Borrow, cfg_iter, ops::Mul}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, commitments::GroupBasedCommitment, traits::SonobeField, + transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeVerifier, + nova::{AbstractNova, AbstractNova2}, +}; + +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); + transcript.add(&u); + transcript.add(cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + 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(&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); + transcript.add(&U2); + transcript.add(cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + 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(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + +impl + FoldingSchemeVerifier<1, 1> for AbstractNova2 +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + pi: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let (cm_w, cm_t) = pi; + + Ok(Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .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..a69bb0835 --- /dev/null +++ b/crates/fs/src/nova/circuits/verifier.rs @@ -0,0 +1,158 @@ +//! 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<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + 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)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +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<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U1)?; + transcript.add(&U2)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + 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)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +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)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + 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..6d0c81007 --- /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_w, &self.cm_e] + } + + 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..a2408bafb --- /dev/null +++ b/crates/fs/src/nova/instances/mod.rs @@ -0,0 +1,102 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! instances. + +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, + transcripts::Absorbable, +}; + +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 { + const N_COMMITMENTS: usize = 2; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm_e, &self.cm_w] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for RunningInstance { + fn dummy(cfg: &Cfg) -> 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 { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for IncomingInstance { + fn dummy(cfg: &Cfg) -> 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/mod.rs b/crates/fs/src/nova/mod.rs new file mode 100644 index 000000000..e9988cf2d --- /dev/null +++ b/crates/fs/src/nova/mod.rs @@ -0,0 +1,372 @@ +//! 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::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; +use sonobe_primitives::{ + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, + }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + traits::{CF2, SonobeField}, +}; + +use self::{ + instances::{ + IncomingInstance as IU, RunningInstance as RU, + circuits::{IncomingInstanceVar as IUVar, RunningInstanceVar as RUVar}, + }, + witnesses::{IncomingWitness as IW, RunningWitness as RW}, +}; +use crate::{ + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondaryDef, PlainInstance as PU, PlainWitness as PW, +}; + +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; + +/// [`NovaKey`] is Nova's decider key. +#[derive(Clone)] +pub struct NovaKey { + arith: Arc, + ck: Arc, +} + +impl DeciderKey for NovaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &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 })) + } +} + +// 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; +} + +// 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. +/// [`AbstractNova2`] implements the Nova folding scheme which can operate on +/// both the primary and secondary curves. +/// +/// This design is experimental, following the definition of accumulation +/// schemes where the incoming witnesses and instances are simply plain vectors +/// in the circuit's assignments. +pub struct AbstractNova2 { + _t: PhantomData<(CM, TF)>, +} + +/// [`Nova2`] is the main Nova folding scheme on the primary curve. +pub type Nova2 = + AbstractNova2::Scalar, CHALLENGE_BITS>; + +/// [`CycleFoldNova2`] is the Nova folding scheme on the secondary curve which +/// can be used as the folding scheme for folding CycleFold instances. +pub type CycleFoldNova2 = + AbstractNova2::Commitment>, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractNova2 +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = PW; + type IU = PU; + + 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, 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 CM = CM; + type RU = RUVar; + type IU = IUVar; + type VerifierKey = (); + 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::thread_rng}; + use sonobe_primitives::{ + circuits::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, + )?; + + 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, + )?; + 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..5f144df36 --- /dev/null +++ b/crates/fs/src/nova/witnesses/mod.rs @@ -0,0 +1,66 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! witnesses. + +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::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 { + const N_OPENINGS: usize = 2; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.e, &self.r_e), (&self.w, &self.r_w)] + } +} + +impl Dummy<&Cfg> for RunningWitness { + fn dummy(cfg: &Cfg) -> 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 { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r_w)] + } +} + +impl Dummy<&Cfg> for IncomingWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r_w: Default::default(), + } + } +} diff --git a/crates/fs/src/ova/algorithms/key_generator.rs b/crates/fs/src/ova/algorithms/key_generator.rs new file mode 100644 index 000000000..f897b3466 --- /dev/null +++ b/crates/fs/src/ova/algorithms/key_generator.rs @@ -0,0 +1,27 @@ +//! Key generation for Ova. + +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig}, + commitments::{CommitmentKey, GroupBasedCommitment}, + traits::SonobeField, +}; + +use crate::{Error, FoldingSchemeKeyGenerator, ova::AbstractOva}; + +impl + FoldingSchemeKeyGenerator for AbstractOva +{ + 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() + cfg.n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key generated by preprocessing is too short for the R1CS instance" + .into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} diff --git a/crates/fs/src/ova/algorithms/mod.rs b/crates/fs/src/ova/algorithms/mod.rs new file mode 100644 index 000000000..7c51fe3d8 --- /dev/null +++ b/crates/fs/src/ova/algorithms/mod.rs @@ -0,0 +1,6 @@ +//! Implementations folding scheme algorithms for Ova. + +pub mod key_generator; +pub mod preprocessor; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/ova/algorithms/preprocessor.rs b/crates/fs/src/ova/algorithms/preprocessor.rs new file mode 100644 index 000000000..36729fb56 --- /dev/null +++ b/crates/fs/src/ova/algorithms/preprocessor.rs @@ -0,0 +1,18 @@ +//! Preprocessing for Ova. + +use ark_std::rand::RngCore; +use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; + +use crate::{Error, FoldingSchemePreprocessor, ova::AbstractOva}; + +impl + FoldingSchemePreprocessor for AbstractOva +{ + fn preprocess( + (n_constraints, n_witnesses): (usize, usize), + mut rng: impl RngCore, + ) -> Result { + let ck = CM::generate_key(n_constraints + n_witnesses, &mut rng)?; + Ok(ck) + } +} diff --git a/crates/fs/src/ova/algorithms/prover.rs b/crates/fs/src/ova/algorithms/prover.rs new file mode 100644 index 000000000..12fc69f94 --- /dev/null +++ b/crates/fs/src/ova/algorithms/prover.rs @@ -0,0 +1,76 @@ +//! Proof generation for Ova. + +use ark_ff::One; +use ark_std::{borrow::Borrow, cfg_iter, rand::RngCore}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, circuits::Assignments, commitments::GroupBasedCommitment, + traits::SonobeField, transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + ova::{AbstractOva, OvaKey}, +}; + +impl + FoldingSchemeProver<1, 1> for AbstractOva +{ + #[allow(non_snake_case)] + fn prove( + pk: &OvaKey, + 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>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the original Nova paper. + let z1 = Assignments::from((U.u, &U.x, &W.w)); + let z2 = Assignments::from((CM::Scalar::one(), &u[..], &w[..])); + let t = pk.arith.evaluate_rows(|((a, b), c)| { + let az1: CM::Scalar = a.iter().map(|(val, col)| z1[*col] * val).sum(); + let az2: CM::Scalar = a.iter().map(|(val, col)| z2[*col] * val).sum(); + let bz1: CM::Scalar = b.iter().map(|(val, col)| z1[*col] * val).sum(); + let bz2: CM::Scalar = b.iter().map(|(val, col)| z2[*col] * val).sum(); + let cz1: CM::Scalar = c.iter().map(|(val, col)| z1[*col] * val).sum(); + let cz2: CM::Scalar = c.iter().map(|(val, col)| z2[*col] * val).sum(); + Ok(az1 * bz2 + az2 * bz1 - z2[0] * cz1 - z1[0] * cz2) + })?; + + let (cm, r) = CM::commit(&pk.ck, &[w, &t[..]].concat(), rng)?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r: W.r + r * rho, + }, + Self::RU { + u: U.u + rho, + cm: U.cm + cm * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/ova/algorithms/verifier.rs b/crates/fs/src/ova/algorithms/verifier.rs new file mode 100644 index 000000000..72754dd9e --- /dev/null +++ b/crates/fs/src/ova/algorithms/verifier.rs @@ -0,0 +1,43 @@ +//! Proof verification for Ova. + +use ark_std::{borrow::Borrow, cfg_iter}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, commitments::GroupBasedCommitment, traits::SonobeField, + transcripts::Transcript, +}; + +use crate::{Error, FoldingSchemeVerifier, ova::AbstractOva}; + +impl + FoldingSchemeVerifier<1, 1> for AbstractOva +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + u: U.u + rho, + cm: U.cm + *cm * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} diff --git a/crates/fs/src/ova/circuits/mod.rs b/crates/fs/src/ova/circuits/mod.rs new file mode 100644 index 000000000..ffed2641a --- /dev/null +++ b/crates/fs/src/ova/circuits/mod.rs @@ -0,0 +1,3 @@ +//! In-circuit gadgets for Ova. + +pub mod verifier; diff --git a/crates/fs/src/ova/circuits/verifier.rs b/crates/fs/src/ova/circuits/verifier.rs new file mode 100644 index 000000000..8581e809d --- /dev/null +++ b/crates/fs/src/ova/circuits/verifier.rs @@ -0,0 +1,92 @@ +//! Partial and full in-circuit verifier implementations for Ova. + +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, ova::AbstractOvaGadget, +}; + +impl FoldingSchemePartialVerifierGadget<1, 1> + for AbstractOvaGadget +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<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: CM::CommitmentVar::new_witness(U.cm.cs().or(proof.cs()).or(rho.cs()), || { + Ok(U.cm.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> + for AbstractOvaGadget +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)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: proof.scalar_mul_le(rho_bits.iter())? + &U.cm, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} diff --git a/crates/fs/src/ova/instances/circuits.rs b/crates/fs/src/ova/instances/circuits.rs new file mode 100644 index 000000000..3b3ee1a29 --- /dev/null +++ b/crates/fs/src/ova/instances/circuits.rs @@ -0,0 +1,119 @@ +//! In-circuit variables for Ova instances. + +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + fields::fp::FpVar, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; + +use super::RunningInstance; +use crate::FoldingInstanceVar; + +/// [`RunningInstanceVar`] defines Ova's running instance variable. +#[derive(Clone, Debug, PartialEq)] +pub struct RunningInstanceVar { + /// [`RunningInstanceVar::u`] is the constant term. + pub u: CM::ScalarVar, + /// [`RunningInstanceVar::cm`] is the combined witness and error term + /// commitment. + pub cm: 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 { u, cm, x } = v.borrow(); + Ok(Self { + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + cm: AllocVar::new_variable(cs.clone(), || Ok(cm), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningInstanceVar { + type Value = RunningInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.u.cs().or(self.cm.cs()).or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(RunningInstance { + u: self.u.value()?, + cm: self.cm.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.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 { + u: cond.select(&true_value.u, &false_value.u)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + cm: cond.select(&true_value.cm, &false_value.cm)?, + }) + } +} + +impl FoldingInstanceVar for RunningInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm] + } + + 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 { + u: AllocVar::new_witness(cs.clone(), || Ok(&u.u))?, + cm: AllocVar::new_witness(cs.clone(), || Ok(&u.cm))?, + x, + }) + } +} diff --git a/crates/fs/src/ova/instances/mod.rs b/crates/fs/src/ova/instances/mod.rs new file mode 100644 index 000000000..498373588 --- /dev/null +++ b/crates/fs/src/ova/instances/mod.rs @@ -0,0 +1,58 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Ova +//! instances. + +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, + transcripts::Absorbable, +}; + +use crate::FoldingInstance; + +pub mod circuits; + +/// [`RunningInstance`] defines Ova's running instance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningInstance { + /// [`RunningInstance::u`] is the constant term. + pub u: CM::Scalar, + /// [`RunningInstance::cm`] is the combined witness and error term + /// commitment. + pub cm: CM::Commitment, + /// [`RunningInstance::x`] is the vector of public inputs (to the circuit). + pub x: Vec, +} + +impl FoldingInstance for RunningInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for RunningInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + u: Default::default(), + cm: 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.absorb_into(dest); + } +} diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs new file mode 100644 index 000000000..f9299091c --- /dev/null +++ b/crates/fs/src/ova/mod.rs @@ -0,0 +1,259 @@ +//! This module implements the Ova folding scheme, which is introduced in this +//! [note]. +//! +//! [note]: https://hackmd.io/V4838nnlRKal9ZiTHiGYzw + +use ark_r1cs_std::boolean::Boolean; +use ark_std::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; +use sonobe_primitives::{ + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, + }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + traits::{CF2, SonobeField}, +}; + +use self::{ + instances::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, + witnesses::RunningWitness as RW, +}; +use crate::{ + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondaryDef, PlainInstance as IU, PlainInstanceVar as IUVar, + PlainWitness as IW, +}; + +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; + +/// [`OvaKey`] is Ova's decider key. +#[derive(Clone)] +pub struct OvaKey { + arith: Arc, + ck: Arc, +} + +impl DeciderKey for OvaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &Self::ArithConfig { + self.arith.config() + } +} + +impl Relation, RU> for OvaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w.w, e: &[] }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &[&w.w[..], &e].concat(), &w.r, &u.cm)?; + Ok(()) + } +} + +impl Relation, IU> for OvaKey +where + A: ArithRelation, Vec>, + CM: CommitmentDef, +{ + type Error = Error; + + fn check_relation(&self, w: &IW, u: &IU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + Ok(()) + } +} + +impl WitnessInstanceSampler, IU> + for OvaKey +{ + type Source = AssignmentsOwned; + type Error = Error; + + fn sample( + &self, + z: Self::Source, + _rng: impl RngCore, + ) -> Result<(IW, IU), Error> { + Ok((z.private.into(), z.public.into())) + } +} + +impl WitnessInstanceSampler, RU> for OvaKey +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, r) = CM::commit(&self.ck, &[&w[..], &e].concat(), &mut rng)?; + Ok((RW { w, r }, RU { x, cm, u })) + } +} + +/// [`AbstractOva`] implements the Ova folding scheme which can operate on +/// both the primary and secondary curves. +pub struct AbstractOva { + _t: PhantomData<(CM, TF)>, +} + +/// [`Ova`] is the main Ova folding scheme on the primary curve. +pub type Ova = + AbstractOva::Scalar, CHALLENGE_BITS>; + +/// [`CycleFoldOva`] is the Ova folding scheme on the secondary curve which can +/// be used as the folding scheme for folding CycleFold instances. +pub type CycleFoldOva = + AbstractOva::Commitment>, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractOva +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = TF; + type Arith = R1CS; + + type Config = (usize, usize); + type PublicParam = CM::Key; + type DeciderKey = OvaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = CM::Commitment; +} + +/// [`AbstractOvaGadget`] is the in-circuit gadget for [`AbstractOva`]. +pub struct AbstractOvaGadget { + _t: PhantomData, +} + +impl FoldingSchemeDefGadget + for AbstractOvaGadget +where + CM: CommitmentDefGadget, +{ + type Widget = AbstractOva; + + type CM = CM; + type RU = RUVar; + type IU = IUVar; + type VerifierKey = (); + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = CM::CommitmentVar; +} + +impl GroupBasedFoldingSchemePrimaryDef + for AbstractOva +{ + type Gadget = AbstractOvaGadget; +} + +impl GroupBasedFoldingSchemeSecondaryDef + for AbstractOva, CHALLENGE_BITS> +{ + type Gadget = AbstractOvaGadget; +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fq, Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{error::Error, rand::thread_rng}; + use sonobe_primitives::{ + circuits::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_ova_opt( + rounds: usize, + mut rng: impl RngCore, + ) -> Result<(), Box> { + let config = (4, 4); + + test_folding_scheme::, TF>, 1, 1>( + config, + 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>( + config, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + Ok(()) + } + + #[test] + fn test_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ova_opt::(10, &mut rng)?; + test_ova_opt::(10, &mut rng)?; + Ok(()) + } +} diff --git a/crates/fs/src/ova/witnesses/circuits.rs b/crates/fs/src/ova/witnesses/circuits.rs new file mode 100644 index 000000000..515c22a29 --- /dev/null +++ b/crates/fs/src/ova/witnesses/circuits.rs @@ -0,0 +1,53 @@ +//! In-circuit variables for Ova 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::RunningWitness; + +/// [`RunningWitnessVar`] defines Ova's running witness variable. +#[derive(Debug, PartialEq)] +pub struct RunningWitnessVar { + /// [`RunningWitnessVar::w`] is the witness (to the circuit). + pub w: Vec, + /// [`RunningWitnessVar::r`] is the randomness for the witness commitment. + pub r: 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 { w, r } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r: AllocVar::new_variable(cs.clone(), || Ok(r), mode)?, + }) + } +} + +impl GR1CSVar for RunningWitnessVar { + type Value = RunningWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r.cs()) + } + + fn value(&self) -> Result { + Ok(RunningWitness { + w: self.w.value()?, + r: self.r.value()?, + }) + } +} diff --git a/crates/fs/src/ova/witnesses/mod.rs b/crates/fs/src/ova/witnesses/mod.rs new file mode 100644 index 000000000..7fefbf852 --- /dev/null +++ b/crates/fs/src/ova/witnesses/mod.rs @@ -0,0 +1,34 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Ova +//! witnesses. + +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; + +use crate::FoldingWitness; + +pub mod circuits; + +/// [`RunningWitness`] defines Ova's running witness. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningWitness { + /// [`RunningWitness::w`] is the witness (to the circuit). + pub w: Vec, + /// [`RunningWitness::r`] is the randomness for the witness commitment. + pub r: CM::Randomness, +} + +impl FoldingWitness for RunningWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r)] + } +} + +impl Dummy<&Cfg> for RunningWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r: Default::default(), + } + } +} diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml new file mode 100644 index 000000000..a57dda5d7 --- /dev/null +++ b/crates/ivc/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "sonobe-ivc" +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-r1cs-std = { workspace = true } +ark-relations = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +num-bigint = { workspace = true, features = ["rand"] } +thiserror = { workspace = true } + +sonobe-primitives = { workspace = true } +sonobe-fs = { 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 = [] +parallel = [ + "sonobe-fs/parallel", +] \ No newline at end of file diff --git a/crates/ivc/src/compilers/cyclefold/adapters/hypernova.rs b/crates/ivc/src/compilers/cyclefold/adapters/hypernova.rs new file mode 100644 index 000000000..a552e22da --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/hypernova.rs @@ -0,0 +1,183 @@ +//! HyperNova CycleFold adapter that bridges HyperNova into the CycleFold IVC +//! compiler. + +use ark_ff::{PrimeField, Zero}; +use ark_r1cs_std::{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, hypernova::HyperNova, nova::CycleFoldNova, ova::CycleFoldOva, +}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, + }, + arithmetizations::{ccs::CCSVariant, r1cs::R1CSConfig}, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// [`HyperNovaCycleFoldCircuit`] defines CycleFold circuit for HyperNova. +pub struct HyperNovaCycleFoldCircuit +{ + r: Vec, + points: Vec, +} + +impl Default + for HyperNovaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); M + N], + } + } +} + +impl + CycleFoldCircuit> for HyperNovaCycleFoldCircuit +{ + 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[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + let mut p_folded = C::Var::zero(); + for i in (1..M + N).rev() { + p_folded += &points[i]; + p_folded = p_folded.scalar_mul_le(rho_bits.iter())?; + } + p_folded += &points[0]; + + Self::mark_point_as_public(&p_folded) + } +} + +impl< + CM: GroupBasedCommitment, + V: CCSVariant, + const M: usize, + const N: usize, + const CHALLENGE_BITS: usize, +> FoldingSchemeCycleFoldExt for HyperNova +{ + const N_CYCLEFOLDS: usize = 1; + + type CFCircuit = HyperNovaCycleFoldCircuit; + + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + _proof: &Self::Proof, + rho: Self::Challenge, + ) -> Vec { + vec![HyperNovaCycleFoldCircuit { + r: rho.into(), + points: Us + .iter() + .map(|U| U.borrow().cm) + .chain(us.iter().map(|u| u.borrow().cm)) + .collect(), + }] + } + + #[allow(non_snake_case)] + fn to_cyclefold_inputs( + Us: [::RU; M], + us: [::IU; N], + UU: ::RU, + _proof: ::Proof, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + Ok(vec![ + once(EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?) + .chain( + Us.into_iter() + .map(|U| U.cm) + .chain(us.into_iter().map(|u| u.cm)) + .chain(once(UU.cm)) + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + +/// [`HyperNovaOvaIVC`] defines a CycleFold-based IVC using HyperNova as the +/// primary folding scheme and Ova as the secondary folding scheme. +pub type HyperNovaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + +/// [`HyperNovaNovaIVC`] defines a CycleFold-based IVC using HyperNova as the +/// primary folding scheme and Nova as the secondary folding scheme. +pub type HyperNovaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(test)] +mod tests { + use ark_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}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_hypernova_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, (2048, 2048), Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + + #[test] + fn test_hypernova_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, + )?; + + Ok(()) + } +} 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..e8a843ed1 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs @@ -0,0 +1,6 @@ +//! Per-scheme adapters that implement [`super::CycleFoldCircuit`] for supported +//! folding schemes. + +pub mod hypernova; +pub mod nova; +pub mod ova; 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..a2d199a42 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -0,0 +1,257 @@ +//! 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}, + ova::CycleFoldOva, +}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + group::emulated::EmulatedAffineVar, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, + }, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +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[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + Self::mark_point_as_public(&(points[1].scalar_mul_le(rho_bits.iter())? + &points[0])) + } +} + +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>, + rho: Self::Challenge, + ) -> Vec { + vec![ + NovaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm_e, *proof], + }, + NovaCycleFoldCircuit { + r: rho.into(), + 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>, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + 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>, + rho_bits: Self::Challenge, + ) -> Vec { + let rho = CM::Scalar::from_bits_le(&rho_bits); + vec![ + NovaCycleFoldCircuit { + r: rho_bits.into(), + points: vec![*proof, U2.borrow().cm_e], + }, + NovaCycleFoldCircuit { + r: rho_bits.into(), + points: vec![U1.borrow().cm_e, U2.borrow().cm_e * rho + proof], + }, + NovaCycleFoldCircuit { + r: rho_bits.into(), + 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>, + rho_bits: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho_bits = rho_bits.to_vec(); + 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 x = + 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, x.clone()] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho.clone()) + .chain([U1.cm_e, x, 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(), + ]) + } +} + +/// [`NovaOvaIVC`] defines a CycleFold-based IVC using Nova as the primary +/// folding scheme and Ova as the secondary folding scheme. +pub type NovaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + +/// [`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(test)] +mod tests { + use ark_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}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_nova_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, (2048, 2048), Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + + #[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, + )?; + + Ok(()) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs new file mode 100644 index 000000000..6fb0e5d46 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs @@ -0,0 +1,164 @@ +//! Ova CycleFold adapter that bridges Ova into the CycleFold IVC compiler. + +use ark_ff::{PrimeField, Zero}; +use ark_r1cs_std::{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, + ova::{CycleFoldOva, Ova}, +}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, + }, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// [`OvaCycleFoldCircuit`] defines CycleFold circuit for Ova. +pub struct OvaCycleFoldCircuit { + r: Vec, + points: Vec, +} + +impl Default + for OvaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); 2], + } + } +} + +impl CycleFoldCircuit> + for OvaCycleFoldCircuit +{ + 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[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + Self::mark_point_as_public(&(points[1].scalar_mul_le(rho_bits.iter())? + &points[0])) + } +} + +impl FoldingSchemeCycleFoldExt<1, 1> + for Ova +{ + const N_CYCLEFOLDS: usize = 1; + + type CFCircuit = OvaCycleFoldCircuit; + + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + [U]: &[impl Borrow; 1], + _us: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + rho: Self::Challenge, + ) -> Vec { + vec![OvaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm, *proof], + }] + } + + #[allow(non_snake_case)] + fn to_cyclefold_inputs( + [U]: [::RU; 1], + _us: [::IU; 1], + UU: ::RU, + proof: ::Proof<1, 1>, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + Ok(vec![ + once(EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?) + .chain([U.cm, proof, UU.cm].into_iter().flat_map(|p| [p.x, p.y])) + .collect(), + ]) + } +} + +/// [`OvaOvaIVC`] defines a CycleFold-based IVC using Ova as the primary folding +/// scheme and Ova as the secondary folding scheme. +pub type OvaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + +/// [`OvaNovaIVC`] defines a CycleFold-based IVC using Ova as the primary +/// folding scheme and Nova as the secondary folding scheme. +pub type OvaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(test)] +mod tests { + use ark_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}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_ova_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + ( + (65536, 65536), + (2048, 2048), + Arc::new(GriffinParams::new(16, 5, 9)), + ), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + + #[test] + fn test_ova_nova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + ((65536, 65536), 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs new file mode 100644 index 000000000..18ef8cd04 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -0,0 +1,260 @@ +//! Augmented and CycleFold circuits for the CycleFold-based IVC compiler. + +use ark_ff::PrimeField; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + convert::ToConstraintFieldGadget, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use sonobe_fs::{ + FoldingInstanceVar, FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, +}; +use sonobe_primitives::{ + arithmetizations::Arith, + circuits::FCircuit, + commitments::CommitmentDef, + traits::{Dummy, SonobeCurve}, + transcripts::{Transcript, TranscriptGadget}, +}; + +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: Transcript, +> { + pub(super) hash_config: T::Config, + pub(super) arith1_config: &'a ::Config, + pub(super) arith2_config: &'a ::Config, + pub(super) step_circuit: &'a FC, +} + +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: Transcript, +{ + /// [`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::Gadget::new_with_pp_hash( + &self.hash_config, + &FpVar::new_witness(cs.clone(), || Ok(pp_hash))?, + )?; + let sponge = hash.separate_domain("sponge".as_ref())?; + let mut transcript = 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))?; + + // 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, rho) = 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. + let mut cf_UU = cf_U; + for ((cf_u, cf_u_x), cf_proof) in cf_us + .iter() + // 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`. + .zip(FS1::to_cyclefold_inputs([U], [u], UU, proof, rho)?) + .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 + .generate_step_constraints(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()?; + // This line "converts" `uu_x` from witnesses to public inputs. + // Instead of directly modifying the constraint system, we explicitly + // allocate a public input and enforce that its value is indeed `uu_x`. + // While comparing `uu_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 `uu_x` computed in-circuit. + uu_x.enforce_equal(&FpVar::new_input(cs.clone(), || { + Ok(uu_x.value().unwrap_or_default()) + })?)?; + + 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: Transcript, +{ + 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::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: &V, + ) -> 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(), || x.value())?.enforce_equal(x)?; + } + Ok(()) + } + + /// [`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/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs new file mode 100644 index 000000000..2a42bf0ba --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -0,0 +1,350 @@ +//! Implementation of the CycleFold-based IVC compiler. +//! +//! 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. + +use ark_ff::Zero; +use ark_relations::gr1cs::{ConstraintSystem, SynthesisError}; +use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; +use sonobe_fs::{ + DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeDefGadget, + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, +}; +use sonobe_primitives::{ + algebra::field::emulated::EmulatedFieldVar, + arithmetizations::Arith, + circuits::{ArithExtractor, AssignmentsExtractor, FCircuit}, + commitments::CommitmentDef, + relations::WitnessInstanceSampler, + traits::{CF1, CF2, Dummy, SonobeCurve}, + transcripts::Transcript, +}; + +use crate::{ + Error, IVC, + compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit}, +}; + +pub mod adapters; +pub mod circuits; + +/// [`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, + rho: Self::Challenge, + ) -> 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, + rho: ::Challenge, + ) -> Result< + Vec< + Vec< + EmulatedFieldVar< + ::Scalar, + CF2<::Commitment>, + >, + >, + >, + SynthesisError, + >; +} + +/// [`Key`] is the prover / verifier key for the CycleFold-based IVC scheme. +pub struct Key( + pub FS1::DeciderKey, + pub FS2::DeciderKey, + 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 IVC 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>>, +{ + 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; + + 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, + )) + } + + 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 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 = FS1::Arith::default(); + + loop { + let new_arith1 = { + let cs = ArithExtractor::new(); + cs.execute_synthesizer(AugmentedCircuit:: { + hash_config: hash_config.clone(), + arith1_config: arith1.config(), + arith2_config: arith2.config(), + step_circuit, + })?; + cs.arith::()? + }; + if new_arith1.config() == arith1.config() { + break; + } + arith1 = new_arith1; + } + + let dk1 = FS1::generate_keys(pp1, arith1)?; + let dk2 = FS2::generate_keys(pp2, arith2)?; + + let pp_hash = Zero::zero(); // TODO + + Ok(( + Key(dk1.clone(), dk2.clone(), (hash_config.clone(), pp_hash)), + Key(dk1, dk2, (hash_config, pp_hash)), + )) + } + + #[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, *pp_hash); + let mut transcript = hash.separate_domain("transcript".as_ref()); + + let arith1_config = dk1.to_arith_config(); + let arith2_config = dk2.to_arith_config(); + let augmented_circuit = AugmentedCircuit:: { + hash_config: hash_config.clone(), + arith1_config, + arith2_config, + step_circuit, + }; + + let mut WW = Dummy::dummy(arith1_config); + let mut UU = 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 = Dummy::dummy(arith2_config); + let mut cf_WW = Dummy::dummy(arith2_config); + + if i != 0 { + let challenge; + (WW, UU, proof, challenge) = FS1::prove( + dk1.to_pk(), + &mut transcript, + &[W], + &[U], + &[w], + &[u], + &mut rng, + )?; + + let cf_circuits = FS1::to_cyclefold_circuits(&[U], &[u], &proof, challenge); + for (i, cf_circuit) in cf_circuits.into_iter().enumerate() { + let 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 cs = AssignmentsExtractor::new(); + let (next_state, external_outputs) = cs.execute_fn(|cs| { + 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), + )) + } + + #[allow(non_snake_case)] + fn verify>( + Key(dk1, dk2, (hash_config, pp_hash)): &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> { + if i == 0 { + return (initial_state == current_state) + .then_some(()) + .ok_or(Error::IVCVerificationFail); + } + + let hash = T::new_with_pp_hash(hash_config, *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(()) + } +} 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..790b4f534 --- /dev/null +++ b/crates/ivc/src/lib.rs @@ -0,0 +1,264 @@ +#![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_relations::gr1cs::SynthesisError; +use ark_std::rand::RngCore; +use sonobe_fs::Error as FoldingError; +use sonobe_primitives::{arithmetizations::Error as ArithError, circuits::FCircuit, traits::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::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, +} + +/// [`IVC`] defines the interface of Incremental Verifiable Computation schemes. +/// It follows the general definition of proof/argument systems, with +/// preprocessing, key generation, proving, and verification algorithms. +pub trait IVC { + /// [`IVC::Field`] defines the field over which the IVC scheme operates. + type Field: PrimeField; + + /// [`IVC::Config`] defines the configuration (e.g., the size of public + /// parameters) for the IVC scheme. + type Config; + /// [`IVC::PublicParam`] defines the public parameters produced by + /// preprocessing. + type PublicParam; + /// [`IVC::ProverKey`] defines the prover key type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a prover key + /// for one step circuit cannot be used for another step circuit. + type ProverKey; + /// [`IVC::VerifierKey`] defines the verifier key type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a verifier key + /// for one step circuit cannot be used for another step circuit. + type VerifierKey; + /// [`IVC::Proof`] defines the proof type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a proof for + /// one step circuit cannot be used for another step circuit. + type Proof: for<'a> Dummy<&'a Self::ProverKey>; + + /// [`IVC::preprocess`] defines the preprocessing algorithm, which is a + /// randomized algorithm that takes as input the config / parameterization + /// `config` of the IVC 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. + /// + /// This is usually called once for the given configuration and can be + /// reused for generating multiple keys for different step circuits, as long + /// as the step circuits conform to the configuration. + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; + + /// [`IVC::generate_keys`] defines the key generation algorithm, which is a + /// deterministic algorithm that takes as input the public parameters `pp` + /// and the step circuit `step_circuit`, and outputs a prover key and a + /// verifier key. + #[allow(clippy::type_complexity)] + fn generate_keys>( + pp: Self::PublicParam, + step_circuit: &FC, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + + /// [`IVC::prove`] defines the proof updating algorithm, which is a + /// (probably) randomized algorithm that takes as input the prover key `pk`, + /// the step circuit `step_circuit`, the current step `i`, the initial state + /// `initial_state`, the current state `current_state`, the external inputs + /// `external_inputs`, and the current proof `current_proof`. + /// It executes the step circuit on the current state and external inputs, + /// and outputs its returned next state and external outputs, along with the + /// new proof. + /// + /// Here, `current_proof` attests that `current_state` is correctly derived + /// from `initial_state` after `i` steps of executing `step_circuit`, and + /// the returned next proof attests that the next state is correctly derived + /// from `initial_state` after `i+1` steps with the given `external_inputs`. + /// + /// The prover may further use `rng` as the randomness source. + #[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>; + + /// [`IVC::verify`] defines the proof verification algorithm, which is a + /// deterministic algorithm that takes as input the verifier key `vk`, the + /// current step `i`, the initial state `initial_state`, the current state + /// `current_state`, and the proof `proof`, and outputs `Ok(())` if the + /// proof is valid, or an error otherwise. + fn verify>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + proof: &Self::Proof, + ) -> 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. +pub struct IVCStatefulProver<'a, FC: FCircuit, I: IVC> { + pk: &'a I::ProverKey, + step_circuit: &'a FC, + i: usize, + initial_state: FC::State, + current_state: FC::State, + 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) + } +} + +/// [`Decider`] defines a decider / proof-compression SNARK, which produces a +/// final succinct zero-knowledge proof from an IVC proof. +// TODO (@winderica): Still WIP +pub trait Decider { + /// [`Decider::IVC`] defines the underlying IVC scheme that the decider + /// compiles. + type IVC: IVC; + + /// [`Decider::ProverKey`] defines the prover key type for the decider. + type ProverKey; + /// [`Decider::VerifierKey`] defines the verifier key type for the decider. + type VerifierKey; + /// [`Decider::Instance`] defines the instance type for the decider. + type Instance; + /// [`Decider::Witness`] defines the witness type for the decider. + type Witness; + /// [`Decider::Proof`] defines the proof type for the decider. + type Proof; + + /// [`Decider::preprocess_and_generate_keys`] preprocesses the IVC prover + /// key `ivc_pk` and generates the decider's prover key and verifier key. + /// + /// This can be seen as a SNARK with circuit-specific setup. + // TODO (@winderica): consider universal/transparent setup + fn preprocess_and_generate_keys( + ivc_pk: &::ProverKey, + rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + + /// [`Decider::prove`] generates a decider proof from the given IVC proof + /// and instance/witness. + fn prove( + pk: &Self::ProverKey, + w: &Self::Witness, + x: &Self::Instance, + rng: impl RngCore, + ) -> Result; + + /// [`Decider::verify`] verifies the decider proof against the given + /// instance. + fn verify(vk: &Self::VerifierKey, x: &Self::Instance, proof: &Self::Proof) + -> Result<(), Error>; +} + +#[cfg(test)] +mod tests { + use ark_std::{error::Error, rand::Rng}; + + use super::*; + + 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(); + + let mut prover = IVCStatefulProver::<_, I>::new(&pk, &step_circuit, initial_state)?; + + 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(()) + } +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index feca812a6..15b3e513f 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -17,20 +17,25 @@ ark-r1cs-std = { workspace = true } ark-serialize = { workspace = true } num-bigint = { workspace = true, features = ["rand"] } num-integer = { workspace = true } +num-traits = { workspace = true } rayon = { workspace = true } +sha3 = { workspace = true } thiserror = { workspace = true } -sonobe-traits = { workspace = true } - [dev-dependencies] ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } ark-pallas = { workspace = true, features = ["curve", "r1cs"] } -ark-vesta = { 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 = ["parallel"] +default = [] parallel = [ + "ark-poly-commit/parallel", "ark-relations/parallel", "ark-r1cs-std/parallel", - "sonobe-traits/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..096a0d1ea --- /dev/null +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -0,0 +1,1561 @@ +//! 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}, + }, + }, + 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`] computes the bounds after left-shifting by `shift` bits. + 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::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 { + // For a field `F`, we consider an integer `x` to be safe if and only if + // `-(|F| - 1) / 2 <= x <= (|F| - 1) / 2`. + let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); + (self.0 >= -&limit && self.1 <= 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()?); + let (sign, abs) = v.into_parts(); + if abs >= Target::MODULUS.into() { + return Err(SynthesisError::Unsatisfiable); + } + match sign { + Sign::Plus | Sign::NoSign => Ok(Target::from(abs)), + Sign::Minus => Ok(Target::zero() - Target::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 + } +} + +impl LimbedVar { + /// [`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> { + let len = max(self.limbs.len(), other.limbs.len()); + let zero = FpVar::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.limbs.get(i).unwrap_or(&zero); + let y = other.limbs.get(i).unwrap_or(&zero); + 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.limbs.cs().or(other.limbs.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; + } + } + 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) { + // 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., + // `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`. + (p - FpVar::one()).enforce_bit_length(F::BITS_PER_LIMB)?; + + 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> { + let len = min(self.limbs.len(), other.limbs.len()); + + let mut i = 0; + let mut carry = FpVar::zero(); + let mut x_bound = Bounds::zero(); + let mut y_bound = Bounds::zero(); + let mut step = 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 pair `(x_i, y_i)` in `self` and `other`, we first try + // to group their _bounds_ into `x_bound` and `y_bound`. + // If both new bounds do not overflow / underflow, we can safely group + // the _limbs_. + // + // 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 pair of grouped limb `(x', y')` consists of `k` original + // limbs. + // Then the lower `k * W` bits of `x'` and `y'` must be equal. + // To check that, we need to enforce that `2^{k * W}` divides `x' - y'`, + // which is done by computing the quotient `q = (x' - y') / 2^{k * W}` + // and enforcing `q` is small that doesn't cause the multiplication + // `q * 2^{k * W}` to overflow. + // + // Moreover, we need to take into account the carry from the previous + // grouped limb, i.e., we actually enforce `x' - y' + carry` is a + // multiple of `2^{k * W}`, and derive the next carry by computing the + // quotient `q`. + // + // We can further avoid storing `x'` and `y'` by updating the carry on + // the fly for each limb, i.e., `carry = (carry + x_i - y_i) / 2^W`. + while i < len { + if let (Some(new_x_bound), Some(new_y_bound)) = ( + self.bounds[i].shl(step).add(&x_bound).filter_safe::(), + other.bounds[i].shl(step).add(&y_bound).filter_safe::(), + ) { + carry = (carry + &self.limbs[i] - &other.limbs[i]) * inv; + + // The current limb pair is successfully grouped, so we move on + // to the next limb pair. + i += 1; + + // Update the bounds and step for the current group. + x_bound = new_x_bound; + y_bound = new_y_bound; + step += F::BITS_PER_LIMB; + } else { + // New bounds overflow / underflow, meaning the current group is + // finalized. + + // `bits` is the maximum possible bit-length of the carry's + // absolute value. + let bits = (max( + min(&x_bound.0, &y_bound.0).bits(), + max(&x_bound.1, &y_bound.1).bits(), + ) as usize) + .saturating_sub(step); + + // We ensure `carry` is small, i.e., `|carry| < 2^bits`, which + // guarantees that `carry * 2^{step}` does not overflow. + (&carry + F::from(BigUint::one() << bits)).enforce_bit_length(bits + 1)?; + + // Reset the bounds and step for the next group. + x_bound = Bounds::zero(); + y_bound = Bounds::zero(); + step = 0; + } + } + + let remaining_limbs = if i < self.limbs.len() { + &self.limbs[i..] + } else { + &other.limbs[i..] + }; + let remaining_bounds = if i < self.bounds.len() { + &self.bounds[i..] + } else { + &other.bounds[i..] + }; + if remaining_limbs.is_empty() { + carry.enforce_equal(&FpVar::zero())?; + } else { + // If there is any remaining limb, the first one must be the final + // carry (which will be checked later), and the following ones must + // be zero. + + // Ensure that the final carry equals the remaining limb. + carry.enforce_equal(&remaining_limbs[0])?; + + // 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 we first check that the bounds of their sum + // fit within the field capacity, which guarantees that the sum does + // not overflow or underflow, meaning that the sum is zero if and + // only if each limb is zero. + Bounds::add_many(&remaining_bounds[1..]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + FpVar::zero().enforce_equal(&remaining_limbs[1..].iter().sum())?; + } + + 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, r) = { + let v = compose(self.limbs.value().unwrap_or_default()); + let q = v.div_floor(&m); + let r = v - &q * &m; + + ( + LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { + Ok(( + q, + Bounds(self.lbound().div_floor(&m), self.ubound().div_floor(&m)), + )) + })?, + LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { + Ok((r, Bounds(Zero::zero(), m.clone()))) + })?, + ) + }; + + 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)?; + + 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(); + let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); + // Provide the quotient as hint + let q = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { + let x = compose(self.limbs.value().unwrap_or_default()); + let y = compose(other.limbs.value().unwrap_or_default()); + Ok(( + (x - y).div_floor(&m), + Bounds(self.lbound().div_floor(&m), self.ubound().div_floor(&m)), + )) + })?; + + 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 Intermediate = LimbedVar; +} + +// Only implement `EqGadget` for aligned variables. +impl EqGadget for LimbedVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + let mut result = Boolean::TRUE; + 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); + } + result &= self.limbs[i].is_eq(&other.limbs[i])?; + } + Ok(result) + } + + 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 enforce_not_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_not_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 self.enforce_not_equal(other); + } + } + 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(), + ), + ) + } + + 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), + )) + } +} + +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 MatrixGadget> + for SparseMatrixVar> +{ + fn mul_vector( + &self, + v: &impl Index>, + ) -> Result>, SynthesisError> { + self.0 + .iter() + .map(|row| { + let len = row + .iter() + .map(|(value, col_i)| value.limbs.len() + v[*col_i].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( + &row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].bounds.len()) - v[*col_i].bounds.len(); + let end = min(i + 1, value.bounds.len()); + (start..end) + .map(|j| value.bounds[j].mul(&v[*col_i].bounds[i - j])) + }) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + let limbs = (0..len) + .map(|i| { + row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].limbs.len()) - v[*col_i].limbs.len(); + let end = min(i + 1, value.limbs.len()); + (start..end).map(|j| &value.limbs[j] * &v[*col_i].limbs[i - j]) + }) + .sum() + }) + .collect(); + Ok(LimbedVar::new(limbs, bounds)) + }) + .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 { + 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 x_is_neg = x.is_negative(); + let mut x_bits = x + .magnitude() + .to_radix_le(2) + .into_iter() + .map(|i| i == 1) + .collect::>(); + x_bits.resize(len, false); + + 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), mode)? + }; + let x_bits = Vec::new_variable(cs, || Ok(x_bits), mode)?; + + let limbs = x_bits + .chunks(F::BITS_PER_LIMB) + .map(|chunk| { + let limb_abs = Boolean::le_bits_to_fp(chunk)?; + 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` + #[allow(clippy::if_same_then_else)] + if lb.is_zero() && ub + BigInt::one() == BigInt::one() << len { + } else if BigInt::one() - lb == BigInt::one() << len && ub.is_zero() { + } else if BigInt::one() - lb == BigInt::one() << len + && ub + BigInt::one() == BigInt::one() << len + { + } else { + var.enforce_lt(&Self::constant(ub + BigInt::one()))?; + Self::constant(lb - BigInt::one()).enforce_lt(&var)?; + } + + Ok(var) + } + + 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| { + ( + BigInt::from_biguint(Sign::Plus, (*v.borrow()).into()), + Bounds(Zero::zero(), G::MODULUS.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::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_alloc() -> Result<(), Box> { + let rng = &mut thread_rng(); + + let size = 1024; + let mut lbs = vec![BigInt::zero()]; + let mut ubs: Vec = vec![(BigInt::one() << size) - BigInt::one()]; + lbs.push(-ubs[0].clone()); + ubs.push(BigInt::zero()); + lbs.push(-ubs[0].clone()); + ubs.push(ubs[0].clone()); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(BigInt::zero()); + lbs.push(BigInt::zero()); + ubs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(rng.gen_bigint_range(lbs.last().unwrap(), &BigInt::zero())); + lbs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + ubs.push(rng.gen_bigint_range(lbs.last().unwrap(), &ubs[0])); + + for (lb, ub) in lbs.into_iter().zip(ubs.into_iter()) { + 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 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 = EmulatedFieldVar::new_witness(cs.clone(), || Ok(c))?; + + let mut r_var: LimbedVar = + EmulatedFieldVar::constant(BigUint::zero().into()).into(); + for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { + r_var = r_var.add_unaligned(&a.mul_unaligned(&b)?)?; + } + 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..8538cae77 --- /dev/null +++ b/crates/primitives/src/algebra/field/mod.rs @@ -0,0 +1,152 @@ +//! This module defines extension traits for field elements and their in-circuit +//! counterparts, along with some common implementations. + +use ark_ff::{BigInteger, Fp, FpConfig, PrimeField}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{ + any::TypeId, + mem::transmute_copy, + ops::{Add, Mul}, +}; + +use crate::{ + algebra::{Val, field::emulated::EmulatedFieldVar}, + traits::{Inputize, InputizeEmulated}, + 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 + + Inputize + + Val, EmulatedVar = EmulatedFieldVar> +{ + /// [`SonobeField::BITS_PER_LIMB`] defines the bit length of each limb when + /// representing field elements as limbs in an emulated field variable. + const BITS_PER_LIMB: usize; +} + +impl, const N: usize> SonobeField for Fp { + // 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 `EmulatedFieldVar` to represent the base field `Fq`. + // Since 250 / 55 = 4.46, the `EmulatedFieldVar` has 5 limbs. + // Now, the multiplication of two `EmulatedFieldVar`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 `EmulatedFieldVar`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 constraint 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. + // TODO: make this configurable + const BITS_PER_LIMB: usize = 55; +} + +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, const N: usize> Inputize for Fp { + fn inputize(&self) -> Vec { + vec![*self] + } +} + +impl InputizeEmulated for P { + fn inputize_emulated(&self) -> Vec { + self.into_bigint() + .to_bits_le() + .chunks(F::BITS_PER_LIMB) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) + .collect() + } +} + +/// [`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 arithmetic 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 [`Add`] and [`Mul`] operations between two field variables +/// yield an intermediate variable. +pub trait TwoStageFieldVar: + Clone + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate> +{ + /// The intermediate variable type used during arithmetic 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 [`Add`] and [`Mul`] 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> + + Mul + + for<'a> Mul<&'a Self::Intermediate, Output = Self::Intermediate> + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate>; +} + +// Operations over the canonical variable `FpVar` always yield another `FpVar`. +impl TwoStageFieldVar for FpVar { + type Intermediate = Self; +} diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs new file mode 100644 index 000000000..6e6b6f3bb --- /dev/null +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -0,0 +1,209 @@ +//! 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::emulated::EmulatedFieldVar, group::SonobeCurve}, + traits::SonobeField, + 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::{ + traits::{Inputize, InputizeEmulated}, + 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(), + p.inputize_emulated() + ); + + 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()?], + p.inputize() + ); + 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..da37399c5 --- /dev/null +++ b/crates/primitives/src/algebra/group/mod.rs @@ -0,0 +1,106 @@ +//! 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::{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; + +use crate::{ + algebra::{Val, field::SonobeField, group::emulated::EmulatedAffineVar}, + traits::{Dummy, Inputize, InputizeEmulated}, + transcripts::{Absorbable, AbsorbableVar}, +}; + +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 + + Inputize + + InputizeEmulated + + Val< + Var: CurveVar + AbsorbableVar, + 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 Projective

{ + 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> + InputizeEmulated for Projective

+{ + fn inputize_emulated(&self) -> Vec { + let affine = self.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + + [x, y].inputize_emulated() + } +} diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs new file mode 100644 index 000000000..f84a20e46 --- /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::traits::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..7616d08fc --- /dev/null +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -0,0 +1,73 @@ +//! 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; + +use crate::algebra::field::emulated::Bounds; + +/// [`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; + + /// [`FromBitsGadget::from_bounded_bits_le`] computes a variable from its + /// little-endian bits with explicitly supplied [`Bounds`]. + fn from_bounded_bits_le(bits: &[Boolean], bounds: Bounds) -> 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) + } + + fn from_bounded_bits_le(bits: &[Boolean], _bounds: Bounds) -> Result { + Self::from_bits_le(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..294ba63aa --- /dev/null +++ b/crates/primitives/src/algebra/ops/eq.rs @@ -0,0 +1,32 @@ +//! 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> EquivalenceGadget<[T]> for [T] { + fn enforce_equivalent(&self, other: &[T]) -> Result<(), SynthesisError> { + self.iter() + .zip(other) + .try_for_each(|(a, b)| a.enforce_equivalent(b)) + } +} diff --git a/crates/primitives/src/gadgets/math/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs similarity index 66% rename from crates/primitives/src/gadgets/math/matrix.rs rename to crates/primitives/src/algebra/ops/matrix.rs index bc9d524f3..fef5a7d04 100644 --- a/crates/primitives/src/gadgets/math/matrix.rs +++ b/crates/primitives/src/algebra/ops/matrix.rs @@ -1,19 +1,26 @@ +//! This module defines in-circuit sparse matrix types and implements operations +//! over them. + use ark_ff::PrimeField; use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::{fp::FpVar, FieldVar}, GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::{FieldVar, fp::FpVar}, }; use ark_relations::gr1cs::{Matrix, Namespace, SynthesisError}; -use ark_std::borrow::Borrow; - -use std::ops::Index; +use ark_std::{borrow::Borrow, ops::Index}; +/// [`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>; } -// same format as the native SparseMatrix (which follows ark_relations::gr1cs::Matrix format) +/// [`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>); @@ -53,6 +60,15 @@ impl MatrixGadget> for SparseMatrixVar> { .0 .iter() .map(|row| { + // Theoretically we can use `Iterator::sum` directly: + // ```rs + // row + // .iter() + // .map(|(value, col_i)| value * &v[*col_i]) + // .sum() + // ``` + // But it seems that arkworks will throw an error if we do so + // when the products are all constant values... let products = row .iter() .map(|(value, col_i)| value * &v[*col_i]) 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..b25a23d70 --- /dev/null +++ b/crates/primitives/src/algebra/ops/rlc.rs @@ -0,0 +1,64 @@ +//! 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}, +}; + +/// [`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(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(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(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..cfcdc6fb5 --- /dev/null +++ b/crates/primitives/src/algebra/ops/vector.rs @@ -0,0 +1,57 @@ +//! This module provides definitions and implementations of in-circuit vector +//! operations. + +use ark_relations::gr1cs::SynthesisError; +use ark_std::ops::{Add, Mul, Sub}; + +/// [`VectorGadget`] defines operations on in-circuit vector variables. +pub trait VectorGadget { + /// [`VectorGadget::add`] computes the element-wise sum of two vectors. + fn add(&self, other: &Self) -> Result, SynthesisError>; + + /// [`VectorGadget::sub`] computes the element-wise difference of two + /// vectors. + fn sub(&self, other: &Self) -> Result, SynthesisError>; + + /// [`VectorGadget::scale`] multiplies every element by a scalar. + fn scale(&self, scalar: &Scalar) -> Result, SynthesisError> + where + for<'a> &'a Scalar: Mul<&'a FV, Output = Output>; + + /// [`VectorGadget::hadamard`] computes the element-wise (Hadamard) product + /// of two vectors. + fn hadamard(&self, other: &Self) -> Result, SynthesisError>; +} + +impl VectorGadget for [FV] +where + for<'a> &'a FV: Add<&'a FV, Output = FV> + Sub<&'a FV, Output = FV> + Mul<&'a FV, Output = FV>, +{ + 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 sub(&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 scale(&self, scalar: &Scalar) -> Result, SynthesisError> + where + for<'a> &'a Scalar: Mul<&'a FV, Output = Output>, + { + Ok(self.iter().map(|a| scalar * a).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()) + } +} diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index f239d691f..fe9011ae7 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -1,3 +1,5 @@ +//! This module implements in-circuit CCS variables. + use ark_ff::PrimeField; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, @@ -6,31 +8,38 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use super::CCS; -use crate::gadgets::math::matrix::SparseMatrixVar; +use super::{CCS, CCSVariant}; +use crate::algebra::ops::matrix::SparseMatrixVar; -/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. +/// [`CCSMatricesVar`] is an in-circuit variable of a given CCS structure. +/// +/// Only the matrices are represented, while the remaining CCS parameters are +/// constants to the circuit. +#[allow(non_snake_case)] #[derive(Debug, Clone)] pub struct CCSMatricesVar { - // we only need native representation, so the constraint field==F - pub M: Vec>>, + #[allow(dead_code)] + M: Vec>>, } -impl AllocVar, F> for CCSMatricesVar { - fn new_variable>>( +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 }) + Ok(Self { + M: val + .borrow() + .M + .iter() + .map(|m| SparseMatrixVar::new_constant(cs.clone(), m)) + .collect::>()?, + }) }) } } + +// TODO: add relation check gadgets when needed. diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 32357dd01..cebed30fe 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,47 +1,151 @@ +//! 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_relations::gr1cs::Matrix; -use ark_std::{cfg_into_iter, log2}; +use ark_poly::DenseMultilinearExtension; +use ark_relations::gr1cs::{ConstraintSystem, Matrix}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, fmt::Debug, marker::PhantomData}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use crate::arithmetizations::{Assignments, Error}; - -use super::{r1cs::R1CS, Arith, ArithRelation, ArithSerializer}; +use super::{Arith, ArithRelation, Error, r1cs::R1CS}; +use crate::{ + algebra::ops::poly::MLEHelper, + arithmetizations::{ArithConfig, r1cs::R1CSConfig}, + circuits::Assignments, +}; 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 { +/// [`CCSVariant`] defines the methods that a CCS variant (e.g., R1CS) should +/// implement. +pub trait CCSVariant: Clone + Debug + PartialEq + Default + Sync { + /// [`CCSVariant::n_matrices`] returns the number of matrices in the CCS + /// variant. + fn n_matrices() -> usize; + + /// [`CCSVariant::degree`] returns the degree of the CCS variant. + fn degree() -> usize; + + /// [`CCSVariant::multisets_vec`] returns the vector of multisets in the CCS + /// variant. + fn multisets_vec() -> Vec>; + + /// [`CCSVariant::coefficients_vec`] returns the vector of coefficients in + /// the CCS variant. + fn coefficients_vec() -> Vec; +} + +/// [`CCSConfig`] stores the shape parameters of a CCS structure. +#[allow(non_snake_case)] +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CCSConfig { + _v: PhantomData, /// 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: Assignments) -> Result, Error> { +impl ArithConfig for CCSConfig { + #[inline] + fn degree(&self) -> usize { + V::degree() + } + + #[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 + } +} + +impl, V: CCSVariant> From for CCSConfig { + fn from(cfg: Cfg) -> Self { + let cfg = cfg.borrow(); + Self { + _v: PhantomData, + m: cfg.n_constraints(), + n: cfg.n_variables(), + l: cfg.n_public_inputs(), + } + } +} + +impl From<&ConstraintSystem> for CCSConfig { + fn from(cs: &ConstraintSystem) -> Self { + R1CSConfig::from(cs).into() + } +} + +/// [`CCS`] holds the CCS matrices `M` together with the configuration. +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct CCS { + cfg: CCSConfig, + + pub(super) M: Vec>, +} + +impl CCS { + /// [`CCS::evaluate_at`] evaluates the CCS relation at a given vector of + /// assignments `z`. + pub fn evaluate_at(&self, z: Assignments + Sync>) -> Result, Error> { + let cfg = &self.cfg; + + 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: - // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i * z))$, + // `Σ_{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 @@ -49,23 +153,23 @@ impl CCS { // 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), because `m`, the number of constraints in the CCS, is - // typically large in practice. - Ok(cfg_into_iter!(0..self.m) + // 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: - // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i[row] * z))$ - self.S - .iter() - .zip(&self.c) - .map(|(s, &c)| { + // The `row`-th entry of the resulting vector is: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j[row] · z))` + V::multisets_vec() + .into_iter() + .zip(V::coefficients_vec::()) + .map(|(s, c)| { // Each term in the sum is: - // $c_j * \prod_{i \in S_j} (M_i[row] * z)$ + // `c_i · 〇_{j ∈ S_i} (M_j[row] · z)` c * s .iter() .map(|&i| { - // Each factor in the product is $M_i[row] * z$, - // i.e., the dot product of $M_i[row]$ and $z$. + // Each factor in the product is `M_j[row] · z`, + // i.e., the dot product of `M_j[row]` and `z`. self.M[i][row] .iter() .map(|(val, col)| z[*col] * val) @@ -77,40 +181,54 @@ impl CCS { }) .collect()) } -} -impl Arith for CCS { - #[inline] - fn degree(&self) -> usize { - self.d + /// [`CCS::mles`] returns the multilinear extensions of all CCS matrices + /// `M_i` evaluated over the assignments `z`. + pub fn mles( + &self, + z: Assignments + Sync>, + ) -> Vec> { + (0..V::n_matrices()) + .map(|i| { + DenseMultilinearExtension::from_evaluations( + &cfg_iter!(self.M[i]) + .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) + .collect::>(), + ) + }) + .collect() } +} +impl Default for CCS { #[inline] - fn n_constraints(&self) -> usize { - self.m + fn default() -> Self { + Self { + cfg: CCSConfig::default(), + M: vec![vec![]; V::n_matrices()], + } } +} - #[inline] - fn n_variables(&self) -> usize { - self.n - } +impl Arith for CCS { + type Config = CCSConfig; #[inline] - fn n_public_inputs(&self) -> usize { - self.l + fn config(&self) -> &Self::Config { + &self.cfg } #[inline] - fn n_witnesses(&self) -> usize { - self.n_variables() - self.n_public_inputs() - 1 + fn config_mut(&mut self) -> &mut Self::Config { + &mut self.cfg } } -impl, U: AsRef<[F]>> ArithRelation for CCS { +impl, U: AsRef<[F]>, V: CCSVariant> 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()).into()) + self.evaluate_at((F::one(), u.as_ref(), w.as_ref()).into()) } fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { @@ -123,38 +241,91 @@ impl, U: AsRef<[F]>> ArithRelation for CCS { } } -impl ArithSerializer for CCS { - fn params_to_le_bytes(&self) -> Vec { - [ - self.l.to_le_bytes(), - self.m.to_le_bytes(), - self.n.to_le_bytes(), - self.t.to_le_bytes(), - self.q.to_le_bytes(), - self.d.to_le_bytes(), - ] - .concat() - } -} - -impl From> for CCS { +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()], + Self { + cfg: r1cs.config().into(), M: vec![r1cs.A, r1cs.B, r1cs.C], } } } -// TODO: add back tests \ No newline at end of file +impl From<&ConstraintSystem> for CCS { + fn from(cs: &ConstraintSystem) -> Self { + R1CS::from(cs).into() + } +} + +impl From> for CCS { + fn from(cs: ConstraintSystem) -> Self { + Self::from(&cs) + } +} + +#[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::utils::{constraints_for_test, satisfying_assignments_for_test}, + relations::Relation, + }; + + #[test] + fn test_eval() -> Result<(), Box> { + let mut rng = thread_rng(); + let ccs: CCS = constraints_for_test::().into(); + + assert!( + ccs.evaluate_at(satisfying_assignments_for_test(Fr::rand(&mut rng)))? + .into_iter() + .all(|e| e.is_zero()) + ); + assert!( + !ccs.evaluate_at(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 ccs: CCS = constraints_for_test::().into(); + + let assignments = satisfying_assignments_for_test(Fr::rand(&mut rng)); + + assert!( + ccs.check_relation(&assignments.private, &assignments.public) + .is_ok() + ); + assert!( + ccs.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/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 24adb29e3..6fc4170e8 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,262 +1,188 @@ -use std::ops::Index; +//! 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_relations::gr1cs::SynthesisError; -use ark_std::rand::RngCore; -use sonobe_traits::Dummy; +use ark_std::{fmt::Debug, log2}; use thiserror::Error; +use crate::relations::{Relation, RelationGadget}; + pub mod ccs; pub mod r1cs; -#[derive(Debug, Error)] +/// [`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("Failed to extract constraints from the constraint system: {0}")] - ConstraintExtractionFailure(String), -} - -pub struct Assignments<'a, F> { - pub constant: F, - pub public: &'a [F], - pub private: &'a [F], -} - -impl<'a, F> From<(F, &'a [F], &'a [F])> for Assignments<'a, F> { - fn from((u, x, w): (F, &'a [F], &'a [F])) -> Self { - Self { - constant: u, - public: x, - private: w, - } - } -} - -impl<'a, F> Index for Assignments<'a, F> { - type Output = F; - - fn index(&self, index: usize) -> &Self::Output { - if index == 0 { - &self.constant - } else if index <= self.public.len() { - &self.public[index - 1] - } else { - &self.private[index - 1 - self.public.len()] - } - } -} - -pub struct AssignmentsVar<'a, FV> { - pub constant: FV, - pub public: &'a [FV], - pub private: &'a [FV], -} - -impl<'a, FV> From<(FV, &'a [FV], &'a [FV])> for AssignmentsVar<'a, FV> { - fn from((u, x, w): (FV, &'a [FV], &'a [FV])) -> Self { - Self { - constant: u, - public: x, - private: w, - } - } + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. + #[error(transparent)] + SynthesisError(#[from] SynthesisError), } -impl<'a, FV> Index for AssignmentsVar<'a, FV> { - type Output = FV; - - fn index(&self, index: usize) -> &Self::Output { - if index == 0 { - &self.constant - } else if index <= self.public.len() { - &self.public[index - 1] - } else { - &self.private[index - 1 - self.public.len()] - } - } -} - -/// [`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 +/// [`ArithConfig`] describes the configuration of a constraint system. +pub trait ArithConfig: Clone + Debug + Default + PartialEq { + /// [`ArithConfig::degree`] returns the degree of the constraint system. fn degree(&self) -> usize; - /// Returns the number of constraints in the constraint system + /// [`ArithConfig::n_constraints`] returns the number of constraints in the + /// constraint system. fn n_constraints(&self) -> usize; - /// Returns the number of variables in the constraint system + /// [`ArithConfig::log_constraints`] returns the base-2 logarithm of the + /// number of constraints in the constraint system. + fn log_constraints(&self) -> usize { + log2(self.n_constraints()) as usize + } + + /// [`ArithConfig::n_variables`] 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 + /// [`ArithConfig::n_public_inputs`] returns the number of public inputs in + /// the constraint system. fn n_public_inputs(&self) -> usize; - /// Returns the number of witnesses / secret inputs in the constraint system + /// [`ArithConfig::n_witnesses`] returns the number of witnesses in the + /// constraint system. fn n_witnesses(&self) -> usize; } -/// `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) +/// [`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 { + /// [`Arith::Config`] specifies the arithmetization's configuration. + type Config: ArithConfig; + + /// [`Arith::config`] returns a reference to the configuration of the + /// constraint system. + fn config(&self) -> &Self::Config; + + /// [`Arith::config_mut`] returns a mutable reference to the configuration + /// of the constraint system. + fn config_mut(&mut self) -> &mut Self::Config; +} + +/// [`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. /// -/// Also, `W` and `U` have non-native field elements as their components when -/// used as CycleFold witness and instance. +/// 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. /// -/// - 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) +/// 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 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 { +/// 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; - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. + /// [`ArithRelation::eval_relation`] evaluates the constraint system at + /// witness `w` and instance `u`. It 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`. + /// [`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 the witness. - /// + /// 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 the committed instance. + /// term in `U`. fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; } -pub trait Relation { - type Error; - - /// 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)) - } - - /// Checks if witness `w` and instance `u` satisfy the relation `self` - fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; -} - impl> Relation for A { type Error = 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<(), 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) } } -/// `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 { - fn sample_witness_instance() { - todo!() - } -} -// 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. +/// [`ArithRelationGadget`] defines the in-circuit gadget for constraint system +/// operations in the same way as [`ArithRelation`]. pub trait ArithRelationGadget { + /// [`ArithRelationGadget::Evaluation`] defines the type of the evaluation + /// result returned by [`ArithRelationGadget::eval_relation`], and consumed + /// by [`ArithRelationGadget::check_evaluation`]. type Evaluation; - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. + /// [`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; - /// 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> { + /// [`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::enforce_evaluation(w, u, e) + Self::check_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/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index 9aadc648b..0d983a4a8 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -1,94 +1,167 @@ -use ark_ff::PrimeField; +//! This module implements in-circuit R1CS variables and relation check gadgets. + +use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, marker::PhantomData, One}; +use ark_std::{One, borrow::Borrow, ops::Mul}; use super::R1CS; use crate::{ - arithmetizations::{ArithRelationGadget, AssignmentsVar}, - gadgets::math::{ + algebra::ops::{ eq::EquivalenceGadget, matrix::{MatrixGadget, SparseMatrixVar}, vector::VectorGadget, }, + arithmetizations::ArithRelationGadget, + circuits::Assignments, }; -/// An in-circuit representation of the `R1CS` struct. +/// [`R1CSMatricesVar`] 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. /// -/// `M` is for the modulo operation involved in the satisfiability check when -/// the underlying `FVar` is `NonNativeUintVar`. +/// The naming is chosen to distinguish from arkworks' `(G)R1CSVar`. +#[allow(non_snake_case)] #[derive(Debug, Clone)] -pub struct R1CSMatricesVar { - _m: PhantomData, - pub A: SparseMatrixVar, - pub B: SparseMatrixVar, - pub C: SparseMatrixVar, +pub struct R1CSMatricesVar { + A: SparseMatrixVar, + B: SparseMatrixVar, + C: SparseMatrixVar, } impl> - AllocVar, ConstraintF> for R1CSMatricesVar + AllocVar, ConstraintF> for R1CSMatricesVar { fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, - _mode: AllocationMode, + mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); + let val = val.borrow(); + 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)?, + A: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.A), mode)?, + B: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.B), mode)?, + C: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.C), mode)?, }) }) } } -impl R1CSMatricesVar +impl R1CSMatricesVar where SparseMatrixVar: MatrixGadget, [FVar]: VectorGadget, + for<'a> &'a FVar: Mul<&'a FVar, Output = FVar>, { - pub fn eval_at_z( + /// [`R1CSMatricesVar::evaluate_at`] is the in-circuit version of + /// [`R1CS::evaluate_at`] that evaluates the R1CS variable at a given vector + /// of assignments `z`. + #[allow(non_snake_case)] + pub fn evaluate_at( &self, - z: AssignmentsVar, - ) -> Result<(Vec, Vec), SynthesisError> { + z: Assignments>, + ) -> Result, SynthesisError> { // Multiply Cz by z[0] (u) here, allowing this method to be reused for - // both relaxed and unrelaxed R1CS. + // both relaxed and plain 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.scale(&z[0])?; let AzBz = Az.hadamard(&Bz)?; - Ok((AzBz, uCz)) + AzBz.sub(&uCz) } } -impl, UVar: AsRef<[FVar]>> ArithRelationGadget - for R1CSMatricesVar +impl, UVar: AsRef<[FVar]>> ArithRelationGadget + for R1CSMatricesVar where SparseMatrixVar: MatrixGadget, - [FVar]: VectorGadget + EquivalenceGadget, - FVar: Clone + One, + [FVar]: VectorGadget + EquivalenceGadget<[FVar]>, + // TODO (@winderica): this will not work for our incoming decider + FVar: Clone + Zero + One, + for<'a> &'a FVar: Mul<&'a FVar, Output = FVar>, { - /// 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); + type Evaluation = Vec; fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { - self.eval_at_z((FVar::one(), u.as_ref(), w.as_ref()).into()) + self.evaluate_at((FVar::one(), u.as_ref(), w.as_ref()).into()) } - fn enforce_evaluation( - _w: &WVar, - _u: &UVar, - (lhs, rhs): Self::Evaluation, - ) -> Result<(), SynthesisError> { - lhs.enforce_equivalent(&rhs) + fn check_evaluation(_w: &WVar, _u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError> { + e.enforce_equivalent(&vec![FVar::zero(); e.len()]) } } -// TODO: add back tests \ No newline at end of file +#[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::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_at(satisfying_assignments_for_test(Fr::rand(&mut rng)))? + .into_iter() + .all(|e| e.is_zero()) + ); + assert!( + !r1cs + .evaluate_at(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 index 4804ee19e..54be091c0 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,55 +1,40 @@ +//! 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}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{cfg_into_iter, cfg_iter, rand::Rng}; +use ark_relations::gr1cs::{ConstraintSystem, Matrix, R1CS_PREDICATE_LABEL}; +use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use sonobe_traits::Dummy; - -use super::{ccs::CCS, Arith, ArithRelation, ArithSerializer}; -use crate::arithmetizations::{Assignments, Error}; +use super::{Arith, ArithRelation, Error, ccs::CCS}; +use crate::{ + arithmetizations::{ArithConfig, ccs::CCSVariant}, + circuits::Assignments, +}; pub mod circuits; -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct R1CS { - l: usize, // io len +/// [`R1CSConfig`] stores the shape parameters of an R1CS structure. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct R1CSConfig { m: usize, // number of constraints n: usize, // number of variables - pub A: Matrix, - pub B: Matrix, - pub C: Matrix, + l: usize, // io len } -impl R1CS { - /// Evaluates the R1CS relation at a given vector of variables `z` - pub fn eval_at_z(&self, z: Assignments) -> Result, Error> { - if z.public.len() != self.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 ({}).", self.n_public_inputs(), z.public.len()) - )); - } - if z.private.len() != self.n_witnesses() { - return Err(Error::MalformedAssignments( - format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), z.private.len()) - )); +impl R1CSConfig { + /// [`R1CSConfig::new`] creates a new R1CS configuration. + pub fn new(n_constraints: usize, n_variables: usize, n_public_inputs: usize) -> Self { + Self { + m: n_constraints, + n: n_variables, + l: n_public_inputs, } - - Ok(cfg_iter!(self.A) - .zip(&self.B) - .zip(&self.C) - .map(|((a, b), c)| { - let az = a.iter().map(|(val, col)| z[*col] * val).sum::(); - let bz = b.iter().map(|(val, col)| z[*col] * val).sum::(); - let cz = c.iter().map(|(val, col)| z[*col] * val).sum::(); - az * bz - z[0] * cz - }) - .collect()) } } -impl Arith for R1CS { +impl ArithConfig for R1CSConfig { #[inline] fn degree(&self) -> usize { 2 @@ -76,130 +61,262 @@ impl Arith for R1CS { } } -impl, U: AsRef<[F]>> ArithRelation for R1CS { - type Evaluation = Vec; +impl From<&ConstraintSystem> for R1CSConfig { + fn from(cs: &ConstraintSystem) -> Self { + Self::new( + cs.num_constraints(), + cs.num_instance_variables + cs.num_witness_variables, + cs.num_instance_variables - 1, // -1 to subtract the first '1' + ) + } +} - fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_at_z((F::one(), u.as_ref(), w.as_ref()).into()) +impl CCSVariant for R1CSConfig { + #[inline] + fn n_matrices() -> usize { + 3 } - fn check_evaluation(_w: &W, _u: &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(), - )) + #[inline] + fn degree() -> usize { + 2 } -} -impl ArithSerializer for R1CS { - fn params_to_le_bytes(&self) -> Vec { - [ - self.l.to_le_bytes(), - self.m.to_le_bytes(), - self.n.to_le_bytes(), - ] - .concat() + #[inline] + fn multisets_vec() -> Vec> { + vec![vec![0, 1], vec![2]] } -} -impl Dummy<(usize, usize, usize)> for R1CS { - fn dummy((n_constraints, n_variables, n_public_inputs): (usize, usize, usize)) -> Self { - Self { - m: n_constraints, - n: n_variables, - l: n_public_inputs, - A: vec![], - B: vec![], - C: vec![], - } + #[inline] + fn coefficients_vec() -> Vec { + vec![F::one(), -F::one()] } } +/// [`R1CS`] holds the three sparse matrices `A`, `B`, `C` together with the +/// configuration. +#[allow(non_snake_case)] +#[derive(Debug, Clone, Default, PartialEq)] +pub struct R1CS { + cfg: R1CSConfig, + pub(super) A: Matrix, + pub(super) B: Matrix, + pub(super) C: Matrix, +} + +type Row = Vec<(F, usize)>; + impl R1CS { - pub fn empty() -> Self { - Self::dummy((0, 0, 0)) - } - - pub fn new( - (n_constraints, n_variables, n_public_inputs): (usize, usize, usize), - mut matrices: Vec>, - ) -> Result { - // R1CS should have exactly 3 matrices (A, B, C) - if matrices.len() != 3 { - return Err(Error::ConstraintExtractionFailure(format!( - "R1CS should only have 3 matrices (A, B, C) but found {} matrices", - matrices.len() + /// [`R1CS::evaluate_rows`] evaluates the R1CS relation by applying the + /// provided function `f` to each triplet of rows `(A[i], B[i], C[i])`. + pub fn evaluate_rows( + &self, + f: impl Fn(((&Row, &Row), &Row)) -> Result + Send + Sync, + ) -> Result, Error> { + cfg_iter!(self.A).zip(&self.B).zip(&self.C).map(f).collect() + } + + /// [`R1CS::evaluate_at`] evaluates the R1CS relation at a given vector of + /// assignments `z`. + pub fn evaluate_at(&self, z: Assignments + Sync>) -> Result, Error> { + let cfg = &self.cfg; + + 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 ))); } - let C = matrices.pop().unwrap(); - let B = matrices.pop().unwrap(); - let A = matrices.pop().unwrap(); - - Ok(Self { - m: n_constraints, - n: n_variables, - l: n_public_inputs, - A, - B, - C, + self.evaluate_rows(|((a, b), c)| { + let az = a.iter().map(|(val, col)| z[*col] * val).sum::(); + let bz = b.iter().map(|(val, col)| z[*col] * val).sum::(); + let cz = c.iter().map(|(val, col)| z[*col] * val).sum::(); + // use `z[0]` here since the constant term at index 0 may not be 1 + // for relaxed instances + Ok(az * bz - z[0] * cz) }) } } -impl TryFrom> for R1CS { - type Error = Error; +impl Arith for R1CS { + type Config = R1CSConfig; - fn try_from(ccs: CCS) -> Result { - Self::new( - ( - ccs.n_constraints(), - ccs.n_variables(), - ccs.n_public_inputs(), - ), - ccs.M, - ) + #[inline] + fn config(&self) -> &Self::Config { + &self.cfg + } + + #[inline] + fn config_mut(&mut self) -> &mut Self::Config { + &mut self.cfg + } +} + +impl R1CS { + /// [`R1CS::new`] creates a new R1CS structure from the given configuration + /// and matrices. + #[allow(non_snake_case)] + pub fn new(cfg: R1CSConfig, [A, B, C]: [Matrix; 3]) -> Self { + Self { cfg, A, B, C } } } -/// Extracts R1CS from arkworks ConstraintSystem matrices -impl TryFrom<&ConstraintSystem> for R1CS { +impl TryFrom> for R1CS { type Error = Error; - fn try_from(cs: &ConstraintSystem) -> Result { - // Get the R1CS predicate matrices - let r1cs_predicate = cs.predicate_constraint_systems.get("R1CS").ok_or_else(|| { - Error::ConstraintExtractionFailure( - "No R1CS predicate found in constraint system".into(), - ) - })?; - Self::new( - ( - cs.num_constraints(), - cs.num_instance_variables + cs.num_witness_variables, - cs.num_instance_variables - 1, // -1 to subtract the first '1' + fn try_from(ccs: CCS) -> Result { + let cfg = ccs.config(); + Ok(Self::new( + R1CSConfig::new( + cfg.n_constraints(), + cfg.n_variables(), + cfg.n_public_inputs(), ), - r1cs_predicate.to_matrices(cs), - ) + // `unwrap` is safe here because the type parameter T = 3 + ccs.M.try_into().unwrap(), + )) + } +} + +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); + // `unwrap` is safe here because R1CS always has 3 matrices + R1CS::new(cs.into(), matrices.try_into().unwrap()) + } +} + +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_at((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(), + )) } } -/// 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(), - ) +/// [`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, } -// TODO: add back tests \ No newline at end of file +impl ArithRelation, RelaxedInstance<&[F]>> for R1CS { + type Evaluation = Vec; + + fn eval_relation( + &self, + w: &RelaxedWitness<&[F]>, + u: &RelaxedInstance<&[F]>, + ) -> Result { + self.evaluate_at((*u.u, u.x, w.w).into()) + } + + fn check_evaluation( + w: &RelaxedWitness<&[F]>, + _u: &RelaxedInstance<&[F]>, + v: Self::Evaluation, + ) -> Result<(), Error> { + 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_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::*; + use crate::circuits::{ + ArithExtractor, AssignmentsExtractor, + utils::{CircuitForTest, constraints_for_test, satisfying_assignments_for_test}, + }; + + #[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 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 cs = AssignmentsExtractor::new(); + cs.execute_synthesizer(circuit)?; + assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x)); + Ok(()) + } +} diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs new file mode 100644 index 000000000..ef7ce03cd --- /dev/null +++ b/crates/primitives/src/circuits/mod.rs @@ -0,0 +1,266 @@ +//! This module defines circuits and helpers used by Sonobe. + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar}; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, +}; +use ark_std::{ + fmt::Debug, + ops::{Deref, Index, IndexMut}, +}; + +use crate::transcripts::{Absorbable, AbsorbableVar}; + +pub mod 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::generate_step_constraints`]. +/// 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; + /// [`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::dummy_state`] returns a dummy state for the circuit. + 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::generate_step_constraints`] 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 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 generate_step_constraints( + &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 +{ + cs: ConstraintSystemRef, +} + +impl Deref + for ConstraintSystemExt +{ + type Target = ConstraintSystemRef; + + fn deref(&self) -> &Self::Target { + &self.cs + } +} + +impl + ConstraintSystemExt +{ + /// [`ConstraintSystemExt::new`] creates a new constraint system wrapper + /// with the specified flags. + pub fn new() -> Self { + let cs = ConstraintSystem::::new_ref(); + 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( + &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( + &self, + circuit: impl FnOnce(ConstraintSystemRef) -> Result, + ) -> Result { + let result = circuit(self.cs.clone())?; + if ARITH_ENABLED { + self.cs.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.cs.into_inner().unwrap().into()) + } +} + +impl AssignmentsExtractor { + /// [`AssignmentsExtractor::assignments`] extracts the assignments from the + /// circuit and returns them as `Assignments`. + pub fn assignments(self) -> Result>, SynthesisError> { + let witness = self.cs.witness_assignment()?.to_vec(); + // skip the first element which is '1' + let instance = self.cs.instance_assignment()?[1..].to_vec(); + + Ok((F::one(), instance, witness).into()) + } +} diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs new file mode 100644 index 000000000..464d248d3 --- /dev/null +++ b/crates/primitives/src/circuits/utils.rs @@ -0,0 +1,158 @@ +//! 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::{ + arithmetizations::r1cs::{R1CS, R1CSConfig}, + circuits::FCircuit, + traits::SonobeField, +}; + +/// [`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 dummy_external_inputs(&self) -> Self::ExternalInputs {} + + fn generate_step_constraints( + &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(R1CSConfig::new(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 index dac037619..c2d676230 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,49 +1,200 @@ -use ark_r1cs_std::alloc::AllocVar; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::fmt::Debug; -use ark_std::rand::RngCore; +//! Abstract traits and implementations for commitment schemes. + +use ark_ff::UniformRand; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{ + fmt::Debug, + iter::Sum, + ops::{Add, Mul}, + rand::RngCore, +}; use thiserror::Error; -use sonobe_traits::Curve; +use crate::{ + algebra::{ + Val, + field::{TwoStageFieldVar, emulated::EmulatedFieldVar}, + group::emulated::EmulatedAffineVar, + ops::bits::FromBitsGadget, + }, + traits::{CF1, CF2, SonobeCurve, SonobeField}, + 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 { - // Commitment errors - #[error("The message being committed to has length {1}, which exceeds the maximum supported length of {0}")] + /// [`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("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::CommitmentVerificationFail`] indicates that the provided + /// opening does not verify against the commitment. #[error("Commitment verification failed")] CommitmentVerificationFail, } -pub trait VectorCommitment { +/// [`CommitmentKey`] represents a commitment key (e.g., a vector of group +/// generators for many group-based commitment schemes). +pub trait CommitmentKey: Clone { + /// [`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; - type Key; - type Scalar; - type Commitment; - type Randomness; + /// [`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; +} - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result; +/// [`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: &mut impl RngCore, + 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; + ) -> 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; + /// [`CommitmentDefGadget::ScalarVar`] is the in-circuit variable type for + /// the scalars being committed to. + type ScalarVar: AbsorbableVar + + CondSelectGadget + + FromBitsGadget + + AllocVar<::Scalar, Self::ConstraintField> + + GR1CSVar::Scalar> + + TwoStageFieldVar; + /// [`CommitmentDefGadget::CommitmentVar`] is the in-circuit variable type + /// for the commitment. + type CommitmentVar: Clone + + AbsorbableVar + + CondSelectGadget + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment>; + /// [`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 + + CommitmentDefGadget< + 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: CommitmentDefGadget< + ConstraintField = Self::Scalar, + ScalarVar = FpVar, + CommitmentVar = EmulatedAffineVar, + Widget = Self, + >; } #[cfg(test)] @@ -53,15 +204,17 @@ mod tests { use super::*; - pub fn test_commitment_opt>( - rng: &mut impl RngCore, + pub fn test_commitment_correctness( + mut rng: impl RngCore, len: usize, ) -> Result<(), Box> { - let v = (0..len).map(|_| VC::Scalar::rand(rng)).collect::>(); + let v = (0..len) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); - let ck = VC::generate_key(rng, len)?; - let (cm, r) = VC::commit(&ck, &v, rng)?; - assert!(VC::open(&ck, &v, &r, &cm)?); + let ck = CM::generate_key(len, &mut rng)?; + let (cm, r) = CM::commit(&ck, &v, &mut rng)?; + CM::open(&ck, &v, &r, &cm)?; Ok(()) } } diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 0f97cca9c..7d5c6c295 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -1,105 +1,162 @@ -use ark_r1cs_std::{boolean::Boolean, convert::ToBitsGadget, groups::CurveVar}; +//! 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_r1cs_std::{ + boolean::Boolean, convert::ToBitsGadget, eq::EqGadget, fields::fp::FpVar, groups::CurveVar, +}; use ark_relations::gr1cs::SynthesisError; -use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; +use ark_std::{UniformRand, iter::repeat_with, marker::PhantomData, rand::RngCore}; -use super::{Error, VectorCommitment}; -use sonobe_traits::{Curve, CF2}; +use super::{CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, Error}; +use crate::{ + algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, + commitments::{CommitmentOpsGadget, GroupBasedCommitment}, + traits::{CF1, CF2, SonobeCurve}, + utils::null::Null, +}; -#[derive(Debug)] -pub struct Pedersen { - _c: PhantomData, +/// [`PedersenKey`] stores the public parameters for the Pedersen commitment +/// scheme, where `H` controls whether the scheme is hiding or not. +#[derive(Clone)] +pub struct PedersenKey { + g: Vec, + h: C, +} + +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::rand(&mut rng) } else { C::zero() }, + } + } } -impl Pedersen { - fn msm(g: &[C::Affine], v: &[C::ScalarField]) -> Result { - if g.len() < v.len() { - return Err(Error::MessageTooLong(g.len(), v.len())); +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.mul(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(g, v)) + Ok(C::msm_unchecked(&self.g, v)) } } -impl VectorCommitment for Pedersen { +/// [`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 = Vec; + type Key = PedersenKey; type Scalar = C::ScalarField; type Commitment = C; - type Randomness = (); + type Randomness = Null; +} - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result { - let generators = repeat_with(|| C::rand(rng)) - .take(len.next_power_of_two()) - .collect::>(); - Ok(C::normalize_batch(&generators)) +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 = PedersenEmulatedGadget; +} + +impl GroupBasedCommitment for Pedersen { + type Gadget1 = PedersenGadget; + type Gadget2 = PedersenEmulatedGadget; +} + +impl CommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { + Ok(PedersenKey::new(len, rng)) } fn commit( - g: &Self::Key, - v: &[Self::Scalar], - _rng: &mut impl RngCore, - ) -> Result<(Self::Commitment, Self::Randomness), Error> { - Ok((Self::msm(g, v)?, ())) + ck: &PedersenKey, + v: &[CF1], + _rng: impl RngCore, + ) -> Result<(C, Null), Error> { + Ok((ck.commit(v)?, Null)) } - fn open( - ck: &Self::Key, - v: &[Self::Scalar], - _r: &Self::Randomness, - cm: &Self::Commitment, - ) -> Result { - Ok(&Self::msm(ck, v)? == cm) + fn open(ck: &PedersenKey, v: &[CF1], _r: &Null, cm: &C) -> Result<(), Error> { + (&ck.commit(v)? == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) } } -impl VectorCommitment for Pedersen { - const IS_HIDING: bool = true; - - type Key = (Vec, C); - type Scalar = C::ScalarField; - type Commitment = C; - type Randomness = C::ScalarField; - - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result { - Ok((Pedersen::::generate_key(rng, len)?, C::rand(rng))) +impl CommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { + Ok(PedersenKey::new(len, rng)) } fn commit( - (g, h): &Self::Key, - v: &[Self::Scalar], - rng: &mut impl RngCore, - ) -> Result<(Self::Commitment, Self::Randomness), Error> { - let r = C::ScalarField::rand(rng); - Ok((Self::msm(g, v)? + h.mul(r), r)) + 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( - (g, h): &Self::Key, - v: &[Self::Scalar], - r: &Self::Randomness, - cm: &Self::Commitment, - ) -> Result { - Ok(&(Self::msm(g, v)? + h.mul(r)) == cm) + fn open(ck: &PedersenKey, v: &[CF1], r: &CF1, cm: &C) -> Result<(), Error> { + (&(ck.commit(v, r)?) == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) } } -pub struct PedersenGadget { +/// [`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 { - pub fn commit( - h: &C::Var, - g: &[C::Var], - v: &[Vec>>], - r: &[Boolean>], - ) -> Result { +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(); - 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())?; @@ -121,27 +178,129 @@ impl PedersenGadget { } } +impl CommitmentOpsGadget for PedersenGadget { + fn open( + ck: &Vec, + v: &[EmulatedFieldVar, CF1>], + _r: &Null, + cm: &C::Var, + ) -> Result<(), SynthesisError> { + Self::msm( + ck, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )? + .enforce_equal(cm) + } +} + +impl CommitmentOpsGadget for PedersenGadget { + fn open( + (g, h): &(Vec, C::Var), + v: &[EmulatedFieldVar, CF1>], + r: &EmulatedFieldVar, CF1>, + cm: &C::Var, + ) -> Result<(), SynthesisError> { + let gv = Self::msm( + g, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )?; + let hr = 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 = Vec; + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = (Vec, C::Var); + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = EmulatedFieldVar, CF1>; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = Vec, C>>; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = ( + Vec, C>>, + EmulatedAffineVar, C>, + ); + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + type Widget = Pedersen; +} + #[cfg(test)] mod tests { - use ark_bn254::{constraints::GVar, Fq, Fr, G1Projective}; - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_ff::{BigInteger, PrimeField}; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, rand::Rng, test_rng}; - - use crate::commitments::tests::test_commitment_opt; + 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::transcripts::poseidon::poseidon_canonical_config; + use crate::commitments::tests::test_commitment_correctness; #[test] fn test_pedersen_commitment() -> Result<(), Box> { - let rng = &mut test_rng(); + let mut rng = thread_rng(); for i in 0..10 { let len = rng.gen_range((1 << i)..(1 << (i + 1))); - test_commitment_opt::>(rng, len)?; - test_commitment_opt::>(rng, len)?; + test_commitment_correctness::>(&mut rng, len)?; + test_commitment_correctness::>(&mut rng, len)?; } Ok(()) } diff --git a/crates/primitives/src/gadgets/math/eq.rs b/crates/primitives/src/gadgets/math/eq.rs deleted file mode 100644 index a63373fa1..000000000 --- a/crates/primitives/src/gadgets/math/eq.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; -use ark_relations::gr1cs::SynthesisError; - -/// `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)) - } -} diff --git a/crates/primitives/src/gadgets/math/mod.rs b/crates/primitives/src/gadgets/math/mod.rs deleted file mode 100644 index 40ed4e53e..000000000 --- a/crates/primitives/src/gadgets/math/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod eq; -pub mod matrix; -pub mod vector; \ No newline at end of file diff --git a/crates/primitives/src/gadgets/math/vector.rs b/crates/primitives/src/gadgets/math/vector.rs deleted file mode 100644 index 8a7019a6d..000000000 --- a/crates/primitives/src/gadgets/math/vector.rs +++ /dev/null @@ -1,31 +0,0 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::gr1cs::SynthesisError; - -pub trait VectorGadget { - fn add(&self, other: &Self) -> Result, SynthesisError>; - - fn scale(&self, scalar: &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 scale(&self, scalar: &FpVar) -> Result>, SynthesisError> { - Ok(self.iter().map(|a| a * scalar).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()) - } -} diff --git a/crates/primitives/src/gadgets/mod.rs b/crates/primitives/src/gadgets/mod.rs deleted file mode 100644 index 89346e75c..000000000 --- a/crates/primitives/src/gadgets/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod math; -pub mod nonnative; diff --git a/crates/primitives/src/gadgets/nonnative/affine.rs b/crates/primitives/src/gadgets/nonnative/affine.rs deleted file mode 100644 index 182525cc9..000000000 --- a/crates/primitives/src/gadgets/nonnative/affine.rs +++ /dev/null @@ -1,202 +0,0 @@ -use ark_ec::{short_weierstrass::SWFlags, AffineRepr}; -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, Zero}; - -use sonobe_traits::{AbsorbNonNativeGadget, Curve}; - -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 AbsorbNonNativeGadget for NonNativeAffineVar { - fn to_native_sponge_field_elements( - &self, - ) -> Result>, SynthesisError> { - [&self.x, &self.y].to_native_sponge_field_elements() - } -} - -#[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::{error::Error, UniformRand}; - use sonobe_traits::{AbsorbNonNative, Inputize, InputizeNonNative}; - - use super::*; - - #[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<(), Box> { - 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 p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - p_var.to_native_sponge_field_elements()?.value()?, - p.to_native_sponge_field_elements_as_vec() - ); - Ok(()) - } - - #[test] - fn test_inputize() -> Result<(), Box> { - // 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 p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - [p_var.x.0.value()?, p_var.y.0.value()?].concat(), - p.inputize_nonnative() - ); - - 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()?], - p.inputize() - ); - Ok(()) - } -} diff --git a/crates/primitives/src/gadgets/nonnative/mod.rs b/crates/primitives/src/gadgets/nonnative/mod.rs deleted file mode 100644 index 497b9870f..000000000 --- a/crates/primitives/src/gadgets/nonnative/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod affine; -pub mod uint; diff --git a/crates/primitives/src/gadgets/nonnative/uint.rs b/crates/primitives/src/gadgets/nonnative/uint.rs deleted file mode 100644 index 754908392..000000000 --- a/crates/primitives/src/gadgets/nonnative/uint.rs +++ /dev/null @@ -1,994 +0,0 @@ -use std::ops::Index; - -use ark_ff::{BigInteger, 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 ark_std::{ - borrow::Borrow, - cmp::{max, min}, -}; -use num_bigint::BigUint; -use num_integer::Integer; - -use sonobe_traits::{AbsorbNonNativeGadget, Field}; - -use crate::gadgets::math::{ - eq::EquivalenceGadget, - matrix::{MatrixGadget, SparseMatrixVar}, - vector::VectorGadget, -}; - -/// `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) - .sum::>(); - // `r = Σ other[i] c^i` - let r = other - .0 - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .sum::>(); - // `o = Σ z[i] c^i` - let o = z - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .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 AbsorbNonNativeGadget for NonNativeUintVar { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - - self - .to_bits_le()? - .chunks(bits_per_limb) - .map(Boolean::le_bits_to_fp) - .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 scale( - &self, - other: &NonNativeUintVar, - ) -> Result>, SynthesisError> { - self.iter().map(|x| x.mul_no_align(other)).collect() - } -} - -impl MatrixGadget> for SparseMatrixVar> { - fn mul_vector( - &self, - v: &impl Index>, - ) -> Result>, SynthesisError> { - self.0 - .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 std::error::Error; - - 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::*; - - #[test] - fn test_mul_biguint() -> Result<(), Box> { - 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<(), Box> { - 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<(), Box> { - 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<(), Box> { - 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/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 9400c51d2..ddbe91c8a 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,4 +1,19 @@ -pub mod commitments; +#![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 gadgets; +pub mod circuits; +pub mod commitments; +pub mod relations; +pub mod sumcheck; +pub mod traits; 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/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs new file mode 100644 index 000000000..60075d9a3 --- /dev/null +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -0,0 +1,160 @@ +//! In-circuit verifier gadget for the sumcheck protocol. +//! +//! The code is forked from Testudo's sumcheck circuit [implementation] and +//! modified to fit Sonobe's design & use case. +//! +//! [implementation]: https://github.com/cryptonetlab/testudo/blob/7db2d30972ce72ee7622070a1debc3b72580f4c7/src/constraints.rs#L116-L143 + +// Below we attach Testudo's original license notice. +// (Note: since the Testudo repo was forked from Microsoft's Spartan repo but no +// modifications were made to the license in Testudo, their copyright notice +// still credits Microsoft.) +// +// MIT License +// +// Copyright (c) Microsoft Corporation. +// +// 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::PrimeField; +use ark_r1cs_std::{ + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, + poly::polynomial::univariate::dense::DensePolynomialVar, +}; +use ark_relations::gr1cs::SynthesisError; + +use crate::{sumcheck::utils::VPAuxInfo, transcripts::TranscriptGadget}; + +/// [`SumCheckGadget`] is the in-circuit sumcheck verifier gadget. +pub struct SumCheckGadget; + +impl SumCheckGadget { + /// [`SumCheckGadget::verify`] provides an implementation of the sumcheck + /// verification algorithm in circuit. + /// + /// Given the claimed sum `claimed_sum = z`, the proof `proofs` (i.e., round + /// polynomials `g_1, ..., g_n`), the auxiliary info `aux_info`, and the + /// transcript `transcript`. + /// It returns the final evaluation `z_{n+1} = f(r_1, ..., r_n)` and the + /// Fiat-Shamir challenges `r_1, ..., r_n`. + /// + /// It mirrors the verifier widget [`super::SumCheck::verify`] with exactly + /// the same logic. + pub fn verify( + mut claimed_sum: FpVar, + proofs: &Vec>>, + aux_info: &VPAuxInfo, + transcript: &mut impl TranscriptGadget, + ) -> Result<(FpVar, Vec>), SynthesisError> { + transcript.add(&FpVar::constant(F::from(aux_info.num_variables as u64)))?; + transcript.add(&FpVar::constant(F::from(aux_info.max_degree as u64)))?; + if proofs.len() != aux_info.num_variables { + return Err(SynthesisError::Unsatisfiable); + } + + let mut challenges = Vec::with_capacity(aux_info.num_variables); + + for coeffs in proofs { + if coeffs.len() - 1 != aux_info.max_degree { + return Err(SynthesisError::Unsatisfiable); + } + + let eval_at_zero = &coeffs[0]; + let eval_at_one = coeffs.iter().sum::>(); + + (eval_at_zero + eval_at_one).enforce_equal(&claimed_sum)?; + + transcript.add(&coeffs)?; + let challenge = transcript.challenge_field_element()?; + + claimed_sum = + DensePolynomialVar::from_coefficients_slice(coeffs).evaluate(&challenge)?; + challenges.push(challenge); + } + + Ok((claimed_sum, challenges)) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_crypto_primitives::sponge::{ + CryptographicSponge, + poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}, + }; + use ark_ff::{One, Zero}; + use ark_poly::{ + DenseMultilinearExtension, DenseUVPolynomial, MultilinearExtension, Polynomial, + univariate::DensePolynomial, + }; + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; + 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::{ + sumcheck::{SumCheck, utils::VirtualPolynomial}, + transcripts::poseidon::poseidon_canonical_config, + }; + + #[test] + fn test_sum_check_circuit() -> Result<(), Box> { + let mut rng = thread_rng(); + let poseidon_config = poseidon_canonical_config::(); + for num_vars in 1..15 { + let mut transcript_p = PoseidonSponge::new(&poseidon_config); + let mut transcript_v = PoseidonSponge::new(&poseidon_config); + + let poly_mle = DenseMultilinearExtension::rand(num_vars, &mut rng); + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, One::one()); + let aux_info = virtual_poly.aux_info.clone(); + + let (proofs, challenges, _) = SumCheck::prove(virtual_poly, &mut transcript_p)?; + + let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); + let claimed_sum = poly.evaluate(&One::one()) + poly.evaluate(&Zero::zero()); + + let (expected, _) = + SumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; + + let cs = ConstraintSystem::new_ref(); + let mut transcript_var = PoseidonSpongeVar::new(&poseidon_config); + + let (expected_var, challenges_var) = SumCheckGadget::verify( + FpVar::new_witness(cs.clone(), || Ok(claimed_sum))?, + &proofs + .into_iter() + .map(|v| Vec::new_witness(cs.clone(), || Ok(v))) + .collect::>()?, + &aux_info, + &mut transcript_var, + )?; + + assert!(cs.is_satisfied()?); + + assert_eq!(expected_var.value()?, expected); + assert_eq!(challenges_var.value()?, challenges); + } + Ok(()) + } +} diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs new file mode 100644 index 000000000..7d131c708 --- /dev/null +++ b/crates/primitives/src/sumcheck/mod.rs @@ -0,0 +1,325 @@ +//! This module implements the sumcheck protocol and its in-circuit gadgets for +//! verification. +//! +//! The code is forked from HyperPlonk's sumcheck [implementation] and modified +//! to fit Sonobe's design & use case. +//! +//! [implementation]: https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check + +// Below we attach HyperPlonk's original license notice. +// +// The MIT License (MIT) +// +// Copyright (c) 2022 Espresso Systems (espressosys.com) +// +// 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::PrimeField; +use ark_poly::{ + DenseMultilinearExtension, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, +}; +use ark_std::{cfg_chunks, cfg_into_iter, fmt::Debug}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use thiserror::Error; + +use self::utils::{ + VPAuxInfo, VirtualPolynomial, barycentric_weights, compute_lagrange_interpolated_poly, + extrapolate, +}; +use crate::transcripts::{Absorbable, Transcript}; + +pub mod circuits; +pub mod utils; + +/// [`Error`] enumerates possible errors during the sumcheck protocol. +#[derive(Debug, Error)] +pub enum Error { + /// [`Error::IncorrectEvaluation`] indicates that the evaluation does not + /// match the claimed value. + #[error("Incorrect evaluation: claimed {0}, got {1}")] + IncorrectEvaluation(String, String), + /// [`Error::UnexpectedProofLength`] indicates that the proof length does + /// not match the expected length. + #[error("Incorrect proof length: expected {0}, got {1}")] + UnexpectedProofLength(usize, usize), + /// [`Error::UnexpectedPolynomialDegree`] indicates that the polynomial + /// degree exceeds the expected degree. + #[error("Unexpected polynomial degree: expected at most {0}, got {1}")] + UnexpectedPolynomialDegree(usize, usize), +} + +/// [`SumCheck`] implements the sumcheck protocol. +/// +/// In the sumcheck protocol, a prover wants to convince a verifier that the sum +/// of a multilinear polynomial `f` over the Boolean hypercube equals a claimed +/// value `z`, i.e., `∑_{x_1, ..., x_n ∈ {0,1}} f(x_1, ..., x_n) = z`, without +/// having the verifier evaluate the sum themselves. +/// +/// To this end, the prover and verifier engage in `n` rounds of interaction. +/// In each round `i`, we consider a variant of the original problem: given +/// polynomial `f_i` of `n - i + 1` variables `x_i, ..., x_n` and a claim `z_i`, +/// check if `∑_{x_i, ..., x_n ∈ {0,1}} f_i(x_i, ..., x_n) = z_i`. +/// The prover and the verifier's goal is to reduce this problem to the next +/// round's problem, where the new polynomial and claim are defined as: +/// - `f_{i+1}(x_{i+1}, ..., x_n) = f_i(r_i, x_{i+1}, ..., x_n)` for a random +/// `r_i` +/// - `z_{i+1} = ∑_{x_{i+1}, ..., x_n ∈ {0,1}} f_i(r_i, x_{i+1}, ..., x_n)` +/// +/// Such a reduction is achieved by the following steps: +/// 1. The prover sends to the verifier the univariate polynomial +/// `g_i(x_i) = ∑_{x_{i+1}, ..., x_n ∈ {0,1}} f_i(x_i, x_{i+1}, ..., x_n)`. +/// 2. The verifier checks if the current claim `z_i = g_i(0) + g_i(1)`. +/// 3. The verifier sends to the prover a random challenge `r_i`. +/// 4. Both parties prepares for the next round's polynomial +/// `f_{i+1}(x_{i+1}, ..., x_n) = f_i(r_i, x_{i+1}, ..., x_n)` and claim +/// `z_{i+1} = g_i(r_i)`, until the last round where all variables are fixed. +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] +pub struct SumCheck; + +impl SumCheck { + /// [`SumCheck::prove`] runs the prover of the sumcheck protocol over a + /// [`VirtualPolynomial`] `poly = f` with the given `transcript`. + /// It returns the proof (i.e., round polynomials `g_1, ..., g_n`), + /// Fiat-Shamir challenges `r_1, ..., r_n`, and the final "polynomial" with + /// all variables fixed (i.e., the evaluation `f_{n+1} = f(r_1, ..., r_n)`). + #[allow(clippy::type_complexity)] + pub fn prove( + mut poly: VirtualPolynomial, + transcript: &mut impl Transcript, + ) -> Result<(Vec>, Vec, Vec>), Error> { + transcript.add(&F::from(poly.aux_info.num_variables as u64)); + transcript.add(&F::from(poly.aux_info.max_degree as u64)); + let extrapolation_aux = (1..poly.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::>(); + let mut prover_msgs = Vec::with_capacity(poly.aux_info.num_variables); + let mut challenges = Vec::with_capacity(poly.aux_info.num_variables); + for i in 0..poly.aux_info.num_variables { + let mut products_sum = vec![F::ZERO; poly.aux_info.max_degree + 1]; + + // Step 2: generate sum for the partial evaluated polynomial: + // `f_i(x_i, ..., x_n) = f(r_1, ... r_{i-1}, x_i, ..., x_n)` + + poly.products.iter().for_each(|(coefficient, products)| { + #[cfg(feature = "parallel")] + let mut sum = cfg_into_iter!(0..1 << (poly.aux_info.num_variables - i - 1)) + .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 = &poly.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); + *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 + }, + ); + #[cfg(not(feature = "parallel"))] + let mut sum = cfg_into_iter!(0..1 << (poly.aux_info.num_variables - i - 1)) + .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 = &poly.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) + }, + ) + .1; + sum.iter_mut().for_each(|sum| *sum *= coefficient); + let extraploation = cfg_into_iter!(0..poly.aux_info.max_degree - products.len()) + .map(|i| { + let (points, weights) = &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); + }); + + let mut prover_poly = compute_lagrange_interpolated_poly(&products_sum).coeffs; + prover_poly.resize(poly.aux_info.max_degree + 1, F::ZERO); + transcript.add(&prover_poly); + prover_msgs.push(prover_poly); + + let challenge = transcript.challenge_field_element(); + challenges.push(challenge); + poly.flattened_ml_extensions.iter_mut().for_each(|mle| { + mle.evaluations = cfg_chunks!(mle.evaluations, 2) + .map(|chunk| chunk[0] + challenge * (chunk[1] - chunk[0])) + .collect(); + mle.num_vars -= 1; + }); + } + + Ok((prover_msgs, challenges, poly.flattened_ml_extensions)) + } + + /// [`SumCheck::verify`] runs the verifier of the sumcheck protocol given + /// the claimed sum `claimed_sum = z`, the proof `proofs` (i.e., round + /// polynomials `g_1, ..., g_n`), the auxiliary info `aux_info`, and the + /// transcript `transcript`. + /// It returns the final evaluation `z_{n+1} = f(r_1, ..., r_n)` and the + /// Fiat-Shamir challenges `r_1, ..., r_n`. + pub fn verify( + mut claimed_sum: F, + proofs: &[Vec], + aux_info: &VPAuxInfo, + transcript: &mut impl Transcript, + ) -> Result<(F, Vec), Error> { + transcript.add(&F::from(aux_info.num_variables as u64)); + transcript.add(&F::from(aux_info.max_degree as u64)); + if proofs.len() != aux_info.num_variables { + return Err(Error::UnexpectedProofLength( + aux_info.num_variables, + proofs.len(), + )); + } + + let mut challenges = Vec::with_capacity(aux_info.num_variables); + + // Outer loop is not parallelized because `DensePolynomial::evaluate` is + // already parallelized internally. + for coeffs in proofs { + if coeffs.len() - 1 != aux_info.max_degree { + return Err(Error::UnexpectedPolynomialDegree( + aux_info.max_degree, + coeffs.len() - 1, + )); + } + + let eval_at_zero = coeffs[0]; + let eval_at_one = coeffs.iter().sum::(); + + // the deferred check during the interactive phase: + // 1. check if the received 'g_i(0) + g_i(1) = z_i`. + if eval_at_zero + eval_at_one != claimed_sum { + return Err(Error::IncorrectEvaluation( + claimed_sum.to_string(), + format!("{} + {}", eval_at_zero, eval_at_one), + )); + } + + transcript.add(coeffs); + let challenge = transcript.challenge_field_element(); + + // 2. set next `z_{i+1}` to `g_i(r_i)` + claimed_sum = DensePolynomial::from_coefficients_slice(coeffs).evaluate(&challenge); + challenges.push(challenge); + } + + Ok((claimed_sum, challenges)) + } +} + +#[cfg(test)] +mod tests { + use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; + use ark_ff::Field; + use ark_pallas::Fr; + use ark_poly::MultilinearExtension; + use ark_std::{One, Zero, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + use crate::transcripts::poseidon::poseidon_canonical_config; + + #[test] + fn test_sumcheck() -> Result<(), Error> { + let n_vars = 10; + + let mut rng = thread_rng(); + let poly_mle = DenseMultilinearExtension::rand(n_vars, &mut rng); + + test_sumcheck_opt(poly_mle)?; + + // test with zero poly + let poly_mle = DenseMultilinearExtension::from_evaluations_vec( + n_vars, + vec![Fr::zero(); 2usize.pow(n_vars as u32)], + ); + test_sumcheck_opt(poly_mle)?; + Ok(()) + } + + fn test_sumcheck_opt(poly_mle: DenseMultilinearExtension) -> Result<(), Error> { + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); + + let aux_info = virtual_poly.aux_info.clone(); + let poseidon_config = poseidon_canonical_config::(); + + // sum-check prove + let mut transcript_p: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); + let (proofs, challenges_p, eval_p) = SumCheck::prove(virtual_poly, &mut transcript_p)?; + + // sum-check verify + let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); + let claimed_sum = poly.evaluate(&Fr::one()) + poly.evaluate(&Fr::zero()); + let mut transcript_v: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); + let (eval_v, challenges_v) = + SumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; + + assert_eq!(eval_p[0].evaluate(&vec![]), eval_v); + assert_eq!(challenges_p, challenges_v); + Ok(()) + } +} diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs new file mode 100644 index 000000000..cccac6eb7 --- /dev/null +++ b/crates/primitives/src/sumcheck/utils.rs @@ -0,0 +1,451 @@ +//! Virtual polynomial implementation and polynomial utilities. +//! +//! The code is forked from our previous [multifolding PoC implementation], +//! which is itself forked from HyperPlonk's [virtual polynomial code]. +//! +//! [multifolding PoC implementation]: https://github.com/privacy-scaling-explorations/multifolding-poc/blob/main/src/espresso/virtual_polynomial.rs, +//! [virtual polynomial code]: https://github.com/EspressoSystems/hyperplonk/blob/main/arithmetic/src/virtual_polynomial.rs + +// Below we attach HyperPlonk's original license notice. +// +// The MIT License (MIT) +// +// Copyright (c) 2022 Espresso Systems (espressosys.com) +// +// 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::{Field, PrimeField, batch_inversion}; +use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, univariate::DensePolynomial}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +use ark_serialize::CanonicalSerialize; +use ark_std::cfg_into_iter; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +/// [`VirtualPolynomial`] 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 { + /// [`VirtualPolynomial::aux_info`] is the aux information about the + /// multilinear polynomial. + pub aux_info: VPAuxInfo, + /// [`VirtualPolynomial::flattened_ml_extensions`] stores multilinear + /// extensions in which product multiplicand can refer to. + pub flattened_ml_extensions: Vec>, + /// [`VirtualPolynomial::products`] is a list of reference to products + /// (as usize) of multilinear extension + pub products: Vec<(F, Vec)>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, CanonicalSerialize)] +/// [`VPAuxInfo`] is auxiliary information about the multilinear polynomial. +pub struct VPAuxInfo { + /// [`VPAuxInfo::max_degree`] is the max number of multiplicands in each + /// product. + pub max_degree: usize, + /// [`VPAuxInfo::num_variables`] is the number of variables of the + /// polynomial. + pub num_variables: usize, +} + +impl VirtualPolynomial { + /// Creates a new virtual polynomial from a MLE and its coefficient. + pub fn new_from_mle(mle: DenseMultilinearExtension, coefficient: F) -> Self { + VirtualPolynomial { + aux_info: VPAuxInfo { + // The max degree is the max degree of any individual variable + max_degree: 1, + num_variables: mle.num_vars, + }, + // here `0` points to the first polynomial of `flattened_ml_extensions` + products: vec![(coefficient, vec![0])], + flattened_ml_extensions: vec![mle], + } + } +} + +/// [`EqPoly`] represents the multilinear equality polynomial +/// `eq(x, y) = Π_{i ∈ {0,1}} (x_i y_i + (1 - x_i)(1 - y_i))`. +pub struct EqPoly; + +impl EqPoly { + /// [`EqPoly::fix_y_evals`] function evaluates `eq(x, y)` by fixing `y = r` + /// and outputting the evaluations over all `x` in `[0, 2^n)`. + pub fn fix_y_evals(r: &[F]) -> Vec { + // we build eq(x,r) from its evaluations + // we want to evaluate eq(x,r) over all binary strings `x` of length `n` + // for example, with n = 4, x is a binary string of length 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 + + // initializing the buffer with [1] + let mut buf = vec![F::one()]; + + for i in r.iter().rev() { + // suppose at the previous step we received [b_1, ..., b_k] + // for the current step we will need + // if x_i = 0: (1-ri) * [b_1, ..., b_k] + // if x_i = 1: ri * [b_1, ..., b_k] + buf = cfg_into_iter!(buf) + .flat_map(|j| { + let v = j * i; + [j - v, v] + }) + .collect(); + } + + buf + } + + /// [`EqPoly::fix_xy_eval`] evaluates `eq(x, y)` by fixing both `x` and `y`. + pub fn fix_xy_eval(x: &[F], y: &[F]) -> F { + debug_assert_eq!(x.len(), y.len()); + x.iter() + .zip(y.iter()) + .map(|(xi, yi)| xi.double() * yi - xi - yi + F::one()) + .product() + } +} + +/// [`EqPolyGadget`] is the in-circuit gadget of [`EqPoly`]. +pub struct EqPolyGadget; + +impl EqPolyGadget { + /// [`EqPolyGadget::fix_xy_eval`] evaluates `eq(x, y)` in-circuit by fixing + /// both `x` and `y`. + pub fn fix_xy_eval(x: &[FpVar], y: &[FpVar]) -> FpVar { + debug_assert_eq!(x.len(), y.len()); + let mut eval = FpVar::::one(); + for (xi, yi) in x.iter().zip(y.iter()) { + eval *= (xi + xi) * yi - xi - yi + F::one(); + } + eval + } +} + +/// [`barycentric_weights`] computes the barycentric weights for a given set of +/// evaluation `points`. +/// +/// Used to extrapolate polynomial evaluations via the barycentric formula. +#[allow(clippy::filter_map_bool_then)] +pub 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 +} + +/// [`extrapolate`] extrapolates the polynomial defined by `(points, evals)` to +/// a new point `at`, using the precomputed barycentric `weights`. +pub 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 +} + +/// [`compute_lagrange_interpolated_poly`] computes the lagrange interpolated +/// polynomial from the given points `p_i`. +pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePolynomial { + let v = (0..p_i.len()) + .map(|i| F::from(i as u64)) + .collect::>(); + + // compute l(x), common to every basis polynomial + let mut l_x = DensePolynomial::from_coefficients_vec(vec![F::ONE]); + for i in &v { + let prod_m = DensePolynomial::from_coefficients_vec(vec![-*i, F::ONE]); + l_x = &l_x * &prod_m; + } + + // compute each w_j - barycentric weights + let w_j_vector = barycentric_weights(&v); + + // 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 = 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![-v[x_j], F::ONE]); + let poly = &poly_numerator / &poly_denominator; + lagrange_poly = &lagrange_poly + &poly; + } + + lagrange_poly +} + +#[cfg(test)] +mod tests { + use ark_pallas::Fr; + use ark_poly::{DenseUVPolynomial, Polynomial, univariate::DensePolynomial}; + use ark_std::{UniformRand, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; + + use super::*; + + /// 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. + fn interpolate_uni_poly(p_i: &[F], eval_at: F) -> F { + 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); + } + } + } + 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 + } + + #[test] + fn test_compute_lagrange_interpolated_poly() { + let mut prng = thread_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 = thread_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/crates/primitives/src/traits.rs b/crates/primitives/src/traits.rs new file mode 100644 index 000000000..6ee687a85 --- /dev/null +++ b/crates/primitives/src/traits.rs @@ -0,0 +1,78 @@ +//! This module defines helper traits used across Sonobe's crates. + +pub use crate::algebra::{ + field::SonobeField, + group::{CF1, CF2, SonobeCurve}, +}; + +/// [`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)) + } +} + +/// [`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 canonical way in the circuit when allocated as public +/// input. +/// +/// This is useful for the verifier to compute the public inputs. +pub trait Inputize { + /// [`Inputize::inputize`] outputs the underlying field elements of `self` + /// as if it is allocated in the canonical way in-circuit. + fn inputize(&self) -> Vec; +} + +impl> Inputize for [T] { + fn inputize(&self) -> Vec { + self.iter().flat_map(Inputize::::inputize).collect() + } +} + +/// [`InputizeEmulated`] 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 emulated way in the circuit when allocated as +/// public input. +/// +/// 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 can be represented in both the canonical and emulated ways +/// in-circuit (e.g., field elements or elliptic curve points). +pub trait InputizeEmulated { + /// [`InputizeEmulated::inputize_emulated`] outputs the underlying field + /// elements of `self` as if it is allocated in the emulated way in-circuit. + fn inputize_emulated(&self) -> Vec; +} + +impl> InputizeEmulated for [T] { + fn inputize_emulated(&self) -> Vec { + self.iter() + .flat_map(InputizeEmulated::::inputize_emulated) + .collect() + } +} 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..dbf1c7b64 --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -0,0 +1,631 @@ +//! 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}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + fields::{FieldVar, fp::FpVar}, +}; +use ark_relations::gr1cs::SynthesisError; +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)] +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> { + fn field_element_from_shake(reader: &mut impl XofReader) -> F { + let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; + + loop { + reader.read(&mut buf); + if let Some(element) = F::from_random_bytes(&buf) { + return element; + } + } + } + + (0..rounds - 1) + .map(|_| (0..t).map(|_| field_element_from_shake(shake)).collect()) + .collect() + } + + fn instantiate_alpha_beta(t: usize, shake: &mut Shake128Reader) -> Vec<[F; 2]> { + fn field_element_from_shake_without_0(reader: &mut impl XofReader) -> F { + let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; + + loop { + reader.read(&mut buf); + if let Some(element) = F::from_random_bytes(&buf) + && !element.is_zero() + { + return element; + } + } + } + + let mut alpha_beta = Vec::with_capacity(t - 2); + + // random alpha/beta + loop { + let alpha = field_element_from_shake_without_0::(shake); + let mut beta = field_element_from_shake_without_0::(shake); + // distinct + while alpha == beta { + beta = field_element_from_shake_without_0::(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 = field_element_from_shake_without_0::(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(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(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(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(row).map(|(a, b)| a * *b).sum()) + .collect(); + if r < params.rounds - 1 { + current_state = current_state + .iter() + .zip(¶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..70cfa71ee --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -0,0 +1,464 @@ +//! Implementation of transcript traits for Griffin sponge. + +use ark_crypto_primitives::sponge::DuplexSpongeMode; +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{ + fields::{FieldVar, fp::FpVar}, + prelude::{Boolean, ToBitsGadget}, +}; +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_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 + } + + 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 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 + ?Sized>( + &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_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) + } + + 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); + 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); + 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); + 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); + 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 index a7239a11d..1ddf1c29a 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -1,90 +1,233 @@ -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}; +//! 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, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -use sonobe_traits::{AbsorbNonNative, AbsorbNonNativeGadget}; +pub use self::absorbable::{Absorbable, AbsorbableVar}; + +pub mod absorbable; +pub mod griffin; pub mod poseidon; -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; +/// [`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; + + /// [`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(); + + let mut input = domain.len().to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + let limbs = input + .chunks(F::MODULUS_BIT_SIZE.div_ceil(8) as usize) + .map(|chunk| F::from_le_bytes_mod_order(chunk)) + .collect::>(); + + new_sponge.add_field_elements(&limbs); + + new_sponge + } - /// `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`. + /// [`Transcript::challenge_field_element`] squeezes a challenge from the + /// transcript / sponge as a field element. /// - /// 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: + /// Internally, it first squeezes a field element and then absorbs it back + /// into the transcript / sponge 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 + /// / sponge as a bit vector. /// - /// - 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. + /// Internally, it first squeezes the bits and then absorbs packed field + /// elements formed by the bits back into the transcript / sponge to ensure + /// security. + fn challenge_bits(&mut self, nbits: usize) -> Vec { + let bits = self.get_bits(nbits); + self.add_field_elements( + &bits + .chunks(F::MODULUS_BIT_SIZE as usize - 1) + .map(F::BigInt::from_bits_le) + .map(F::from) + .collect::>(), + ); + bits + } + + /// [`Transcript::challenge_field_elements`] squeezes `n` challenges from + /// the transcript / sponge as field elements. /// - /// 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; + /// Internally, it first squeezes the field elements and then absorbs them + /// back into the transcript / sponge 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 + } } -pub trait TranscriptVar: - CryptographicSpongeVar -{ - /// `new_with_pp_hash` creates a new transcript / sponge with the given - /// hash of the public parameters. +/// [`TranscriptGadget`] is the in-circuit gadget for transcripts and sponges. +pub trait TranscriptGadget: 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: &>::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::Parameters, + config: &>::Config, pp_hash: &FpVar, - ) -> Result; + ) -> 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 + ?Sized>(&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>; + + /// [`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>; - /// `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`. + /// [`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(); + + let mut input = domain.len().to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + let limbs = input + .chunks(F::MODULUS_BIT_SIZE.div_ceil(8) as usize) + .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 / sponge variable as a field element variable. /// - /// 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: + /// Internally, it first squeezes a field element variable and then absorbs + /// it back into the transcript / sponge 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 / sponge variable as a vector of bit variables. /// - /// - 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. + /// Internally, it first squeezes the bit variables and then absorbs packed + /// field element variables formed by the bit variables back into the + /// transcript / sponge variable to ensure security. + fn challenge_bits(&mut self, nbits: usize) -> Result>, SynthesisError> { + let bits = self.get_bits(nbits)?; + self.add( + &bits + .chunks(F::MODULUS_BIT_SIZE as usize - 1) + .map(Boolean::le_bits_to_fp) + .collect::, _>>()?, + )?; + Ok(bits) + } + + /// [`TranscriptGadget::challenge_field_elements`] squeezes `n` challenges + /// from the transcript / sponge variable as field element variables. /// - /// 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>; + /// Internally, it first squeezes the field element variables and then + /// absorbs them back into the transcript / sponge 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.rs b/crates/primitives/src/transcripts/poseidon.rs deleted file mode 100644 index 7eb281113..000000000 --- a/crates/primitives/src/transcripts/poseidon.rs +++ /dev/null @@ -1,277 +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::{error::Error, test_rng}; - - use super::*; - use crate::gadgets::nonnative::affine::NonNativeAffineVar; - - // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 - #[test] - fn check_against_circom_poseidon() -> Result<(), Box> { - 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(x)) - .collect::>(); - poseidon_sponge.absorb(&v); - poseidon_sponge.squeeze_field_elements::(1); - assert!( - poseidon_sponge.state[0] - == Fr::from_str( - "18821383157269793795438455681495246036402687001665670618754263018637548127333" - ) - .unwrap() - ); - Ok(()) - } - - #[test] - fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Box> { - // 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<(), Box> { - // 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<(), Box> { - // 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<(), Box> { - let nbits = 128; - - // 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 p_var = 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 - let c = Fr::from(::BigInt::from_bits_le(&c_bits)); - - // check that native c*P and in-circuit c*P using scalar_mul_le are equal - assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); - // check that native c*P using mul_bits_be and in-circuit c*P using scalar_mul_le are equal - // (notice the .rev to convert the LE to BE) - assert_eq!( - p.mul_bits_be(c_bits.into_iter().rev()), - p_var.scalar_mul_le(c_var.iter())?.value()? - ); - Ok(()) - } -} diff --git a/crates/primitives/src/transcripts/poseidon/mod.rs b/crates/primitives/src/transcripts/poseidon/mod.rs new file mode 100644 index 000000000..54bdbaff8 --- /dev/null +++ b/crates/primitives/src/transcripts/poseidon/mod.rs @@ -0,0 +1,42 @@ +//! Poseidon-based transcript configurations and implementations. + +use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, find_poseidon_ark_and_mds}; +use ark_ff::PrimeField; + +pub mod sponge; + +/// [`poseidon_custom_config`] 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) +} + +/// [`poseidon_canonical_config`] produces a Poseidon configuration with default +/// parameters, which agrees with Circom's Poseidon(4) when `F` is the scalar +/// field of BN254. +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) +} diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs new file mode 100644 index 000000000..89068655b --- /dev/null +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -0,0 +1,213 @@ +//! Implementation of transcript traits for arkworks' Poseidon sponge. + +use ark_crypto_primitives::sponge::{ + Absorb, CryptographicSponge, FieldBasedCryptographicSponge, + constraints::CryptographicSpongeVar, + poseidon::{PoseidonConfig, PoseidonSponge, constraints::PoseidonSpongeVar}, +}; +use ark_ff::PrimeField; +use ark_r1cs_std::{boolean::Boolean, fields::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 { + CryptographicSponge::new(config) + } + + 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_bits(&mut self, num_bits: usize) -> Vec { + CryptographicSponge::squeeze_bits(self, num_bits) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + self.squeeze_native_field_elements(num_elements) + } +} + +impl TranscriptGadget for PoseidonSpongeVar { + type Widget = PoseidonSponge; + + fn new(config: &PoseidonConfig) -> Self + where + Self: Sized, + { + CryptographicSpongeVar::new(ConstraintSystemRef::None, config) + } + + fn add + ?Sized>( + &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_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { + self.squeeze_bits(num_bits) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + self.squeeze_field_elements(num_elements) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fq, Fr, G1Projective as G1, g1::Config}; + use ark_crypto_primitives::sponge::poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}; + 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, 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_canonical_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_canonical_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_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); + 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_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); + 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 = PoseidonSpongeVar::::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 = poseidon_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); + 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 = 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_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); + 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/utils/mod.rs b/crates/primitives/src/utils/mod.rs new file mode 100644 index 000000000..7f61a9721 --- /dev/null +++ b/crates/primitives/src/utils/mod.rs @@ -0,0 +1,3 @@ +//! Miscellaneous utilities shared across the primitives crate. + +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/traits/Cargo.toml b/crates/traits/Cargo.toml deleted file mode 100644 index f2ee8164e..000000000 --- a/crates/traits/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "sonobe-traits" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-ec = { workspace = true } -ark-ff = { workspace = true, features = ["asm"] } -ark-std = { workspace = true, features = ["getrandom"] } -ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } -ark-relations = { workspace = true } -ark-r1cs-std = { workspace = true } - -[features] -default = ["parallel"] -parallel = [ - "ark-relations/parallel", - "ark-r1cs-std/parallel", -] \ No newline at end of file diff --git a/crates/traits/src/lib.rs b/crates/traits/src/lib.rs deleted file mode 100644 index 1106def43..000000000 --- a/crates/traits/src/lib.rs +++ /dev/null @@ -1,245 +0,0 @@ -use ark_crypto_primitives::sponge::Absorb; -use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig}, - AffineRepr, CurveGroup, PrimeGroup, -}; -use ark_ff::{BigInteger, Field as ArkField, Fp, FpConfig, One, PrimeField, Zero}; -use ark_r1cs_std::{ - fields::{fp::FpVar, FieldVar}, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; - -pub type CF1 = ::ScalarField; -pub type CF2 = <::BaseField as ArkField>::BasePrimeField; - -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() - } -} - -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> 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 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(F::BITS_PER_LIMB) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect() - } -} - -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() - } -} - -/// `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 -{ - const BITS_PER_LIMB: usize; - /// The in-circuit variable type for this field. - type Var: FieldVar; -} - -impl, const N: usize> Field for Fp { - const BITS_PER_LIMB: usize = 55; // TODO: make this configurable - 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>; -} - -/// 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) - } -} - -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> 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); - } -} - -/// 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 { - type ExternalInputs; - - /// 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::ExternalInputs, // inputs that are not part of the state - ) -> Result>, SynthesisError>; -} 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