From 9834f96373d17f24e026e00c372367ff91442d40 Mon Sep 17 00:00:00 2001 From: Gabriel Barreto Date: Tue, 5 May 2026 15:45:25 -0300 Subject: [PATCH] multi-plonk --- Cargo.lock | 583 ++++++++++++++++++++- Cargo.toml | 3 + multi-plonk/Cargo.toml | 61 +++ multi-plonk/examples/lookup_proof.rs | 166 ++++++ multi-plonk/examples/preprocessed_proof.rs | 130 +++++ multi-plonk/examples/pythagorean.rs | 89 ++++ multi-plonk/src/air.rs | 124 +++++ multi-plonk/src/builder/check.rs | 104 ++++ multi-plonk/src/builder/folder.rs | 153 ++++++ multi-plonk/src/builder/mod.rs | 3 + multi-plonk/src/builder/symbolic.rs | 473 +++++++++++++++++ multi-plonk/src/lib.rs | 36 ++ multi-plonk/src/lookup.rs | 291 ++++++++++ multi-plonk/src/matrix.rs | 80 +++ multi-plonk/src/prover.rs | 576 ++++++++++++++++++++ multi-plonk/src/system.rs | 176 +++++++ multi-plonk/src/types.rs | 81 +++ multi-plonk/src/verifier.rs | 389 ++++++++++++++ 18 files changed, 3503 insertions(+), 15 deletions(-) create mode 100644 multi-plonk/Cargo.toml create mode 100644 multi-plonk/examples/lookup_proof.rs create mode 100644 multi-plonk/examples/preprocessed_proof.rs create mode 100644 multi-plonk/examples/pythagorean.rs create mode 100644 multi-plonk/src/air.rs create mode 100644 multi-plonk/src/builder/check.rs create mode 100644 multi-plonk/src/builder/folder.rs create mode 100644 multi-plonk/src/builder/mod.rs create mode 100644 multi-plonk/src/builder/symbolic.rs create mode 100644 multi-plonk/src/lib.rs create mode 100644 multi-plonk/src/lookup.rs create mode 100644 multi-plonk/src/matrix.rs create mode 100644 multi-plonk/src/prover.rs create mode 100644 multi-plonk/src/system.rs create mode 100644 multi-plonk/src/types.rs create mode 100644 multi-plonk/src/verifier.rs diff --git a/Cargo.lock b/Cargo.lock index 2be8a2b..c71c321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +23,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -23,6 +41,224 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "hashbrown 0.14.5", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "rayon", +] + +[[package]] +name = "ark-poly-commit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d68a105d915bcde6c0687363591c97e72d2d3758f3532d48fd0bf21a3261ce7" +dependencies = [ + "ahash", + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "merlin", + "num-traits", + "rand 0.8.6", + "rayon", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", + "rayon", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", + "rayon", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" @@ -49,12 +285,36 @@ dependencies = [ "virtue", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cast" version = "0.3.0" @@ -119,6 +379,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.5.1" @@ -186,12 +455,103 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "half" version = "2.7.1" @@ -203,6 +563,24 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -229,6 +607,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -254,12 +641,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "lock_api" version = "0.4.14" @@ -275,6 +677,32 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "multi-plonk" +version = "0.1.0" +dependencies = [ + "ark-bls12-381", + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-poly-commit", + "ark-serialize", + "ark-std", +] + [[package]] name = "multi-stark" version = "0.1.0" @@ -323,6 +751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -399,7 +828,7 @@ dependencies = [ "p3-maybe-rayon", "p3-util", "paste", - "rand", + "rand 0.10.0", "serde", "tracing", ] @@ -418,7 +847,7 @@ dependencies = [ "p3-matrix", "p3-maybe-rayon", "p3-util", - "rand", + "rand 0.10.0", "serde", "spin", "thiserror", @@ -440,7 +869,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand", + "rand 0.10.0", "serde", ] @@ -474,7 +903,7 @@ dependencies = [ "p3-field", "p3-maybe-rayon", "p3-util", - "rand", + "rand 0.10.0", "serde", "tracing", ] @@ -496,7 +925,7 @@ dependencies = [ "p3-field", "p3-symmetric", "p3-util", - "rand", + "rand 0.10.0", ] [[package]] @@ -511,7 +940,7 @@ dependencies = [ "p3-maybe-rayon", "p3-symmetric", "p3-util", - "rand", + "rand 0.10.0", "serde", "thiserror", "tracing", @@ -534,7 +963,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand", + "rand 0.10.0", "serde", "spin", "tracing", @@ -547,7 +976,7 @@ source = "git+https://github.com/Plonky3/Plonky3?rev=e9d75614dd6816f9b5dbb4413c6 dependencies = [ "p3-field", "p3-symmetric", - "rand", + "rand 0.10.0", ] [[package]] @@ -559,7 +988,7 @@ dependencies = [ "p3-mds", "p3-symmetric", "p3-util", - "rand", + "rand 0.10.0", ] [[package]] @@ -622,6 +1051,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -640,13 +1078,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", ] [[package]] @@ -752,7 +1220,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -768,6 +1236,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "spin" version = "0.10.0" @@ -783,6 +1262,23 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -811,7 +1307,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -852,7 +1348,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -860,6 +1356,19 @@ name = "tracing-core" version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] [[package]] name = "transpose" @@ -871,6 +1380,12 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -883,6 +1398,18 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "virtue" version = "0.0.18" @@ -899,6 +1426,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.115" @@ -931,7 +1464,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -995,7 +1528,27 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 87db484..4991070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["multi-plonk"] + [package] name = "multi-stark" version = "0.1.0" diff --git a/multi-plonk/Cargo.toml b/multi-plonk/Cargo.toml new file mode 100644 index 0000000..c44e856 --- /dev/null +++ b/multi-plonk/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "multi-plonk" +version = "0.1.0" +edition = "2024" +authors = ["Argument Engineering "] +license = "MIT OR Apache-2.0" +rust-version = "1.88" + +[dependencies] +ark-bls12-381 = { version = "0.5", features = ["curve"] } +ark-ec = "0.5" +ark-ff = "0.5" +ark-poly = "0.5" +ark-std = "0.5" +ark-crypto-primitives = { version = "0.5", features = ["sponge"] } +ark-poly-commit = "0.5" +ark-serialize = "0.5" + +[lints.clippy] +cast_lossless = "warn" +cast_possible_truncation = "warn" +cast_precision_loss = "warn" +cast_sign_loss = "warn" +cast_possible_wrap = "warn" +ptr_as_ptr = "warn" +checked_conversions = "warn" +dbg_macro = "warn" +derive_partial_eq_without_eq = "warn" +enum_glob_use = "warn" +explicit_into_iter_loop = "warn" +fallible_impl_from = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +from_iter_instead_of_collect = "warn" +implicit_clone = "warn" +inefficient_to_string = "warn" +large_stack_arrays = "warn" +large_types_passed_by_value = "warn" +macro_use_imports = "warn" +manual_assert = "warn" +map_err_ignore = "warn" +map_unwrap_or = "warn" +match_same_arms = "warn" +match_wild_err_arm = "warn" +needless_continue = "warn" +needless_for_each = "warn" +needless_pass_by_value = "warn" +option_option = "warn" +same_functions_in_if_condition = "warn" +trait_duplication_in_bounds = "warn" +unnecessary_wraps = "warn" +unnested_or_patterns = "warn" +use_self = "warn" + +[lints.rust] +nonstandard_style = "warn" +rust_2024_compatibility = "warn" +trivial_numeric_casts = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" +unreachable_pub = "warn" diff --git a/multi-plonk/examples/lookup_proof.rs b/multi-plonk/examples/lookup_proof.rs new file mode 100644 index 0000000..0c889a9 --- /dev/null +++ b/multi-plonk/examples/lookup_proof.rs @@ -0,0 +1,166 @@ +//! Multi-circuit PLONK example with lookup arguments. +//! +//! Mirrors the multi-stark `lookup_proof` example. Two circuits (Even, Odd) +//! compute parity via a recursive lookup: Even(n) → Odd(n-1), Odd(n) → Even(n-1). +//! The claim encodes the initial query: is_even(4) == 1. +//! +//! Run with: +//! ```sh +//! cargo run --example lookup_proof --release +//! ``` + +use ark_ff::{Field, One, Zero}; +use ark_serialize::CanonicalSerialize; +use ark_std::test_rng; + +use multi_plonk::air::{Air, AirBuilder, BaseAir}; +use multi_plonk::builder::symbolic::{SymbolicExpression, var}; +use multi_plonk::lookup::{Lookup, LookupAir}; +use multi_plonk::matrix::Matrix; +use multi_plonk::system::{System, SystemWitness}; +use multi_plonk::types::{PlonkConfig, Val}; + +/// Width: [multiplicity, input, input_inverse, input_is_zero, input_not_zero, recursion_output] +enum ParityAir { + Even, + Odd, +} + +impl ParityAir { + fn lookups(&self) -> Vec> { + let multiplicity = var(0); + let input = var(1); + let input_is_zero = var(3); + let input_not_zero = var(4); + let recursion_output = var(5); + let even_index: SymbolicExpression = Val::zero().into(); + let odd_index: SymbolicExpression = Val::one().into(); + let one: SymbolicExpression = Val::one().into(); + match self { + Self::Even => vec![ + Lookup::pull( + multiplicity, + vec![ + even_index, + input.clone(), + input_not_zero.clone() * recursion_output.clone() + input_is_zero, + ], + ), + Lookup::push( + input_not_zero, + vec![odd_index, input - one, recursion_output], + ), + ], + Self::Odd => vec![ + Lookup::pull( + multiplicity, + vec![ + odd_index, + input.clone(), + input_not_zero.clone() * recursion_output.clone(), + ], + ), + Lookup::push( + input_not_zero, + vec![even_index, input - one, recursion_output], + ), + ], + } + } +} + +impl BaseAir for ParityAir { + fn width(&self) -> usize { + 6 + } +} + +impl Air for ParityAir +where + AB: AirBuilder, + AB::Var: Copy, +{ + fn eval(&self, builder: &mut AB) { + let local = builder.main_local(); + let multiplicity = local[0]; + let input = local[1]; + let input_inverse = local[2]; + let input_is_zero = local[3]; + let input_not_zero = local[4]; + builder.assert_bool(input_is_zero); + builder.assert_bool(input_not_zero); + builder + .when(multiplicity) + .assert_one(input_is_zero.into() + input_not_zero.into()); + builder.when(input_is_zero).assert_zero(input); + builder + .when(input_not_zero) + .assert_one(input.into() * input_inverse.into()); + } +} + +fn main() { + let mut rng = test_rng(); + // max_constraint_degree = 3 (not_zero * input * input_inverse - 1) + // trace size = 4, q_factor = next_pow2(3) = 4, quotient_degree ≤ 3*4 = 12 + let config = PlonkConfig::setup(64, &mut rng); + + let even = LookupAir::new(ParityAir::Even, ParityAir::Even.lookups()); + let odd = LookupAir::new(ParityAir::Odd, ParityAir::Odd.lookups()); + let (system, key) = System::new(&config, [even, odd]); + + let f = |x: u64| Val::from(x); + // When input = 0 the constraint is gated by input_not_zero = 0, so the + // inverse column is left as 0 (any value is valid). + let inv = |x: u64| -> Val { + if x == 0 { + Val::zero() + } else { + Val::from(x).inverse().unwrap() + } + }; + + #[rustfmt::skip] + let witness = SystemWitness::from_stage_1( + vec![ + // Even circuit trace (4 rows × 6 cols): + // [multiplicity, input, input_inverse, input_is_zero, input_not_zero, recursion_output] + Matrix::new( + vec![ + f(1), f(4), inv(4), f(0), f(1), f(1), + f(1), f(2), inv(2), f(0), f(1), f(1), + f(1), f(0), f(0), f(1), f(0), f(0), + f(0), f(0), f(0), f(0), f(0), f(0), + ], + 6, + ), + // Odd circuit trace (4 rows × 6 cols) + Matrix::new( + vec![ + f(1), f(3), inv(3), f(0), f(1), f(1), + f(1), f(1), inv(1), f(0), f(1), f(1), + f(0), f(0), f(0), f(0), f(0), f(0), + f(0), f(0), f(0), f(0), f(0), f(0), + ], + 6, + ), + ], + &system, + ); + + // claim: [circuit_index=0, input=4, output=1] — is_even(4) should be 1 + let claim = &[f(0), f(4), f(1)]; + let proof = system.prove(&config, &key, claim, &witness); + system + .verify(&config, claim, &proof) + .expect("verify failed"); + println!("Lookup proof verified successfully!"); + + let mut bytes = vec![]; + proof.serialize_uncompressed(&mut bytes).expect("serialize"); + println!( + "Proof size: {} bytes (uncompressed), {} bytes (compressed)", + bytes.len(), + proof.compressed_size() + ); +} diff --git a/multi-plonk/examples/preprocessed_proof.rs b/multi-plonk/examples/preprocessed_proof.rs new file mode 100644 index 0000000..093d449 --- /dev/null +++ b/multi-plonk/examples/preprocessed_proof.rs @@ -0,0 +1,130 @@ +//! Example with preprocessed trace and lookups, ported to multi-plonk. +//! +//! Two circuits: +//! - **RangeTable**: read-only bytes 0..256 committed as a preprocessed trace. +//! Each row pulls a lookup weighted by its multiplicity column. +//! - **Squares**: computes x * x and range-checks both bytes of the result via +//! lookup pushes into RangeTable. +//! +//! Run with: +//! ```sh +//! cargo run --example preprocessed_proof --release +//! ``` + +use ark_ff::{One, Zero}; +use ark_serialize::CanonicalSerialize; +use ark_std::test_rng; + +use multi_plonk::air::{Air, AirBuilder, BaseAir}; +use multi_plonk::builder::symbolic::{SymbolicExpression, preprocessed_var, var}; +use multi_plonk::lookup::{Lookup, LookupAir}; +use multi_plonk::matrix::Matrix; +use multi_plonk::system::{System, SystemWitness}; +use multi_plonk::types::{PlonkConfig, Val}; + +enum SquaresCS { + /// Preprocessed column: bytes 0..256. Main column: multiplicity. + RangeTable, + /// Columns: [x, x², low_byte, high_byte, multiplicity]. + Squares, +} + +impl BaseAir for SquaresCS { + fn width(&self) -> usize { + match self { + Self::RangeTable => 1, + Self::Squares => 5, + } + } + + fn preprocessed_trace(&self) -> Option> { + match self { + Self::RangeTable => Some(Matrix::new((0u64..256).map(Val::from).collect(), 1)), + Self::Squares => None, + } + } +} + +impl Air for SquaresCS +where + AB: AirBuilder, + AB::Var: Copy, +{ + fn eval(&self, builder: &mut AB) { + match self { + Self::RangeTable => {} + Self::Squares => { + let local = builder.main_local(); + let x = local[0]; + let x_sq = local[1]; + let low = local[2]; + let high = local[3]; + // x² == x * x + builder.assert_eq(x_sq, x.into() * x.into()); + // x² == low + 256 * high (byte decomposition) + let c256: AB::Expr = Val::from(256u64).into(); + builder.assert_eq(x_sq, low.into() + high.into() * c256); + } + } + } +} + +impl SquaresCS { + fn lookups(&self) -> Vec> { + match self { + // RangeTable pulls: multiplicity × (preprocessed byte value) + Self::RangeTable => vec![Lookup::pull(var(0), vec![preprocessed_var(0)])], + // Squares pushes each byte of x² into the range table + Self::Squares => vec![ + Lookup::push(var(4), vec![var(2)]), // low byte + Lookup::push(var(4), vec![var(3)]), // high byte + ], + } + } +} + +fn main() { + let mut rng = test_rng(); + // max_constraint_degree = 2 (x² = x*x), trace size up to 256 rows, + // q_factor = 2, quotient_degree ≤ 256, preprocessed polys degree ≤ 255 + let config = PlonkConfig::setup(512, &mut rng); + + let range_table = LookupAir::new(SquaresCS::RangeTable, SquaresCS::RangeTable.lookups()); + let squares = LookupAir::new(SquaresCS::Squares, SquaresCS::Squares.lookups()); + let (system, key) = System::new(&config, [range_table, squares]); + + let n = 16usize; + let f = |x: usize| Val::from(x as u64); + + // Range-table main trace: multiplicity per byte value (256 rows × 1 col) + let mut range_mults = vec![Val::zero(); 256]; + // Squares trace: 16 rows × 5 cols + let mut sq_values = Vec::with_capacity(5 * n); + for x in 0..n { + let sq = x * x; + let low = sq & 0xFF; + let high = (sq >> 8) & 0xFF; + sq_values.extend([f(x), f(sq), f(low), f(high), Val::one()]); + range_mults[low] += Val::one(); + range_mults[high] += Val::one(); + } + + let range_trace = Matrix::new(range_mults, 1); + let squares_trace = Matrix::new(sq_values, 5); + let witness = SystemWitness::from_stage_1(vec![range_trace, squares_trace], &system); + + let no_claims: &[&[Val]] = &[]; + let proof = system.prove_multiple_claims(&config, &key, no_claims, &witness); + system + .verify_multiple_claims(&config, no_claims, &proof) + .expect("verify failed"); + println!("Preprocessed proof verified successfully!"); + + let mut bytes = vec![]; + proof.serialize_uncompressed(&mut bytes).expect("serialize"); + println!( + "Proof size: {} bytes (uncompressed), {} bytes (compressed)", + bytes.len(), + proof.compressed_size() + ); +} diff --git a/multi-plonk/examples/pythagorean.rs b/multi-plonk/examples/pythagorean.rs new file mode 100644 index 0000000..4501966 --- /dev/null +++ b/multi-plonk/examples/pythagorean.rs @@ -0,0 +1,89 @@ +//! End-to-end smoke test: prove `a² + b² == c²` over a 4-row trace using +//! the multi-plonk KZG-based prover/verifier on BLS12-381. + +use ark_ff::Zero; +use ark_serialize::CanonicalSerialize; +use ark_std::test_rng; + +use multi_plonk::air::{Air, AirBuilder, BaseAir}; +use multi_plonk::lookup::LookupAir; +use multi_plonk::matrix::Matrix; +use multi_plonk::system::{System, SystemWitness}; +use multi_plonk::types::{PlonkConfig, Val}; + +struct PythagoreanAir; + +impl BaseAir for PythagoreanAir { + fn width(&self) -> usize { + 3 + } +} + +impl Air for PythagoreanAir { + fn eval(&self, builder: &mut AB) { + let row = builder.main_local(); + let a: AB::Expr = row[0].into(); + let b: AB::Expr = row[1].into(); + let c: AB::Expr = row[2].into(); + // a² + b² == c² + builder.assert_eq(a.clone() * a + b.clone() * b, c.clone() * c); + } +} + +fn main() { + let mut rng = test_rng(); + // largest expected polynomial degree fits comfortably below this: + // trace size n = 4, max constraint degree 2 → coset size 8, + // quotient degree ≤ 2n - 1 = 7. + let config = PlonkConfig::setup(64, &mut rng); + + let air = PythagoreanAir; + let width = air.width(); + let lookup_air = LookupAir::new(air, vec![]); + let (system, key) = System::new(&config, [lookup_air]); + + let f = |x: u64| Val::from(x); + let trace = Matrix::new( + vec![ + f(3), + f(4), + f(5), + f(5), + f(12), + f(13), + f(8), + f(15), + f(17), + f(7), + f(24), + f(25), + ], + width, + ); + let witness = SystemWitness::from_stage_1(vec![trace], &system); + + let no_claims = &[]; + let proof = system.prove_multiple_claims(&config, &key, no_claims, &witness); + println!( + "intermediate accumulators: {:?}", + proof.intermediate_accumulators + ); + assert_eq!( + proof.intermediate_accumulators.last().copied(), + Some(Val::zero()) + ); + + system + .verify_multiple_claims(&config, no_claims, &proof) + .expect("verify"); + println!("Verified."); + + let mut bytes = vec![]; + proof.serialize_uncompressed(&mut bytes).expect("serialize"); + let compressed_size = proof.compressed_size(); + println!( + "Proof size: {} bytes (uncompressed), {} bytes (compressed)", + bytes.len(), + compressed_size + ); +} diff --git a/multi-plonk/src/air.rs b/multi-plonk/src/air.rs new file mode 100644 index 0000000..f46f4b6 --- /dev/null +++ b/multi-plonk/src/air.rs @@ -0,0 +1,124 @@ +//! AIR trait + minimal `AirBuilder` hierarchy. +//! +//! Mirrors the `p3_air::Air` / `p3_air::AirBuilder` shape, simplified for a +//! single field (no extension). Each builder exposes: +//! * a 2-row window over the main, preprocessed, and stage-2 traces +//! * row selectors (`is_first_row`, `is_last_row`, `is_transition`) +//! * stage-2 public values (lookup challenges + accumulators) +//! * an `assert_zero` sink that constraint folders accumulate into + +use std::ops::{Add, Mul, Neg, Sub}; + +use crate::matrix::Matrix; +use crate::types::Val; + +/// Static circuit metadata: trace width and optional preprocessed trace. +pub trait BaseAir { + fn width(&self) -> usize; + fn preprocessed_trace(&self) -> Option> { + None + } +} + +/// Constraint-evaluation interface implemented by symbolic, prover, verifier +/// and debug builders. +pub trait AirBuilder: Sized { + type Var: Copy + Into; + type Expr: Sized + + Clone + + From + + From + + Add + + Sub + + Mul + + Neg; + + fn main_local(&self) -> &[Self::Var]; + fn main_next(&self) -> &[Self::Var]; + fn preprocessed_local(&self) -> &[Self::Var]; + fn preprocessed_next(&self) -> &[Self::Var]; + fn stage_2_local(&self) -> &[Self::Var]; + fn stage_2_next(&self) -> &[Self::Var]; + fn stage_2_public_values(&self) -> &[Self::Var]; + + fn is_first_row(&self) -> Self::Expr; + fn is_last_row(&self) -> Self::Expr; + fn is_transition(&self) -> Self::Expr; + + fn assert_zero>(&mut self, x: I); + + fn assert_eq(&mut self, x: I, y: J) + where + I: Into, + J: Into, + { + self.assert_zero(x.into() - y.into()); + } + + fn assert_one>(&mut self, x: I) { + let one: Self::Expr = Val::from(1u64).into(); + self.assert_zero(x.into() - one); + } + + fn assert_bool>(&mut self, x: I) { + let x = x.into(); + let one: Self::Expr = Val::from(1u64).into(); + self.assert_zero(x.clone() * (x - one)); + } + + fn when>(&mut self, condition: I) -> FilteredBuilder<'_, Self> { + FilteredBuilder { + inner: self, + condition: condition.into(), + } + } + + fn when_first_row(&mut self) -> FilteredBuilder<'_, Self> { + let cond = self.is_first_row(); + self.when(cond) + } + + fn when_last_row(&mut self) -> FilteredBuilder<'_, Self> { + let cond = self.is_last_row(); + self.when(cond) + } + + fn when_transition(&mut self) -> FilteredBuilder<'_, Self> { + let cond = self.is_transition(); + self.when(cond) + } +} + +/// Sub-builder that scopes assertions under a boolean `condition` expression +/// (multiplies the assertion by the condition before forwarding it). +pub struct FilteredBuilder<'a, AB: AirBuilder> { + inner: &'a mut AB, + condition: AB::Expr, +} + +impl<'a, AB: AirBuilder> FilteredBuilder<'a, AB> { + pub fn assert_zero>(&mut self, x: I) { + let x: AB::Expr = x.into(); + self.inner.assert_zero(self.condition.clone() * x); + } + + pub fn assert_eq(&mut self, x: I, y: J) + where + I: Into, + J: Into, + { + self.assert_zero(x.into() - y.into()); + } + + pub fn assert_one>(&mut self, x: I) { + let one: AB::Expr = Val::from(1u64).into(); + self.assert_eq(x, one); + } +} + +/// AIR generic over the builder type, so the same constraints can be evaluated +/// symbolically (degree analysis), concretely on a coset (prover), or at a +/// single point (verifier). +pub trait Air: BaseAir { + fn eval(&self, builder: &mut AB); +} diff --git a/multi-plonk/src/builder/check.rs b/multi-plonk/src/builder/check.rs new file mode 100644 index 0000000..18d1f23 --- /dev/null +++ b/multi-plonk/src/builder/check.rs @@ -0,0 +1,104 @@ +//! Debug builder that checks every constraint on every row, used in tests to +//! locate constraint violations before invoking the full prover. + +use ark_ff::Zero; + +use crate::air::{Air, AirBuilder}; +use crate::matrix::Matrix; +use crate::types::Val; + +pub fn check_constraints( + air: &A, + preprocessed: Option<&Matrix>, + stage_1: &Matrix, + stage_2: &Matrix, + stage_2_public_values: &[Val], +) where + A: for<'a> Air>, +{ + let height = stage_1.height(); + for i in 0..height { + let i_next = (i + 1) % height; + let mut builder = DebugConstraintBuilder { + row_index: i, + preprocessed_local: &[], + preprocessed_next: &[], + stage_1_local: stage_1.row(i), + stage_1_next: stage_1.row(i_next), + stage_2_local: stage_2.row(i), + stage_2_next: stage_2.row(i_next), + stage_2_public_values, + is_first_row: Val::from(u64::from(i == 0)), + is_last_row: Val::from(u64::from(i == height - 1)), + is_transition: Val::from(u64::from(i != height - 1)), + }; + if let Some(preprocessed) = preprocessed { + builder.preprocessed_local = preprocessed.row(i); + builder.preprocessed_next = preprocessed.row(i_next); + air.eval(&mut builder); + } else { + air.eval(&mut builder); + } + } +} + +pub struct DebugConstraintBuilder<'a> { + pub row_index: usize, + pub preprocessed_local: &'a [Val], + pub preprocessed_next: &'a [Val], + pub stage_1_local: &'a [Val], + pub stage_1_next: &'a [Val], + pub stage_2_local: &'a [Val], + pub stage_2_next: &'a [Val], + pub stage_2_public_values: &'a [Val], + pub is_first_row: Val, + pub is_last_row: Val, + pub is_transition: Val, +} + +impl<'a> AirBuilder for DebugConstraintBuilder<'a> { + type Var = Val; + type Expr = Val; + + fn main_local(&self) -> &[Val] { + self.stage_1_local + } + fn main_next(&self) -> &[Val] { + self.stage_1_next + } + fn preprocessed_local(&self) -> &[Val] { + self.preprocessed_local + } + fn preprocessed_next(&self) -> &[Val] { + self.preprocessed_next + } + fn stage_2_local(&self) -> &[Val] { + self.stage_2_local + } + fn stage_2_next(&self) -> &[Val] { + self.stage_2_next + } + fn stage_2_public_values(&self) -> &[Val] { + self.stage_2_public_values + } + + fn is_first_row(&self) -> Val { + self.is_first_row + } + fn is_last_row(&self) -> Val { + self.is_last_row + } + fn is_transition(&self) -> Val { + self.is_transition + } + + fn assert_zero>(&mut self, x: I) { + let x: Val = x.into(); + assert!( + x.is_zero(), + "constraint had nonzero value on row {}: {:?}", + self.row_index, + x + ); + } +} diff --git a/multi-plonk/src/builder/folder.rs b/multi-plonk/src/builder/folder.rs new file mode 100644 index 0000000..f971ec2 --- /dev/null +++ b/multi-plonk/src/builder/folder.rs @@ -0,0 +1,153 @@ +//! Concrete constraint folders. +//! +//! Both prover and verifier flatten all `assert_zero(x)` calls to a single +//! field element via random-LC by powers of `alpha`. +//! +//! The prover folder is invoked once per row of the **coset evaluation +//! domain** (size `quotient_factor * trace_size`). The verifier folder is +//! invoked once at the out-of-domain challenge point `zeta`. + +use crate::air::AirBuilder; +use crate::types::Val; +use ark_ff::Zero; + +/// Evaluates constraints at a single coset point during quotient computation. +/// All windows are 1-row slices of the coset domain (current + next-by-step). +pub struct ProverConstraintFolder<'a> { + pub preprocessed_local: &'a [Val], + pub preprocessed_next: &'a [Val], + pub stage_1_local: &'a [Val], + pub stage_1_next: &'a [Val], + pub stage_2_local: &'a [Val], + pub stage_2_next: &'a [Val], + pub stage_2_public_values: &'a [Val], + pub is_first_row: Val, + pub is_last_row: Val, + pub is_transition: Val, + pub alpha_powers: &'a [Val], + pub accumulator: Val, + pub constraint_index: usize, +} + +impl<'a> AirBuilder for ProverConstraintFolder<'a> { + type Var = Val; + type Expr = Val; + + fn main_local(&self) -> &[Val] { + self.stage_1_local + } + fn main_next(&self) -> &[Val] { + self.stage_1_next + } + fn preprocessed_local(&self) -> &[Val] { + self.preprocessed_local + } + fn preprocessed_next(&self) -> &[Val] { + self.preprocessed_next + } + fn stage_2_local(&self) -> &[Val] { + self.stage_2_local + } + fn stage_2_next(&self) -> &[Val] { + self.stage_2_next + } + fn stage_2_public_values(&self) -> &[Val] { + self.stage_2_public_values + } + + fn is_first_row(&self) -> Val { + self.is_first_row + } + fn is_last_row(&self) -> Val { + self.is_last_row + } + fn is_transition(&self) -> Val { + self.is_transition + } + + fn assert_zero>(&mut self, x: I) { + let x: Val = x.into(); + let alpha_power = self.alpha_powers[self.constraint_index]; + self.accumulator += alpha_power * x; + self.constraint_index += 1; + } +} + +/// Evaluates constraints at the out-of-domain point `zeta` from the verifier's +/// opened values. +pub struct VerifierConstraintFolder<'a> { + pub preprocessed_local: &'a [Val], + pub preprocessed_next: &'a [Val], + pub stage_1_local: &'a [Val], + pub stage_1_next: &'a [Val], + pub stage_2_local: &'a [Val], + pub stage_2_next: &'a [Val], + pub stage_2_public_values: &'a [Val], + pub is_first_row: Val, + pub is_last_row: Val, + pub is_transition: Val, + pub alpha: Val, + pub accumulator: Val, +} + +impl<'a> AirBuilder for VerifierConstraintFolder<'a> { + type Var = Val; + type Expr = Val; + + fn main_local(&self) -> &[Val] { + self.stage_1_local + } + fn main_next(&self) -> &[Val] { + self.stage_1_next + } + fn preprocessed_local(&self) -> &[Val] { + self.preprocessed_local + } + fn preprocessed_next(&self) -> &[Val] { + self.preprocessed_next + } + fn stage_2_local(&self) -> &[Val] { + self.stage_2_local + } + fn stage_2_next(&self) -> &[Val] { + self.stage_2_next + } + fn stage_2_public_values(&self) -> &[Val] { + self.stage_2_public_values + } + + fn is_first_row(&self) -> Val { + self.is_first_row + } + fn is_last_row(&self) -> Val { + self.is_last_row + } + fn is_transition(&self) -> Val { + self.is_transition + } + + fn assert_zero>(&mut self, x: I) { + let x: Val = x.into(); + self.accumulator = self.accumulator * self.alpha + x; + } +} + +impl Default for ProverConstraintFolder<'_> { + fn default() -> Self { + Self { + preprocessed_local: &[], + preprocessed_next: &[], + stage_1_local: &[], + stage_1_next: &[], + stage_2_local: &[], + stage_2_next: &[], + stage_2_public_values: &[], + is_first_row: Val::zero(), + is_last_row: Val::zero(), + is_transition: Val::zero(), + alpha_powers: &[], + accumulator: Val::zero(), + constraint_index: 0, + } + } +} diff --git a/multi-plonk/src/builder/mod.rs b/multi-plonk/src/builder/mod.rs new file mode 100644 index 0000000..4b4e427 --- /dev/null +++ b/multi-plonk/src/builder/mod.rs @@ -0,0 +1,3 @@ +pub mod check; +pub mod folder; +pub mod symbolic; diff --git a/multi-plonk/src/builder/symbolic.rs b/multi-plonk/src/builder/symbolic.rs new file mode 100644 index 0000000..db6fb15 --- /dev/null +++ b/multi-plonk/src/builder/symbolic.rs @@ -0,0 +1,473 @@ +//! Symbolic constraint builder + expressions. +//! +//! Used to (a) record constraints once at [`Circuit`](crate::system::Circuit) +//! construction time, then derive their max degree, and (b) re-interpret lookup +//! argument expressions against concrete trace rows in both witness generation +//! and the in-circuit constraint evaluation. + +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use ark_ff::{One, Zero}; + +use crate::air::{Air, AirBuilder}; +use crate::types::Val; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Entry { + Preprocessed { offset: usize }, + Main { offset: usize }, + Stage2 { offset: usize }, + Stage2Public, +} + +#[derive(Copy, Clone, Debug)] +pub struct SymbolicVariable { + pub entry: Entry, + pub index: usize, +} + +impl SymbolicVariable { + pub const fn new(entry: Entry, index: usize) -> Self { + Self { entry, index } + } + + pub const fn degree_multiple(&self) -> usize { + match self.entry { + Entry::Preprocessed { .. } | Entry::Main { .. } | Entry::Stage2 { .. } => 1, + Entry::Stage2Public => 0, + } + } +} + +#[derive(Clone, Debug)] +pub enum SymbolicExpression { + Variable(SymbolicVariable), + IsFirstRow, + IsLastRow, + IsTransition, + Constant(Val), + Add { + x: Box, + y: Box, + degree_multiple: usize, + }, + Sub { + x: Box, + y: Box, + degree_multiple: usize, + }, + Neg { + x: Box, + degree_multiple: usize, + }, + Mul { + x: Box, + y: Box, + degree_multiple: usize, + }, +} + +impl SymbolicExpression { + pub const fn degree_multiple(&self) -> usize { + match self { + Self::Variable(v) => v.degree_multiple(), + Self::IsFirstRow | Self::IsLastRow => 1, + Self::IsTransition | Self::Constant(_) => 0, + Self::Add { + degree_multiple, .. + } + | Self::Sub { + degree_multiple, .. + } + | Self::Neg { + degree_multiple, .. + } + | Self::Mul { + degree_multiple, .. + } => *degree_multiple, + } + } + + /// Evaluate against a concrete trace row + (optional) preprocessed row. + /// Used by lookup argument expressions, which can only refer to current-row + /// columns. + pub fn interpret(&self, row: &[E], preprocessed: Option<&[E]>) -> E + where + E: Clone + + From + + Add + + Sub + + Mul + + Neg, + { + match self { + Self::Variable(v) => match v.entry { + Entry::Main { offset: 0 } => row[v.index].clone(), + Entry::Preprocessed { offset: 0 } => preprocessed + .expect("preprocessed row required but not provided")[v.index] + .clone(), + _ => panic!( + "symbolic expression in lookup args may only reference offset-0 main or preprocessed columns" + ), + }, + Self::Constant(c) => E::from(*c), + Self::Add { x, y, .. } => { + x.interpret(row, preprocessed) + y.interpret(row, preprocessed) + } + Self::Sub { x, y, .. } => { + x.interpret(row, preprocessed) - y.interpret(row, preprocessed) + } + Self::Mul { x, y, .. } => { + x.interpret(row, preprocessed) * y.interpret(row, preprocessed) + } + Self::Neg { x, .. } => -x.interpret(row, preprocessed), + Self::IsFirstRow | Self::IsLastRow | Self::IsTransition => { + panic!("row selectors are not allowed in lookup expressions") + } + } + } +} + +impl From for SymbolicExpression { + fn from(v: SymbolicVariable) -> Self { + Self::Variable(v) + } +} + +impl From for SymbolicExpression { + fn from(v: Val) -> Self { + Self::Constant(v) + } +} + +impl Default for SymbolicExpression { + fn default() -> Self { + Self::Constant(Val::zero()) + } +} + +#[inline] +pub fn var(i: usize) -> SymbolicExpression { + SymbolicExpression::Variable(SymbolicVariable::new(Entry::Main { offset: 0 }, i)) +} + +#[inline] +pub fn preprocessed_var(i: usize) -> SymbolicExpression { + SymbolicExpression::Variable(SymbolicVariable::new(Entry::Preprocessed { offset: 0 }, i)) +} + +impl Add for SymbolicExpression { + type Output = Self; + fn add(self, rhs: Self) -> Self { + match (self, rhs) { + (Self::Constant(a), b) if a.is_zero() => b, + (a, Self::Constant(b)) if b.is_zero() => a, + (Self::Constant(a), Self::Constant(b)) => Self::Constant(a + b), + (a, b) => { + let degree_multiple = a.degree_multiple().max(b.degree_multiple()); + Self::Add { + x: Box::new(a), + y: Box::new(b), + degree_multiple, + } + } + } + } +} + +impl Sub for SymbolicExpression { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + match (self, rhs) { + (a, Self::Constant(b)) if b.is_zero() => a, + (Self::Constant(a), Self::Constant(b)) => Self::Constant(a - b), + (a, b) => { + let degree_multiple = a.degree_multiple().max(b.degree_multiple()); + Self::Sub { + x: Box::new(a), + y: Box::new(b), + degree_multiple, + } + } + } + } +} + +impl Mul for SymbolicExpression { + type Output = Self; + fn mul(self, rhs: Self) -> Self { + match (self, rhs) { + (Self::Constant(a), _) if a.is_zero() => Self::Constant(Val::zero()), + (_, Self::Constant(b)) if b.is_zero() => Self::Constant(Val::zero()), + (Self::Constant(a), b) if a.is_one() => b, + (a, Self::Constant(b)) if b.is_one() => a, + (Self::Constant(a), Self::Constant(b)) => Self::Constant(a * b), + (a, b) => { + let degree_multiple = a.degree_multiple() + b.degree_multiple(); + Self::Mul { + x: Box::new(a), + y: Box::new(b), + degree_multiple, + } + } + } + } +} + +impl Neg for SymbolicExpression { + type Output = Self; + fn neg(self) -> Self { + match self { + Self::Constant(c) => Self::Constant(-c), + other => { + let degree_multiple = other.degree_multiple(); + Self::Neg { + x: Box::new(other), + degree_multiple, + } + } + } + } +} + +impl AddAssign for SymbolicExpression { + fn add_assign(&mut self, rhs: Self) { + let lhs = std::mem::take(self); + *self = lhs + rhs; + } +} +impl SubAssign for SymbolicExpression { + fn sub_assign(&mut self, rhs: Self) { + let lhs = std::mem::take(self); + *self = lhs - rhs; + } +} +impl MulAssign for SymbolicExpression { + fn mul_assign(&mut self, rhs: Self) { + let lhs = std::mem::take(self); + *self = lhs * rhs; + } +} + +/// Records constraints emitted by `air.eval(...)` for degree analysis. +pub struct SymbolicAirBuilder { + pub preprocessed_local: Vec, + pub preprocessed_next: Vec, + pub main_local: Vec, + pub main_next: Vec, + pub stage_2_local: Vec, + pub stage_2_next: Vec, + pub stage_2_public: Vec, + pub constraints: Vec, +} + +impl SymbolicAirBuilder { + pub fn new( + preprocessed_width: usize, + stage_1_width: usize, + stage_2_width: usize, + stage_2_public_count: usize, + ) -> Self { + let make = |entry_at_offset: fn(usize) -> Entry, w: usize| -> (Vec<_>, Vec<_>) { + let local = (0..w) + .map(|i| SymbolicVariable::new(entry_at_offset(0), i)) + .collect(); + let next = (0..w) + .map(|i| SymbolicVariable::new(entry_at_offset(1), i)) + .collect(); + (local, next) + }; + let (preprocessed_local, preprocessed_next) = + make(|o| Entry::Preprocessed { offset: o }, preprocessed_width); + let (main_local, main_next) = make(|o| Entry::Main { offset: o }, stage_1_width); + let (stage_2_local, stage_2_next) = make(|o| Entry::Stage2 { offset: o }, stage_2_width); + let stage_2_public = (0..stage_2_public_count) + .map(|i| SymbolicVariable::new(Entry::Stage2Public, i)) + .collect(); + Self { + preprocessed_local, + preprocessed_next, + main_local, + main_next, + stage_2_local, + stage_2_next, + stage_2_public, + constraints: vec![], + } + } +} + +impl AirBuilder for SymbolicAirBuilder { + type Var = SymbolicVariable; + type Expr = SymbolicExpression; + + fn main_local(&self) -> &[Self::Var] { + &self.main_local + } + fn main_next(&self) -> &[Self::Var] { + &self.main_next + } + fn preprocessed_local(&self) -> &[Self::Var] { + &self.preprocessed_local + } + fn preprocessed_next(&self) -> &[Self::Var] { + &self.preprocessed_next + } + fn stage_2_local(&self) -> &[Self::Var] { + &self.stage_2_local + } + fn stage_2_next(&self) -> &[Self::Var] { + &self.stage_2_next + } + fn stage_2_public_values(&self) -> &[Self::Var] { + &self.stage_2_public + } + + fn is_first_row(&self) -> Self::Expr { + SymbolicExpression::IsFirstRow + } + fn is_last_row(&self) -> Self::Expr { + SymbolicExpression::IsLastRow + } + fn is_transition(&self) -> Self::Expr { + SymbolicExpression::IsTransition + } + + fn assert_zero>(&mut self, x: I) { + self.constraints.push(x.into()); + } +} + +pub fn get_symbolic_constraints( + air: &A, + preprocessed_width: usize, + stage_1_width: usize, + stage_2_width: usize, + stage_2_public_count: usize, +) -> Vec +where + A: Air, +{ + let mut builder = SymbolicAirBuilder::new( + preprocessed_width, + stage_1_width, + stage_2_width, + stage_2_public_count, + ); + air.eval(&mut builder); + builder.constraints +} + +pub fn get_max_constraint_degree(constraints: &[SymbolicExpression]) -> usize { + constraints + .iter() + .map(|c| c.degree_multiple()) + .max() + .unwrap_or(0) +} + +// --------------------------------------------------------------------------- +// SymbolicExpression interop with `Val` so that AIR code can write `expr + 1`, +// `expr * Val::from(2)`, etc. directly. +// --------------------------------------------------------------------------- + +impl Add for SymbolicExpression { + type Output = Self; + fn add(self, rhs: Val) -> Self { + self + Self::Constant(rhs) + } +} +impl Sub for SymbolicExpression { + type Output = Self; + fn sub(self, rhs: Val) -> Self { + self - Self::Constant(rhs) + } +} +impl Mul for SymbolicExpression { + type Output = Self; + fn mul(self, rhs: Val) -> Self { + self * Self::Constant(rhs) + } +} + +impl Add for SymbolicExpression { + type Output = Self; + fn add(self, rhs: SymbolicVariable) -> Self { + self + Self::Variable(rhs) + } +} +impl Sub for SymbolicExpression { + type Output = Self; + fn sub(self, rhs: SymbolicVariable) -> Self { + self - Self::Variable(rhs) + } +} +impl Mul for SymbolicExpression { + type Output = Self; + fn mul(self, rhs: SymbolicVariable) -> Self { + self * Self::Variable(rhs) + } +} + +impl Add for SymbolicVariable { + type Output = SymbolicExpression; + fn add(self, rhs: SymbolicExpression) -> SymbolicExpression { + SymbolicExpression::from(self) + rhs + } +} +impl Sub for SymbolicVariable { + type Output = SymbolicExpression; + fn sub(self, rhs: SymbolicExpression) -> SymbolicExpression { + SymbolicExpression::from(self) - rhs + } +} +impl Mul for SymbolicVariable { + type Output = SymbolicExpression; + fn mul(self, rhs: SymbolicExpression) -> SymbolicExpression { + SymbolicExpression::from(self) * rhs + } +} + +impl Add for SymbolicVariable { + type Output = SymbolicExpression; + fn add(self, rhs: Self) -> SymbolicExpression { + SymbolicExpression::from(self) + SymbolicExpression::from(rhs) + } +} +impl Sub for SymbolicVariable { + type Output = SymbolicExpression; + fn sub(self, rhs: Self) -> SymbolicExpression { + SymbolicExpression::from(self) - SymbolicExpression::from(rhs) + } +} +impl Mul for SymbolicVariable { + type Output = SymbolicExpression; + fn mul(self, rhs: Self) -> SymbolicExpression { + SymbolicExpression::from(self) * SymbolicExpression::from(rhs) + } +} +impl Neg for SymbolicVariable { + type Output = SymbolicExpression; + fn neg(self) -> SymbolicExpression { + -SymbolicExpression::from(self) + } +} + +impl Add for SymbolicVariable { + type Output = SymbolicExpression; + fn add(self, rhs: Val) -> SymbolicExpression { + SymbolicExpression::from(self) + SymbolicExpression::Constant(rhs) + } +} +impl Sub for SymbolicVariable { + type Output = SymbolicExpression; + fn sub(self, rhs: Val) -> SymbolicExpression { + SymbolicExpression::from(self) - SymbolicExpression::Constant(rhs) + } +} +impl Mul for SymbolicVariable { + type Output = SymbolicExpression; + fn mul(self, rhs: Val) -> SymbolicExpression { + SymbolicExpression::from(self) * SymbolicExpression::Constant(rhs) + } +} diff --git a/multi-plonk/src/lib.rs b/multi-plonk/src/lib.rs new file mode 100644 index 0000000..fc6d448 --- /dev/null +++ b/multi-plonk/src/lib.rs @@ -0,0 +1,36 @@ +//! multi-plonk: KZG-based variant of multi-stark. +//! +//! Mirrors the structure of multi-stark but uses BLS12-381 / SonicKZG10 +//! polynomial commitments instead of FRI over Goldilocks. Trace polynomials +//! are committed in monomial basis after iFFT; constraints are evaluated on a +//! coset of size `next_pow2(max_constraint_degree) * trace_size` to allow +//! division by the vanishing polynomial. The quotient is committed as a single +//! polynomial (no chunking). +//! +//! No extension field is used: the BLS12-381 scalar field Fr is large enough +//! (~256 bits) that single-field Schwartz-Zippel bounds are already negligible. + +pub mod air; +pub mod builder; +pub mod lookup; +pub mod matrix; +pub mod prover; +pub mod system; +pub mod types; +pub mod verifier; + +#[macro_export] +macro_rules! ensure { + ($condition:expr, $err:expr) => { + if !$condition { + return std::result::Result::Err($err); + } + }; +} + +#[macro_export] +macro_rules! ensure_eq { + ($a:expr, $b:expr, $err:expr) => { + $crate::ensure!(($a) == ($b), $err); + }; +} diff --git a/multi-plonk/src/lookup.rs b/multi-plonk/src/lookup.rs new file mode 100644 index 0000000..d5b73af --- /dev/null +++ b/multi-plonk/src/lookup.rs @@ -0,0 +1,291 @@ +//! LogUp lookup argument. +//! +//! Each circuit may push or pull lookup tuples each row. The argument compresses +//! tuples into single field elements via a fingerprint (Horner evaluation under +//! a per-proof challenge `gamma`), then offsets each by another challenge +//! `beta`. The running accumulator +//! +//! ```text +//! acc_{i+1} = acc_i + Σ_j multiplicity_{i,j} / (beta + fingerprint(gamma, args_{i,j})) +//! ``` +//! +//! must telescope to zero across all circuits if the multiset balance holds. +//! +//! Stage 2 trace per circuit: column 0 is the running accumulator at the start +//! of each row; columns 1..=L_i are the per-lookup message inverses. + +use ark_ff::{Field, One, Zero}; + +use crate::air::{Air, AirBuilder, BaseAir}; +use crate::builder::symbolic::SymbolicExpression; +use crate::matrix::Matrix; +use crate::types::Val; + +/// Stage-2 public values used by the lookup argument: lookup challenge, +/// fingerprint challenge, current accumulator, next accumulator. +pub const LOOKUP_PUBLIC_SIZE: usize = 4; + +#[derive(Clone)] +pub struct Lookup { + pub multiplicity: Expr, + pub args: Vec, +} + +impl Lookup { + #[inline] + pub fn empty() -> Self + where + Expr: From, + { + Self { + multiplicity: Expr::from(Val::zero()), + args: vec![], + } + } + + /// Adds a claim to the lookup channel. + #[inline] + pub fn push(multiplicity: Expr, args: Vec) -> Self { + Self { multiplicity, args } + } + + /// Removes a claim from the lookup channel. + #[inline] + pub fn pull(multiplicity: Expr, args: Vec) -> Self + where + Expr: std::ops::Neg, + { + Self { + multiplicity: -multiplicity, + args, + } + } +} + +pub struct LookupAir { + pub inner_air: A, + pub lookups: Vec>, + pub preprocessed: Option>, +} + +impl LookupAir { + pub fn new(inner_air: A, lookups: Vec>) -> Self { + let preprocessed = inner_air.preprocessed_trace(); + Self { + inner_air, + lookups, + preprocessed, + } + } + + /// Stage 2 width: 1 accumulator column + 1 inverse column per lookup. + pub fn stage_2_width(&self) -> usize { + 1 + self.lookups.len() + } +} + +/// Horner-style fingerprint of a sequence of coefficients. +#[inline] +pub fn fingerprint(r: &Val, coeffs: Iter) -> Val +where + I: Into, + Iter: DoubleEndedIterator, +{ + coeffs + .rev() + .fold(Val::zero(), |acc, coeff| acc * r + coeff.into()) +} + +impl Lookup { + /// Evaluate symbolic lookup args against a concrete trace row + optional + /// preprocessed row. + pub fn compute_expr(&self, row: &[Val], preprocessed: Option<&[Val]>) -> Lookup { + let multiplicity = self.multiplicity.interpret::(row, preprocessed); + let args = self + .args + .iter() + .map(|arg| arg.interpret::(row, preprocessed)) + .collect(); + Lookup { multiplicity, args } + } +} + +impl Lookup { + /// Build the stage-2 trace for every circuit + the per-circuit intermediate + /// accumulator value (the running sum at the **end** of each circuit). + pub fn stage_2_traces( + lookups: &[Vec>], + lookup_challenge: Val, + fingerprint_challenge: &Val, + mut accumulator: Val, + ) -> (Vec>, Vec) { + // count messages per circuit + total + let mut num_lookups_per_circuit = Vec::with_capacity(lookups.len()); + let mut total_num_lookups = 0usize; + for circuit_lookups in lookups { + let num_rows = circuit_lookups.len(); + let num_row_lookups = circuit_lookups[0].len(); + let n = num_rows * num_row_lookups; + num_lookups_per_circuit.push(n); + total_num_lookups += n; + } + + // compute messages + let mut messages = Vec::with_capacity(total_num_lookups); + for circuit_lookups in lookups { + for lookup in circuit_lookups.iter().flatten() { + messages.push(lookup.compute_message(lookup_challenge, fingerprint_challenge)); + } + } + + // batch invert + let messages_inverses = batch_inverse(&messages); + + let mut intermediate_accumulators = Vec::with_capacity(lookups.len()); + let mut traces = Vec::with_capacity(lookups.len()); + let mut offset = 0; + for (circuit_lookups, num_circuit_messages) in lookups.iter().zip(num_lookups_per_circuit) { + let circuit_messages_inverses = + &messages_inverses[offset..offset + num_circuit_messages]; + offset += num_circuit_messages; + + let num_row_lookups = circuit_lookups[0].len(); + let values = if num_row_lookups == 0 { + vec![accumulator; circuit_lookups.len()] + } else { + circuit_lookups + .iter() + .zip(circuit_messages_inverses.chunks_exact(num_row_lookups)) + .flat_map(|(row_lookups, row_inverses)| { + let mut row = Vec::with_capacity(1 + row_lookups.len()); + row.push(accumulator); + for (lookup, &inv) in row_lookups.iter().zip(row_inverses) { + accumulator += lookup.multiplicity * inv; + row.push(inv); + } + row + }) + .collect::>() + }; + let width = 1 + num_row_lookups; + debug_assert_eq!(values.len() % width, 0); + traces.push(Matrix::new(values, width)); + intermediate_accumulators.push(accumulator); + } + (traces, intermediate_accumulators) + } + + fn compute_message(&self, lookup_challenge: Val, fingerprint_challenge: &Val) -> Val { + lookup_challenge + fingerprint(fingerprint_challenge, self.args.iter().copied()) + } +} + +fn batch_inverse(elems: &[Val]) -> Vec { + // Standard Montgomery batch-inversion trick. Avoids a per-element + // `inverse()` call. + let n = elems.len(); + if n == 0 { + return vec![]; + } + let mut prefix = Vec::with_capacity(n); + let mut acc = Val::one(); + for x in elems { + prefix.push(acc); + acc *= *x; + } + let mut inv_acc = acc.inverse().expect("batch inversion: zero element"); + let mut out = vec![Val::zero(); n]; + for i in (0..n).rev() { + out[i] = prefix[i] * inv_acc; + inv_acc *= elems[i]; + } + out +} + +impl BaseAir for LookupAir { + fn width(&self) -> usize { + self.inner_air.width() + } + fn preprocessed_trace(&self) -> Option> { + self.preprocessed.clone() + } +} + +impl Air for LookupAir +where + A: Air, + AB: AirBuilder, +{ + fn eval(&self, builder: &mut AB) { + // 1. inner AIR constraints + self.inner_air.eval(builder); + + // 2. lookup constraints (stage 2) + let (lookup_challenge, fingerprint_challenge, acc, next_acc) = { + let pubs = builder.stage_2_public_values(); + debug_assert_eq!(pubs.len(), LOOKUP_PUBLIC_SIZE); + (pubs[0], pubs[1], pubs[2], pubs[3]) + }; + let stage_2_local = builder.stage_2_local().to_vec(); + let acc_col: AB::Var = stage_2_local[0]; + let messages_inverses: Vec = stage_2_local[1..].to_vec(); + debug_assert_eq!(messages_inverses.len(), self.lookups.len()); + + // Convert row data into Expr form so symbolic interpretation can run + // over the builder's expression type. + let main_local_expr: Vec = + builder.main_local().iter().map(|v| (*v).into()).collect(); + let preprocessed_local_expr: Option> = if self.preprocessed.is_some() { + Some( + builder + .preprocessed_local() + .iter() + .map(|v| (*v).into()) + .collect(), + ) + } else { + None + }; + + let mut acc_expr: AB::Expr = acc_col.into(); + for (lookup, &inv_var) in self.lookups.iter().zip(messages_inverses.iter()) { + let multiplicity: AB::Expr = lookup + .multiplicity + .interpret::(&main_local_expr, preprocessed_local_expr.as_deref()); + let args_iter = lookup.args.iter().map(|arg| { + arg.interpret::(&main_local_expr, preprocessed_local_expr.as_deref()) + }); + let fp_r: AB::Expr = fingerprint_challenge.into(); + let fingerprint_expr = fingerprint_horner::(&fp_r, args_iter); + let message_expr: AB::Expr = AB::Expr::from(lookup_challenge) + fingerprint_expr; + let inv_expr: AB::Expr = inv_var.into(); + // m * m^{-1} == 1 + let one: AB::Expr = Val::one().into(); + builder.assert_zero(message_expr * inv_expr.clone() - one); + acc_expr = acc_expr + multiplicity * inv_expr; + } + + // initial accumulator value + let acc_pub: AB::Expr = acc.into(); + builder.when_first_row().assert_eq(acc_col, acc_pub); + + // transition: acc_expr matches next-row accumulator column + let next_acc_col: AB::Var = builder.stage_2_next()[0]; + builder + .when_transition() + .assert_eq(acc_expr.clone(), next_acc_col); + + // last-row final value matches the public next_acc + let next_acc_pub: AB::Expr = next_acc.into(); + builder.when_last_row().assert_eq(acc_expr, next_acc_pub); + } +} + +/// Horner over a generic ring (used in-circuit; element type need not be `Val`). +fn fingerprint_horner(r: &E, args: impl DoubleEndedIterator) -> E +where + E: Clone + From + std::ops::Add + std::ops::Mul, +{ + args.rev() + .fold(E::from(Val::zero()), |acc, coeff| acc * r.clone() + coeff) +} diff --git a/multi-plonk/src/matrix.rs b/multi-plonk/src/matrix.rs new file mode 100644 index 0000000..6e1dd0d --- /dev/null +++ b/multi-plonk/src/matrix.rs @@ -0,0 +1,80 @@ +//! Minimal row-major matrix used for traces. +//! +//! Mirrors the subset of `p3_matrix::dense::RowMajorMatrix` actually needed +//! by the prover/verifier. + +use crate::types::Val; + +#[derive(Clone, Debug)] +pub struct Matrix { + pub values: Vec, + pub width: usize, +} + +impl Matrix { + pub fn new(values: Vec, width: usize) -> Self { + if width == 0 { + assert!(values.is_empty(), "non-empty values with zero width"); + } else { + assert_eq!(values.len() % width, 0, "non-rectangular matrix"); + } + Self { values, width } + } + + #[inline] + pub fn height(&self) -> usize { + if self.width == 0 { + 0 + } else { + self.values.len() / self.width + } + } + + #[inline] + pub fn width(&self) -> usize { + self.width + } + + pub fn row(&self, i: usize) -> &[F] { + &self.values[i * self.width..(i + 1) * self.width] + } + + pub fn rows(&self) -> impl Iterator { + self.values.chunks_exact(self.width) + } + + /// Returns each column as an owned `Vec`. Used to feed iFFT. + pub fn columns(&self) -> Vec> { + let h = self.height(); + let w = self.width; + let mut cols: Vec> = (0..w).map(|_| Vec::with_capacity(h)).collect(); + for row in self.rows() { + for (c, v) in row.iter().enumerate() { + cols[c].push(v.clone()); + } + } + cols + } + + /// Build a matrix from a list of equally-sized columns. + pub fn from_columns(cols: &[Vec]) -> Self { + let width = cols.len(); + if width == 0 { + return Self { + values: vec![], + width: 0, + }; + } + let height = cols[0].len(); + for c in cols { + assert_eq!(c.len(), height, "columns must have the same length"); + } + let mut values = Vec::with_capacity(height * width); + for i in 0..height { + for c in cols { + values.push(c[i].clone()); + } + } + Self { values, width } + } +} diff --git a/multi-plonk/src/prover.rs b/multi-plonk/src/prover.rs new file mode 100644 index 0000000..55b585e --- /dev/null +++ b/multi-plonk/src/prover.rs @@ -0,0 +1,576 @@ +//! Multi-circuit PLONK-style prover with KZG commitments. +//! +//! Pipeline (mirrors `multi-stark` but in a single field): +//! +//! 1. **Stage 1**: iFFT each trace column → monomial polynomials `T_i(X)`. +//! Commit them all as one labeled batch via SonicKZG10. +//! 2. **Lookup challenges (β, γ)**: sampled from the Fiat-Shamir sponge after +//! observing claims and stage-1 commitments. +//! 3. **Stage 2**: build the LogUp accumulator + per-row message inverses, +//! iFFT each column, commit as a second labeled batch. +//! 4. **Constraint challenge α** + **quotient**: evaluate constraints on a +//! coset domain of size `next_pow2(max_constraint_degree) * trace_size`, +//! fold via powers of α, divide by Z_H. iFFT result back to a single +//! quotient polynomial (no chunking) and commit it. +//! 5. **OOD opening**: sample ζ. Open every committed polynomial at ζ; for +//! polynomials with row-rotation constraints (stage 1, stage 2, +//! preprocessed) also open at ζ·ω_i where ω_i is the trace-domain +//! generator. +//! +//! Each (commit, open) call uses the SAME Poseidon sponge that drives +//! Fiat-Shamir, so every pcs operation is bound to the transcript. + +use ark_ff::{FftField, Field, One, PrimeField, Zero}; +use ark_poly::{EvaluationDomain, GeneralEvaluationDomain, univariate::DensePolynomial}; +use ark_poly_commit::{LabeledCommitment, LabeledPolynomial, PolynomialCommitment}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::log2; + +use ark_crypto_primitives::sponge::{CryptographicSponge, FieldElementSize}; + +use crate::air::Air; +use crate::builder::folder::ProverConstraintFolder; +use crate::lookup::{Lookup, fingerprint}; +use crate::system::{ProverKey, System, SystemWitness, ifft_column}; +use crate::types::{ + Commitment, CommitmentState, OpeningProof, PC, PlonkConfig, Sponge, UniPoly, Val, +}; + +/// Layout of opened values: +/// +/// `values_at_zeta` (length = #preprocessed + #stage1 + #stage2 + #quotient) +/// is concatenated in fixed order: all preprocessed columns (ordered as the +/// preprocessed bundle), then all stage-1 columns (concatenated per circuit), +/// then all stage-2 columns, then one quotient value per circuit. +/// +/// `values_at_zeta_omega` (length = sum over circuits of preprocessed_width + +/// stage_1_width + stage_2_width) is concatenated per circuit: preprocessed | +/// stage1 | stage2. +#[derive(CanonicalSerialize, CanonicalDeserialize, Clone)] +pub struct Proof { + pub stage_1_commitments: Vec, + pub stage_2_commitments: Vec, + pub quotient_commitments: Vec, + pub log_degrees: Vec, + pub intermediate_accumulators: Vec, + pub values_at_zeta: Vec, + pub values_at_zeta_omega: Vec, + pub proof_at_zeta: OpeningProof, + pub proofs_at_zeta_omega: Vec, +} + +impl System +where + A: crate::air::BaseAir + + Air + + for<'a> Air>, +{ + pub fn prove( + &self, + config: &PlonkConfig, + key: &ProverKey, + claim: &[Val], + witness: &SystemWitness, + ) -> Proof { + self.prove_multiple_claims(config, key, &[claim], witness) + } + + pub fn prove_multiple_claims( + &self, + config: &PlonkConfig, + key: &ProverKey, + claims: &[&[Val]], + witness: &SystemWitness, + ) -> Proof { + let mut sponge = config.make_sponge(); + + // ── Stage 1: commit trace columns ──────────────────────────────────── + let mut stage_1_polys: Vec> = vec![]; + let mut stage_1_widths: Vec = vec![]; + let mut log_degrees: Vec = vec![]; + for (i, trace) in witness.traces.iter().enumerate() { + let n = trace.height(); + log_degrees.push(u8::try_from(log2(n)).expect("log2(trace_height) fits u8")); + stage_1_widths.push(trace.width()); + for (col_idx, col) in trace.columns().into_iter().enumerate() { + let poly = ifft_column(&col); + let label = format!("stage1_c{i}_col{col_idx}"); + stage_1_polys.push(LabeledPolynomial::new(label, poly, None, None)); + } + } + let (stage_1_commitments, stage_1_states) = + PC::commit(&config.committer_key, &stage_1_polys, None).expect("stage 1 commit failed"); + + // observe preprocessed + stage 1 commitments + degrees + claims + for c in &self.preprocessed_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + for c in &stage_1_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + for d in &log_degrees { + sponge.absorb(&Val::from(u64::from(*d))); + } + for claim in claims { + sponge.absorb(claim); + } + + // ── Lookup challenges ──────────────────────────────────────────────── + let lookup_challenge = squeeze_field(&mut sponge); + sponge.absorb(&lookup_challenge); + let fingerprint_challenge = squeeze_field(&mut sponge); + sponge.absorb(&fingerprint_challenge); + + // initial accumulator from claims + let mut acc = Val::zero(); + for claim in claims { + let message = + lookup_challenge + fingerprint(&fingerprint_challenge, claim.iter().copied()); + acc += message.inverse().expect("zero claim message"); + } + + // ── Stage 2: lookup traces ─────────────────────────────────────────── + let (stage_2_traces, intermediate_accumulators) = Lookup::stage_2_traces( + &witness.lookups, + lookup_challenge, + &fingerprint_challenge, + acc, + ); + let mut stage_2_polys: Vec> = vec![]; + let mut stage_2_widths: Vec = vec![]; + for (i, trace) in stage_2_traces.iter().enumerate() { + stage_2_widths.push(trace.width()); + for (col_idx, col) in trace.columns().into_iter().enumerate() { + let poly = ifft_column(&col); + let label = format!("stage2_c{i}_col{col_idx}"); + stage_2_polys.push(LabeledPolynomial::new(label, poly, None, None)); + } + } + let (stage_2_commitments, stage_2_states) = + PC::commit(&config.committer_key, &stage_2_polys, None).expect("stage 2 commit failed"); + for c in &stage_2_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + + // ── Constraint challenge α + quotient ──────────────────────────────── + let alpha = squeeze_field(&mut sponge); + + // For each circuit, evaluate all constraints on a coset domain large + // enough to recover the composition polynomial, divide by Z_H, iFFT + // the result back to a single quotient polynomial. + let mut quotient_polys: Vec> = vec![]; + let mut acc_input = acc; + for (i, circuit) in self.circuits.iter().enumerate() { + let n = 1usize << log_degrees[i]; + let q_factor = next_pow2(circuit.max_constraint_degree.max(2)); + let coset_size = q_factor * n; + let coset = GeneralEvaluationDomain::::new(coset_size) + .expect("coset size unsupported by FFT") + .get_coset(Val::GENERATOR) + .expect("could not build coset"); + + let preprocessed_trace_polys: Vec = + if let Some(prep_idx) = self.preprocessed_indices[i] { + polys_for_circuit( + &key.preprocessed_polys, + prep_idx_to_poly_offset(self, prep_idx), + circuit.preprocessed_width, + ) + } else { + vec![] + }; + let stage_1_trace_polys: Vec = + polys_for_circuit_stage1(&stage_1_polys, &stage_1_widths, i); + let stage_2_trace_polys: Vec = + polys_for_circuit_stage2(&stage_2_polys, &stage_2_widths, i); + + // Evaluate every column on the coset. + let preprocessed_evals: Vec> = preprocessed_trace_polys + .iter() + .map(|p| coset.fft(&p.coeffs)) + .collect(); + let stage_1_evals: Vec> = stage_1_trace_polys + .iter() + .map(|p| coset.fft(&p.coeffs)) + .collect(); + let stage_2_evals: Vec> = stage_2_trace_polys + .iter() + .map(|p| coset.fft(&p.coeffs)) + .collect(); + + // Selectors and Z_H on the coset. + let zh_inv = vanishing_inv_on_coset(coset_size, n); + let (is_first_row, is_last_row, is_transition) = row_selectors_on_coset(&coset, n); + + // Stage-2 public values. + let next_acc = intermediate_accumulators[i]; + let stage_2_publics = [lookup_challenge, fingerprint_challenge, acc_input, next_acc]; + acc_input = next_acc; // for next circuit iteration + + // Powers of alpha for this circuit's constraints — reversed so + // that the i-th constraint is multiplied by α^{k-1-i}, matching + // the verifier's Horner scheme `acc = acc * α + x`. + let mut alpha_powers = Vec::with_capacity(circuit.constraint_count); + let mut p = Val::one(); + for _ in 0..circuit.constraint_count { + alpha_powers.push(p); + p *= alpha; + } + alpha_powers.reverse(); + + // Step between local and "next" row in the coset (= q_factor). + let next_step = q_factor; + + // Evaluate composition row-by-row on the coset. + let mut composition_evals = Vec::with_capacity(coset_size); + for k in 0..coset_size { + let k_next = (k + next_step) % coset_size; + let preprocessed_local: Vec = + preprocessed_evals.iter().map(|e| e[k]).collect(); + let preprocessed_next: Vec = + preprocessed_evals.iter().map(|e| e[k_next]).collect(); + let stage_1_local: Vec = stage_1_evals.iter().map(|e| e[k]).collect(); + let stage_1_next: Vec = stage_1_evals.iter().map(|e| e[k_next]).collect(); + let stage_2_local: Vec = stage_2_evals.iter().map(|e| e[k]).collect(); + let stage_2_next: Vec = stage_2_evals.iter().map(|e| e[k_next]).collect(); + + let mut folder = ProverConstraintFolder { + preprocessed_local: &preprocessed_local, + preprocessed_next: &preprocessed_next, + stage_1_local: &stage_1_local, + stage_1_next: &stage_1_next, + stage_2_local: &stage_2_local, + stage_2_next: &stage_2_next, + stage_2_public_values: &stage_2_publics, + is_first_row: is_first_row[k], + is_last_row: is_last_row[k], + is_transition: is_transition[k], + alpha_powers: &alpha_powers, + accumulator: Val::zero(), + constraint_index: 0, + }; + circuit.air.eval(&mut folder); + composition_evals.push(folder.accumulator * zh_inv[k]); + } + + // iFFT composition_evals (already on coset) → coefficient form + // (this divides out the coset offset implicitly via coset.ifft). + let q_coeffs = coset.ifft(&composition_evals); + let mut q_poly = DensePolynomial { coeffs: q_coeffs }; + // trim trailing zeros — real quotient has degree ≤ (q_factor-1)·n + while q_poly.coeffs.last().is_some_and(|c| c.is_zero()) { + q_poly.coeffs.pop(); + } + let label = format!("quotient_c{i}"); + quotient_polys.push(LabeledPolynomial::new(label, q_poly, None, None)); + } + let (quotient_commitments, quotient_states) = + PC::commit(&config.committer_key, "ient_polys, None) + .expect("quotient commit failed"); + for c in "ient_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + + // ── OOD opening at ζ and ζ·ω_i ─────────────────────────────────────── + let zeta = squeeze_field(&mut sponge); + + // Build a unified list of (poly, state, commitment) entries so we can + // batch-open per point in a single SonicKZG10 call. + // Order per circuit: preprocessed | stage1 | stage2 | quotient + // (preprocessed = the slice of `key.preprocessed_polys` for this circuit) + let mut all_polys: Vec<&LabeledPolynomial> = vec![]; + let mut all_states: Vec<&CommitmentState> = vec![]; + let mut all_commitments: Vec<&LabeledCommitment> = vec![]; + // Track per-circuit ranges into all_*. + let mut circuit_ranges: Vec = vec![]; + + for (i, circuit) in self.circuits.iter().enumerate() { + let r = CircuitRanges { + prep: self.preprocessed_indices[i].map(|p| { + let start = poly_offset_for_prep(self, p); + start..start + circuit.preprocessed_width + }), + stage_1: { + let start = stage_1_widths[..i].iter().sum::(); + start..start + stage_1_widths[i] + }, + stage_2: { + let start = stage_2_widths[..i].iter().sum::(); + start..start + stage_2_widths[i] + }, + quotient: i..i + 1, + }; + circuit_ranges.push(r); + } + + // Push into all_* for each batch (preprocessed first, stage1, stage2, + // quotient). Order matters for verifier reconstruction. + for (poly, state, comm) in zip3( + &key.preprocessed_polys, + &key.preprocessed_states, + &self.preprocessed_commitments, + ) { + all_polys.push(poly); + all_states.push(state); + all_commitments.push(comm); + } + for (poly, state, comm) in zip3(&stage_1_polys, &stage_1_states, &stage_1_commitments) { + all_polys.push(poly); + all_states.push(state); + all_commitments.push(comm); + } + for (poly, state, comm) in zip3(&stage_2_polys, &stage_2_states, &stage_2_commitments) { + all_polys.push(poly); + all_states.push(state); + all_commitments.push(comm); + } + for (poly, state, comm) in zip3("ient_polys, "ient_states, "ient_commitments) { + all_polys.push(poly); + all_states.push(state); + all_commitments.push(comm); + } + + // Section bases inside `all_*` (stage1 = after all preprocessed). + let n_prep = key.preprocessed_polys.len(); + let n_s1 = stage_1_polys.len(); + let stage_1_base = n_prep; + let stage_2_base = stage_1_base + n_s1; + + // Open at zeta — batches every poly across every circuit. + let proof_at_zeta = PC::open( + &config.committer_key, + all_polys.iter().copied(), + all_commitments.iter().copied(), + &zeta, + &mut sponge, + all_states.iter().copied(), + None, + ) + .expect("open at zeta failed"); + + // Compute the openings (evaluations) at zeta + zeta*omega. + let mut values_at_zeta = Vec::with_capacity(all_polys.len()); + for poly in &all_polys { + values_at_zeta.push(eval_poly(poly.polynomial(), zeta)); + } + + // Per-circuit open at zeta*omega — only the trace polys (preprocessed, + // stage1, stage2). Quotient does NOT need rotation. + let mut values_at_zeta_omega: Vec = Vec::new(); + let mut proofs_at_zeta_omega: Vec = Vec::new(); + for (i, _circuit) in self.circuits.iter().enumerate() { + let n = 1usize << log_degrees[i]; + let trace_domain = GeneralEvaluationDomain::::new(n).unwrap(); + let zeta_omega = zeta * trace_domain.group_gen(); + + // collect this circuit's trace polys + let mut circ_polys: Vec<&LabeledPolynomial> = vec![]; + let mut circ_states: Vec<&CommitmentState> = vec![]; + let mut circ_commits: Vec<&LabeledCommitment> = vec![]; + if let Some(prep) = &circuit_ranges[i].prep { + for j in prep.clone() { + circ_polys.push(all_polys[j]); + circ_states.push(all_states[j]); + circ_commits.push(all_commitments[j]); + values_at_zeta_omega.push(eval_poly(all_polys[j].polynomial(), zeta_omega)); + } + } + for j in circuit_ranges[i].stage_1.clone().map(|x| stage_1_base + x) { + circ_polys.push(all_polys[j]); + circ_states.push(all_states[j]); + circ_commits.push(all_commitments[j]); + values_at_zeta_omega.push(eval_poly(all_polys[j].polynomial(), zeta_omega)); + } + for j in circuit_ranges[i].stage_2.clone().map(|x| stage_2_base + x) { + circ_polys.push(all_polys[j]); + circ_states.push(all_states[j]); + circ_commits.push(all_commitments[j]); + values_at_zeta_omega.push(eval_poly(all_polys[j].polynomial(), zeta_omega)); + } + + let proof_zw = PC::open( + &config.committer_key, + circ_polys.iter().copied(), + circ_commits.iter().copied(), + &zeta_omega, + &mut sponge, + circ_states.iter().copied(), + None, + ) + .expect("open at zeta*omega failed"); + proofs_at_zeta_omega.push(proof_zw); + } + + let strip = |labeled: &[LabeledCommitment]| -> Vec { + labeled.iter().map(|lc| *lc.commitment()).collect() + }; + + Proof { + stage_1_commitments: strip(&stage_1_commitments), + stage_2_commitments: strip(&stage_2_commitments), + quotient_commitments: strip("ient_commitments), + log_degrees, + intermediate_accumulators, + values_at_zeta, + values_at_zeta_omega, + proof_at_zeta, + proofs_at_zeta_omega, + } + } +} + +/// Per-circuit ranges into the global `all_polys` lists. `prep` is `None` if +/// the circuit has no preprocessed trace. +struct CircuitRanges { + prep: Option>, + stage_1: std::ops::Range, + stage_2: std::ops::Range, + #[allow(dead_code)] + quotient: std::ops::Range, +} + +fn polys_for_circuit( + polys: &[LabeledPolynomial], + start: usize, + width: usize, +) -> Vec { + polys[start..start + width] + .iter() + .map(|lp| lp.polynomial().clone()) + .collect() +} + +fn polys_for_circuit_stage1( + polys: &[LabeledPolynomial], + widths: &[usize], + circuit_idx: usize, +) -> Vec { + let start = widths[..circuit_idx].iter().sum(); + let width = widths[circuit_idx]; + polys[start..start + width] + .iter() + .map(|lp| lp.polynomial().clone()) + .collect() +} + +fn polys_for_circuit_stage2( + polys: &[LabeledPolynomial], + widths: &[usize], + circuit_idx: usize, +) -> Vec { + let start = widths[..circuit_idx].iter().sum(); + let width = widths[circuit_idx]; + polys[start..start + width] + .iter() + .map(|lp| lp.polynomial().clone()) + .collect() +} + +fn prep_idx_to_poly_offset(system: &System, prep_idx: usize) -> usize { + // Sum the preprocessed widths of the circuits that have a preprocessed + // bundle index < prep_idx. + let mut off = 0; + for (c_idx, slot) in system.preprocessed_indices.iter().enumerate() { + if let Some(p) = slot + && *p < prep_idx + { + off += system.circuits[c_idx].preprocessed_width; + } + } + off +} + +fn poly_offset_for_prep(system: &System, prep_idx: usize) -> usize { + prep_idx_to_poly_offset(system, prep_idx) +} + +fn next_pow2(n: usize) -> usize { + n.next_power_of_two() +} + +/// Z_H(x) = x^n - 1; returns 1/Z_H at every coset point. +fn vanishing_inv_on_coset(coset_size: usize, n: usize) -> Vec { + let coset = GeneralEvaluationDomain::::new(coset_size) + .expect("coset size unsupported") + .get_coset(Val::GENERATOR) + .expect("get_coset failed"); + coset + .elements() + .map(|x| { + let v = x.pow([n as u64]) - Val::one(); + v.inverse().expect("Z_H zero on coset (coset overlaps H?)") + }) + .collect() +} + +/// Per-coset-point row selectors for a trace of size `n`. +/// `is_first_row(x) = L_0(x) = (x^n - 1) / (n * (x - 1))` evaluated on coset. +/// We compute them via a vanishing-poly based formula consistent with the +/// shape used by multi-stark. +fn row_selectors_on_coset( + coset: &GeneralEvaluationDomain, + n: usize, +) -> (Vec, Vec, Vec) { + let n_inv = Val::from(n as u64).inverse().unwrap(); + let omega = GeneralEvaluationDomain::::new(n).unwrap().group_gen(); + let omega_inv = omega.inverse().unwrap(); + let mut is_first = Vec::with_capacity(coset.size()); + let mut is_last = Vec::with_capacity(coset.size()); + let mut is_trans = Vec::with_capacity(coset.size()); + for x in coset.elements() { + let xn_minus_one = x.pow([n as u64]) - Val::one(); + // L_i(x) = ω^i * (x^n - 1) / (n * (x - ω^i)) + // + // L_0 : ω^0 = 1, so the leading factor is 1 + // L_{n-1} : ω^{n-1} = ω^{-1} + let denom_first = (x - Val::one()) * Val::from(n as u64); + let l0 = xn_minus_one * denom_first.inverse().unwrap(); + + let omega_nm1 = omega_inv; + let denom_last = (x - omega_nm1) * Val::from(n as u64); + let lnm1 = xn_minus_one * omega_nm1 * denom_last.inverse().unwrap(); + + let trans = Val::one() - lnm1; + let _ = n_inv; + is_first.push(l0); + is_last.push(lnm1); + is_trans.push(trans); + } + (is_first, is_last, is_trans) +} + +/// Squeeze a single field element from the sponge. +fn squeeze_field(sponge: &mut Sponge) -> Val { + let bits_needed = (Val::MODULUS_BIT_SIZE as usize).saturating_sub(1); + let v: Vec = + sponge.squeeze_field_elements_with_sizes(&[FieldElementSize::Truncated(bits_needed)]); + v[0] +} + +/// Absorb a Sonic commitment (kzg10::Commitment) into the sponge. +fn absorb_commitment(sponge: &mut Sponge, c: &Commitment) { + // Serialize the commitment to bytes and absorb. + let mut bytes = vec![]; + c.serialize_uncompressed(&mut bytes).expect("serialize"); + sponge.absorb(&bytes); +} + +fn eval_poly(p: &UniPoly, x: Val) -> Val { + // Horner. + let mut acc = Val::zero(); + for c in p.coeffs.iter().rev() { + acc = acc * x + *c; + } + acc +} + +fn zip3<'a, A, B, C>( + a: &'a [A], + b: &'a [B], + c: &'a [C], +) -> impl Iterator { + a.iter() + .zip(b.iter()) + .zip(c.iter()) + .map(|((x, y), z)| (x, y, z)) +} diff --git a/multi-plonk/src/system.rs b/multi-plonk/src/system.rs new file mode 100644 index 0000000..a51ce8a --- /dev/null +++ b/multi-plonk/src/system.rs @@ -0,0 +1,176 @@ +//! Multi-circuit system + per-circuit metadata + setup of preprocessed +//! commitments. + +use ark_ff::Zero; +use ark_poly::{EvaluationDomain, GeneralEvaluationDomain, univariate::DensePolynomial}; +use ark_poly_commit::{LabeledCommitment, LabeledPolynomial, PolynomialCommitment}; + +use crate::air::{Air, BaseAir}; +use crate::builder::symbolic::{ + SymbolicAirBuilder, get_max_constraint_degree, get_symbolic_constraints, +}; +use crate::lookup::{LOOKUP_PUBLIC_SIZE, Lookup, LookupAir}; +use crate::matrix::Matrix; +use crate::types::{Commitment, CommitmentState, PC, PlonkConfig, UniPoly, Val}; + +pub struct System { + pub circuits: Vec>, + /// Maps circuit index → index into the preprocessed bundle (None if circuit + /// has no preprocessed trace). + pub preprocessed_indices: Vec>, + /// Public per-preprocessed-circuit data (commitment + label). + pub preprocessed_commitments: Vec>, +} + +pub struct ProverKey { + /// Coefficient-form preprocessed polynomials (one per preprocessed circuit). + pub preprocessed_polys: Vec>, + /// PCS commitment-state (randomness) for each preprocessed polynomial. + pub preprocessed_states: Vec, + /// Original eval-form preprocessed traces (kept for row evaluation). + pub preprocessed_traces: Vec>, +} + +pub struct Circuit { + pub air: LookupAir, + pub constraint_count: usize, + pub max_constraint_degree: usize, + pub preprocessed_height: usize, + pub preprocessed_width: usize, + pub stage_1_width: usize, + pub stage_2_width: usize, +} + +#[derive(Clone)] +pub struct SystemWitness { + pub traces: Vec>, + /// Per circuit, per row, per lookup. + pub lookups: Vec>>>, +} + +impl SystemWitness { + pub fn from_stage_1(traces: Vec>, system: &System) -> Self { + let lookups = traces + .iter() + .zip(system.circuits.iter()) + .map(|(trace, circuit)| { + let preprocessed = circuit.air.preprocessed.as_ref(); + trace + .rows() + .enumerate() + .map(|(row_i, row)| { + circuit + .air + .lookups + .iter() + .map(|lookup| { + lookup.compute_expr(row, preprocessed.map(|m| m.row(row_i))) + }) + .collect::>() + }) + .collect::>() + }) + .collect::>(); + Self { traces, lookups } + } +} + +impl System +where + A: BaseAir + Air, +{ + pub fn new( + config: &PlonkConfig, + airs: impl IntoIterator>, + ) -> (Self, ProverKey) { + let mut circuits = vec![]; + let mut preprocessed_traces = vec![]; + let mut preprocessed_indices = vec![]; + for air in airs { + let (circuit, maybe_preprocessed_trace) = Circuit::from_air(air); + circuits.push(circuit); + if let Some(t) = maybe_preprocessed_trace { + preprocessed_indices.push(Some(preprocessed_traces.len())); + preprocessed_traces.push(t); + } else { + preprocessed_indices.push(None); + } + } + // Commit preprocessed polynomials. + let mut preprocessed_polys: Vec> = Vec::new(); + for (i, trace) in preprocessed_traces.iter().enumerate() { + for (col_idx, col) in trace.columns().into_iter().enumerate() { + let poly = ifft_column(&col); + let label = format!("preprocessed_c{i}_col{col_idx}"); + preprocessed_polys.push(LabeledPolynomial::new(label, poly, None, None)); + } + } + let (preprocessed_commitments, preprocessed_states) = if preprocessed_polys.is_empty() { + (vec![], vec![]) + } else { + PC::commit(&config.committer_key, &preprocessed_polys, None) + .expect("preprocessed commit failed") + }; + + let system = Self { + circuits, + preprocessed_indices, + preprocessed_commitments, + }; + let key = ProverKey { + preprocessed_polys, + preprocessed_states, + preprocessed_traces, + }; + (system, key) + } +} + +impl Circuit +where + A: BaseAir + Air, +{ + pub fn from_air(air: LookupAir) -> (Self, Option>) { + let stage_1_width = air.inner_air.width(); + let stage_2_width = air.stage_2_width(); + let preprocessed_trace = air.preprocessed_trace(); + let preprocessed_height = preprocessed_trace.as_ref().map_or(0, |m| m.height()); + let preprocessed_width = preprocessed_trace.as_ref().map_or(0, |m| m.width()); + let constraints = get_symbolic_constraints( + &air, + preprocessed_width, + stage_1_width, + stage_2_width, + LOOKUP_PUBLIC_SIZE, + ); + let constraint_count = constraints.len(); + let max_constraint_degree = get_max_constraint_degree(&constraints); + let circuit = Self { + air, + constraint_count, + max_constraint_degree, + preprocessed_height, + preprocessed_width, + stage_1_width, + stage_2_width, + }; + (circuit, preprocessed_trace) + } +} + +/// Convenience: iFFT a single column from evaluation form (on the +/// canonical subgroup of size `n = col.len()`) into coefficient form. +pub(crate) fn ifft_column(col: &[Val]) -> UniPoly { + let n = col.len(); + if n == 0 { + return DensePolynomial { coeffs: vec![] }; + } + let domain = + GeneralEvaluationDomain::::new(n).expect("trace size must be supported by FFT domain"); + let coeffs = domain.ifft(col); + let mut poly = DensePolynomial { coeffs }; + while poly.coeffs.last().is_some_and(|c| c.is_zero()) { + poly.coeffs.pop(); + } + poly +} diff --git a/multi-plonk/src/types.rs b/multi-plonk/src/types.rs new file mode 100644 index 0000000..4cc22ea --- /dev/null +++ b/multi-plonk/src/types.rs @@ -0,0 +1,81 @@ +//! Core type aliases and configuration for the KZG-based prover/verifier. +//! +//! The PCS used is [`SonicKZG10`] over BLS12-381, which gives: +//! * Single-element (1×G1) batched openings at one point for any number of +//! polynomials, via random-LC at the prover side. +//! * Verifier cost = a constant number of pairings (independent of how many +//! polynomials are batched). +//! +//! Polynomials are stored in **monomial basis** (`DensePolynomial`). + +use ark_bls12_381::{Bls12_381, Fr}; +use ark_crypto_primitives::sponge::{ + CryptographicSponge, + poseidon::{PoseidonConfig, PoseidonSponge}, +}; +use ark_ff::UniformRand; +use ark_poly::univariate::DensePolynomial; +use ark_poly_commit::{ + PolynomialCommitment, kzg10, + sonic_pc::{self, CommitterKey, SonicKZG10, UniversalParams, VerifierKey}, +}; +use ark_std::rand::RngCore; + +pub type Val = Fr; +pub type UniPoly = DensePolynomial; +pub type PC = SonicKZG10; +pub type Sponge = PoseidonSponge; + +pub type SrsParams = UniversalParams; +pub type CkParams = CommitterKey; +pub type VkParams = VerifierKey; +pub type Commitment = sonic_pc::Commitment; +pub type CommitmentState = kzg10::Randomness; +pub type OpeningProof = kzg10::Proof; + +/// Configuration shared by prover and verifier. +#[derive(Clone)] +pub struct PlonkConfig { + pub committer_key: CkParams, + pub verifier_key: VkParams, + pub sponge_config: PoseidonConfig, +} + +impl PlonkConfig { + /// One-time KZG ceremony + Poseidon parameter generation. + /// + /// `max_degree` should be at least `(max_constraint_degree - 1) * largest_trace_size` + /// so the quotient polynomial fits. + pub fn setup(max_degree: usize, rng: &mut R) -> Self { + let pp = PC::setup(max_degree, None, rng).expect("KZG setup failed"); + let (committer_key, verifier_key) = + PC::trim(&pp, max_degree, 0, None).expect("KZG trim failed"); + let sponge_config = test_poseidon_config(rng); + Self { + committer_key, + verifier_key, + sponge_config, + } + } + + pub fn make_sponge(&self) -> Sponge { + PoseidonSponge::new(&self.sponge_config) + } +} + +/// WARNING: insecure parameters intended for examples and tests only. Do not +/// use these in production. Mirrors the parameter set from `kzg_sonic.rs`. +fn test_poseidon_config(rng: &mut R) -> PoseidonConfig { + let full_rounds = 8; + let partial_rounds = 31; + let alpha = 17; + let mds = vec![ + vec![Fr::from(1u64), Fr::from(0u64), Fr::from(1u64)], + vec![Fr::from(1u64), Fr::from(1u64), Fr::from(0u64)], + vec![Fr::from(0u64), Fr::from(1u64), Fr::from(1u64)], + ]; + let ark: Vec> = (0..full_rounds + partial_rounds) + .map(|_| (0..3).map(|_| Fr::rand(rng)).collect()) + .collect(); + PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, 2, 1) +} diff --git a/multi-plonk/src/verifier.rs b/multi-plonk/src/verifier.rs new file mode 100644 index 0000000..a566271 --- /dev/null +++ b/multi-plonk/src/verifier.rs @@ -0,0 +1,389 @@ +//! Multi-circuit PLONK-style verifier. +//! +//! Mirrors `prover.rs`: replays the Fiat-Shamir sponge, deserialises every +//! commitment / opening proof, runs SonicKZG10 `check` for each opening +//! point, then for each circuit recomputes the composition polynomial at ζ +//! from the opened values and verifies +//! +//! ```text +//! composition(ζ) == Z_H(ζ) · quotient(ζ) +//! ``` + +use ark_crypto_primitives::sponge::{CryptographicSponge, FieldElementSize}; +use ark_ff::{Field, One, PrimeField, Zero}; +use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; +use ark_poly_commit::{LabeledCommitment, PolynomialCommitment}; + +use crate::air::Air; +use crate::builder::folder::VerifierConstraintFolder; +use crate::lookup::fingerprint; +use crate::prover::Proof; +use crate::system::System; +use crate::types::{Commitment, PC, PlonkConfig, Sponge, Val}; +use crate::{ensure, ensure_eq}; + +#[derive(Debug)] +pub enum VerificationError { + InvalidProofShape, + InvalidSystem, + UnbalancedChannel, + InvalidOpeningArgument, + OodEvaluationMismatch, +} + +impl System +where + A: crate::air::BaseAir + for<'a> Air>, +{ + pub fn verify( + &self, + config: &PlonkConfig, + claim: &[Val], + proof: &Proof, + ) -> Result<(), VerificationError> { + self.verify_multiple_claims(config, &[claim], proof) + } + + pub fn verify_multiple_claims( + &self, + config: &PlonkConfig, + claims: &[&[Val]], + proof: &Proof, + ) -> Result<(), VerificationError> { + ensure!(!self.circuits.is_empty(), VerificationError::InvalidSystem); + ensure_eq!( + proof.intermediate_accumulators.len(), + self.circuits.len(), + VerificationError::InvalidProofShape + ); + ensure_eq!( + proof.log_degrees.len(), + self.circuits.len(), + VerificationError::InvalidProofShape + ); + // Lookup balance. + ensure_eq!( + proof.intermediate_accumulators.last().copied(), + Some(Val::zero()), + VerificationError::UnbalancedChannel + ); + ensure_eq!( + proof.proofs_at_zeta_omega.len(), + self.circuits.len(), + VerificationError::InvalidProofShape + ); + + // Re-label raw commitments using the same scheme the prover uses. + let stage_1_commitments = relabel_stage_1(self, &proof.stage_1_commitments); + let stage_2_commitments = relabel_stage_2(self, &proof.stage_2_commitments); + let quotient_commitments = relabel_quotient(self, &proof.quotient_commitments); + + // ── replay sponge ──────────────────────────────────────────────────── + let mut sponge = config.make_sponge(); + for c in &self.preprocessed_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + for c in &stage_1_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + for d in &proof.log_degrees { + sponge.absorb(&Val::from(u64::from(*d))); + } + for claim in claims { + sponge.absorb(claim); + } + let lookup_challenge = squeeze_field(&mut sponge); + sponge.absorb(&lookup_challenge); + let fingerprint_challenge = squeeze_field(&mut sponge); + sponge.absorb(&fingerprint_challenge); + for c in &stage_2_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + let alpha = squeeze_field(&mut sponge); + for c in "ient_commitments { + absorb_commitment(&mut sponge, c.commitment()); + } + let zeta = squeeze_field(&mut sponge); + + // ── compute initial accumulator from claims ────────────────────────── + let mut acc = Val::zero(); + for claim in claims { + let message = + lookup_challenge + fingerprint(&fingerprint_challenge, claim.iter().copied()); + acc += message.inverse().expect("zero claim message"); + } + + // ── shape checks for opened-value vectors ──────────────────────────── + let n_prep_polys = self.preprocessed_commitments.len(); + let n_s1_polys: usize = self.circuits.iter().map(|c| c.stage_1_width).sum(); + let n_s2_polys: usize = self.circuits.iter().map(|c| c.stage_2_width).sum(); + let n_q_polys = self.circuits.len(); + ensure_eq!( + stage_1_commitments.len(), + n_s1_polys, + VerificationError::InvalidProofShape + ); + ensure_eq!( + stage_2_commitments.len(), + n_s2_polys, + VerificationError::InvalidProofShape + ); + ensure_eq!( + quotient_commitments.len(), + n_q_polys, + VerificationError::InvalidProofShape + ); + let total_polys = n_prep_polys + n_s1_polys + n_s2_polys + n_q_polys; + ensure_eq!( + proof.values_at_zeta.len(), + total_polys, + VerificationError::InvalidProofShape + ); + + // ── verify single-point opens at zeta ──────────────────────────────── + let all_commitments: Vec<&LabeledCommitment> = self + .preprocessed_commitments + .iter() + .chain(stage_1_commitments.iter()) + .chain(stage_2_commitments.iter()) + .chain(quotient_commitments.iter()) + .collect(); + let opens: Vec = proof.values_at_zeta.clone(); + + let ok = PC::check( + &config.verifier_key, + all_commitments.iter().copied(), + &zeta, + opens, + &proof.proof_at_zeta, + &mut sponge, + None, + ) + .map_err(|_e| VerificationError::InvalidOpeningArgument)?; + ensure!(ok, VerificationError::InvalidOpeningArgument); + + // ── verify per-circuit opens at zeta·omega ─────────────────────────── + // values_at_zeta_omega is laid out per circuit in the same order as + // pushed by the prover: prep | stage1 | stage2. + let mut zw_idx = 0; + for (i, circuit) in self.circuits.iter().enumerate() { + let n = 1usize << proof.log_degrees[i]; + let trace_domain = GeneralEvaluationDomain::::new(n) + .ok_or(VerificationError::InvalidProofShape)?; + let zeta_omega = zeta * trace_domain.group_gen(); + + let mut commits_for_circuit: Vec<&LabeledCommitment> = vec![]; + let mut values_for_circuit: Vec = vec![]; + // preprocessed slice + if let Some(prep_idx) = self.preprocessed_indices[i] { + let start = preprocessed_poly_offset(self, prep_idx); + let end = start + circuit.preprocessed_width; + for c in &self.preprocessed_commitments[start..end] { + commits_for_circuit.push(c); + values_for_circuit.push(proof.values_at_zeta_omega[zw_idx]); + zw_idx += 1; + } + } + // stage1 slice + let s1_start: usize = self.circuits[..i].iter().map(|c| c.stage_1_width).sum(); + for c in &stage_1_commitments[s1_start..s1_start + circuit.stage_1_width] { + commits_for_circuit.push(c); + values_for_circuit.push(proof.values_at_zeta_omega[zw_idx]); + zw_idx += 1; + } + // stage2 slice + let s2_start: usize = self.circuits[..i].iter().map(|c| c.stage_2_width).sum(); + for c in &stage_2_commitments[s2_start..s2_start + circuit.stage_2_width] { + commits_for_circuit.push(c); + values_for_circuit.push(proof.values_at_zeta_omega[zw_idx]); + zw_idx += 1; + } + + let ok = PC::check( + &config.verifier_key, + commits_for_circuit.iter().copied(), + &zeta_omega, + values_for_circuit, + &proof.proofs_at_zeta_omega[i], + &mut sponge, + None, + ) + .map_err(|_e| VerificationError::InvalidOpeningArgument)?; + ensure!(ok, VerificationError::InvalidOpeningArgument); + } + ensure_eq!( + zw_idx, + proof.values_at_zeta_omega.len(), + VerificationError::InvalidProofShape + ); + + // ── per-circuit OOD check ──────────────────────────────────────────── + let mut acc_input = acc; + // Ranges are identical to the prover's "all_polys" layout: + // [preprocessed total | stage1 total | stage2 total | quotient total] + let stage1_base = n_prep_polys; + let stage2_base = stage1_base + n_s1_polys; + let quotient_base = stage2_base + n_s2_polys; + + // First, walk preprocessed circuit-by-circuit to consume the prep slice + // of values_at_zeta. + // We re-index using the preprocessed_indices map. + let mut zeta_omega_idx = 0usize; + for (i, circuit) in self.circuits.iter().enumerate() { + let n = 1usize << proof.log_degrees[i]; + let trace_domain = GeneralEvaluationDomain::::new(n) + .ok_or(VerificationError::InvalidProofShape)?; + let next_acc = proof.intermediate_accumulators[i]; + + // Slice the opened values for this circuit. + // Preprocessed locals/nexts. + let prep_local: Vec; + let prep_next: Vec; + if let Some(prep_idx) = self.preprocessed_indices[i] { + let start = preprocessed_poly_offset(self, prep_idx); + let w = circuit.preprocessed_width; + prep_local = proof.values_at_zeta[start..start + w].to_vec(); + prep_next = proof.values_at_zeta_omega[zeta_omega_idx..zeta_omega_idx + w].to_vec(); + zeta_omega_idx += w; + } else { + prep_local = vec![]; + prep_next = vec![]; + } + // Stage1 locals/nexts. + let s1_start: usize = self.circuits[..i].iter().map(|c| c.stage_1_width).sum(); + let s1_w = circuit.stage_1_width; + let s1_local = proof.values_at_zeta + [stage1_base + s1_start..stage1_base + s1_start + s1_w] + .to_vec(); + let s1_next = + proof.values_at_zeta_omega[zeta_omega_idx..zeta_omega_idx + s1_w].to_vec(); + zeta_omega_idx += s1_w; + // Stage2 locals/nexts. + let s2_start: usize = self.circuits[..i].iter().map(|c| c.stage_2_width).sum(); + let s2_w = circuit.stage_2_width; + let s2_local = proof.values_at_zeta + [stage2_base + s2_start..stage2_base + s2_start + s2_w] + .to_vec(); + let s2_next = + proof.values_at_zeta_omega[zeta_omega_idx..zeta_omega_idx + s2_w].to_vec(); + zeta_omega_idx += s2_w; + + // Stage 2 publics. + let stage_2_publics = [lookup_challenge, fingerprint_challenge, acc_input, next_acc]; + acc_input = next_acc; + + // Selectors at zeta. + let omega = trace_domain.group_gen(); + let omega_inv = omega.inverse().unwrap(); + let zeta_n = zeta.pow([n as u64]); + let zh = zeta_n - Val::one(); + let n_field = Val::from(n as u64); + let l0 = zh * ((zeta - Val::one()) * n_field).inverse().unwrap(); + let l_nm1 = zh * omega_inv * ((zeta - omega_inv) * n_field).inverse().unwrap(); + let trans = Val::one() - l_nm1; + + let mut folder = VerifierConstraintFolder { + preprocessed_local: &prep_local, + preprocessed_next: &prep_next, + stage_1_local: &s1_local, + stage_1_next: &s1_next, + stage_2_local: &s2_local, + stage_2_next: &s2_next, + stage_2_public_values: &stage_2_publics, + is_first_row: l0, + is_last_row: l_nm1, + is_transition: trans, + alpha, + accumulator: Val::zero(), + }; + circuit.air.eval(&mut folder); + let composition = folder.accumulator; + + // Quotient value at zeta (one per circuit). + let q_value = proof.values_at_zeta[quotient_base + i]; + + // composition(ζ) == Z_H(ζ) · quotient(ζ) + ensure_eq!( + composition, + zh * q_value, + VerificationError::OodEvaluationMismatch + ); + } + + Ok(()) + } +} + +fn preprocessed_poly_offset(system: &System, prep_idx: usize) -> usize { + let mut off = 0; + for (c_idx, slot) in system.preprocessed_indices.iter().enumerate() { + if let Some(p) = slot + && *p < prep_idx + { + off += system.circuits[c_idx].preprocessed_width; + } + } + off +} + +fn squeeze_field(sponge: &mut Sponge) -> Val { + let bits_needed = (Val::MODULUS_BIT_SIZE as usize).saturating_sub(1); + let v: Vec = + sponge.squeeze_field_elements_with_sizes(&[FieldElementSize::Truncated(bits_needed)]); + v[0] +} + +fn absorb_commitment(sponge: &mut Sponge, c: &Commitment) { + let mut bytes = vec![]; + use ark_serialize::CanonicalSerialize; + c.serialize_uncompressed(&mut bytes).expect("serialize"); + sponge.absorb(&bytes); +} + +fn relabel_stage_1( + system: &System, + raw: &[Commitment], +) -> Vec> { + let mut out = vec![]; + let mut idx = 0; + for (i, c) in system.circuits.iter().enumerate() { + for col_idx in 0..c.stage_1_width { + out.push(LabeledCommitment::new( + format!("stage1_c{i}_col{col_idx}"), + raw[idx], + None, + )); + idx += 1; + } + } + out +} + +fn relabel_stage_2( + system: &System, + raw: &[Commitment], +) -> Vec> { + let mut out = vec![]; + let mut idx = 0; + for (i, c) in system.circuits.iter().enumerate() { + for col_idx in 0..c.stage_2_width { + out.push(LabeledCommitment::new( + format!("stage2_c{i}_col{col_idx}"), + raw[idx], + None, + )); + idx += 1; + } + } + out +} + +fn relabel_quotient( + _system: &System, + raw: &[Commitment], +) -> Vec> { + raw.iter() + .enumerate() + .map(|(i, c)| LabeledCommitment::new(format!("quotient_c{i}"), *c, None)) + .collect() +}