diff --git a/Cargo.lock b/Cargo.lock index b1f19ea22..8872d7d14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,8 +29,8 @@ version = "1.0.0-beta.19" source = "git+https://github.com/noir-lang/noir?rev=v1.0.0-beta.19#74d6be658e1ad252f87943292ba09bdd4da80bd4" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", "cfg-if", "hex", "num-bigint", @@ -123,16 +123,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "alloy-rlp" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" -dependencies = [ - "arrayvec", - "bytes", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -242,8 +232,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", ] [[package]] @@ -253,10 +243,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown 0.15.5", @@ -267,54 +257,16 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.4.1", - "zeroize", -] - [[package]] name = "ark-ff" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -325,26 +277,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -355,31 +287,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -401,8 +308,8 @@ checksum = "ef677b59f5aff4123207c4dceb1c0ec8fdde2d4af7886f48be42ad864bfa0352" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", ] [[package]] @@ -412,35 +319,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown 0.15.5", ] -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint", -] - [[package]] name = "ark-serialize" version = "0.5.0" @@ -448,7 +334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ "ark-serialize-derive", - "ark-std 0.5.0", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint", @@ -465,26 +351,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -595,17 +461,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -827,12 +682,12 @@ name = "bn254-multiplier" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "bn254-multiplier-codegen", "divan", "fp-rounding", "hla", - "primitive-types 0.13.1", + "primitive-types", "proptest", "rand 0.9.2", "seq-macro", @@ -854,7 +709,7 @@ dependencies = [ "acvm_blackbox_solver", "ark-bn254", "ark-ec", - "ark-ff 0.5.0", + "ark-ff", "ark-grumpkin", "hex", ] @@ -1551,17 +1406,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive-where" version = "1.6.1" @@ -1573,15 +1417,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" @@ -1881,28 +1716,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - [[package]] name = "fd-lock" version = "3.0.13" @@ -2284,7 +2097,7 @@ checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", "hash32", - "rustc_version 0.4.1", + "rustc_version", "serde", "spin 0.9.8", "stable_deref_trait", @@ -2628,15 +2441,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - [[package]] name = "impl-codec" version = "0.7.1" @@ -3211,7 +3015,7 @@ version = "0.1.0" source = "git+https://github.com/reilabs/mavros?rev=7550b42e03d35b44781ff37f15b50773eb2a6fa0#7550b42e03d35b44781ff37f15b50773eb2a6fa0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "serde", "tracing", ] @@ -3232,7 +3036,7 @@ name = "mavros-vm" version = "0.1.0" source = "git+https://github.com/reilabs/mavros?rev=7550b42e03d35b44781ff37f15b50773eb2a6fa0#7550b42e03d35b44781ff37f15b50773eb2a6fa0" dependencies = [ - "ark-ff 0.5.0", + "ark-ff", "mavros-artifacts", "mavros-opcode-gen", "tracing", @@ -3425,7 +3229,7 @@ dependencies = [ "nargo", "noirc_driver", "noirc_frontend", - "semver 1.0.27", + "semver", "serde", "thiserror 1.0.69", "toml 0.7.8", @@ -3831,8 +3635,8 @@ name = "ntt" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", "bn254-multiplier", "divan", "proptest", @@ -4285,7 +4089,7 @@ dependencies = [ "anyhow", "argh", "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "base64", "chrono", "hex", @@ -4339,16 +4143,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - [[package]] name = "petgraph" version = "0.8.3" @@ -4435,8 +4229,8 @@ name = "poseidon2" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", ] [[package]] @@ -4523,17 +4317,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec 0.6.0", - "uint 0.9.5", -] - [[package]] name = "primitive-types" version = "0.13.1" @@ -4541,8 +4324,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" dependencies = [ "fixed-hash", - "impl-codec 0.7.1", - "uint 0.10.0", + "impl-codec", + "uint", ] [[package]] @@ -4584,11 +4367,11 @@ dependencies = [ [[package]] name = "provekit-bench" -version = "0.1.0" +version = "1.0.0" dependencies = [ "acir", "anyhow", - "ark-ff 0.5.0", + "ark-ff", "divan", "nargo", "nargo_cli", @@ -4606,12 +4389,12 @@ dependencies = [ [[package]] name = "provekit-cli" -version = "0.1.0" +version = "1.0.0" dependencies = [ "acir", "anyhow", "argh", - "ark-ff 0.5.0", + "ark-ff", "base64", "hex", "nargo", @@ -4635,14 +4418,14 @@ dependencies = [ [[package]] name = "provekit-common" -version = "0.1.0" +version = "1.0.0" dependencies = [ "acir", "anyhow", "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "base64", "blake3", "bytes", @@ -4658,7 +4441,6 @@ dependencies = [ "postcard", "proptest", "rayon", - "ruint", "serde", "serde_json", "sha2 0.10.9", @@ -4675,7 +4457,7 @@ dependencies = [ [[package]] name = "provekit-ffi" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "libc", @@ -4705,12 +4487,12 @@ dependencies = [ [[package]] name = "provekit-prover" -version = "0.1.0" +version = "1.0.0" dependencies = [ "acir", "anyhow", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", "bn254_blackbox_solver", "mavros-artifacts", "mavros-vm", @@ -4720,18 +4502,19 @@ dependencies = [ "num-bigint", "postcard", "provekit-common", + "provekit-verifier", "tracing", "whir", ] [[package]] name = "provekit-r1cs-compiler" -version = "0.1.0" +version = "1.0.0" dependencies = [ "acir", "anyhow", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", "bincode", "mavros-artifacts", "noirc_abi", @@ -4747,10 +4530,10 @@ dependencies = [ [[package]] name = "provekit-verifier" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", - "ark-std 0.5.0", + "ark-std", "provekit-common", "rayon", "tracing", @@ -5201,16 +4984,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - [[package]] name = "rmp" version = "0.8.15" @@ -5251,40 +5024,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ruint" -version = "1.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" -dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types 0.12.2", - "proptest", - "rand 0.8.5", - "rand 0.9.2", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - [[package]] name = "rust-embed" version = "8.11.0" @@ -5343,22 +5082,13 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08" -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver", ] [[package]] @@ -5696,30 +5426,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - [[package]] name = "seq-macro" version = "0.3.6" @@ -6009,7 +5721,7 @@ name = "skyscraper" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "bn254-multiplier", "divan", "fp-rounding", @@ -6142,8 +5854,8 @@ name = "spongefish" version = "1.0.0-rc1" source = "git+https://github.com/arkworks-rs/spongefish?rev=fcc277f8a857fdeeadd7cca92ab08de63b1ff1a1#fcc277f8a857fdeeadd7cca92ab08de63b1ff1a1" dependencies = [ - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "blake3", "digest 0.10.7", "keccak 0.1.6", @@ -6893,24 +6605,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - [[package]] name = "uint" version = "0.10.0" @@ -7034,7 +6728,7 @@ dependencies = [ [[package]] name = "verifier-server" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "axum", @@ -7248,7 +6942,7 @@ dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap 2.13.1", - "semver 1.0.27", + "semver", ] [[package]] @@ -7284,9 +6978,9 @@ name = "whir" version = "0.1.0" source = "git+https://github.com/WizardOfMenlo/whir/?rev=0aeaa7f337c743d9ddfcb9d909628d6491e3355c#0aeaa7f337c743d9ddfcb9d909628d6491e3355c" dependencies = [ - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "arrayvec", "blake3", "ciborium", @@ -7813,7 +7507,7 @@ dependencies = [ "id-arena", "indexmap 2.13.1", "log", - "semver 1.0.27", + "semver", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index dc6ce9676..a2a1e3a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,12 +97,15 @@ poseidon2 = { path = "poseidon2" } # Workspace members - ProveKit provekit-bench = { path = "tooling/provekit-bench" } provekit-cli = { path = "tooling/cli" } -provekit-common = { path = "provekit/common" , features = ["provekit_ntt"]} +# No default features: consumers pick the field (`bn254`/`goldilocks`) plus +# `parallel`/`provekit_ntt` explicitly — required so the goldilocks build can +# exclude the default `bn254` feature. +provekit-common = { path = "provekit/common", default-features = false } provekit-ffi = { path = "tooling/provekit-ffi" } provekit-gnark = { path = "tooling/provekit-gnark" } provekit-prover = { path = "provekit/prover", default-features = false } provekit-r1cs-compiler = { path = "provekit/r1cs-compiler" } -provekit-verifier = { path = "provekit/verifier" } +provekit-verifier = { path = "provekit/verifier", default-features = false } provekit-verifier-server = { path = "tooling/verifier-server" } provekit-wasm = { path = "tooling/provekit-wasm" } @@ -130,7 +133,6 @@ rand = "0.9.1" rand08 = { package = "rand", version = "0.8" } rayon = "1.10.0" reqwest = "0.12.23" -ruint = { version = "1.12.3", features = ["num-traits", "rand"] } seq-macro = "0.3.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/playground/passport-input-gen/Cargo.toml b/playground/passport-input-gen/Cargo.toml index 55166f1e9..907474204 100644 --- a/playground/passport-input-gen/Cargo.toml +++ b/playground/passport-input-gen/Cargo.toml @@ -21,7 +21,7 @@ ark-ff.workspace = true base64.workspace = true hex.workspace = true noirc_abi.workspace = true -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } provekit-prover = { workspace = true, features = ["witness-generation"] } serde.workspace = true serde_json.workspace = true diff --git a/provekit/common/Cargo.toml b/provekit/common/Cargo.toml index 2d761d7e0..cdb94dac1 100644 --- a/provekit/common/Cargo.toml +++ b/provekit/common/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "provekit-common" version = "1.0.0" +# Warns when both field features are on (silent BN254 precedence); see build.rs. +build = "build.rs" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -9,9 +11,16 @@ homepage.workspace = true repository.workspace = true [features] -default = ["parallel"] +default = ["parallel", "bn254"] +# At least one field feature must be enabled; `bn254` takes precedence when +# both are on (so `--all-features` builds as BN254). `bn254` is the +# production default; `goldilocks` selects the Goldilocks cubic extension +# (Field64_3, ~192 bits) for off-chain, crate-scoped builds only — always +# pair it with `--no-default-features`. +bn254 = ["dep:mavros-vm", "dep:mavros-artifacts"] +goldilocks = [] parallel = [] -provekit_ntt = [] +provekit_ntt = ["bn254"] [dependencies] # Workspace crates @@ -41,7 +50,6 @@ hex.workspace = true itertools.workspace = true postcard.workspace = true rayon.workspace = true -ruint.workspace = true serde.workspace = true serde_json.workspace = true blake3.workspace = true @@ -53,8 +61,8 @@ zerocopy.workspace = true # Target-specific dependencies: only on non-WASM targets [target.'cfg(not(target_arch = "wasm32"))'.dependencies] zstd.workspace = true -mavros-vm.workspace = true -mavros-artifacts.workspace = true +mavros-vm = { workspace = true, optional = true } +mavros-artifacts = { workspace = true, optional = true } xz2.workspace = true [package.metadata.cargo-machete] @@ -69,6 +77,7 @@ proptest.workspace = true [[bench]] name = "rs_bench" harness = false +required-features = ["bn254"] [lints] workspace = true diff --git a/provekit/common/build.rs b/provekit/common/build.rs new file mode 100644 index 000000000..0e786d87a --- /dev/null +++ b/provekit/common/build.rs @@ -0,0 +1,27 @@ +//! Build script for `provekit-common`. +//! +//! Surfaces the one field-feature footgun the `compile_error!` in `src/lib.rs` +//! cannot catch. `bn254` wins cfg precedence over `goldilocks` (so the crate +//! builds as BN254 whenever both are on — the intended resolution under +//! `--all-features`, which CI uses). But several BN254-only features pull in +//! `bn254` transitively (`provekit_ntt`, and the prover's +//! `witness-generation`), so an explicit `--features goldilocks,` build +//! silently compiles as BN254 instead of Goldilocks. `compile_error!` only +//! fires when *no* field is selected, never here. +//! +//! A `cargo:warning` makes the override visible without failing the build +//! (a hard error would break the legitimate `--all-features` path). Build- +//! script warnings are advisory, so this is safe even under `-D warnings`. +fn main() { + let bn254 = std::env::var_os("CARGO_FEATURE_BN254").is_some(); + let goldilocks = std::env::var_os("CARGO_FEATURE_GOLDILOCKS").is_some(); + if bn254 && goldilocks { + println!( + "cargo:warning=provekit-common: both `bn254` and `goldilocks` features are enabled; \ + FieldElement is BN254 (bn254 takes precedence), so `goldilocks` is inert. For a real \ + Goldilocks build use `--no-default-features --features goldilocks` and drop any \ + feature that pulls in bn254 (provekit_ntt, the prover's witness-generation). This \ + warning is expected under --all-features." + ); + } +} diff --git a/provekit/common/src/file/io/mod.rs b/provekit/common/src/file/io/mod.rs index 049c984a7..e3fa1023e 100644 --- a/provekit/common/src/file/io/mod.rs +++ b/provekit/common/src/file/io/mod.rs @@ -3,6 +3,8 @@ mod buf_ext; mod counting_writer; mod json; +#[cfg(feature = "bn254")] +use crate::{NoirProof, NoirProofScheme, Prover, Verifier}; use { self::{ bin::{ @@ -13,7 +15,7 @@ use { counting_writer::CountingWriter, json::{read_json, write_json}, }, - crate::{HashConfig, NoirProof, NoirProofScheme, Prover, Verifier}, + crate::HashConfig, anyhow::Result, serde::{Deserialize, Serialize}, std::{ffi::OsStr, path::Path}, @@ -34,6 +36,7 @@ pub(crate) trait MaybeHashAware { } /// Impl for Prover (has hash config). +#[cfg(feature = "bn254")] impl MaybeHashAware for Prover { fn maybe_hash_config(&self) -> Option { match self { @@ -44,6 +47,7 @@ impl MaybeHashAware for Prover { } /// Impl for Verifier (has hash config). +#[cfg(feature = "bn254")] impl MaybeHashAware for Verifier { fn maybe_hash_config(&self) -> Option { Some(self.hash_config) @@ -51,6 +55,7 @@ impl MaybeHashAware for Verifier { } /// Impl for NoirProof (no hash config). +#[cfg(feature = "bn254")] impl MaybeHashAware for NoirProof { fn maybe_hash_config(&self) -> Option { None @@ -58,6 +63,7 @@ impl MaybeHashAware for NoirProof { } /// Impl for NoirProofScheme (has hash config). +#[cfg(feature = "bn254")] impl MaybeHashAware for NoirProofScheme { fn maybe_hash_config(&self) -> Option { match self { @@ -67,6 +73,7 @@ impl MaybeHashAware for NoirProofScheme { } } +#[cfg(feature = "bn254")] impl FileFormat for NoirProofScheme { const FORMAT: [u8; 8] = crate::binary_format::NOIR_PROOF_SCHEME_FORMAT; const EXTENSION: &'static str = "nps"; @@ -74,6 +81,7 @@ impl FileFormat for NoirProofScheme { const COMPRESSION: Compression = Compression::Zstd; } +#[cfg(feature = "bn254")] impl FileFormat for Prover { const FORMAT: [u8; 8] = crate::binary_format::PROVER_FORMAT; const EXTENSION: &'static str = "pkp"; @@ -81,6 +89,7 @@ impl FileFormat for Prover { const COMPRESSION: Compression = Compression::Xz; } +#[cfg(feature = "bn254")] impl FileFormat for Verifier { const FORMAT: [u8; 8] = crate::binary_format::VERIFIER_FORMAT; const EXTENSION: &'static str = "pkv"; @@ -88,6 +97,7 @@ impl FileFormat for Verifier { const COMPRESSION: Compression = Compression::Zstd; } +#[cfg(feature = "bn254")] impl FileFormat for NoirProof { const FORMAT: [u8; 8] = crate::binary_format::NOIR_PROOF_FORMAT; const EXTENSION: &'static str = "np"; diff --git a/provekit/common/src/hash_config.rs b/provekit/common/src/hash_config.rs index 515b74eb6..1c112e0c9 100644 --- a/provekit/common/src/hash_config.rs +++ b/provekit/common/src/hash_config.rs @@ -7,36 +7,84 @@ use { crate::{utils::field_to_bytes_le, FieldElement}, - ark_ff::{BigInt, PrimeField}, serde::{Deserialize, Serialize}, - std::{fmt, sync::LazyLock}, + std::fmt, +}; +#[cfg(feature = "bn254")] +use { + ark_ff::{BigInt, PrimeField}, + std::sync::LazyLock, }; /// Hash algorithm configuration that can be selected at runtime. /// /// Each variant selects the same algorithm for Merkle commitments, -/// Fiat-Shamir sponge, and public-input binding. [`Self::Skyscraper`] is the -/// default. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +/// Fiat-Shamir sponge, and public-input binding. Skyscraper and Poseidon2 +/// are BN254-only constructions and exist only under the `bn254` feature; +/// the default is Skyscraper under `bn254` and [`Self::Sha256`] under +/// `goldilocks`. +/// +/// Serialization is hand-written (see the `Serialize`/`Deserialize` impls +/// below) rather than derived: the derive numbers variants positionally, so +/// `#[cfg]`-gating Skyscraper/Poseidon2 out of a goldilocks build silently +/// shifts every remaining index (`Sha256` becomes `0` instead of `1`, etc.), +/// which would let a postcard/CBOR-encoded config written by one field build +/// decode as the wrong variant under the other. The manual impls route binary +/// formats through the field-independent [`Self::to_byte`]/[`Self::from_byte`] +/// (the same stable byte used in proof-file headers) and human-readable +/// formats through [`Self::name`]/[`Self::parse`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum HashConfig { + #[cfg(feature = "bn254")] #[default] - #[serde(alias = "sky")] Skyscraper, - #[serde(alias = "sha", alias = "sha-256")] + #[cfg_attr(all(feature = "goldilocks", not(feature = "bn254")), default)] Sha256, - #[serde(alias = "keccak-256", alias = "shake")] Keccak, - #[serde(alias = "blake-3", alias = "b3")] Blake3, - #[serde(alias = "pos2", alias = "p2")] + #[cfg(feature = "bn254")] Poseidon2, } +impl Serialize for HashConfig { + /// Human-readable formats (JSON, …) get the canonical name string; binary + /// formats (postcard, CBOR, …) get the field-independent [`Self::to_byte`] + /// value. Under `bn254` both forms are byte-identical to the old derive, + /// so existing proofs/schemes are unaffected. + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(self.name()) + } else { + serializer.serialize_u8(self.to_byte()) + } + } +} + +impl<'de> Deserialize<'de> for HashConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + if deserializer.is_human_readable() { + let name = String::deserialize(deserializer)?; + Self::parse(&name) + .ok_or_else(|| D::Error::custom(format!("unknown hash configuration: {name:?}"))) + } else { + let byte = u8::deserialize(deserializer)?; + Self::from_byte(byte) + .ok_or_else(|| D::Error::custom(format!("invalid hash configuration byte: {byte}"))) + } + } +} + /// Domain-separation tag for public-input instance binding. /// /// **Protocol-visible constant.** This string is absorbed into the SHA-256, @@ -54,6 +102,7 @@ pub enum HashConfig { /// Regression trip-wires: the KATs in `witness::tests` freeze the /// byte-exact output of each variant under this constant. const PUBLIC_INPUTS_DST: &[u8] = b"PROVEKIT_PUBLIC_INPUTS_V1"; +#[cfg(feature = "bn254")] static PUBLIC_INPUTS_DST_FE: LazyLock = LazyLock::new(|| { use sha2::{Digest, Sha256}; FieldElement::from_le_bytes_mod_order(&Sha256::digest(PUBLIC_INPUTS_DST)) @@ -64,10 +113,12 @@ impl HashConfig { #[must_use] pub fn name(&self) -> &'static str { match self { + #[cfg(feature = "bn254")] Self::Skyscraper => "skyscraper", Self::Sha256 => "sha256", Self::Keccak => "keccak", Self::Blake3 => "blake3", + #[cfg(feature = "bn254")] Self::Poseidon2 => "poseidon2", } } @@ -76,10 +127,12 @@ impl HashConfig { #[must_use] pub fn engine_id(&self) -> whir::engines::EngineId { match self { + #[cfg(feature = "bn254")] Self::Skyscraper => crate::skyscraper::SKYSCRAPER, Self::Sha256 => whir::hash::SHA2, Self::Keccak => whir::hash::KECCAK, Self::Blake3 => whir::hash::BLAKE3, + #[cfg(feature = "bn254")] Self::Poseidon2 => crate::poseidon2::POSEIDON2, } } @@ -88,10 +141,12 @@ impl HashConfig { #[must_use] pub fn to_byte(&self) -> u8 { match self { + #[cfg(feature = "bn254")] Self::Skyscraper => 0, Self::Sha256 => 1, Self::Keccak => 2, Self::Blake3 => 3, + #[cfg(feature = "bn254")] Self::Poseidon2 => 4, } } @@ -100,10 +155,12 @@ impl HashConfig { #[must_use] pub fn from_byte(byte: u8) -> Option { match byte { + #[cfg(feature = "bn254")] 0 => Some(Self::Skyscraper), 1 => Some(Self::Sha256), 2 => Some(Self::Keccak), 3 => Some(Self::Blake3), + #[cfg(feature = "bn254")] 4 => Some(Self::Poseidon2), _ => None, } @@ -114,10 +171,12 @@ impl HashConfig { pub fn parse(s: &str) -> Option { let lower = s.to_lowercase(); match lower.as_str() { + #[cfg(feature = "bn254")] "skyscraper" | "sky" => Some(Self::Skyscraper), "sha256" | "sha" | "sha-256" => Some(Self::Sha256), "keccak" | "keccak-256" | "shake" => Some(Self::Keccak), "blake3" | "blake-3" | "b3" => Some(Self::Blake3), + #[cfg(feature = "bn254")] "poseidon2" | "pos2" | "p2" => Some(Self::Poseidon2), _ => None, } @@ -142,10 +201,12 @@ impl HashConfig { #[must_use] pub fn hash_field_elements(self, elements: &[FieldElement]) -> FieldElement { match self { + #[cfg(feature = "bn254")] Self::Skyscraper => hash_skyscraper(elements), Self::Sha256 => hash_digest::(PUBLIC_INPUTS_DST, elements), Self::Keccak => hash_digest::(PUBLIC_INPUTS_DST, elements), Self::Blake3 => hash_blake3(PUBLIC_INPUTS_DST, elements), + #[cfg(feature = "bn254")] Self::Poseidon2 => hash_poseidon2(elements), } } @@ -161,10 +222,13 @@ impl std::str::FromStr for HashConfig { type Err = String; fn from_str(s: &str) -> Result { + #[cfg(feature = "bn254")] + const VALID: &str = "skyscraper, sha256, keccak, blake3, poseidon2"; + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + const VALID: &str = "sha256, keccak, blake3"; Self::parse(s).ok_or_else(|| { format!( - "Invalid hash configuration: '{}'. Valid options: skyscraper, sha256, keccak, \ - blake3, poseidon2", + "Invalid hash configuration: '{}'. Valid options: {VALID}", s ) }) @@ -173,6 +237,7 @@ impl std::str::FromStr for HashConfig { /// Pairwise Skyscraper compression; empty input hashes to 0. Not /// domain-separated (see [`PUBLIC_INPUTS_DST`]). +#[cfg(feature = "bn254")] #[inline] fn hash_skyscraper(elements: &[FieldElement]) -> FieldElement { #[inline] @@ -189,12 +254,47 @@ fn hash_skyscraper(elements: &[FieldElement]) -> FieldElement { } } -/// DST-tagged [`sha2::digest::Digest`] hash (SHA-256, Keccak-256) over -/// `elements`. +/// Reduces a hash digest into a [`FieldElement`] (BN254: straight +/// little-endian mod-p reduction; ~2⁻²⁵⁴ bias — negligible for FS instance +/// binding, but not a uniform field sampler). +#[cfg(feature = "bn254")] +#[inline] +fn digest_to_field(digest: &[u8]) -> FieldElement { + FieldElement::from_le_bytes_mod_order(digest) +} + +/// Reduces a hash digest into a [`FieldElement`] by spreading it across all +/// three coordinates of the Goldilocks cubic extension. /// -/// The final [`FieldElement::from_le_bytes_mod_order`] reduction introduces -/// ~2⁻²⁵⁴ bias — negligible for FS instance binding, but this is not a -/// uniform field sampler. +/// The digest is split into three contiguous chunks, each reduced mod the +/// ~64-bit Goldilocks base prime, so the image is the full ~192-bit cubic +/// extension rather than the ~64-bit base subfield a single reduction would +/// produce. This value is *both* the Fiat-Shamir instance tag and the absorbed +/// public-inputs hash the verifier recomputes and compares. +/// +/// Why spread: the collision resistance of a binding hash is the birthday +/// bound over its image (~2^(bits/2)), not the image size. A base-subfield +/// image (~2⁶⁴ values) would give only ~2³² resistance; the full extension +/// (~2¹⁹²) gives ~2⁹⁶, itself bounded by the 256-bit digest's own ~2¹²⁸. ~2⁹⁶ +/// is still below the 128-bit WHIR target, but this tag is *defense-in-depth*, +/// not the sole binding: public-input binding is enforced independently by the +/// verifier's direct value check (`verify_public_input_binding`, soundness +/// error ~deg/|F|), so soundness does not rest on this hash's collision +/// resistance. Like the BN254 sibling, this is a binding hash, not a uniform +/// field sampler. +#[cfg(all(feature = "goldilocks", not(feature = "bn254")))] +#[inline] +fn digest_to_field(digest: &[u8]) -> FieldElement { + use {ark_ff::PrimeField, whir::algebra::fields::Field64}; + let chunk = digest.len().div_ceil(3); + let c0 = Field64::from_le_bytes_mod_order(&digest[..chunk.min(digest.len())]); + let c1 = Field64::from_le_bytes_mod_order(digest.get(chunk..2 * chunk).unwrap_or(&[])); + let c2 = Field64::from_le_bytes_mod_order(digest.get(2 * chunk..).unwrap_or(&[])); + FieldElement::new(c0, c1, c2) +} + +/// DST-tagged [`sha2::digest::Digest`] hash (SHA-256, Keccak-256) over +/// `elements`, reduced to a field element via [`digest_to_field`]. #[inline] fn hash_digest(dst: &[u8], elements: &[FieldElement]) -> FieldElement where @@ -205,7 +305,7 @@ where for fe in elements { hasher.update(field_to_bytes_le(*fe)); } - FieldElement::from_le_bytes_mod_order(&hasher.finalize()) + digest_to_field(&hasher.finalize()) } /// Poseidon2 one-shot hash over `elements` (including empty input). @@ -216,6 +316,7 @@ where /// [`poseidon2::poseidon2_hash`] separately provides **length** domain- /// separation, so the two combined mirror what SHA/Keccak/BLAKE3 get via /// the raw [`PUBLIC_INPUTS_DST`] byte prefix. +#[cfg(feature = "bn254")] #[inline] fn hash_poseidon2(elements: &[FieldElement]) -> FieldElement { let mut tagged = Vec::with_capacity(elements.len() + 1); @@ -234,7 +335,7 @@ fn hash_blake3(dst: &[u8], elements: &[FieldElement]) -> FieldElement { for fe in elements { hasher.update(&field_to_bytes_le(*fe)); } - FieldElement::from_le_bytes_mod_order(hasher.finalize().as_bytes()) + digest_to_field(hasher.finalize().as_bytes()) } #[cfg(test)] @@ -244,6 +345,7 @@ mod tests { /// All known variants. If a new variant is added to `HashConfig`, this /// list must be updated — causing the exhaustiveness tests below to fail /// until `from_byte` / `to_byte` are also updated. + #[cfg(feature = "bn254")] const ALL_VARIANTS: &[HashConfig] = &[ HashConfig::Skyscraper, HashConfig::Sha256, @@ -251,6 +353,75 @@ mod tests { HashConfig::Blake3, HashConfig::Poseidon2, ]; + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + const ALL_VARIANTS: &[HashConfig] = + &[HashConfig::Sha256, HashConfig::Keccak, HashConfig::Blake3]; + + /// Binary serde must encode the field-independent [`HashConfig::to_byte`] + /// value, NOT serde's positional variant index. Otherwise cfg-gating + /// Skyscraper/Poseidon2 out of a goldilocks build shifts `Sha256` from 1 + /// to 0, silently colliding with Skyscraper's byte across field builds. + #[test] + fn binary_serde_is_field_independent_to_byte() { + for &v in ALL_VARIANTS { + let bytes = postcard::to_allocvec(&v).unwrap(); + assert_eq!( + bytes, + vec![v.to_byte()], + "{v:?}: postcard must equal to_byte()" + ); + let back: HashConfig = postcard::from_bytes(&bytes).unwrap(); + assert_eq!(back, v, "{v:?}: postcard roundtrip"); + } + // Pinned: these bytes must be identical in every field build. + assert_eq!(postcard::to_allocvec(&HashConfig::Sha256).unwrap(), vec![ + 1u8 + ]); + assert_eq!(postcard::to_allocvec(&HashConfig::Keccak).unwrap(), vec![ + 2u8 + ]); + assert_eq!(postcard::to_allocvec(&HashConfig::Blake3).unwrap(), vec![ + 3u8 + ]); + } + + /// Human-readable serde uses the canonical name string (stable across + /// fields) and round-trips, including the legacy aliases via `parse`. + #[test] + fn human_readable_serde_is_canonical_name() { + for &v in ALL_VARIANTS { + let json = serde_json::to_string(&v).unwrap(); + assert_eq!( + json, + format!("\"{}\"", v.name()), + "{v:?}: json must be name()" + ); + let back: HashConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(back, v, "{v:?}: json roundtrip"); + } + assert_eq!( + serde_json::to_string(&HashConfig::Sha256).unwrap(), + "\"sha256\"" + ); + let aliased: HashConfig = serde_json::from_str("\"sha-256\"").unwrap(); + assert_eq!(aliased, HashConfig::Sha256, "legacy alias must still parse"); + } + + /// A goldilocks build must *reject* binary bytes for variants it does not + /// have (0 = Skyscraper, 4 = Poseidon2) rather than silently misdecode — + /// this is the cross-field-substitution guard the derive lacked. + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + #[test] + fn binary_serde_rejects_bn254_only_bytes() { + assert!( + postcard::from_bytes::(&[0u8]).is_err(), + "byte 0 (Skyscraper) must be rejected, not decoded as Sha256" + ); + assert!( + postcard::from_bytes::(&[4u8]).is_err(), + "byte 4 (Poseidon2) must be rejected" + ); + } #[test] fn from_byte_roundtrips_with_to_byte() { @@ -264,10 +435,10 @@ mod tests { #[test] fn from_byte_returns_none_for_invalid() { - let first_invalid = ALL_VARIANTS.len() as u8; + // 5 is the first byte beyond the full (bn254) variant space. assert!( - HashConfig::from_byte(first_invalid).is_none(), - "from_byte({first_invalid}) should be None" + HashConfig::from_byte(5).is_none(), + "from_byte(5) should be None" ); assert!( HashConfig::from_byte(u8::MAX).is_none(), @@ -275,6 +446,16 @@ mod tests { ); } + /// Goldilocks builds must reject the BN254-only header bytes + /// (0 = Skyscraper, 4 = Poseidon2) gracefully rather than panic. + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + #[test] + fn from_byte_rejects_bn254_only_headers() { + assert!(HashConfig::from_byte(0).is_none(), "byte 0 is Skyscraper"); + assert!(HashConfig::from_byte(4).is_none(), "byte 4 is Poseidon2"); + } + + #[cfg(feature = "bn254")] #[test] fn to_byte_values_are_contiguous_from_zero() { let mut bytes: Vec = ALL_VARIANTS.iter().map(|v| v.to_byte()).collect(); diff --git a/provekit/common/src/lib.rs b/provekit/common/src/lib.rs index 3953207d8..ec7ab4738 100644 --- a/provekit/common/src/lib.rs +++ b/provekit/common/src/lib.rs @@ -1,20 +1,36 @@ +// `bn254` takes precedence when both field features are enabled (CI builds +// with `--all-features`, which unavoidably turns both on). A goldilocks +// build must therefore disable the default: +// cargo build -p provekit-common -p provekit-prover -p provekit-verifier \ +// --no-default-features --features goldilocks +#[cfg(not(any(feature = "bn254", feature = "goldilocks")))] +compile_error!("enable a field feature: `bn254` (default) or `goldilocks`"); + pub mod file; pub use file::binary_format; pub mod hash_config; mod interner; +#[cfg(feature = "bn254")] mod mavros; +#[cfg(feature = "bn254")] mod noir_proof_scheme; +#[cfg(feature = "bn254")] pub mod ntt; +#[cfg(feature = "bn254")] pub mod optimize; +#[cfg(feature = "bn254")] pub mod poseidon2; pub mod prefix_covector; +#[cfg(feature = "bn254")] mod prover; mod r1cs; +#[cfg(feature = "bn254")] pub mod skyscraper; pub mod sparse_matrix; mod transcript_sponge; pub mod u256_arith; pub mod utils; +#[cfg(feature = "bn254")] mod verifier; mod whir_r1cs; pub mod witness; @@ -23,18 +39,61 @@ use crate::{ interner::{InternedFieldElement, Interner}, sparse_matrix::{HydratedSparseMatrix, SparseMatrix}, }; +/// The proof system's field. BN254 scalar field by default; the Goldilocks +/// cubic extension (`Field64_3`, ~192 bits — `Field`/`FftField` but not +/// `PrimeField`) under the `goldilocks` feature. +#[cfg(feature = "bn254")] +pub use ark_bn254::Fr as FieldElement; +#[cfg(all(feature = "goldilocks", not(feature = "bn254")))] +pub use whir::algebra::fields::Field64_3 as FieldElement; + +/// Which field [`FieldElement`] resolved to in this build: `true` for the +/// BN254 scalar field, `false` for the Goldilocks cubic extension. Because +/// `bn254` wins precedence when both features are on, this tracks the actual +/// type — not merely whether `goldilocks` was requested. +/// +/// Downstream crates that carry their own `bn254`/`goldilocks` features assert +/// their intent against this via [`assert_field_matches_common`], so Cargo +/// feature unification (a sibling forcing `provekit-common/bn254` while the +/// crate was built for `goldilocks`) becomes a build error rather than silently +/// compiling field-gated code over the wrong `FieldElement`. +pub const FIELD_IS_BN254: bool = cfg!(feature = "bn254"); + +/// Compile-time guard that the invoking crate's field feature matches +/// [`FIELD_IS_BN254`] (i.e. `provekit-common`'s resolved [`FieldElement`]). +/// +/// Invoke once at crate root in any crate that has its own `bn254`/`goldilocks` +/// features and uses [`FieldElement`]. `cfg!(feature = "bn254")` is evaluated +/// in the caller, so a divergence introduced by feature unification fails the +/// build with a clear message instead of producing a wrong-field binary. +#[macro_export] +macro_rules! assert_field_matches_common { + () => { + const _: () = ::core::assert!( + ::core::cfg!(feature = "bn254") == $crate::FIELD_IS_BN254, + "this crate's field feature disagrees with provekit-common's resolved FieldElement: a \ + sibling crate likely enabled provekit-common/bn254 via Cargo feature unification \ + while this crate was built for goldilocks. Build every provekit crate in the \ + dependency graph over the same field.", + ); + }; +} +#[cfg(feature = "bn254")] pub use { acir::FieldElement as NoirElement, - ark_bn254::Fr as FieldElement, - hash_config::HashConfig, mavros::{MavrosProver, MavrosSchemeData}, noir_proof_scheme::{NoirProof, NoirProofScheme, NoirSchemeData}, - prefix_covector::{OffsetCovector, PrefixCovector, SparseCovector}, prover::{NoirProver, Prover}, + verifier::Verifier, +}; +pub use { + hash_config::HashConfig, + prefix_covector::{OffsetCovector, PrefixCovector, SparseCovector}, r1cs::R1CS, transcript_sponge::TranscriptSponge, - verifier::Verifier, - whir_r1cs::{R1csHash, WhirConfig, WhirR1CSProof, WhirR1CSScheme, WhirZkConfig}, + whir_r1cs::{ + R1csHash, WhirConfig, WhirR1CSProof, WhirR1CSScheme, WhirR1CSSchemeBuilder, WhirZkConfig, + }, witness::PublicInputs, }; @@ -59,7 +118,11 @@ pub fn register_ntt() { // Register ProveKit-specific engines; WHIR's built-in engines // (SHA2, Keccak, Blake3, etc.) are pre-registered via whir::hash::ENGINES. - whir::hash::ENGINES.register(Arc::new(skyscraper::SkyscraperHashEngine)); - whir::hash::ENGINES.register(Arc::new(poseidon2::Poseidon2HashEngine)); + // Skyscraper and Poseidon2 are BN254-only constructions. + #[cfg(feature = "bn254")] + { + whir::hash::ENGINES.register(Arc::new(skyscraper::SkyscraperHashEngine)); + whir::hash::ENGINES.register(Arc::new(poseidon2::Poseidon2HashEngine)); + } }); } diff --git a/provekit/common/src/transcript_sponge.rs b/provekit/common/src/transcript_sponge.rs index eb42294c9..0858bbd64 100644 --- a/provekit/common/src/transcript_sponge.rs +++ b/provekit/common/src/transcript_sponge.rs @@ -5,8 +5,10 @@ //! The branch cost is negligible — the sponge is called O(log n) times //! per proof for Fiat-Shamir challenges, not in a tight inner loop. +#[cfg(feature = "bn254")] +use crate::{poseidon2::Poseidon2Sponge, skyscraper::SkyscraperSponge}; use { - crate::{poseidon2::Poseidon2Sponge, skyscraper::SkyscraperSponge, HashConfig}, + crate::HashConfig, spongefish::{instantiations, DuplexSpongeInterface}, std::fmt, }; @@ -19,7 +21,9 @@ pub enum TranscriptSponge { Sha256(instantiations::SHA256), Blake3(instantiations::Blake3), Keccak(instantiations::Keccak), + #[cfg(feature = "bn254")] Skyscraper(SkyscraperSponge), + #[cfg(feature = "bn254")] Poseidon2(Poseidon2Sponge), } @@ -29,7 +33,9 @@ impl fmt::Debug for TranscriptSponge { Self::Sha256(_) => f.debug_tuple("Sha256").finish(), Self::Blake3(_) => f.debug_tuple("Blake3").finish(), Self::Keccak(_) => f.debug_tuple("Keccak").finish(), + #[cfg(feature = "bn254")] Self::Skyscraper(_) => f.debug_tuple("Skyscraper").finish(), + #[cfg(feature = "bn254")] Self::Poseidon2(_) => f.debug_tuple("Poseidon2").finish(), } } @@ -42,7 +48,9 @@ impl TranscriptSponge { HashConfig::Sha256 => Self::Sha256(Default::default()), HashConfig::Blake3 => Self::Blake3(Default::default()), HashConfig::Keccak => Self::Keccak(Default::default()), + #[cfg(feature = "bn254")] HashConfig::Skyscraper => Self::Skyscraper(Default::default()), + #[cfg(feature = "bn254")] HashConfig::Poseidon2 => Self::Poseidon2(Default::default()), } } @@ -68,9 +76,11 @@ impl DuplexSpongeInterface for TranscriptSponge { Self::Keccak(s) => { s.absorb(input); } + #[cfg(feature = "bn254")] Self::Skyscraper(s) => { s.absorb(input); } + #[cfg(feature = "bn254")] Self::Poseidon2(s) => { s.absorb(input); } @@ -89,9 +99,11 @@ impl DuplexSpongeInterface for TranscriptSponge { Self::Keccak(s) => { s.squeeze(output); } + #[cfg(feature = "bn254")] Self::Skyscraper(s) => { s.squeeze(output); } + #[cfg(feature = "bn254")] Self::Poseidon2(s) => { s.squeeze(output); } @@ -110,9 +122,11 @@ impl DuplexSpongeInterface for TranscriptSponge { Self::Keccak(s) => { s.ratchet(); } + #[cfg(feature = "bn254")] Self::Skyscraper(s) => { s.ratchet(); } + #[cfg(feature = "bn254")] Self::Poseidon2(s) => { s.ratchet(); } diff --git a/provekit/common/src/utils/mod.rs b/provekit/common/src/utils/mod.rs index dff782968..fc64ca4fd 100644 --- a/provekit/common/src/utils/mod.rs +++ b/provekit/common/src/utils/mod.rs @@ -9,6 +9,7 @@ pub mod sumcheck; pub use self::print_abi::PrintAbi; /// Deserializes a BN254 field element from up to 32 little-endian bytes. +#[cfg(feature = "bn254")] #[inline] pub fn bytes_to_field(bytes: &[u8]) -> FieldElement { FieldElement::from_le_bytes_mod_order(bytes) @@ -17,6 +18,7 @@ pub fn bytes_to_field(bytes: &[u8]) -> FieldElement { /// Serializes a BN254 field element to its canonical 32-byte little-endian /// representation. Zero-allocation: copies the 4 canonical limbs directly /// instead of routing through `BigInt::to_bytes_le`'s `Vec`. +#[cfg(feature = "bn254")] #[inline] pub fn field_to_bytes_le(fe: FieldElement) -> [u8; 32] { let limbs = fe.into_bigint().0; @@ -27,21 +29,40 @@ pub fn field_to_bytes_le(fe: FieldElement) -> [u8; 32] { out } +/// Serializes a `Field64_3` element to its canonical 24-byte little-endian +/// representation (three 8-byte Goldilocks base-field coefficients). +#[cfg(all(feature = "goldilocks", not(feature = "bn254")))] +#[inline] +pub fn field_to_bytes_le(fe: FieldElement) -> [u8; 24] { + use ark_serialize::CanonicalSerialize; + let mut out = Vec::with_capacity(24); + fe.serialize_compressed(&mut out) + .expect("Field64_3 serialization is infallible"); + out.try_into().expect("Field64_3 serializes to 24 bytes") +} + use { - crate::{FieldElement, NoirElement}, - ark_ff::{BigInt, Field, PrimeField}, - ruint::{aliases::U256, uint}, + crate::FieldElement, + ark_ff::Field, std::{ fmt::{Display, Formatter, Result as FmtResult}, mem::MaybeUninit, + sync::LazyLock, }, tracing::instrument, }; +#[cfg(feature = "bn254")] +use { + crate::NoirElement, + ark_ff::{BigInt, PrimeField}, +}; -/// 1/2 for the BN254 -pub const HALF: FieldElement = uint_to_field(uint!( - 10944121435919637611123202872628637544274182200208017171849102093287904247809_U256 -)); +/// 1/2 in the proof field. +pub static HALF: LazyLock = LazyLock::new(|| { + FieldElement::from(2u64) + .inverse() + .expect("2 is invertible in a field of odd characteristic") +}); /// Target single-thread workload size for `T`. /// Should ideally be a multiple of a cache line (64 bytes) @@ -76,11 +97,8 @@ fn unzip_double_array( (left, right) } -pub const fn uint_to_field(i: U256) -> FieldElement { - FieldElement::new(BigInt(i.into_limbs())) -} - /// Convert a Noir field element to a native `FieldElement` +#[cfg(feature = "bn254")] #[inline(always)] pub fn noir_to_native(n: NoirElement) -> FieldElement { let limbs = n.into_repr().into_bigint().0; @@ -181,7 +199,7 @@ pub fn batch_inverse_montgomery(values: &[FieldElement]) -> Vec { inverses } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] pub fn convert_mavros_r1cs_to_provekit(mavros_r1cs: &mavros_artifacts::R1CS) -> crate::R1CS { let num_witnesses = mavros_r1cs.witness_layout.size(); let num_constraints = mavros_r1cs.constraints.len(); diff --git a/provekit/common/src/whir_r1cs.rs b/provekit/common/src/whir_r1cs.rs index 5b26d3d17..d2a07d602 100644 --- a/provekit/common/src/whir_r1cs.rs +++ b/provekit/common/src/whir_r1cs.rs @@ -1,12 +1,20 @@ +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] +use mavros_artifacts::R1CS as MavrosR1CS; #[cfg(debug_assertions)] use std::fmt::Debug; #[cfg(debug_assertions)] use whir::transcript::Interaction; use { - crate::{utils::serde_hex, FieldElement, HashConfig}, + crate::{ + utils::{next_power_of_two, serde_hex}, + FieldElement, HashConfig, R1CS, + }, + anyhow::{ensure, Result}, serde::{Deserialize, Serialize}, whir::{ algebra::embedding::Identity, + engines::EngineId, + parameters::ProtocolParameters, protocols::{whir::Config as GenericWhirConfig, whir_zk::Config as GenericWhirZkConfig}, transcript, }, @@ -74,6 +82,211 @@ impl WhirR1CSScheme { } } +const MIN_WHIR_NUM_VARIABLES: usize = 13; +const MIN_SUMCHECK_NUM_VARIABLES: usize = 1; + +/// Constructors for [`WhirR1CSScheme`], sizing the WHIR configuration from an +/// R1CS instance (or raw dimensions) and stamping the instance-binding +/// `r1cs_hash`. +pub trait WhirR1CSSchemeBuilder: Sized { + /// Build a scheme for `r1cs`, stamping `r1cs_hash = r1cs.hash()`. + /// + /// Errors if `num_challenges != challenge_offsets.len()` or if `w1_size` + /// exceeds the R1CS witness count. + fn new_for_r1cs( + r1cs: &R1CS, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result; + + /// Build a scheme from a Mavros R1CS, leaving `r1cs_hash` unset. + /// + /// Errors on the same dimension inconsistencies as + /// [`Self::new_from_dimensions`]. + #[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] + fn new_from_mavros_r1cs( + r1cs: &MavrosR1CS, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result; + + /// Build a scheme from raw dimensions, leaving `r1cs_hash` unset. + /// + /// Errors if `num_challenges != challenge_offsets.len()` or if `w1_size` + /// exceeds `num_witnesses`. + fn new_from_dimensions( + num_witnesses: usize, + num_constraints: usize, + a_num_entries: usize, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result; + + /// Build the WHIR ZK configuration for `num_variables` (clamped to the + /// protocol minimum) and `num_polynomials`. + fn new_whir_zk_config_for_size( + num_variables: usize, + num_polynomials: usize, + hash_id: EngineId, + ) -> WhirZkConfig; +} + +impl WhirR1CSSchemeBuilder for WhirR1CSScheme { + fn new_for_r1cs( + r1cs: &R1CS, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result { + ensure!( + num_challenges == challenge_offsets.len(), + "num_challenges ({num_challenges}) != challenge_offsets.len() ({})", + challenge_offsets.len() + ); + let total_witnesses = r1cs.num_witnesses(); + ensure!( + w1_size <= total_witnesses, + "w1_size ({w1_size}) exceeds total witnesses ({total_witnesses})" + ); + let w2_size = total_witnesses - w1_size; + + let m1_raw = next_power_of_two(w1_size); + let m2_raw = next_power_of_two(w2_size); + let m0_raw = next_power_of_two(r1cs.num_constraints()); + + let mut m_raw = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); + let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); + + // Ensure w1's zero-padding has room for the blinding polynomial coefficients. + if (1usize << m_raw) - w1_size < 4 * m_0 { + m_raw += 1; + } + + Ok(Self { + m: m_raw, + w1_size, + m_0, + a_num_terms: next_power_of_two(r1cs.a().iter().count()), + num_challenges, + challenge_offsets, + whir_witness: Self::new_whir_zk_config_for_size(m_raw, 1, hash_config.engine_id()), + has_public_inputs, + r1cs_hash: r1cs.hash(), + hash_config, + }) + } + + fn new_whir_zk_config_for_size( + num_variables: usize, + num_polynomials: usize, + hash_id: EngineId, + ) -> WhirZkConfig { + let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); + + // Parameters tuned for 128-bit security under the Johnson bound (the old + // ConjectureList soundness was disproven). Rate=2 balances query count vs + // codeword size; ff=3 keeps blinding polynomials small; pow_bits=10 shifts + // security budget toward algebraic hardness (118 bits) with light PoW per + // round, which is faster than the default ~18-bit grinding. + let whir_params = ProtocolParameters { + unique_decoding: false, + security_level: 128, + pow_bits: 10, + initial_folding_factor: 3, + folding_factor: 3, + starting_log_inv_rate: 2, + batch_size: 1, + hash_id, + }; + WhirZkConfig::new(1 << nv, &whir_params, num_polynomials) + } + + #[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] + fn new_from_mavros_r1cs( + r1cs: &MavrosR1CS, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result { + let num_witnesses = r1cs.witness_layout.size(); + let num_constraints = r1cs.constraints.len(); + let a_num_entries: usize = r1cs.constraints.iter().map(|c| c.a.len()).sum(); + + Self::new_from_dimensions( + num_witnesses, + num_constraints, + a_num_entries, + w1_size, + num_challenges, + challenge_offsets, + has_public_inputs, + hash_config, + ) + } + + fn new_from_dimensions( + num_witnesses: usize, + num_constraints: usize, + a_num_entries: usize, + w1_size: usize, + num_challenges: usize, + challenge_offsets: Vec, + has_public_inputs: bool, + hash_config: HashConfig, + ) -> Result { + ensure!( + num_challenges == challenge_offsets.len(), + "num_challenges ({num_challenges}) != challenge_offsets.len() ({})", + challenge_offsets.len() + ); + ensure!( + w1_size <= num_witnesses, + "w1_size ({w1_size}) exceeds total witnesses ({num_witnesses})" + ); + // Size the witness commitment domain off the largest single commitment + // (w1 or w2), not the total witness count — Mavros commits w1 and w2 + // separately, so the domain only needs to hold the larger of the two. + let w2_size = num_witnesses - w1_size; + let m1_raw = next_power_of_two(w1_size); + let m2_raw = next_power_of_two(w2_size); + let m0_raw = next_power_of_two(num_constraints); + + let mut m = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); + let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); + + // Ensure w1's zero-padding has room for the blinding polynomial coefficients. + if (1usize << m) - w1_size < 4 * m_0 { + m += 1; + } + + Ok(Self { + m, + m_0, + a_num_terms: next_power_of_two(a_num_entries), + whir_witness: Self::new_whir_zk_config_for_size(m, 1, hash_config.engine_id()), + w1_size, + num_challenges, + challenge_offsets, + has_public_inputs, + r1cs_hash: R1csHash::UNSET, + hash_config, + }) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WhirR1CSProof { #[serde(with = "serde_hex")] @@ -87,3 +300,121 @@ pub struct WhirR1CSProof { #[serde(skip)] pub pattern: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_security_level() { + let config = WhirR1CSScheme::new_whir_zk_config_for_size(20, 1, whir::hash::SHA2); + let sec_blinded = config + .blinded_commitment + .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); + let sec_blinding = config + .blinding_commitment + .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); + assert!( + sec_blinded >= 128.0, + "Blinded commitment security {sec_blinded:.2} < 128 bits" + ); + assert!( + sec_blinding >= 128.0, + "Blinding commitment security {sec_blinding:.2} < 128 bits" + ); + } + + #[test] + fn verify_security_level_min_variables() { + let config = WhirR1CSScheme::new_whir_zk_config_for_size( + MIN_WHIR_NUM_VARIABLES, + 1, + whir::hash::SHA2, + ); + let sec_blinded = config + .blinded_commitment + .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); + let sec_blinding = config + .blinding_commitment + .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); + assert!( + sec_blinded >= 128.0, + "Blinded commitment security {sec_blinded:.2} < 128 bits at nv={}", + MIN_WHIR_NUM_VARIABLES + ); + assert!( + sec_blinding >= 128.0, + "Blinding commitment security {sec_blinding:.2} < 128 bits at nv={}", + MIN_WHIR_NUM_VARIABLES + ); + } + + fn r1cs_with_dimensions(num_witnesses: usize, num_constraints: usize) -> R1CS { + let mut r1cs = R1CS::new(); + r1cs.grow_matrices(num_constraints, num_witnesses); + r1cs + } + + fn assert_dimension_builders( + num_witnesses: usize, + num_constraints: usize, + w1_size: usize, + expected_m: usize, + expected_m_0: usize, + ) { + let from_dimensions = WhirR1CSScheme::new_from_dimensions( + num_witnesses, + num_constraints, + 0, + w1_size, + 0, + vec![], + false, + HashConfig::Sha256, + ) + .unwrap(); + assert_eq!(from_dimensions.m, expected_m); + assert_eq!(from_dimensions.m_0, expected_m_0); + assert_eq!(from_dimensions.w1_size, w1_size); + + let r1cs = r1cs_with_dimensions(num_witnesses, num_constraints); + let from_r1cs = + WhirR1CSScheme::new_for_r1cs(&r1cs, w1_size, 0, vec![], false, HashConfig::Sha256) + .unwrap(); + assert_eq!(from_r1cs.m, expected_m); + assert_eq!(from_r1cs.m_0, expected_m_0); + assert_eq!(from_r1cs.w1_size, w1_size); + } + + #[test] + fn mavros_dimensions_use_largest_commitment_not_total_witnesses() { + let scheme = WhirR1CSScheme::new_from_dimensions( + 600_000, + 8, + 8, + 300_000, + 2, + vec![0, 1], + false, + HashConfig::Sha256, + ) + .unwrap(); + + assert_eq!(scheme.m, 19); + } + + #[test] + fn dimension_builders_handle_empty_w2() { + assert_dimension_builders(64, 8, 64, MIN_WHIR_NUM_VARIABLES, 3); + } + + #[test] + fn dimension_builders_handle_empty_w1() { + assert_dimension_builders(64, 8, 0, MIN_WHIR_NUM_VARIABLES, 3); + } + + #[test] + fn dimension_builders_bump_exact_power_of_two_w1_for_blinding_room() { + assert_dimension_builders(12_288, 2_048, 8_192, 14, 11); + } +} diff --git a/provekit/common/src/witness/mod.rs b/provekit/common/src/witness/mod.rs index 403a58e45..96367a0be 100644 --- a/provekit/common/src/witness/mod.rs +++ b/provekit/common/src/witness/mod.rs @@ -1,9 +1,19 @@ +// The witness builders are Noir/BN254-coupled (BigInt decompositions, EC +// gadgets, Noir witness maps); Phase 1 of the goldilocks build hand-builds +// R1CS witnesses instead. +#[cfg(feature = "bn254")] mod binops; +#[cfg(feature = "bn254")] mod digits; +#[cfg(feature = "bn254")] mod limbs; +#[cfg(feature = "bn254")] mod ram; +#[cfg(feature = "bn254")] mod scheduling; +#[cfg(feature = "bn254")] mod witness_builder; +#[cfg(feature = "bn254")] mod witness_generator; use { @@ -14,6 +24,7 @@ use { ark_ff::One, serde::{Deserialize, Serialize}, }; +#[cfg(feature = "bn254")] pub use { binops::BINOP_ATOMIC_BITS, digits::{decompose_into_digits, DigitalDecompositionWitnesses}, @@ -98,11 +109,24 @@ impl PublicInputs { /// /// Used as the Fiat-Shamir instance tag binding the transcript to these /// public inputs. + #[cfg(feature = "bn254")] #[inline] #[must_use] pub fn hash_bytes(&self, config: HashConfig) -> [u8; 32] { field_to_bytes_le(self.hash(config)) } + + /// Returns [`Self::hash`] as a 32-byte little-endian instance tag: the + /// canonical 24-byte `Field64_3` serialization, zero-padded to keep the + /// transcript instance width stable. + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + #[inline] + #[must_use] + pub fn hash_bytes(&self, config: HashConfig) -> [u8; 32] { + let mut out = [0u8; 32]; + out[..24].copy_from_slice(&field_to_bytes_le(self.hash(config))); + out + } } impl Default for PublicInputs { @@ -115,6 +139,7 @@ impl Default for PublicInputs { mod tests { use {super::*, proptest::prelude::*}; + #[cfg(feature = "bn254")] const ALL_CONFIGS: [HashConfig; 5] = [ HashConfig::Skyscraper, HashConfig::Sha256, @@ -122,6 +147,9 @@ mod tests { HashConfig::Blake3, HashConfig::Poseidon2, ]; + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + const ALL_CONFIGS: [HashConfig; 3] = + [HashConfig::Sha256, HashConfig::Keccak, HashConfig::Blake3]; fn fe(n: u64) -> FieldElement { FieldElement::from(n) @@ -157,6 +185,7 @@ mod tests { } } + #[cfg(feature = "bn254")] #[test] fn hash_bytes_is_le_serialization_of_hash() { let inputs = pi(&[7, 13]); @@ -169,8 +198,43 @@ mod tests { } } + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + #[test] + fn hash_bytes_is_padded_le_serialization_of_hash() { + let inputs = pi(&[7, 13]); + for config in ALL_CONFIGS { + let bytes = inputs.hash_bytes(config); + assert_eq!( + bytes[..24], + field_to_bytes_le(inputs.hash(config)), + "{config:?}: hash_bytes[..24] must equal LE(hash())" + ); + assert_eq!(bytes[24..], [0u8; 8], "{config:?}: padding must be zero"); + } + } + + /// Regression: the binding hash must span all three cubic-extension + /// coordinates, not collapse into the ~64-bit base subfield. A + /// base-subfield image leaves the upper 16 bytes (coords `c1`, `c2`) + /// zero and caps public-input binding collision resistance at ~2⁶⁴, + /// below the 128-bit security target. See `digest_to_field` in + /// `hash_config`. + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + #[test] + fn goldilocks_binding_hash_spans_full_extension() { + for config in ALL_CONFIGS { + let bytes = field_to_bytes_le(pi(&[1, 2]).hash(config)); + assert_ne!( + bytes[8..24], + [0u8; 16], + "{config:?}: binding hash collapsed into the ~64-bit base subfield" + ); + } + } + // --- empty input --- + #[cfg(feature = "bn254")] #[test] fn skyscraper_empty_returns_zero() { // Transcript-visible back-compat: Skyscraper hashes [] to 0. @@ -262,6 +326,7 @@ mod tests { // mod-reduction, Skyscraper compression order) will fail these and must be // a deliberate, reviewed format change. + #[cfg(feature = "bn254")] #[test] fn kat_empty_skyscraper() { // Skyscraper on empty input is 0 by construction; no DST. @@ -269,6 +334,7 @@ mod tests { assert_eq!(got, [0u8; 32], "Skyscraper empty-input KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_one_two_skyscraper() { let got = pi(&[1, 2]).hash_bytes(HashConfig::Skyscraper); @@ -283,42 +349,49 @@ mod tests { ); } + #[cfg(feature = "bn254")] #[test] fn kat_empty_sha256() { let got = PublicInputs::new().hash_bytes(HashConfig::Sha256); assert_eq!(got, KAT_EMPTY_SHA256, "SHA-256 empty-input KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_one_two_sha256() { let got = pi(&[1, 2]).hash_bytes(HashConfig::Sha256); assert_eq!(got, KAT_ONE_TWO_SHA256, "SHA-256 [1, 2] KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_empty_keccak() { let got = PublicInputs::new().hash_bytes(HashConfig::Keccak); assert_eq!(got, KAT_EMPTY_KECCAK, "Keccak-256 empty-input KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_one_two_keccak() { let got = pi(&[1, 2]).hash_bytes(HashConfig::Keccak); assert_eq!(got, KAT_ONE_TWO_KECCAK, "Keccak-256 [1, 2] KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_empty_blake3() { let got = PublicInputs::new().hash_bytes(HashConfig::Blake3); assert_eq!(got, KAT_EMPTY_BLAKE3, "BLAKE3 empty-input KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_one_two_blake3() { let got = pi(&[1, 2]).hash_bytes(HashConfig::Blake3); assert_eq!(got, KAT_ONE_TWO_BLAKE3, "BLAKE3 [1, 2] KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_empty_poseidon2() { // Non-zero: even with no user inputs, the DST field element is @@ -327,6 +400,7 @@ mod tests { assert_eq!(got, KAT_EMPTY_POSEIDON2, "Poseidon2 empty-input KAT drift"); } + #[cfg(feature = "bn254")] #[test] fn kat_one_two_poseidon2() { let got = pi(&[1, 2]).hash_bytes(HashConfig::Poseidon2); @@ -335,31 +409,37 @@ mod tests { // Frozen outputs. Regenerate only for a deliberate, reviewed format change. + #[cfg(feature = "bn254")] const KAT_EMPTY_SHA256: [u8; 32] = [ 0xc6, 0xa2, 0x48, 0x23, 0x44, 0xd4, 0x29, 0xf5, 0x53, 0x37, 0xc3, 0xb6, 0x87, 0xb5, 0xc3, 0x54, 0x47, 0x5c, 0x7c, 0x7f, 0x17, 0xac, 0x26, 0xeb, 0x47, 0x92, 0x78, 0x00, 0x11, 0xfe, 0xa0, 0x26, ]; + #[cfg(feature = "bn254")] const KAT_ONE_TWO_SHA256: [u8; 32] = [ 0x0f, 0x7b, 0x4c, 0xec, 0x9b, 0x45, 0x3f, 0xe5, 0x2f, 0xf4, 0x32, 0x96, 0x96, 0x60, 0xd2, 0xd8, 0x92, 0x5e, 0x7c, 0x34, 0xdd, 0x27, 0x59, 0x05, 0x7f, 0xc0, 0xf2, 0x73, 0x43, 0x53, 0x76, 0x1d, ]; + #[cfg(feature = "bn254")] const KAT_EMPTY_KECCAK: [u8; 32] = [ 0xb2, 0x2f, 0xf9, 0x91, 0x4f, 0xaf, 0xbd, 0xd0, 0x3c, 0x4f, 0xa2, 0x7a, 0xb0, 0x8a, 0x34, 0x5f, 0x0e, 0x1c, 0x62, 0x53, 0xf4, 0xc0, 0x02, 0x37, 0x2b, 0xaa, 0x50, 0x3c, 0x82, 0xb1, 0x2d, 0x23, ]; + #[cfg(feature = "bn254")] const KAT_ONE_TWO_KECCAK: [u8; 32] = [ 0xb1, 0xe0, 0x10, 0xfa, 0x01, 0x19, 0xcf, 0x35, 0x85, 0xac, 0x34, 0xb3, 0xdb, 0xb0, 0x11, 0x17, 0x57, 0xa9, 0x63, 0xff, 0x8d, 0x3c, 0x76, 0xc9, 0xf7, 0xc6, 0x79, 0xb0, 0xfb, 0xf1, 0x41, 0x16, ]; + #[cfg(feature = "bn254")] const KAT_EMPTY_BLAKE3: [u8; 32] = [ 0x7b, 0x01, 0x61, 0xea, 0x26, 0xb6, 0x36, 0xbc, 0x69, 0x23, 0xf3, 0x87, 0x7d, 0x4d, 0xca, 0xb8, 0xf7, 0xa9, 0xb4, 0x8d, 0x38, 0x56, 0x01, 0x13, 0x93, 0x57, 0xa0, 0x55, 0x37, 0x0c, 0xda, 0x27, ]; + #[cfg(feature = "bn254")] const KAT_ONE_TWO_BLAKE3: [u8; 32] = [ 0x84, 0x08, 0x71, 0x4e, 0xb3, 0xb2, 0x8e, 0x8f, 0xd6, 0xb5, 0xd0, 0x3d, 0x35, 0x99, 0x08, 0x4e, 0x47, 0x7d, 0x1f, 0xf9, 0xf5, 0x79, 0xc1, 0x46, 0xb4, 0x28, 0x84, 0xa5, 0x6b, 0xc5, @@ -369,11 +449,13 @@ mod tests { // the empty-input case is still a one-element absorb of the DST tag // (role-DS) with the length-IV set for `n = 1`. The DST field element // is derived as SHA256(PUBLIC_INPUTS_DST) reduced mod p. + #[cfg(feature = "bn254")] const KAT_EMPTY_POSEIDON2: [u8; 32] = [ 0x88, 0x8d, 0xd0, 0xb7, 0xbb, 0x12, 0xee, 0x46, 0xf0, 0x73, 0x14, 0x15, 0x2c, 0xec, 0x94, 0xf8, 0x5f, 0x5a, 0xbd, 0x58, 0xe3, 0xfd, 0x8a, 0x96, 0xb5, 0x18, 0x4c, 0x23, 0xd8, 0x7d, 0xf3, 0x01, ]; + #[cfg(feature = "bn254")] const KAT_ONE_TWO_POSEIDON2: [u8; 32] = [ 0x54, 0xfa, 0xbf, 0xce, 0x1b, 0xe4, 0xbb, 0xe9, 0x92, 0xb0, 0x6a, 0x42, 0xeb, 0xf7, 0x2d, 0xf4, 0x47, 0x8a, 0x2d, 0xb1, 0x9c, 0x5f, 0x35, 0xbf, 0x7c, 0x62, 0xba, 0x9d, 0x65, 0x67, @@ -382,6 +464,7 @@ mod tests { // --- property tests --- + #[cfg(feature = "bn254")] fn any_hash_config() -> impl Strategy { prop_oneof![ Just(HashConfig::Skyscraper), @@ -392,6 +475,15 @@ mod tests { ] } + #[cfg(all(feature = "goldilocks", not(feature = "bn254")))] + fn any_hash_config() -> impl Strategy { + prop_oneof![ + Just(HashConfig::Sha256), + Just(HashConfig::Keccak), + Just(HashConfig::Blake3), + ] + } + fn any_public_inputs() -> impl Strategy { prop::collection::vec(any::(), 0..32) .prop_map(|v| PublicInputs::from_vec(v.into_iter().map(FieldElement::from).collect())) diff --git a/provekit/prover/Cargo.toml b/provekit/prover/Cargo.toml index dd9498608..5a94f11e9 100644 --- a/provekit/prover/Cargo.toml +++ b/provekit/prover/Cargo.toml @@ -9,8 +9,21 @@ homepage.workspace = true repository.workspace = true [features] -default = ["witness-generation", "parallel"] -witness-generation = ["nargo", "bn254_blackbox_solver", "noir_artifact_cli"] +default = ["witness-generation", "parallel", "bn254"] +# At least one field feature must be enabled; `bn254` takes precedence when +# both are on. Pair `goldilocks` with `--no-default-features`. +# `provekit-verifier/` forwards the field selection to the dev-dep used +# by `goldilocks_fixtures`, so `cargo test -p provekit-prover` builds the verifier +# over the same field as common (it has no field feature of its own otherwise). +bn254 = [ + "provekit-common/bn254", + "provekit-common/provekit_ntt", + "provekit-verifier/bn254", + "dep:mavros-vm", + "dep:mavros-artifacts", +] +goldilocks = ["provekit-common/goldilocks", "provekit-verifier/goldilocks"] +witness-generation = ["bn254", "nargo", "bn254_blackbox_solver", "noir_artifact_cli"] parallel = ["provekit-common/parallel"] [dependencies] @@ -37,8 +50,13 @@ tracing.workspace = true bn254_blackbox_solver = { workspace = true, optional = true } nargo = { workspace = true, optional = true } noir_artifact_cli = { workspace = true, optional = true } -mavros-vm.workspace = true -mavros-artifacts.workspace = true +mavros-vm = { workspace = true, optional = true } +mavros-artifacts = { workspace = true, optional = true } + +[dev-dependencies] +# No default features: the field feature must unify from this crate's own +# `bn254`/`goldilocks` selection, not be forced to bn254 by the dev-dep. +provekit-verifier = { workspace = true, default-features = false } [lints] workspace = true diff --git a/provekit/prover/src/goldilocks_fixtures.rs b/provekit/prover/src/goldilocks_fixtures.rs new file mode 100644 index 000000000..8ad51ef88 --- /dev/null +++ b/provekit/prover/src/goldilocks_fixtures.rs @@ -0,0 +1,476 @@ +//! Hand-built R1CS fixtures, runnable under `bn254` or `goldilocks`. +//! +//! Goldilocks gates out the compiler, so `R1CS::new` + `add_constraint` is the +//! only way to build one. Fixtures are plain-arithmetic (`num_challenges = 0`), +//! hash pinned to SHA-256 (no Skyscraper under goldilocks). Only +//! `goldilocks_field_size_is_192_bits` is field-gated. + +use { + crate::whir_r1cs::WhirR1CSProver, + ark_ff::{One, UniformRand}, + ark_std::rand::{rngs::StdRng, Rng, SeedableRng}, + provekit_common::{ + register_ntt, FieldElement, HashConfig, PublicInputs, TranscriptSponge, WhirR1CSProof, + WhirR1CSScheme, WhirR1CSSchemeBuilder, R1CS, + }, + provekit_verifier::whir_r1cs::WhirR1CSVerifier, + std::time::{Duration, Instant}, + whir::transcript::{ProverState, VerifierMessage}, +}; + +const HASH: HashConfig = HashConfig::Sha256; + +/// WHIR witness-domain floor (`MIN_WHIR_NUM_VARIABLES`): prover work is flat at +/// or below `2^13` on both axes. +const FOLD_FLOOR: usize = 8192; + +/// `(A·w) * (B·w) = C·w` for every row (the bn254-gated oracle is unavailable). +fn satisfies(r1cs: &R1CS, w: &[FieldElement]) -> bool { + (0..r1cs.num_constraints()).all(|row| { + let a: FieldElement = r1cs.a().iter_row(row).map(|(col, v)| v * w[col]).sum(); + let b: FieldElement = r1cs.b().iter_row(row).map(|(col, v)| v * w[col]).sum(); + let c: FieldElement = r1cs.c().iter_row(row).map(|(col, v)| v * w[col]).sum(); + a * b == c + }) +} + +/// `w[i]^2 = w[i+1]` chain: `depth` constraints, `depth + 2` witnesses, `x` +/// public. The size knob — `depth + 2 > 2^13` crosses the fold floor. +fn squaring_chain(x: u64, depth: usize) -> (R1CS, Vec) { + let mut r1cs = R1CS::new(); + r1cs.add_witnesses(depth + 2); + r1cs.num_public_inputs = 1; + let one = FieldElement::one(); + let mut w = vec![one, FieldElement::from(x)]; + for i in 1..=depth { + r1cs.add_constraint(&[(one, i)], &[(one, i)], &[(one, i + 1)]); + let sq = w[i] * w[i]; + w.push(sq); + } + (r1cs, w) +} + +/// `p0 * p1 = z` with two public inputs — exercises the binding loop at `N ≥ 2` +/// (PR #321 class), unreachable with one input. +fn two_public_inputs(p0: u64, p1: u64) -> (R1CS, Vec) { + let mut r1cs = R1CS::new(); + r1cs.add_witnesses(4); + r1cs.num_public_inputs = 2; + let one = FieldElement::one(); + r1cs.add_constraint(&[(one, 1)], &[(one, 2)], &[(one, 3)]); + let (a, b) = (FieldElement::from(p0), FieldElement::from(p1)); + let w = vec![one, a, b, a * b]; + (r1cs, w) +} + +/// `count` random `(coeff, col)` terms over `[0, cols)` (`count` may be 0). +fn random_terms(rng: &mut StdRng, cols: usize, count: usize) -> Vec<(FieldElement, usize)> { + (0..count) + .map(|_| { + let coeff = FieldElement::from(rng.gen_range(1u64..=7)); + let col = rng.gen_range(0..cols); + (coeff, col) + }) + .collect() +} + +/// A random 1–3 term linear combination. +fn random_lc(rng: &mut StdRng, cols: usize) -> Vec<(FieldElement, usize)> { + let count = rng.gen_range(1..=3usize.min(cols)); + random_terms(rng, cols, count) +} + +/// `Σ coeff·w[col]` (duplicate columns sum, matching `add_constraint`). +fn eval_lc(terms: &[(FieldElement, usize)], w: &[FieldElement]) -> FieldElement { + terms.iter().map(|&(coeff, col)| coeff * w[col]).sum() +} + +/// `num_gates` multiply gates over `num_inputs` random inputs, satisfiable by +/// construction; each gate's output holds `(A·w)·(B·w)`. First input is public. +fn random_satisfiable(seed: u64, num_inputs: usize, num_gates: usize) -> (R1CS, Vec) { + assert!( + num_inputs >= 1, + "need at least one input to expose as public" + ); + let mut rng = StdRng::seed_from_u64(seed); + let mut r1cs = R1CS::new(); + let total = 1 + num_inputs + num_gates; + r1cs.add_witnesses(total); + r1cs.num_public_inputs = 1; + let one = FieldElement::one(); + + let mut w = Vec::with_capacity(total); + w.push(one); + for _ in 0..num_inputs { + w.push(FieldElement::rand(&mut rng)); + } + for g in 0..num_gates { + let out = 1 + num_inputs + g; // == current w.len() + let a_row = random_lc(&mut rng, out); + let b_row = random_lc(&mut rng, out); + let product = eval_lc(&a_row, &w) * eval_lc(&b_row, &w); + w.push(product); + r1cs.add_constraint(&a_row, &b_row, &[(one, out)]); + } + (r1cs, w) +} + +/// Satisfiable synthetic R1CS of a target size + density, to proxy a real +/// circuit's (structural) prover cost. Density ≈ `complete_age_check`: +/// ~2.5/1/1.5 non-zeros per A/B/C row. `num_public_inputs = 0`. +fn sized_r1cs( + num_witnesses: usize, + num_constraints: usize, + seed: u64, +) -> (R1CS, Vec) { + assert!( + num_witnesses > num_constraints + 1, + "need room for input witnesses" + ); + let num_inputs = num_witnesses - num_constraints - 1; + let mut rng = StdRng::seed_from_u64(seed); + let mut r1cs = R1CS::new(); + r1cs.add_witnesses(num_witnesses); + r1cs.num_public_inputs = 0; + r1cs.reserve_constraints(num_constraints, num_constraints * 5); + let one = FieldElement::one(); + + let mut w = Vec::with_capacity(num_witnesses); + w.push(one); + for _ in 0..num_inputs { + // `from(u64)`, not `rand`: field-independent RNG → identical structure + // across fields. + w.push(FieldElement::from(rng.gen::())); + } + for g in 0..num_constraints { + let out = 1 + num_inputs + g; // == current w.len() + let na = rng.gen_range(1..=4usize); // A ~2.5/row + let a_row = random_terms(&mut rng, out, na); + let b_row = random_terms(&mut rng, out, 1); // B 1/row + let nc = rng.gen_range(0..=1usize); // C ~1.5/row + let extra = random_terms(&mut rng, out, nc); + // pick `out` so C·w = out + Σ(extra·w) == (A·w)·(B·w) + let out_val = eval_lc(&a_row, &w) * eval_lc(&b_row, &w) - eval_lc(&extra, &w); + w.push(out_val); + let mut c_row = vec![(one, out)]; + c_row.extend(extra); + r1cs.add_constraint(&a_row, &b_row, &c_row); + } + (r1cs, w) +} + +/// Single-commitment prove (no challenge phase). +fn prove( + r1cs: &R1CS, + full_witness: Vec, + public_inputs: &PublicInputs, +) -> anyhow::Result<(WhirR1CSScheme, WhirR1CSProof)> { + register_ntt(); + let scheme = WhirR1CSScheme::new_for_r1cs( + r1cs, + full_witness.len(), // w1_size: everything in w1, num_challenges = 0 + 0, + vec![], + true, + HASH, + )?; + + let instance = public_inputs.hash_bytes(HASH); + let ds = scheme.create_domain_separator().instance(&instance); + let mut merlin = ProverState::new(&ds, TranscriptSponge::from_config(HASH)); + + let commitment = scheme.commit( + &mut merlin, + r1cs.num_witnesses(), + r1cs.num_constraints(), + full_witness.clone(), + true, + )?; + let proof = scheme.prove_noir( + merlin, + r1cs.clone(), + vec![commitment], + full_witness, + public_inputs, + )?; + Ok((scheme, proof)) +} + +/// Prove then verify, asserting both succeed. +fn prove_and_verify(r1cs: &R1CS, witness: Vec, public_inputs: &PublicInputs) { + let (scheme, proof) = prove(r1cs, witness, public_inputs).expect("proving failed"); + scheme + .verify(&proof, public_inputs, r1cs) + .expect("verification failed"); +} + +#[test] +fn oracle_accepts_satisfying_and_rejects_broken() { + let (r1cs, w) = squaring_chain(3, 6); + assert!(satisfies(&r1cs, &w)); + + let mut broken = w.clone(); + let last = broken.len() - 1; + broken[last] += FieldElement::one(); + assert!(!satisfies(&r1cs, &broken)); +} + +#[test] +fn squaring_chain_small_roundtrip() { + let (r1cs, w) = squaring_chain(3, 8); // depth 8: m = 13 floor + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + assert!(satisfies(&r1cs, &w)); + prove_and_verify(&r1cs, w, &public_inputs); +} + +#[test] +fn two_public_inputs_roundtrip() { + let (r1cs, w) = two_public_inputs(6, 7); + let public_inputs = PublicInputs::from_vec(vec![w[1], w[2]]); + assert!(satisfies(&r1cs, &w)); + prove_and_verify(&r1cs, w, &public_inputs); +} + +/// Blinding-room bump: 8192 = `2^13` witnesses bump `m: 13 -> 14` via the +/// `(2^m - w1_size) < 4·m_0` padding check, not floor-crossing. +#[test] +fn blinding_bump_roundtrip() { + let depth = FOLD_FLOOR - 2; + let (r1cs, w) = squaring_chain(2, depth); + assert_eq!(r1cs.num_witnesses(), FOLD_FLOOR); + + let scheme = WhirR1CSScheme::new_for_r1cs(&r1cs, w.len(), 0, vec![], true, HASH) + .expect("scheme construction"); + assert_eq!(scheme.m, 14); + + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + assert!(satisfies(&r1cs, &w)); + prove_and_verify(&r1cs, w, &public_inputs); +} + +/// `≥ 2^14` scale check (m = 15, m_0 = 14): past the fold floor. ~0.13s +/// release, runs in CI. +#[test] +fn milestone_2pow14_roundtrip() { + let depth = 16_384; + let (r1cs, w) = squaring_chain(2, depth); + assert!(r1cs.num_witnesses() >= 16_384 && r1cs.num_constraints() >= 16_384); + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + assert!(satisfies(&r1cs, &w)); + prove_and_verify(&r1cs, w, &public_inputs); +} + +// Three distinct rejection paths. `prove` never checks satisfaction (always +// returns a proof), so `.expect()` keeps the verify-rejection assert live. + +/// Path 1 — R1CS satisfaction: a broken witness must not verify. +#[test] +fn corrupted_witness_is_rejected() { + let (r1cs, mut w) = squaring_chain(3, 8); + let last = w.len() - 1; + w[last] += FieldElement::one(); + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + assert!(!satisfies(&r1cs, &w)); + + let (scheme, proof) = prove(&r1cs, w, &public_inputs) + .expect("prover produces a proof even for a non-satisfying witness"); + assert!(scheme.verify(&proof, &public_inputs, &r1cs).is_err()); +} + +/// Path 2 — instance binding: a proof must not verify against public inputs +/// substituted after proving. +#[test] +fn tampered_public_input_is_rejected() { + let (r1cs, w) = squaring_chain(3, 8); + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + assert!(satisfies(&r1cs, &w)); + let (scheme, proof) = prove(&r1cs, w, &public_inputs).expect("proving failed"); + + let tampered = PublicInputs::from_vec(vec![FieldElement::from(999u64)]); + assert!(scheme.verify(&proof, &tampered, &r1cs).is_err()); +} + +/// Path 3 — public-input binding (PR #321): `witness[1] != public[0]` must not +/// verify though the R1CS is satisfied. Unlike path 2, prove and verify agree +/// on the wrong input, so the binding is what rejects. +#[test] +fn mismatched_public_input_binding_is_rejected() { + let (r1cs, w) = squaring_chain(3, 8); + assert!(satisfies(&r1cs, &w)); + + let wrong = PublicInputs::from_vec(vec![w[1] + FieldElement::one()]); + let (scheme, proof) = + prove(&r1cs, w, &wrong).expect("proving succeeds; the binding is checked at verify time"); + assert!(scheme.verify(&proof, &wrong, &r1cs).is_err()); +} + +/// PR #321 at `N = 2`: corrupting only the second public input must still +/// reject (the binding loop is non-trivial here, unlike `N = 1`). +#[test] +fn two_public_inputs_binding_mismatch_is_rejected() { + let (r1cs, w) = two_public_inputs(6, 7); + assert!(satisfies(&r1cs, &w)); + + let wrong = PublicInputs::from_vec(vec![w[1], w[2] + FieldElement::one()]); + let (scheme, proof) = + prove(&r1cs, w, &wrong).expect("proving succeeds; the binding is checked at verify time"); + assert!(scheme.verify(&proof, &wrong, &r1cs).is_err()); +} + +/// Per seed: the instance proves+verifies, and a perturbed output breaks +/// satisfaction. +#[test] +fn random_satisfiable_proves_and_perturbation_rejects() { + for seed in 0..8u64 { + let (r1cs, w) = random_satisfiable(seed, 4, 8); + assert!(satisfies(&r1cs, &w), "seed {seed}: must satisfy its R1CS"); + + let mut broken = w.clone(); + *broken.last_mut().unwrap() += FieldElement::one(); + assert!( + !satisfies(&r1cs, &broken), + "seed {seed}: perturbation must break a constraint" + ); + + let public_inputs = PublicInputs::from_vec(vec![w[1]]); + let (scheme, proof) = prove(&r1cs, w, &public_inputs).expect("proving failed"); + assert!( + scheme.verify(&proof, &public_inputs, &r1cs).is_ok(), + "seed {seed}: honest proof must verify" + ); + } +} + +/// Goldilocks must be the ~192-bit cubic extension (clears the 128-bit floor). +#[cfg(all(feature = "goldilocks", not(feature = "bn254")))] +#[test] +fn goldilocks_field_size_is_192_bits() { + use whir::algebra::fields::FieldWithSize; + let bits = ::field_size_bits(); + assert!( + (190.0..=194.0).contains(&bits), + "expected ~192-bit field, got {bits}" + ); + assert!(bits >= 128.0, "below the 128-bit security floor"); +} + +// Proving-time benchmark: a synthetic R1CS sized to `complete_age_check`. Cost +// is structural, so same-size proxies real proving time. `#[ignore]`d; run +// under each field and compare the printed durations. + +/// `complete_age_check`: 711,664 constraints · 1,247,227 witnesses · w1 ≈ 50% +/// (m = 20) · 118 challenges · ~3.55M non-zeros · 0 public inputs. +const AGE_CHECK_WITNESSES: usize = 1_247_227; +const AGE_CHECK_CONSTRAINTS: usize = 711_664; +const AGE_CHECK_W1: usize = 620_627; +const AGE_CHECK_CHALLENGES: usize = 118; + +/// Same shape at m ≈ 16 — fast smoke for the dual-commit path. +const SCALED_WITNESSES: usize = 90_000; +const SCALED_CONSTRAINTS: usize = 51_000; +const SCALED_W1: usize = 45_000; + +/// Time a dual-commit (`num_challenges > 0`) prove from a precomputed witness. +/// We draw the challenges directly (the real flow does it inside the gated +/// solver) so the path is field-agnostic. Timing-only: w2 is the witness tail, +/// not challenge-derived, so it won't verify; it also omits the subdominant w2 +/// solve (LogUp inversion), so absolutes slightly under-count (ratio +/// unaffected). +fn time_dual_commit_prove( + r1cs: &R1CS, + full_witness: Vec, + w1_size: usize, + num_challenges: usize, +) -> Duration { + register_ntt(); + let challenge_offsets: Vec = (w1_size..w1_size + num_challenges).collect(); + let scheme = WhirR1CSScheme::new_for_r1cs( + r1cs, + w1_size, + num_challenges, + challenge_offsets, + false, + HASH, + ) + .expect("scheme construction"); + + let num_witnesses = r1cs.num_witnesses(); + let num_constraints = r1cs.num_constraints(); + let w1 = full_witness[..w1_size].to_vec(); + let w2 = full_witness[w1_size..num_witnesses].to_vec(); + let public = PublicInputs::from_vec(vec![]); + let instance = public.hash_bytes(HASH); + let ds = scheme.create_domain_separator().instance(&instance); + + // Clone outside the timer — `prove_noir` consumes it, but the copy isn't + // proving work. + let r1cs_owned = r1cs.clone(); + + let start = Instant::now(); + let mut merlin = ProverState::new(&ds, TranscriptSponge::from_config(HASH)); + let c1 = scheme + .commit(&mut merlin, num_witnesses, num_constraints, w1, true) + .expect("commit w1"); + // Challenge draws (the real flow does these in `solve_witness_vec`). + let _challenges: Vec = merlin.verifier_message_vec(num_challenges); + let c2 = scheme + .commit(&mut merlin, num_witnesses, num_constraints, w2, false) + .expect("commit w2"); + scheme + .prove_noir(merlin, r1cs_owned, vec![c1, c2], full_witness, &public) + .expect("prove"); + start.elapsed() +} + +/// Build, validate, time, and print one sized bench. +fn run_sized_bench( + label: &str, + witnesses: usize, + constraints: usize, + w1: usize, + challenges: usize, +) { + let field = if cfg!(feature = "bn254") { + "bn254" + } else { + "goldilocks" + }; + let (r1cs, w) = sized_r1cs(witnesses, constraints, 0x0a6e_c4ec); + // Validate the generator (cheap, outside the timer). + assert!( + satisfies(&r1cs, &w), + "[{label}] generated R1CS must be satisfiable" + ); + let nnz = r1cs.a().iter().count() + r1cs.b().iter().count() + r1cs.c().iter().count(); + let dur = time_dual_commit_prove(&r1cs, w, w1, challenges); + println!( + "[{label}] field={field} witnesses={} constraints={} w1={w1} challenges={challenges} \ + nnz={nnz} prove={dur:?}", + r1cs.num_witnesses(), + r1cs.num_constraints(), + ); +} + +#[test] +#[ignore = "scaled (m~16) smoke for the dual-commit benchmark path; run with --nocapture"] +fn bench_age_check_sized_scaled() { + run_sized_bench( + "scaled", + SCALED_WITNESSES, + SCALED_CONSTRAINTS, + SCALED_W1, + AGE_CHECK_CHALLENGES, + ); +} + +#[test] +#[ignore = "full complete_age_check-sized proving-time benchmark; run --release --nocapture"] +fn bench_age_check_sized_full() { + run_sized_bench( + "age-check", + AGE_CHECK_WITNESSES, + AGE_CHECK_CONSTRAINTS, + AGE_CHECK_W1, + AGE_CHECK_CHALLENGES, + ); +} diff --git a/provekit/prover/src/lib.rs b/provekit/prover/src/lib.rs index 1f5474a9f..bf1f457b3 100644 --- a/provekit/prover/src/lib.rs +++ b/provekit/prover/src/lib.rs @@ -1,5 +1,6 @@ -#[cfg(test)] +#[cfg(all(test, feature = "bn254"))] use crate::r1cs::R1CSSolver; +#[cfg(feature = "bn254")] use { crate::{ r1cs::{CompressedLayers, CompressedR1CS}, @@ -20,27 +21,48 @@ use { bn254_blackbox_solver::Bn254BlackBoxSolver, nargo::foreign_calls::DefaultForeignCallBuilder, noir_artifact_cli::fs::inputs::read_inputs_from_file, noirc_abi::InputMap, }; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] use { mavros_vm::interpreter as mavros_interpreter, provekit_common::MavrosProver, std::path::Path, whir::transcript::VerifierMessage, }; +#[cfg(feature = "bn254")] pub(crate) mod bigint_mod; +#[cfg(feature = "bn254")] pub(crate) mod ec_arith; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod goldilocks_fixtures; +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] pub mod input_utils; +#[cfg(feature = "bn254")] mod logging; +#[cfg(feature = "bn254")] pub(crate) mod r1cs; +// Private under bn254 (the `Prove` trait is the public entry point); public +// under goldilocks, where `WhirR1CSProver` is the only proving API for +// hand-built R1CS instances. +#[cfg(feature = "bn254")] mod whir_r1cs; +#[cfg(all(feature = "goldilocks", not(feature = "bn254")))] +pub mod whir_r1cs; +#[cfg(feature = "bn254")] mod witness; +// Guard against Cargo feature unification building this prover's field-gated +// code (notably the goldilocks-only public `whir_r1cs` API) over the wrong +// `provekit_common::FieldElement` — e.g. when a sibling crate forces +// `provekit-common/bn254` while this prover is built for goldilocks. +provekit_common::assert_field_matches_common!(); + // Public re-exports for items used by integration tests and benchmarks. +#[cfg(feature = "bn254")] pub use {ec_arith::ec_scalar_mul, r1cs::solve_witness_vec}; /// `prove` and `prove_with_toml` are native-only (cfg-gated out on wasm32). /// `prove_with_witness` is available on all targets. `MavrosProver` does not /// support `prove_with_witness` (errors at runtime). +#[cfg(feature = "bn254")] pub trait Prove { #[cfg(all(feature = "witness-generation", not(target_arch = "wasm32")))] fn prove(self, input_map: InputMap) -> Result; @@ -83,6 +105,7 @@ fn generate_noir_witness( .witness) } +#[cfg(feature = "bn254")] impl Prove for NoirProver { #[cfg(all(feature = "witness-generation", not(target_arch = "wasm32")))] #[instrument(skip_all)] @@ -271,7 +294,7 @@ impl Prove for NoirProver { } } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] impl Prove for MavrosProver { #[cfg(feature = "witness-generation")] fn prove(mut self, input_map: InputMap) -> Result { @@ -405,6 +428,7 @@ impl Prove for MavrosProver { } } +#[cfg(feature = "bn254")] impl Prove for Prover { #[cfg(all(feature = "witness-generation", not(target_arch = "wasm32")))] fn prove(self, input_map: InputMap) -> Result { diff --git a/provekit/prover/src/whir_r1cs.rs b/provekit/prover/src/whir_r1cs.rs index 3b1a5a97e..450417a60 100644 --- a/provekit/prover/src/whir_r1cs.rs +++ b/provekit/prover/src/whir_r1cs.rs @@ -28,7 +28,7 @@ use { transcript::{ProverState, VerifierMessage}, }, }; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] use { mavros_artifacts::{ConstraintsLayout, WitnessLayout}, mavros_vm::interpreter::WitgenResult, @@ -64,7 +64,7 @@ pub trait WhirR1CSProver { public_inputs: &PublicInputs, ) -> Result; - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] fn prove_mavros( &self, merlin: ProverState, @@ -186,7 +186,7 @@ impl WhirR1CSProver for WhirR1CSScheme { ) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "bn254", not(target_arch = "wasm32")))] #[instrument(skip_all)] fn prove_mavros( &self, @@ -578,7 +578,7 @@ pub fn run_zk_sumcheck_prover( let g_at_minus_one = g_poly[0] - g_poly[1] + g_poly[2] - g_poly[3]; let combined_at_em1 = hhat_i_at_em1 + rho * g_at_minus_one; - combined_hhat_i_coeffs[2] = HALF + combined_hhat_i_coeffs[2] = *HALF * (saved_val_for_sumcheck_equality_assertion + combined_at_em1 - combined_hhat_i_coeffs[0] - combined_hhat_i_coeffs[0] diff --git a/provekit/r1cs-compiler/Cargo.toml b/provekit/r1cs-compiler/Cargo.toml index 334514124..ef3537dfc 100644 --- a/provekit/r1cs-compiler/Cargo.toml +++ b/provekit/r1cs-compiler/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } poseidon2.workspace = true # Noir language diff --git a/provekit/r1cs-compiler/src/lib.rs b/provekit/r1cs-compiler/src/lib.rs index 0b9890a8a..c96f003ce 100644 --- a/provekit/r1cs-compiler/src/lib.rs +++ b/provekit/r1cs-compiler/src/lib.rs @@ -10,11 +10,10 @@ pub mod range_check; mod sha256_compression; mod spread; mod uints; -mod whir_r1cs; mod witness_generator; pub use { noir_proof_scheme::{MavrosCompiler, NoirCompiler}, noir_to_r1cs::{noir_to_r1cs, noir_to_r1cs_with_breakdown, R1CSBreakdown}, - whir_r1cs::WhirR1CSSchemeBuilder, + provekit_common::WhirR1CSSchemeBuilder, }; diff --git a/provekit/r1cs-compiler/src/noir_proof_scheme.rs b/provekit/r1cs-compiler/src/noir_proof_scheme.rs index 468107e40..ff8214558 100644 --- a/provekit/r1cs-compiler/src/noir_proof_scheme.rs +++ b/provekit/r1cs-compiler/src/noir_proof_scheme.rs @@ -1,8 +1,5 @@ use { - crate::{ - noir_to_r1cs, whir_r1cs::WhirR1CSSchemeBuilder, - witness_generator::NoirWitnessGeneratorBuilder, - }, + crate::{noir_to_r1cs, witness_generator::NoirWitnessGeneratorBuilder}, anyhow::{ensure, Context as _, Result}, mavros_artifacts::R1CS as MavrosR1CS, noirc_abi::AbiVisibility, @@ -10,7 +7,7 @@ use { provekit_common::{ utils::{convert_mavros_r1cs_to_provekit, PrintAbi}, witness::{NoirWitnessGenerator, WitnessBuilder}, - MavrosSchemeData, NoirProofScheme, NoirSchemeData, WhirR1CSScheme, + MavrosSchemeData, NoirProofScheme, NoirSchemeData, WhirR1CSScheme, WhirR1CSSchemeBuilder, }, serde::Deserialize, std::{collections::HashSet, fs::File, path::Path}, @@ -108,7 +105,7 @@ impl NoirCompiler { challenge_offsets, has_public_inputs, hash_config, - ); + )?; Ok(NoirProofScheme::Noir(NoirSchemeData { program: program.bytecode, @@ -181,7 +178,7 @@ impl MavrosCompiler { challenge_offsets, num_public_inputs > 0, hash_config, - ); + )?; whir_for_witness.r1cs_hash = r1cs.hash(); Ok(NoirProofScheme::Mavros(MavrosSchemeData { diff --git a/provekit/r1cs-compiler/src/whir_r1cs.rs b/provekit/r1cs-compiler/src/whir_r1cs.rs deleted file mode 100644 index d71f05f04..000000000 --- a/provekit/r1cs-compiler/src/whir_r1cs.rs +++ /dev/null @@ -1,305 +0,0 @@ -use { - mavros_artifacts::R1CS as MavrosR1CS, - provekit_common::{ - utils::next_power_of_two, HashConfig, R1csHash, WhirR1CSScheme, WhirZkConfig, R1CS, - }, - whir::{engines::EngineId, parameters::ProtocolParameters}, -}; - -const MIN_WHIR_NUM_VARIABLES: usize = 13; -const MIN_SUMCHECK_NUM_VARIABLES: usize = 1; - -pub trait WhirR1CSSchemeBuilder { - fn new_for_r1cs( - r1cs: &R1CS, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self; - - fn new_from_mavros_r1cs( - r1cs: &MavrosR1CS, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self; - - fn new_from_dimensions( - num_witnesses: usize, - num_constraints: usize, - a_num_entries: usize, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self; - - fn new_whir_zk_config_for_size( - num_variables: usize, - num_polynomials: usize, - hash_id: EngineId, - ) -> WhirZkConfig; -} - -impl WhirR1CSSchemeBuilder for WhirR1CSScheme { - fn new_for_r1cs( - r1cs: &R1CS, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self { - assert_eq!( - num_challenges, - challenge_offsets.len(), - "num_challenges ({num_challenges}) != challenge_offsets.len() ({})", - challenge_offsets.len() - ); - let total_witnesses = r1cs.num_witnesses(); - assert!( - w1_size <= total_witnesses, - "w1_size exceeds total witnesses" - ); - let w2_size = total_witnesses - w1_size; - - let m1_raw = next_power_of_two(w1_size); - let m2_raw = next_power_of_two(w2_size); - let m0_raw = next_power_of_two(r1cs.num_constraints()); - - let mut m_raw = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); - let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); - - // Ensure w1's zero-padding has room for the blinding polynomial coefficients. - if (1usize << m_raw) - w1_size < 4 * m_0 { - m_raw += 1; - } - - Self { - m: m_raw, - w1_size, - m_0, - a_num_terms: next_power_of_two(r1cs.a().iter().count()), - num_challenges, - challenge_offsets, - whir_witness: Self::new_whir_zk_config_for_size(m_raw, 1, hash_config.engine_id()), - has_public_inputs, - r1cs_hash: r1cs.hash(), - hash_config, - } - } - - fn new_whir_zk_config_for_size( - num_variables: usize, - num_polynomials: usize, - hash_id: EngineId, - ) -> WhirZkConfig { - let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); - - // Parameters tuned for 128-bit security under the Johnson bound (the old - // ConjectureList soundness was disproven). Rate=2 balances query count vs - // codeword size; ff=3 keeps blinding polynomials small; pow_bits=10 shifts - // security budget toward algebraic hardness (118 bits) with light PoW per - // round, which is faster than the default ~18-bit grinding. - let whir_params = ProtocolParameters { - unique_decoding: false, - security_level: 128, - pow_bits: 10, - initial_folding_factor: 3, - folding_factor: 3, - starting_log_inv_rate: 2, - batch_size: 1, - hash_id, - }; - WhirZkConfig::new(1 << nv, &whir_params, num_polynomials) - } - - fn new_from_mavros_r1cs( - r1cs: &MavrosR1CS, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self { - let num_witnesses = r1cs.witness_layout.size(); - let num_constraints = r1cs.constraints.len(); - let a_num_entries: usize = r1cs.constraints.iter().map(|c| c.a.len()).sum(); - - Self::new_from_dimensions( - num_witnesses, - num_constraints, - a_num_entries, - w1_size, - num_challenges, - challenge_offsets, - has_public_inputs, - hash_config, - ) - } - - fn new_from_dimensions( - num_witnesses: usize, - num_constraints: usize, - a_num_entries: usize, - w1_size: usize, - num_challenges: usize, - challenge_offsets: Vec, - has_public_inputs: bool, - hash_config: HashConfig, - ) -> Self { - debug_assert_eq!( - num_challenges, - challenge_offsets.len(), - "num_challenges ({num_challenges}) != challenge_offsets.len() ({})", - challenge_offsets.len() - ); - assert!(w1_size <= num_witnesses, "w1_size exceeds total witnesses"); - let w2_size = num_witnesses - w1_size; - - let m1_raw = next_power_of_two(w1_size); - let m2_raw = next_power_of_two(w2_size); - let m0_raw = next_power_of_two(num_constraints); - - let mut m = m1_raw.max(m2_raw).max(MIN_WHIR_NUM_VARIABLES); - let m_0 = m0_raw.max(MIN_SUMCHECK_NUM_VARIABLES); - - // Ensure w1's zero-padding has room for the blinding polynomial coefficients. - if (1usize << m) - w1_size < 4 * m_0 { - m += 1; - } - - Self { - m, - m_0, - a_num_terms: next_power_of_two(a_num_entries), - whir_witness: Self::new_whir_zk_config_for_size(m, 1, hash_config.engine_id()), - w1_size, - num_challenges, - challenge_offsets, - has_public_inputs, - r1cs_hash: R1csHash::UNSET, - hash_config, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn r1cs_with_dimensions(num_witnesses: usize, num_constraints: usize) -> R1CS { - let mut r1cs = R1CS::new(); - r1cs.grow_matrices(num_constraints, num_witnesses); - r1cs - } - - fn assert_dimension_builders( - num_witnesses: usize, - num_constraints: usize, - w1_size: usize, - expected_m: usize, - expected_m_0: usize, - ) { - let from_dimensions = WhirR1CSScheme::new_from_dimensions( - num_witnesses, - num_constraints, - 0, - w1_size, - 0, - vec![], - false, - HashConfig::Sha256, - ); - assert_eq!(from_dimensions.m, expected_m); - assert_eq!(from_dimensions.m_0, expected_m_0); - assert_eq!(from_dimensions.w1_size, w1_size); - - let r1cs = r1cs_with_dimensions(num_witnesses, num_constraints); - let from_r1cs = - WhirR1CSScheme::new_for_r1cs(&r1cs, w1_size, 0, vec![], false, HashConfig::Sha256); - assert_eq!(from_r1cs.m, expected_m); - assert_eq!(from_r1cs.m_0, expected_m_0); - assert_eq!(from_r1cs.w1_size, w1_size); - } - - #[test] - fn verify_security_level() { - let config = WhirR1CSScheme::new_whir_zk_config_for_size(20, 1, whir::hash::SHA2); - let sec_blinded = config - .blinded_commitment - .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); - let sec_blinding = config - .blinding_commitment - .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); - assert!( - sec_blinded >= 128.0, - "Blinded commitment security {sec_blinded:.2} < 128 bits" - ); - assert!( - sec_blinding >= 128.0, - "Blinding commitment security {sec_blinding:.2} < 128 bits" - ); - } - - #[test] - fn verify_security_level_min_variables() { - let config = WhirR1CSScheme::new_whir_zk_config_for_size( - MIN_WHIR_NUM_VARIABLES, - 1, - whir::hash::SHA2, - ); - let sec_blinded = config - .blinded_commitment - .security_level(config.blinded_commitment.initial_committer.num_vectors, 1); - let sec_blinding = config - .blinding_commitment - .security_level(config.blinding_commitment.initial_committer.num_vectors, 1); - assert!( - sec_blinded >= 128.0, - "Blinded commitment security {sec_blinded:.2} < 128 bits at nv={}", - MIN_WHIR_NUM_VARIABLES - ); - assert!( - sec_blinding >= 128.0, - "Blinding commitment security {sec_blinding:.2} < 128 bits at nv={}", - MIN_WHIR_NUM_VARIABLES - ); - } - - #[test] - fn mavros_dimensions_use_largest_commitment_not_total_witnesses() { - let scheme = WhirR1CSScheme::new_from_dimensions( - 600_000, - 8, - 8, - 300_000, - 2, - vec![0, 1], - false, - HashConfig::Sha256, - ); - - assert_eq!(scheme.m, 19); - } - - #[test] - fn dimension_builders_handle_empty_w2() { - assert_dimension_builders(64, 8, 64, MIN_WHIR_NUM_VARIABLES, 3); - } - - #[test] - fn dimension_builders_handle_empty_w1() { - assert_dimension_builders(64, 8, 0, MIN_WHIR_NUM_VARIABLES, 3); - } - - #[test] - fn dimension_builders_bump_exact_power_of_two_w1_for_blinding_room() { - assert_dimension_builders(12_288, 2_048, 8_192, 14, 11); - } -} diff --git a/provekit/verifier/Cargo.toml b/provekit/verifier/Cargo.toml index 595b04d70..57ec36ac8 100644 --- a/provekit/verifier/Cargo.toml +++ b/provekit/verifier/Cargo.toml @@ -8,6 +8,19 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[features] +default = ["bn254"] +# At least one field feature must be enabled; `bn254` takes precedence when +# both are on. Pair `goldilocks` with `--no-default-features`. +# `provekit_ntt`/`parallel` mirror what the workspace-wide BN254 build +# enabled before the field split. +bn254 = [ + "provekit-common/bn254", + "provekit-common/provekit_ntt", + "provekit-common/parallel", +] +goldilocks = ["provekit-common/goldilocks"] + [dependencies] # Workspace crates provekit-common.workspace = true diff --git a/provekit/verifier/src/lib.rs b/provekit/verifier/src/lib.rs index 1c3461fa8..9810a8b36 100644 --- a/provekit/verifier/src/lib.rs +++ b/provekit/verifier/src/lib.rs @@ -1,5 +1,11 @@ -mod whir_r1cs; +pub mod whir_r1cs; +// Guard against Cargo feature unification building this verifier over the wrong +// `provekit_common::FieldElement` (a sibling forcing `provekit-common/bn254` +// while this verifier is built for goldilocks). +provekit_common::assert_field_matches_common!(); + +#[cfg(feature = "bn254")] use { crate::whir_r1cs::WhirR1CSVerifier, anyhow::{Context, Result}, @@ -7,10 +13,13 @@ use { tracing::instrument, }; +/// Verify a [`NoirProof`] against a Noir proof scheme's [`Verifier`]. +#[cfg(feature = "bn254")] pub trait Verify { fn verify(&mut self, proof: &NoirProof) -> Result<()>; } +#[cfg(feature = "bn254")] impl Verify for Verifier { #[instrument(skip_all)] fn verify(&mut self, proof: &NoirProof) -> Result<()> { diff --git a/provekit/verifier/src/whir_r1cs.rs b/provekit/verifier/src/whir_r1cs.rs index 662475eee..822e183f2 100644 --- a/provekit/verifier/src/whir_r1cs.rs +++ b/provekit/verifier/src/whir_r1cs.rs @@ -43,6 +43,12 @@ impl WhirR1CSVerifier for WhirR1CSScheme { public_inputs: &PublicInputs, r1cs: &R1CS, ) -> Result<()> { + // Register the NTT/hash engines this verify path retrieves. The bn254 + // `Verify` wrapper also calls this, but under goldilocks that wrapper is + // gone and this is the sole (public) entry point; `register_ntt` is + // idempotent, so the double call under bn254 is a no-op. + provekit_common::register_ntt(); + let actual_r1cs_hash = r1cs.hash(); anyhow::ensure!( self.r1cs_hash == actual_r1cs_hash, diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index c45feb548..5029d918c 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -10,11 +10,11 @@ repository.workspace = true [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } provekit-gnark.workspace = true provekit-prover = { workspace = true, features = ["witness-generation", "parallel"] } provekit-r1cs-compiler.workspace = true -provekit-verifier.workspace = true +provekit-verifier = { workspace = true, features = ["bn254"] } # Noir language acir.workspace = true diff --git a/tooling/provekit-bench/Cargo.toml b/tooling/provekit-bench/Cargo.toml index 0121caa37..27fec107d 100644 --- a/tooling/provekit-bench/Cargo.toml +++ b/tooling/provekit-bench/Cargo.toml @@ -10,10 +10,10 @@ repository.workspace = true [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } provekit-prover = { workspace = true, features = ["witness-generation", "parallel"] } provekit-r1cs-compiler.workspace = true -provekit-verifier.workspace = true +provekit-verifier = { workspace = true, features = ["bn254"] } # Noir language acir.workspace = true diff --git a/tooling/provekit-ffi/Cargo.toml b/tooling/provekit-ffi/Cargo.toml index f602f43f6..42692aaa1 100644 --- a/tooling/provekit-ffi/Cargo.toml +++ b/tooling/provekit-ffi/Cargo.toml @@ -13,10 +13,10 @@ crate-type = ["staticlib", "rlib"] [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } provekit-prover = { workspace = true, features = ["witness-generation", "parallel"] } provekit-r1cs-compiler = { workspace = true } -provekit-verifier = { workspace = true } +provekit-verifier = { workspace = true, features = ["bn254"] } # 3rd party anyhow.workspace = true diff --git a/tooling/provekit-gnark/Cargo.toml b/tooling/provekit-gnark/Cargo.toml index da48874cb..20da14c19 100644 --- a/tooling/provekit-gnark/Cargo.toml +++ b/tooling/provekit-gnark/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } # Cryptography and proof systems ark-poly.workspace = true diff --git a/tooling/provekit-wasm/Cargo.toml b/tooling/provekit-wasm/Cargo.toml index e1a76003f..ab00be861 100644 --- a/tooling/provekit-wasm/Cargo.toml +++ b/tooling/provekit-wasm/Cargo.toml @@ -22,9 +22,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] # Workspace crates - enable parallel features with wasm-bindgen-rayon -provekit-common.workspace = true -provekit-prover = { workspace = true, default-features = false, features = ["parallel"] } -provekit-verifier.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } +provekit-prover = { workspace = true, default-features = false, features = ["bn254", "parallel"] } +provekit-verifier = { workspace = true, features = ["bn254"] } # Noir language acir.workspace = true diff --git a/tooling/verifier-server/Cargo.toml b/tooling/verifier-server/Cargo.toml index 302749158..5073ac61e 100644 --- a/tooling/verifier-server/Cargo.toml +++ b/tooling/verifier-server/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] # Workspace crates -provekit-common.workspace = true +provekit-common = { workspace = true, features = ["bn254", "parallel", "provekit_ntt"] } provekit-gnark.workspace = true # 3rd party