diff --git a/.github/workflows/refactor-audit-stack.yml b/.github/workflows/refactor-audit-stack.yml new file mode 100644 index 0000000000..cafb12d390 --- /dev/null +++ b/.github/workflows/refactor-audit-stack.yml @@ -0,0 +1,62 @@ +name: Refactor Audit Stack + +on: + push: + branches: + - refactor/audit-prep + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: refactor-audit-stack + cancel-in-progress: true + +jobs: + update-stack: + name: Update draft stack branches + runs-on: ubuntu-latest + + steps: + - name: Checkout source branch + uses: actions/checkout@v4 + with: + ref: refactor/audit-prep + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch stack bases + run: | + git fetch origin main refactor/audit-prep + git fetch origin '+refs/heads/stack/*:refs/remotes/origin/stack/*' || true + + - name: Configure git author + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Rebuild and push stack + run: | + bash stack/update-stack.sh \ + --apply \ + --rebuild \ + --commit \ + --push \ + --cargo-metadata \ + --check-coverage \ + --from origin/refactor/audit-prep \ + --base origin/main \ + --start-at 07 + + - name: Create or update draft PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git switch refactor/audit-prep + bash stack/open-prs.sh \ + --apply \ + --base main \ + --source refactor/audit-prep \ + --start-at 07 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c5f68e0f54..9a4d678baa 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -134,7 +134,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: crate-ci/typos@v1.46.0 + - uses: crate-ci/typos@v1.46.2 with: files: . @@ -239,7 +239,12 @@ jobs: run: | set -euo pipefail for pkg in ${{ steps.discover.outputs.pkgs }}; do - cargo nextest run -p "$pkg" --no-tests=pass + features=() + if cargo metadata --format-version=1 --no-deps \ + | jq -e --arg pkg "$pkg" '.packages[] | select(.name == $pkg) | .features | has("test-utils")' >/dev/null; then + features=(--features test-utils) + fi + cargo nextest run -p "$pkg" "${features[@]}" --no-tests=pass done test-inlines: @@ -297,7 +302,7 @@ jobs: - name: Install nextest uses: taiki-e/install-action@nextest - name: Run tracer tests - run: cargo nextest run --release -p tracer + run: cargo nextest run --release -p tracer --features test-utils zklean-extractor-tests: name: ZkLean extractor tests diff --git a/Cargo.lock b/Cargo.lock index 399b335d7b..dad95ed252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-consensus" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8c24c95e90c1608c2d91cff1b451d796474168d3310ccc8b7cd12502ca8169" +checksum = "83447eeb17816e172f1dfc0db1f9dc0b7c5d069bd1f7cecbecceb382bf931015" dependencies = [ "alloy-eips", "alloy-primitives", @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d211ad0ef468a70a7a829e49683ff59ad25f02b4ab3764344c4c2663329a52c" +checksum = "5406343e306856dc2be762700e98a16904de45dee14a07f233e742ce68daff2f" dependencies = [ "alloy-consensus", "alloy-eips", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c0456f5f7a4497e9342d20f528e30f5288ddfa0d6a012bd5044afee46cd8a0" +checksum = "0dca4c89ace90684b4b77366d00631ed498c9af962079af2a5dbc593a0618a77" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -275,9 +275,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" +checksum = "7c36c9d7f9021601b04bfef14a4b64849f6d73116a4e91e071d7fbfe10247901" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e7c4bb0ebbd6d7406d2808968f43c0d5186c69c5e58cedcbee7380f4cd1fcf" +checksum = "cd28d9bfd11729037d194f2b1d43db8642eb3f342032691f4ca96bb745479c3c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" dependencies = [ "alloy-rlp", "bytes", @@ -310,7 +310,7 @@ dependencies = [ "const-hex", "derive_more", "foldhash", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "indexmap 2.14.0", "itoa", "k256", @@ -321,8 +321,9 @@ dependencies = [ "rapidhash", "ruint", "rustc-hash", + "secp256k1 0.31.1", "serde", - "sha3 0.10.8", + "sha3 0.11.0", ] [[package]] @@ -349,9 +350,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda4ece0050154ab278241aeffade58916b04f38254832e8cb6e4671c6e72ed2" +checksum = "175a2a5b6017d7f61b5e4b800d21215fe8e94fe729d00828e13bb6d93dcf3492" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -370,9 +371,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eada2558e921b39dfcead33c487364df9b31374f5733c1c9d2c891c4529933" +checksum = "cc21a8772af7d78bba286726aa245bd2ff81cd9abe230afea2e91578996831c9" dependencies = [ "alloy-primitives", "serde", @@ -381,9 +382,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" +checksum = "840128ed2b2971d6d4668a553fe403a82683d3acc646c73e75887e7157408033" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -395,9 +396,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" +checksum = "63ec265e5d65d725175f6ca7711c970824c90ef9c0d1f1973711d4150ee612dd" dependencies = [ "alloy-sol-macro-input", "const-hex", @@ -406,16 +407,16 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "sha3 0.10.8", + "sha3 0.11.0", "syn 2.0.117", "syn-solidity", ] [[package]] name = "alloy-sol-macro-input" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" +checksum = "89bf01077f18650876cfa682eb1f949967b5cde03f1a51c955c469d2c9b4aa67" dependencies = [ "const-hex", "dunce", @@ -429,19 +430,19 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" +checksum = "857b470ecdd2ed38beaf82ad1a38c516a8ff75266750f38b9eeed001d575241b" dependencies = [ "serde", - "winnow 0.7.15", + "winnow", ] [[package]] name = "alloy-sol-types" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" +checksum = "384cf252de0db2dec52821eac037a7f57e2aa33fe5b900ce6fe39973402341f1" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -468,9 +469,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520337f3d3d063a7fe20f47aaa62d695e3dc0372b34f601560dee24e76988b9" +checksum = "01a0035943b75fe1e249f52e688492d7a1b1826bc2d19b8e1d5d3c24a2ad8f50" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -1033,16 +1034,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -1098,6 +1099,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "btreemap" version = "0.1.0" @@ -1196,9 +1207,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -1504,6 +1515,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1532,6 +1556,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1808,6 +1841,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + [[package]] name = "dory-derive" version = "0.3.0" @@ -1950,18 +1993,18 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +checksum = "7f96a4a12fe60ac746ae295a1a4ecb5bb02debc20856506c8635288065f142de" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -2272,6 +2315,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick 1.1.4", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "group" version = "0.13.0" @@ -2344,17 +2400,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", - "serde", - "serde_core", -] - [[package]] name = "hashbrown" version = "0.17.0" @@ -2362,6 +2407,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ "foldhash", + "serde", + "serde_core", ] [[package]] @@ -2698,6 +2745,19 @@ dependencies = [ "zeroos-build", ] +[[package]] +name = "jolt-claims" +version = "0.1.0" +dependencies = [ + "derive_more", + "jolt-field", + "jolt-lookup-tables", + "jolt-poly", + "jolt-riscv", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "jolt-core" version = "0.1.0" @@ -2763,6 +2823,7 @@ dependencies = [ "bincode 2.0.1", "criterion", "jolt-field", + "jolt-poly", "jolt-transcript", "num-bigint", "num-integer", @@ -2805,17 +2866,21 @@ dependencies = [ "criterion", "enumset", "eyre", + "hex", "jolt-core", "jolt-eval-macros", "jolt-field", "jolt-inlines-secp256k1", "jolt-inlines-sha2", + "jolt-program", + "jolt-riscv", "postcard", "rand 0.8.5", "rust-code-analysis", "schemars 1.2.1", "serde", "serde_json", + "sha2 0.11.0", "tempfile", "tracer", "tracing", @@ -2848,6 +2913,26 @@ dependencies = [ "serde", ] +[[package]] +name = "jolt-hyperkzg" +version = "0.1.0" +dependencies = [ + "criterion", + "jolt-crypto", + "jolt-field", + "jolt-openings", + "jolt-poly", + "jolt-transcript", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "rayon", + "serde", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "jolt-inlines-bigint" version = "0.1.0" @@ -2953,6 +3038,7 @@ dependencies = [ name = "jolt-lookup-tables" version = "0.1.0" dependencies = [ + "jolt-core", "jolt-field", "jolt-riscv", "rand 0.8.5", @@ -3018,6 +3104,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "thiserror 2.0.18", "tracing", ] @@ -3042,9 +3129,12 @@ version = "0.1.0" dependencies = [ "ark-serialize 0.5.0", "common", + "hex", "jolt-riscv", "object 0.39.1", "serde", + "serde_json", + "sha2 0.11.0", "thiserror 2.0.18", ] @@ -3052,6 +3142,7 @@ dependencies = [ name = "jolt-r1cs" version = "0.1.0" dependencies = [ + "jolt-claims", "jolt-field", "jolt-poly", "num-traits", @@ -3059,6 +3150,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "thiserror 2.0.18", "tracing", ] @@ -3069,6 +3161,7 @@ dependencies = [ "ark-serialize 0.5.0", "rand 0.8.5", "serde", + "serde_json", "strum 0.28.0", ] @@ -3104,8 +3197,11 @@ dependencies = [ name = "jolt-sumcheck" version = "0.1.0" dependencies = [ + "jolt-crypto", "jolt-field", + "jolt-openings", "jolt-poly", + "jolt-r1cs", "jolt-transcript", "num-traits", "rand_core 0.6.4", @@ -3254,9 +3350,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "macro-string" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" dependencies = [ "proc-macro2", "quote", @@ -3657,6 +3753,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -3664,6 +3769,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", ] [[package]] @@ -3676,6 +3799,17 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -4161,7 +4295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.12.1", "log", "multimap", @@ -4195,7 +4329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.117", @@ -4538,8 +4672,8 @@ dependencies = [ [[package]] name = "reth-ethereum-primitives" -version = "2.1.0" -source = "git+https://github.com/paradigmxyz/reth#6377a957c1a36be65678b9d2fb85645e1223ef60" +version = "2.2.0" +source = "git+https://github.com/paradigmxyz/reth#27737df66d73a43a03e3ac3ebc91cd213fe295a2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -4717,17 +4851,20 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rust-code-analysis" -version = "0.0.24" +version = "0.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a0f85e044428a7b58538f95fa58a157d89d5bcc5b37df6e7024957e52bdc5a" +checksum = "8aef20e35eb94fc114e15eedf547c54f39c51b5a1078ee1ea41d64e660025136" dependencies = [ "aho-corasick 0.7.20", + "crossbeam", "fxhash", + "globset", "lazy_static", "num", "num-derive 0.3.3", "num-format", "num-traits", + "once_cell", "petgraph", "regex", "serde", @@ -4742,6 +4879,7 @@ dependencies = [ "tree-sitter-python", "tree-sitter-rust", "tree-sitter-typescript", + "walkdir", ] [[package]] @@ -5531,9 +5669,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" +checksum = "ec005042c7d952febc1a3ef5b0f6674e9054aa836877a31c90b20e25b3d31744" dependencies = [ "paste", "proc-macro2", @@ -5543,15 +5681,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.4" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", + "objc2-open-directory", "windows", ] @@ -5707,7 +5846,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.1", + "winnow", ] [[package]] @@ -5716,7 +5855,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow", ] [[package]] @@ -5845,9 +5984,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.19.3" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab" +checksum = "d4423c784fe11398ca91e505cdc71356b07b1a924fc8735cfab5333afe3e18bc" dependencies = [ "cc", "regex", @@ -5855,9 +5994,9 @@ dependencies = [ [[package]] name = "tree-sitter-ccomment" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b402bc539927bb457e5ab59aac7260e2c3b97c5fcfc043575788654eedd69a" +checksum = "9e346e85d350ae07c4a42ec9438f20100927215d7c97313f41ee6be6239c8bb9" dependencies = [ "cc", "tree-sitter", @@ -5865,9 +6004,9 @@ dependencies = [ [[package]] name = "tree-sitter-cpp" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7bd90c7b7db59369ed00fbc40458d9c9b2b8ed145640e337e839ac07aa63e15" +checksum = "0dbedbf4066bfab725b3f9e2a21530507419a7d2f98621d3c13213502b734ec0" dependencies = [ "cc", "tree-sitter", @@ -5875,9 +6014,9 @@ dependencies = [ [[package]] name = "tree-sitter-java" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301ae2ee7813e1bf935dc06db947642400645bbea8878431e1b31131488d5430" +checksum = "f0bf5d3f508cbffcbfe1805834101c0d24297a8b6c2184ad9c595556c46d2420" dependencies = [ "cc", "tree-sitter", @@ -5885,9 +6024,9 @@ dependencies = [ [[package]] name = "tree-sitter-javascript" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840bb4d5f3c384cb76b976ff07297f5a24b6e61a708baa4464f53e395caaa5f9" +checksum = "2490fab08630b2c8943c320f7b63473cbf65511c8d83aec551beb9b4375906ed" dependencies = [ "cc", "tree-sitter", @@ -5895,9 +6034,9 @@ dependencies = [ [[package]] name = "tree-sitter-mozcpp" -version = "0.19.5" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5439f32b7685af19efcd0165d28dab80261e1cc922ed259c9c7909c96ac4cc6" +checksum = "e9514ebbde0a575c43027fffa2702788ae7fb967be5e1a43daae92667400d13e" dependencies = [ "cc", "tree-sitter", @@ -5906,9 +6045,9 @@ dependencies = [ [[package]] name = "tree-sitter-mozjs" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def6b21c10157d3d79b912191fa4549008885da827451a62be9f30abeb7319c8" +checksum = "836d32956e968db7fe66e15ad5cf6a46a3fc05c9710fffb9612487584da34b40" dependencies = [ "cc", "tree-sitter", @@ -5917,9 +6056,9 @@ dependencies = [ [[package]] name = "tree-sitter-preproc" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b2a77578e83efa7a193919660ffc88c22e357f9c2d9f27b5b11898a8682d3" +checksum = "d8a31d01067bbe7a827115ce36366af0738780b62cd5343b3378a5cce20d1f85" dependencies = [ "cc", "tree-sitter", @@ -5927,9 +6066,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.19.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5646bfe71c4eb1c21b714ce0c38334c311eab767095582859e85da6281e9fd6c" +checksum = "dda114f58048f5059dcf158aff691dffb8e113e6d2b50d94263fd68711975287" dependencies = [ "cc", "tree-sitter", @@ -5937,9 +6076,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.19.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784f7ef9cdbd4c895dc2d4bb785e95b4a5364a602eec803681db83d1927ddf15" +checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" dependencies = [ "cc", "tree-sitter", @@ -5947,9 +6086,9 @@ dependencies = [ [[package]] name = "tree-sitter-typescript" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f62d49c6e56bf291c412ee5e178ea14dff40f14a5f01a8847933f56d65bf3b" +checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d" dependencies = [ "cc", "tree-sitter", @@ -6439,15 +6578,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 7c1aedb172..d643be1a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ keywords = ["SNARK", "cryptography", "proofs"] [workspace] members = [ + "crates/jolt-claims", "crates/jolt-crypto", "crates/jolt-program", "crates/jolt-poly", @@ -32,6 +33,7 @@ members = [ "crates/jolt-sumcheck", "crates/jolt-openings", "crates/jolt-dory", + "crates/jolt-hyperkzg", "crates/jolt-riscv", "crates/jolt-transcript", "crates/jolt-profiling", @@ -250,7 +252,7 @@ ark-std = { version = "0.5.0", default-features = false } sha2 = "0.11" sha3 = "0.11" blake2 = "0.11.0-rc.6" -blake3 = { version = "1.5.0" } +blake3 = { version = "1.8.5" } light-poseidon = "0.4" digest = "0.11" jolt-optimizations = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist-shout" } @@ -302,7 +304,7 @@ num-derive = "0.4.2" num-traits = { version = "0.2.19", default-features = false } # System and Platform -sysinfo = "0.38" +sysinfo = "0.39" memory-stats = { version = "1.0.0", features = ["always_use_statm"] } # Parallel Processing @@ -375,13 +377,15 @@ secp256k1 = { version = "0.31", features = ["recovery", "rand"] } common = { path = "./common", default-features = false } tracer = { path = "./tracer", default-features = false } jolt-core = { path = "./jolt-core", default-features = false } +jolt-claims = { path = "./crates/jolt-claims" } jolt-crypto = { path = "./crates/jolt-crypto" } jolt-field = { path = "./crates/jolt-field" } jolt-openings = { path = "./crates/jolt-openings" } jolt-poly = { path = "./crates/jolt-poly" } +jolt-r1cs = { path = "./crates/jolt-r1cs" } jolt-transcript = { path = "./crates/jolt-transcript" } jolt-sumcheck = { path = "./crates/jolt-sumcheck" } -jolt-riscv = { path = "./crates/jolt-riscv" } +jolt-riscv = { path = "./crates/jolt-riscv", default-features = false } jolt-program = { path = "./crates/jolt-program", default-features = false } jolt-lookup-tables = { path = "./crates/jolt-lookup-tables" } jolt-platform = { path = "./jolt-platform", default-features = false } diff --git a/README.md b/README.md index e7f3b4430f..a8241b13c4 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ For faster incremental builds, use the `build-fast` profile: Unit and end-to-end tests for `jolt-core` can be run using the following command: -```cargo test -p jolt-core``` +```cargo nextest run -p jolt-core --features host``` Examples in the [`examples`](./examples/) directory can be run using e.g. diff --git a/STACK.md b/STACK.md new file mode 100644 index 0000000000..e859e2055b --- /dev/null +++ b/STACK.md @@ -0,0 +1,189 @@ +# Refactor Audit Prep Stack + +This stack splits `refactor/audit-prep` into draft PRs that can be reviewed +independently while the integration branch continues to move. The stack branches +are generated from `refactor/audit-prep`; do not treat them as the source of +truth. + +## Invariants + +- `refactor/audit-prep` is the source branch. +- Open stack PR branches are disposable materializations of slices from that + source. Merged stack rows stay in `main` and are not rebuilt. +- Each open PR branch is based on the previous open PR branch, with the first + open row based on `origin/main`. +- Root `Cargo.toml` and `Cargo.lock` are generated incrementally per PR. Do not + restore the whole root manifest from `refactor/audit-prep` into early branches. +- Open every PR as draft until the verifier frontier is complete. + +## Stack Order + +| # | Branch | Base | Contents | +|---|---|---|---| +| 00 | `stack/00-stack-automation` | `origin/main` | stack docs, branch plan, update script, and GitHub Actions workflow | +| 01 | `stack/01-foundation-helpers` | `stack/00-stack-automation` | small helper changes in `jolt-field`, `jolt-poly`, `jolt-transcript`, `jolt-riscv` | +| 02 | `stack/02-lookup-table-core-abi` | `stack/01-foundation-helpers` | modular lookup-table enum ordering and core ABI parity test | +| 03 | `stack/03-public-io-preprocessing` | `stack/02-lookup-table-core-abi` | public I/O memory helpers in `common` and `jolt-program` | +| 04 | `stack/04-commitment-opening-infra` | `stack/03-public-io-preprocessing` | commitment, vector commitment, PCS, and opening-reduction infrastructure | +| 05 | `stack/05-jolt-claims-crate` | `stack/04-commitment-opening-infra` | new `jolt-claims` crate | +| 06 | `stack/06-jolt-r1cs-builder-lowering` | `stack/05-jolt-claims-crate` | `jolt-r1cs` builder/lowering/expression integration | +| 07 | `stack/07-committed-sumcheck-r1cs` | `stack/06-jolt-r1cs-builder-lowering` | committed sumcheck messages, domains, verifier changes, R1CS feature | +| 08 | `stack/08-jolt-blindfold-crate` | `stack/07-committed-sumcheck-r1cs` | new generic `jolt-blindfold` crate | +| 08a | `stack/08a-jolt-core-blindfold-hardening` | `stack/08-jolt-blindfold-crate` | `jolt-core` BlindFold construction hardening | +| 09 | `stack/09-jolt-verifier-crate` | `stack/08a-jolt-core-blindfold-hardening` | new `jolt-verifier` crate, verifier spec, boundary checks, fixtures, and verifier test config | +| 10 | `stack/10-jolt-prover-spec` | `stack/09-jolt-verifier-crate` | `specs/jolt-prover-model-crate.md` | +| 11 | `stack/11-extended-jolt-field-inline-wrapper-spec` | `stack/10-jolt-prover-spec` | extended Jolt / field inline / wrapper spec plus supporting recursion reference doc | +| 12 | `stack/12-selected-verifier-integration-spec` | `stack/11-extended-jolt-field-inline-wrapper-spec` | selected verifier integration spec | +| 13 | `stack/13-field-inline-protocol-spec` | `stack/12-selected-verifier-integration-spec` | field inline protocol spec | +| 14 | `stack/14-dory-assist-protocol-spec` | `stack/13-field-inline-protocol-spec` | Dory assist protocol spec | +| 15 | `stack/15-wrapper-protocol-spec` | `stack/14-dory-assist-protocol-spec` | wrapper protocol and SNARK backend spec | + +The `jolt-core` BlindFold hardening PR carries the compatibility/security patch +that makes core BlindFold construction match the modular stack before the +`jolt-verifier` crate consumes it. The `jolt-verifier` PR carries the current +verifier frontier from `refactor/audit-prep`. + +## Automatic Updates + +Pushing to `origin/refactor/audit-prep` runs +[`.github/workflows/refactor-audit-stack.yml`](.github/workflows/refactor-audit-stack.yml). +The workflow starts at the first open stack row (`07` at the moment). Rows +`00` through `06` have merged to `main` and are intentionally not rebuilt. + +The workflow: + +1. checks out the pushed `refactor/audit-prep` commit; +2. rebuilds each open `stack/*` branch from the previous open stack branch; +3. restores the owned paths from `origin/refactor/audit-prep`; +4. applies the incremental root manifest changes for that stack point; +5. runs `cargo metadata` to refresh `Cargo.lock`; +6. checks that every path changed by `refactor/audit-prep` is assigned to a + stack slice; +7. force-pushes all stack branches with lease; +8. creates or updates the draft PRs. + +Once the PRs exist, GitHub will update them automatically when the workflow +force-pushes their branches. + +## Growing Spec PRs Into Implementation PRs + +The protocol spec PRs after PR 11 are allowed to grow implementation paths for +their feature area. Keep ownership disjoint by adding the implementation paths +to the same row as the relevant spec: + +- PR 12: selected verifier integration, proof-shape validation, selected stage + schedule, and selected computation export. +- PR 13: field-inline claims, R1CS rows, selected verifier hooks, trace/prover + wiring, and field-inline fixtures. +- PR 14: Dory-assist claims, Hyrax support, selected verifier hooks, and + Dory-assist fixtures. +- PR 15: wrapper assembly, verifier R1CS lowering, transcript R1CS, and SNARK + backend integration. + +If a later feature row names a path inside a directory owned by an earlier row, +the later row wins for changed files under that path. This keeps broad crate +bootstrap PRs stable while letting later implementation PRs own narrower +submodules in the same crate. + +## Manual Materialization + +Dry-run the stack: + +```bash +./stack/update-stack.sh +``` + +Create or update one branch: + +```bash +./stack/update-stack.sh --apply --only 08 +``` + +Rebuild all stack branches from the source branch: + +```bash +./stack/update-stack.sh --apply --rebuild --commit --push --cargo-metadata --check-coverage --from origin/refactor/audit-prep --start-at 07 +``` + +The CI workflow runs the same command. Without `--commit`, the script leaves +changes unstaged for local inspection. + +## Manifest Generation + +The update script applies these root manifest changes in the branch where the +crate first appears: + +- PR 05: add `crates/jolt-claims` to workspace members and add + `jolt-claims = { path = "./crates/jolt-claims" }` to workspace dependencies. +- PR 06: add `jolt-r1cs = { path = "./crates/jolt-r1cs" }` to workspace + dependencies if it is not already present. +- PR 08: add `crates/jolt-blindfold` to workspace members and add + `jolt-blindfold = { path = "./crates/jolt-blindfold" }`. +- PR 08a: no root manifest changes; this slice patches existing `jolt-core` + BlindFold construction before `jolt-verifier`. +- PR 09: add `crates/jolt-verifier` and `examples/advice-consumer/guest` to + workspace members, add `jolt-verifier = { path = "./crates/jolt-verifier" }`, + and add verifier-specific test fixture config. + +With `--cargo-metadata`, the script refreshes `Cargo.lock` after those manifest +changes. + +## Validation + +Use focused checks per branch while the stack is draft: + +```bash +cargo metadata -q >/dev/null +cargo check -p jolt-claims -q +cargo check -p jolt-r1cs -q +cargo check -p jolt-sumcheck -q --features r1cs +cargo check -p jolt-blindfold -q +cargo check -p jolt-verifier -q +``` + +For the full stack tip: + +```bash +cargo clippy --all --features host -q --all-targets -- -D warnings +cargo clippy --all --features host,zk -q --all-targets -- -D warnings +cargo fmt -q +``` + +Use `cargo nextest`, not `cargo test`, once the stack is ready for correctness +checks. + +## Opening Draft PRs + +The workflow opens or updates draft PRs automatically. To do the same locally +after materializing and pushing the branches: + +```bash +./stack/open-prs.sh --apply --base main --source refactor/audit-prep +``` + +If using a stacked-PR extension, create the same branch chain and run the +extension's submit command from the final stack branch. + +## Updating From `refactor/audit-prep` + +1. Commit WIP on `refactor/audit-prep`. The source must be pushed as a git ref. +2. Push: + + ```bash + git push origin refactor/audit-prep + ``` + +3. The workflow rebuilds and force-pushes all stack branches. To do the same + locally: + + ```bash + ./stack/update-stack.sh --apply --rebuild --commit --push --cargo-metadata --check-coverage --from origin/refactor/audit-prep --start-at 07 + ``` + +4. Compare the stack tip to the source branch: + + ```bash + git diff --stat refactor/audit-prep..stack/15-wrapper-protocol-spec + ``` + +Only intentional WIP exclusions or branch-order differences should remain. diff --git a/book/src/usage/guests_hosts/guests.md b/book/src/usage/guests_hosts/guests.md index 86243097eb..e9531c9583 100644 --- a/book/src/usage/guests_hosts/guests.md +++ b/book/src/usage/guests_hosts/guests.md @@ -145,6 +145,8 @@ Beyond the usual Rust rules around `unsafe` and undefined behavior, Jolt guest p - **Outputs and the panic flag are public.** The return value and `io_device.panic` are revealed to the verifier and are what the proof attests to. Returning a value derived from a private input leaks that value; panicking conditionally on a private input leaks one bit per panic site. Guests handling secret data should be written so that the output and panic behavior depend only on public information. +- **Output byte representation.** The output memory region is zero-initialized, and both prover and verifier strip trailing zero bytes before binding outputs to the Fiat-Shamir transcript. As a result, byte strings that differ only in trailing zeros (e.g., `[0x41]` vs `[0x41, 0x00]`) produce identical proofs. Typed outputs are unaffected — the SDK restores the full serialized length before deserialization — but protocols that consume `program_io.outputs` as raw bytes must not treat trailing zeros as semantically meaningful, or must encode an explicit length. + - **No side-channel resistance.** The `zk` feature, via the [BlindFold](../../how/blindfold.md) protocol, makes proofs zero-knowledge with respect to the verifier. However, Jolt's prover implementation is **not constant-time** and makes no claims of resistance to side-channel attacks. A party that observes prover execution (timing, memory access patterns, power consumption, etc.) may learn information about private inputs. Do not run the prover on secret data in adversarial environments without additional mitigations. ## Standard Library diff --git a/common/src/jolt_device.rs b/common/src/jolt_device.rs index 699a0a252a..f4ce8d267d 100644 --- a/common/src/jolt_device.rs +++ b/common/src/jolt_device.rs @@ -15,6 +15,27 @@ use crate::constants::{ RAM_START_ADDRESS, STACK_CANARY_SIZE, }; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryLayoutError { + ZeroAddress, + AddressBelowLowest { address: u64, lowest_address: u64 }, +} + +impl core::fmt::Display for MemoryLayoutError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::ZeroAddress => write!(f, "cannot remap the zero address"), + Self::AddressBelowLowest { + address, + lowest_address, + } => write!( + f, + "address {address} is below lowest mapped address {lowest_address}" + ), + } + } +} + #[allow(clippy::too_long_first_doc_paragraph)] /// Represented as a "peripheral device" in the RISC-V emulator, this captures /// all reads from the reserved memory address space for program inputs and all writes @@ -153,6 +174,27 @@ impl JoltDevice { fn convert_write_address(&self, address: u64) -> usize { (address - self.memory_layout.output_start) as usize } + + pub fn input_words_le(&self) -> Vec { + bytes_to_words_le(&self.inputs) + } + + pub fn output_words_le(&self) -> Vec { + bytes_to_words_le(&self.outputs) + } +} + +pub fn bytes_to_words_le(bytes: &[u8]) -> Vec { + bytes + .chunks(8) + .map(|chunk| { + let mut value = 0u64; + for (index, byte) in chunk.iter().enumerate() { + value |= u64::from(*byte) << (8 * index); + } + value + }) + .collect() } #[derive(Debug, Copy, Clone)] @@ -399,6 +441,27 @@ impl MemoryLayout { self.trusted_advice_start.min(self.untrusted_advice_start) } + pub fn remap_word_address(&self, address: u64) -> Result, MemoryLayoutError> { + if address == 0 { + return Ok(None); + } + + let lowest_address = self.get_lowest_address(); + if address >= lowest_address { + Ok(Some((address - lowest_address) / 8)) + } else { + Err(MemoryLayoutError::AddressBelowLowest { + address, + lowest_address, + }) + } + } + + pub fn remapped_word_address(&self, address: u64) -> Result { + self.remap_word_address(address)? + .ok_or(MemoryLayoutError::ZeroAddress) + } + /// Returns the total emulator memory (program + canary + stack + heap). pub fn get_total_memory_size(&self) -> u64 { self.heap_end - RAM_START_ADDRESS @@ -423,4 +486,42 @@ mod tests { let overflow_address = device.memory_layout.io_end; device.store(overflow_address, 0x42); } + + #[test] + fn packs_public_io_bytes_into_little_endian_words() { + let mut device = JoltDevice::new(&MemoryConfig { + program_size: Some(1024), + ..Default::default() + }); + device.inputs = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; + device.outputs = vec![0xaa, 0xbb]; + + assert_eq!(device.input_words_le(), vec![0x0807_0605_0403_0201, 9]); + assert_eq!(device.output_words_le(), vec![0xbbaa]); + } + + #[test] + fn remaps_word_addresses_relative_to_lowest_reserved_address() { + let device = JoltDevice::new(&MemoryConfig { + program_size: Some(1024), + ..Default::default() + }); + let layout = &device.memory_layout; + let lowest = layout.get_lowest_address(); + + assert_eq!(layout.remap_word_address(0), Ok(None)); + assert_eq!( + layout.remapped_word_address(0), + Err(MemoryLayoutError::ZeroAddress) + ); + assert_eq!(layout.remapped_word_address(lowest), Ok(0)); + assert_eq!(layout.remapped_word_address(lowest + 16), Ok(2)); + assert_eq!( + layout.remapped_word_address(lowest - 8), + Err(MemoryLayoutError::AddressBelowLowest { + address: lowest - 8, + lowest_address: lowest, + }) + ); + } } diff --git a/crates/jolt-claims/Cargo.toml b/crates/jolt-claims/Cargo.toml new file mode 100644 index 0000000000..19d7534aab --- /dev/null +++ b/crates/jolt-claims/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "jolt-claims" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Shared claim and expression types for Jolt protocols" + +[lints] +workspace = true + +[dependencies] +derive_more.workspace = true +jolt-field.workspace = true +jolt-lookup-tables.workspace = true +jolt-poly.workspace = true +jolt-riscv = { workspace = true, features = ["serialization"] } +serde = { workspace = true, features = ["alloc", "derive"] } +thiserror.workspace = true diff --git a/crates/jolt-claims/REVIEW_NOTES.md b/crates/jolt-claims/REVIEW_NOTES.md new file mode 100644 index 0000000000..44eab073c1 --- /dev/null +++ b/crates/jolt-claims/REVIEW_NOTES.md @@ -0,0 +1,431 @@ +# jolt-claims review triage + +Source: moodlezoup review on PR #1545, `feat: add jolt-claims crate`. + +This is a triage note before wiring `jolt-claims` into `jolt-verifier`. The +comments are grouped by whether they need a design call or can be handled in a +mechanical cleanup pass. + +## Needs Your Input + +These comments affect the protocol API, downstream lowering, or field-register +uniformity. Please mark the `Decision:` line for each item. + +### Consistency claims must be consumed + +Comment: +`instruction.rs` has input expressions that match core standard-mode +`input_claim()`, while core ZK constraints sometimes bind both opening chains. +The equivalent surface here is `JoltStageClaims::consistency`; downstream +BlindFold/R1CS lowering must not ignore it. + +Question: +Should every lowering/verifier consumer be forced to handle consistency claims +as part of the stage contract, or should we fold these same-evaluation bindings +directly into stage input/output expressions where BlindFold needs them? + +Recommendation: +Keep consistency claims, but make them impossible to silently drop: + +- `jolt-verifier` stage wiring consumes them explicitly. +- BlindFold/R1CS lowering lowers them as equality constraints. +- wrapper lowering lowers them as equality constraints. +- tests assert every consistency claim appears in the lowered verifier R1CS. + + +I agree. make the conssitency claims hard to drop and expliict so that we get them in jolt-verifier. if we odnt have them we need to add, and we wanna harden this path to make that harder. + +Field-register impact: +Apply the same rule to `field_inline`: field-register openings that share an +evaluation point should be represented as stage consistency claims and consumed +by BlindFold/wrapper lowering. + +yes. + +Decision: +Done: consistency claims stay explicit on relation claims, are included in +dependency aggregation, and the same representation is used by field-inline. + +### Challenge vs public taxonomy + +Comment: +`InstructionRaVirtualizationChallenge::EqCycle` and +`RamRaVirtualizationPublic::EqCycle` classify the same structural role +differently. This will confuse downstream code that bins ids by source kind. + +Question: +Do we want the taxonomy to mean: + + +They should be Challenge i think, because it is Eq from a challenge type (which is a public computation just over a challenge type) + +- `Challenge`: Fiat-Shamir sampled value, even if it is passed through a helper + as a baked coefficient in BlindFold; and +- `Public`: deterministic verifier-side scalar, public IO/preprocessing value, + or externally supplied public boundary value? + +Recommendation: +Use `Challenge` for transcript-derived values and `Public` for deterministic +non-transcript values. That likely means moving RA-virtualization `EqCycle` +values to challenge IDs consistently. + +Field-register impact: +Use the same taxonomy for `FieldInlineChallengeId` and `FieldInlinePublicId`. +For example, `EqCycle` and product-stage challenges should be challenges; +lagrange/equality coefficients computed from already-known points can remain +publics only if they are not themselves transcript samples in that stage. + +Decision: +Done: transcript-derived Eq values are classified as challenges across the +affected Jolt and field-inline IDs. + +### Forced challenge registration + +Comment: +`ClaimExpression::require_challenge` is unclear. + +Question: +Do we need an explicit API for transcript challenges that are consumed by a +stage but do not appear syntactically in the claim expression? + +Recommendation: +If yes, rename this API to make its role explicit, e.g. +`reserve_challenge`, `with_transcript_challenges`, or +`with_round_challenges`. Keep it only on stage claims, not on raw expressions, +so it reads as stage metadata rather than expression mutation. + + +Maybe call it, pull_challenge_maintain_transcript_sync to be super explicit with what the api is needed for + +Field-register impact: +Use the same API for field-register stages. Avoid direct mutation of +`ClaimExpression` metadata. + +Decision: +Done: the API is named `pull_challenge_for_transcript_sync` / +`pull_challenges_for_transcript_sync`, and relation helpers use it for +transcript-synchronizing challenges. + +### SameEvaluation vs EqualExpressions + +Comment: +`SameEvaluation` is a special case of `EqualExpressions`; remove it unless +there is a clear need. + +Question: +Do we want a semantic `SameEvaluation` variant because it is useful for PCS +opening planning and diagnostics, or should all consistency checks be generic +expression equalities? + +Recommendation: +Prefer one representation for lowering. If the semantic marker is useful, keep +a helper constructor but store it as `EqualExpressions(opening(a), opening(b))` +internally. + +Keep one representation and make sure it is just explicit though so it's hard to miss + +Field-register impact: +Use the same representation for field-register consistency checks so wrapper +and BlindFold lowering do not need protocol-specific branches. + +Decision: +Done: `SameEvaluation` remains as a helper, but the stored representation is +the single `EqualExpressions(opening(left), opening(right))` form. + +### Stage naming + +Comment: +`JoltStageId` is confusing because historically "stage" refers to a batch of +sumchecks. + +Question: +Should this be renamed before wiring to something like `JoltRelationId`, +`JoltCheckId`, or `JoltClaimId`? + +Just do like JoleRelationId + +Recommendation: +Rename now if we want to avoid confusion in `jolt-verifier`; this is easier +before field-inline and Dory-assist add more IDs. If we keep `StageId`, document +that in `jolt-claims` it means "claim-producing verifier component", not +necessarily a numbered prover stage. + +Field-register impact: +Whatever name we choose should apply uniformly to `FieldInlineStageId`. + +Decision: +Done: `JoltStageId`/`FieldInlineStageId` and stage-claim types were renamed to +`JoltRelationId`/`FieldInlineRelationId` and relation-claim types. + +### `pow2` ownership + +Comment: +Define `pow2` as a function on `RingCore`. + +Question: +Are we willing to extend `jolt-field::RingCore` with a `pow2` helper now? + +Recommendation: +If `pow2` is broadly useful in protocol formulas, move it to `RingCore` or a +small field utility trait. If not, keep it local and rename it narrowly. This +is cross-crate API surface, so it is less one-shot than the local cleanups. + +Decision: + +Yes +Done: `RingCore::pow2` owns the helper and jolt-claims now uses the field API. + +## One-Shot Fixes + +These are either already handled locally or can be fixed without a protocol +decision. When you are done marking up the section above, these can be applied +in a cleanup pass. + +### Claim expression metadata safety + +Comment: +`ClaimExpression` should not expose a mutable `expression` field while caching +metadata. + +Status: Done. `expression` is private and exposed through `expression()`. + +Follow-up: +Keep this invariant when adding field-inline formulas. Do not add APIs that +mutate `ClaimExpression.expression` without rebuilding metadata. + +### Remove cached `num_challenges` + +Comment: +Remove the `num_challenges` field and expose it as a method. + +Action: +Delete `ClaimExpression::num_challenges`, compute from +`required_challenges.len()`, and update `JoltRelationClaims` plus +`FieldInlineRelationClaims` tests. + +Status: Done. + +### Resolver naming + +Comment: +Rename evaluator closures to names like `resolve_opening`. + +Action: +Rename `opening_value`, `challenge_value`, and `public_value` parameters in +`Expr::evaluate` / `try_evaluate` to `resolve_opening`, `resolve_challenge`, +and `resolve_public`. + +Status: Done. + +### Zero-coefficient terms + +Comment: +Do not drop terms with zero coefficients if they carry metadata factors. + +Status: Done. `From` only canonicalizes zero constants when +`factors.is_empty()`. + +Follow-up: +Add or keep a regression test that zero-coefficient terms with openings still +surface `required_openings`. + +### `log2_power_of_two` + +Comment: +`debug_assert!` would be stripped in release for non-power-of-two inputs. + +Status: Done with `assert!(value.is_power_of_two())`. + +### Remove unnecessary `From` impls for dimension structs + +Comment: +There are many unnecessary `From` impls; delete them. + +Action: +Remove tuple/usize `From` impls for dimension structs and update tests to call +`::new(...)` directly. Apply the same rule to field-inline dimension types. + +Status: Done. + +### Error enums use `thiserror` + +Comment: +Use `thiserror::Error` for error enums. + +Action: +Add `thiserror.workspace = true` to `jolt-claims`, derive `Error`, and move +manual `Display` impls into `#[error(...)]` attributes. + +Field-register impact: +Any field-inline error enum added later should follow the same style. + +Status: Done. + +### Move error enums to `error.rs` + +Comment: +Pull error enums out of `dimensions.rs` into `error.rs`. + +Action: +Create `crates/jolt-claims/src/protocols/jolt/formulas/error.rs`, move +`JoltFormulaPointError` and `JoltFormulaDimensionsError`, and re-export them +from `formulas` or `dimensions` as needed. + +Status: Done. + +### Move advice-specific dimension types + +Comments: +Move `AdviceClaimReductionLayout` and `AdviceClaimReductionDimensions` from +`dimensions.rs` into `advice.rs`. + +Action: +Move advice-only layout/dimension structs and helpers into +`formulas/claim_reductions/advice.rs` or a sibling advice dimensions module +owned by that formula. + +Status: Done. + +### Delete helper constructors on dimensions error + +Comment: +Delete helper constructors such as `zero`, `overflow`, and `not_divisible`. + +Action: +Inline the enum variants at call sites. With `thiserror`, these helpers add +more indirection than value. + +Status: Done. + +### Default generic types + +Comments: +Default generic parameters for expression types should be `P = (), C = usize`. + +Status: Done for `Source`, `Term`, `Expr`, and `ClaimExpression`. + +Follow-up: +Preserve this convention in any field-inline-specific type aliases. + +### Bytecode constants and XLEN + +Comments: +Move the circuit flag constant to `jolt-riscv`; stop propagating const-generic +`XLEN` through `jolt-claims`. + +Action: +Expose the canonical 64-bit circuit flag list from `jolt-riscv` and make +bytecode formulas use the ordinary Jolt XLEN. Avoid generic XLEN in +`jolt-claims` unless a formula truly needs the full-hypercube lookup-table test +case. + +Status: Done. + +### Arithmetic ops for expression refs + +Comment: +Implement arithmetic ops on reference types to avoid clones. + +Action: +Add `Add`, `Sub`, `Mul`, and `Neg` impls for borrowed `Expr` forms where they +remove clone noise in formulas. This is cleanup, not a protocol blocker. + +Status: Done. + +### Advice opening arrays + +Comment: +Unused advice opening arrays should be removed; arrays are not clearly needed. + +Action: +Delete unused helpers or switch to `Vec` only where variable length is real. +Keep fixed arrays only for public helpers that are used in tests or downstream +opening planners. + +Status: Done. Advice opening helpers are exercised in tests; the variable +cycle-phase output stays as `Vec`. + +### Committed opening order + +Comments: +`proof_commitment_order` can call `final_opening_polynomial_order(layout, false, +false)`, and `final_opening_ids` should derive from the polynomial order plus +an explicit polynomial-to-relation mapping. + +Action: +Add a helper mapping `JoltCommittedPolynomial -> JoltRelationId` for final +openings, then have `final_opening_ids` map over +`final_opening_polynomial_order(...)`. This removes order drift risk. + +Field-register impact: +When `FieldRdInc` enters the final opening/RLC path, use the same explicit +polynomial-to-relation mapping pattern instead of duplicating ordered vectors. + +Status: Done for base Jolt; the same mapping pattern is called out for the +future field-register final-opening path. + +### RA layout total + +Comment: +Make `JoltRaPolynomialLayout::total` a method instead of a stored field. + +Action: +Store only `instruction`, `bytecode`, and `ram`; compute checked nonzero total +in `new`, then compute `total()` from the fields. If preserving nonzero as an +invariant is useful, keep the current field and note why. + +Status: Done. + +### Derive `From` + +Comments: +Use `derive_more::From` for ID wrapper `From` impls. + +Action: +Add `derive_more.workspace = true` to `jolt-claims` and derive `From` on +wrapper enums where the manual impls are purely boilerplate. + +Field-register impact: +Apply this to `FieldInlineChallengeId`, `FieldInlinePolynomialId`, and +`FieldInlinePublicId` too. + +Status: Done. + +### Unique relation IDs + +Comment: +Add debug assertions that relation IDs are unique. + +Action: +Add uniqueness checks in `JoltProtocolClaims::new` and +`FieldInlineProtocolClaims::new`, plus tests that duplicate IDs trip the +assertion in debug builds. + +Status: Done. + +### Deduplicate `extend_unique` + +Comment: +`extend_unique` is duplicated in `claims.rs` and relation modules. + +Action: +Move it to one crate-private helper and reuse it from Jolt and field-inline +relation modules. + +Status: Done. + +## Field-Register Uniformity Checklist + +When addressing the review, apply the same cleanup to the new field-register +claim code instead of only fixing base Jolt: + +- keep `FieldInlineRelationClaims` structurally aligned with `JoltRelationClaims`; +- remove cached challenge counts from both paths; +- use the same challenge/public taxonomy; +- use the same consistency-claim representation and lowering contract; +- use the same relation/claim ID naming choice; +- use the same final-opening mapping pattern for reduced `FieldRdInc`; +- derive or delete boilerplate `From` impls consistently; +- add relation-ID uniqueness checks for field-inline protocol claims too. + +Status: Done. diff --git a/crates/jolt-claims/src/claims.rs b/crates/jolt-claims/src/claims.rs new file mode 100644 index 0000000000..0c6311b726 --- /dev/null +++ b/crates/jolt-claims/src/claims.rs @@ -0,0 +1,538 @@ +use jolt_field::RingCore; +use serde::{Deserialize, Serialize}; + +use crate::util::extend_unique; + +/// An atomic value used inside a symbolic claim expression. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Source { + Opening(O), + Challenge(C), + Public(P), +} + +/// One product term: `coefficient * product(factors)`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Term { + pub coefficient: F, + pub factors: Vec>, +} + +impl Term { + pub fn constant(coefficient: F) -> Self { + Self { + coefficient, + factors: Vec::new(), + } + } +} + +impl Term { + pub fn source(source: Source) -> Self { + Self { + coefficient: F::one(), + factors: vec![source], + } + } +} + +/// A symbolic sum-of-products expression. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Expr { + pub terms: Vec>, +} + +impl Expr { + pub fn zero() -> Self { + Self { terms: Vec::new() } + } + + pub fn is_zero(&self) -> bool { + self.terms.is_empty() + } +} + +impl Expr { + pub fn required_challenges(&self) -> Vec { + let mut challenges = Vec::new(); + for source in self.terms.iter().flat_map(|term| term.factors.iter()) { + if let Source::Challenge(id) = source { + if !challenges.contains(id) { + challenges.push(id.clone()); + } + } + } + challenges + } + + pub fn num_challenges(&self) -> usize { + self.required_challenges().len() + } +} + +impl Expr { + pub fn one() -> Self { + Self { + terms: vec![Term::constant(F::one())], + } + } + + pub fn constant(value: F) -> Self { + if value.is_zero() { + Self::zero() + } else { + Self { + terms: vec![Term::constant(value)], + } + } + } + + pub fn evaluate( + &self, + mut resolve_opening: OpeningValue, + mut resolve_challenge: ChallengeValue, + mut resolve_public: PublicValue, + ) -> F + where + OpeningValue: FnMut(&O) -> F, + ChallengeValue: FnMut(&C) -> F, + PublicValue: FnMut(&P) -> F, + { + let mut result = F::zero(); + for term in &self.terms { + let mut value = term.coefficient; + for factor in &term.factors { + value *= match factor { + Source::Opening(id) => resolve_opening(id), + Source::Challenge(id) => resolve_challenge(id), + Source::Public(id) => resolve_public(id), + }; + } + result += value; + } + result + } + + pub fn try_evaluate( + &self, + mut resolve_opening: OpeningValue, + mut resolve_challenge: ChallengeValue, + mut resolve_public: PublicValue, + ) -> Result + where + OpeningValue: FnMut(&O) -> Result, + ChallengeValue: FnMut(&C) -> Result, + PublicValue: FnMut(&P) -> Result, + { + let mut result = F::zero(); + for term in &self.terms { + let mut value = term.coefficient; + for factor in &term.factors { + value *= match factor { + Source::Opening(id) => resolve_opening(id)?, + Source::Challenge(id) => resolve_challenge(id)?, + Source::Public(id) => resolve_public(id)?, + }; + } + result += value; + } + Ok(result) + } +} + +impl Expr { + pub fn pow(self, mut exponent: usize) -> Self { + let mut result = Self::one(); + let mut base = self; + + while exponent > 0 { + if exponent % 2 == 1 { + result = result * base.clone(); + } + exponent /= 2; + if exponent > 0 { + base = base.clone() * base; + } + } + + result + } +} + +impl Expr { + pub fn evaluate_without_public( + &self, + opening_value: OpeningValue, + challenge_value: ChallengeValue, + ) -> F + where + OpeningValue: FnMut(&O) -> F, + ChallengeValue: FnMut(&C) -> F, + { + self.evaluate(opening_value, challenge_value, |()| F::zero()) + } +} + +impl Expr { + pub fn required_openings(&self) -> Vec { + let mut openings = Vec::new(); + for source in self.terms.iter().flat_map(|term| term.factors.iter()) { + if let Source::Opening(id) = source { + if !openings.contains(id) { + openings.push(id.clone()); + } + } + } + openings + } +} + +impl Expr { + pub fn required_publics(&self) -> Vec

{ + let mut publics = Vec::new(); + for source in self.terms.iter().flat_map(|term| term.factors.iter()) { + if let Source::Public(id) = source { + if !publics.contains(id) { + publics.push(id.clone()); + } + } + } + publics + } +} + +/// Builds an opening source expression. +pub fn opening(id: O) -> Expr { + Expr { + terms: vec![Term::source(Source::Opening(id))], + } +} + +/// Builds a Fiat-Shamir challenge source expression. +pub fn challenge(id: C) -> Expr { + Expr { + terms: vec![Term::source(Source::Challenge(id))], + } +} + +/// Builds a named public-value source expression. +pub fn public(id: P) -> Expr { + Expr { + terms: vec![Term::source(Source::Public(id))], + } +} + +/// Builds a constant expression. +pub fn constant(value: F) -> Expr { + Expr::constant(value) +} + +/// Expression metadata used by claim-check protocols. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClaimExpression { + expression: Expr, + pub required_openings: Vec, + pub required_publics: Vec

, + pub required_challenges: Vec, +} + +impl ClaimExpression { + pub fn expression(&self) -> &Expr { + &self.expression + } +} + +impl From> + for ClaimExpression +{ + fn from(expression: Expr) -> Self { + let required_openings = expression.required_openings(); + let required_publics = expression.required_publics(); + let required_challenges = expression.required_challenges(); + Self { + expression, + required_openings, + required_publics, + required_challenges, + } + } +} + +impl ClaimExpression { + pub fn challenge_index(&self, id: &C) -> Option { + self.required_challenges + .iter() + .position(|challenge| challenge == id) + } + + pub fn num_challenges(&self) -> usize { + self.required_challenges.len() + } +} + +impl ClaimExpression { + pub fn pull_challenge_for_transcript_sync(&mut self, id: C) { + if !self.required_challenges.contains(&id) { + self.required_challenges.push(id); + } + } + + pub fn pull_challenges_for_transcript_sync(&mut self, ids: I) + where + I: IntoIterator, + { + for id in ids { + self.pull_challenge_for_transcript_sync(id); + } + } +} + +pub type InputClaimExpression = ClaimExpression; +pub type OutputClaimExpression = ClaimExpression; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConsistencyClaim { + EqualExpressions { + left: Expr, + right: Expr, + }, +} + +impl ConsistencyClaim { + pub fn same_evaluation(left: O, right: O) -> Self { + Self::EqualExpressions { + left: opening(left), + right: opening(right), + } + } + + pub fn equal_expressions(left: Expr, right: Expr) -> Self { + Self::EqualExpressions { left, right } + } +} + +impl ConsistencyClaim { + pub fn required_openings(&self) -> Vec { + match self { + Self::EqualExpressions { left, right } => { + let mut openings = left.required_openings(); + extend_unique(&mut openings, &right.required_openings()); + openings + } + } + } +} + +impl ConsistencyClaim { + pub fn required_publics(&self) -> Vec

{ + match self { + Self::EqualExpressions { left, right } => { + let mut publics = left.required_publics(); + extend_unique(&mut publics, &right.required_publics()); + publics + } + } + } +} + +impl ConsistencyClaim { + pub fn required_challenges(&self) -> Vec { + match self { + Self::EqualExpressions { left, right } => { + let mut challenges = left.required_challenges(); + extend_unique(&mut challenges, &right.required_challenges()); + challenges + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt, RingCore}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + enum Opening { + A, + B, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + enum Public { + Offset, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + enum Challenge { + Alpha, + Beta, + } + + #[test] + fn expression_evaluates_with_resolvers() { + let expr: Expr = + constant(Fr::from_u64(2)) * opening(Opening::A) * opening(Opening::B) + + challenge(1) * opening(Opening::A) + - constant(Fr::from_u64(5)); + + let value = expr.evaluate_without_public( + |opening| match opening { + Opening::A => Fr::from_u64(3), + Opening::B => Fr::from_u64(7), + }, + |index| match *index { + 1 => Fr::from_u64(11), + _ => Fr::from_u64(0), + }, + ); + + assert_eq!(value, Fr::from_u64(70)); + } + + #[test] + fn expression_evaluates_public_sources() { + let expr: Expr = + opening(Opening::A) * public(Public::Offset) + constant(Fr::from_u64(4)); + + let value = expr.evaluate( + |opening| match opening { + Opening::A => Fr::from_u64(3), + Opening::B => Fr::from_u64(0), + }, + |_| Fr::from_u64(0), + |public| match public { + Public::Offset => Fr::from_u64(9), + }, + ); + + assert_eq!(value, Fr::from_u64(31)); + } + + #[test] + fn pow2_builds_field_native_powers() { + assert_eq!(Fr::pow2(0), Fr::from_u64(1)); + assert_eq!(Fr::pow2(1), Fr::from_u64(2)); + assert_eq!(Fr::pow2(63), Fr::from_u64(1u64 << 63)); + } + + #[test] + fn expression_powers_are_structural_products() { + let gamma: Expr = challenge(Challenge::Alpha); + let expr = gamma.pow(3) * opening(Opening::A); + + assert_eq!(expr.required_challenges(), vec![Challenge::Alpha]); + assert_eq!(expr.required_openings(), vec![Opening::A]); + assert_eq!( + expr.evaluate( + |_| Fr::from_u64(5), + |_| Fr::from_u64(7), + |_| Fr::from_u64(0) + ), + Fr::from_u64(1715) + ); + } + + #[test] + fn expression_zero_power_is_one() { + let expr: Expr = Expr::zero().pow(0); + + assert_eq!( + expr.evaluate( + |_| Fr::from_u64(0), + |_| Fr::from_u64(0), + |_| Fr::from_u64(0) + ), + Fr::from_u64(1) + ); + } + + #[test] + fn zero_coefficient_terms_keep_non_constant_metadata() { + let expr: Expr = Term { + coefficient: Fr::from_u64(0), + factors: vec![Source::Opening(Opening::A)], + } + .into(); + + assert_eq!(expr.required_openings(), vec![Opening::A]); + assert!( + expr.evaluate( + |_| Fr::from_u64(9), + |_| Fr::from_u64(0), + |_| Fr::from_u64(0) + ) == Fr::from_u64(0) + ); + } + + #[test] + fn claim_expression_derives_metadata() { + let expression: Expr = + challenge(2) * opening(Opening::B) * public(Public::Offset) + opening(Opening::A); + let claim = ClaimExpression::from(expression.clone()); + + assert_eq!(claim.expression(), &expression); + assert_eq!(claim.required_openings, vec![Opening::B, Opening::A]); + assert_eq!(claim.required_publics, vec![Public::Offset]); + assert_eq!(claim.required_challenges, vec![2]); + assert_eq!(claim.num_challenges(), 1); + assert_eq!(claim.challenge_index(&2), Some(0)); + assert_eq!(claim.challenge_index(&1), None); + } + + #[test] + fn same_evaluation_claim_requires_both_openings() { + let consistency: ConsistencyClaim = + ConsistencyClaim::same_evaluation(Opening::A, Opening::B); + + assert_eq!( + consistency, + ConsistencyClaim::same_evaluation(Opening::A, Opening::B) + ); + assert_eq!( + consistency.required_openings(), + vec![Opening::A, Opening::B] + ); + assert!(consistency.required_publics().is_empty()); + assert!(consistency.required_challenges().is_empty()); + } + + #[test] + fn expression_consistency_claim_derives_metadata() { + let consistency: ConsistencyClaim = + ConsistencyClaim::equal_expressions( + opening(Opening::A) + challenge(Challenge::Alpha) * public(Public::Offset), + opening(Opening::B) + challenge(Challenge::Alpha) + challenge(Challenge::Beta), + ); + + assert_eq!( + consistency.required_openings(), + vec![Opening::A, Opening::B] + ); + assert_eq!(consistency.required_publics(), vec![Public::Offset]); + assert_eq!( + consistency.required_challenges(), + vec![Challenge::Alpha, Challenge::Beta] + ); + } + + #[test] + fn typed_challenges_have_canonical_order() { + let expression: Expr = challenge(Challenge::Beta) + * opening(Opening::B) + + challenge(Challenge::Alpha) * public(Public::Offset) + + challenge(Challenge::Beta); + let claim = ClaimExpression::from(expression.clone()); + + assert_eq!( + expression.required_challenges(), + vec![Challenge::Beta, Challenge::Alpha] + ); + assert_eq!( + claim.required_challenges, + vec![Challenge::Beta, Challenge::Alpha] + ); + assert_eq!(claim.challenge_index(&Challenge::Beta), Some(0)); + assert_eq!(claim.challenge_index(&Challenge::Alpha), Some(1)); + } +} diff --git a/crates/jolt-claims/src/lib.rs b/crates/jolt-claims/src/lib.rs new file mode 100644 index 0000000000..9656b9b84f --- /dev/null +++ b/crates/jolt-claims/src/lib.rs @@ -0,0 +1,11 @@ +//! Shared claim and expression types for Jolt protocols. + +mod claims; +mod ops; +pub mod protocols; +mod util; + +pub use claims::{ + challenge, constant, opening, public, ClaimExpression, ConsistencyClaim, Expr, + InputClaimExpression, OutputClaimExpression, Source, Term, +}; diff --git a/crates/jolt-claims/src/ops.rs b/crates/jolt-claims/src/ops.rs new file mode 100644 index 0000000000..653434b9e8 --- /dev/null +++ b/crates/jolt-claims/src/ops.rs @@ -0,0 +1,255 @@ +use std::ops::{Add, Mul, Neg, Sub}; + +use jolt_field::{FromPrimitiveInt, RingCore}; + +use crate::{Expr, Term}; + +impl From> for Expr { + fn from(term: Term) -> Self { + if term.coefficient.is_zero() && term.factors.is_empty() { + Self::zero() + } else { + Self { terms: vec![term] } + } + } +} + +impl From for Expr { + fn from(value: i128) -> Self { + Self::constant(F::from_i128(value)) + } +} + +impl Add for Expr { + type Output = Self; + + fn add(mut self, mut rhs: Self) -> Self::Output { + self.terms.append(&mut rhs.terms); + self + } +} + +impl Add<&Expr> for Expr { + type Output = Self; + + fn add(mut self, rhs: &Expr) -> Self::Output { + self.terms.extend(rhs.terms.iter().cloned()); + self + } +} + +impl Add> for &Expr { + type Output = Expr; + + fn add(self, rhs: Expr) -> Self::Output { + (*self).clone() + rhs + } +} + +impl Add<&Expr> for &Expr { + type Output = Expr; + + fn add(self, rhs: &Expr) -> Self::Output { + (*self).clone() + rhs + } +} + +impl Add for Expr { + type Output = Self; + + fn add(self, rhs: i128) -> Self::Output { + self + Self::from(rhs) + } +} + +impl Add> for i128 { + type Output = Expr; + + fn add(self, rhs: Expr) -> Self::Output { + Expr::from(self) + rhs + } +} + +impl Sub for Expr { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + -rhs + } +} + +impl Sub<&Expr> + for Expr +{ + type Output = Self; + + fn sub(self, rhs: &Expr) -> Self::Output { + self - rhs.clone() + } +} + +impl Sub> + for &Expr +{ + type Output = Expr; + + fn sub(self, rhs: Expr) -> Self::Output { + (*self).clone() - rhs + } +} + +impl Sub<&Expr> + for &Expr +{ + type Output = Expr; + + fn sub(self, rhs: &Expr) -> Self::Output { + (*self).clone() - rhs + } +} + +impl Sub for Expr { + type Output = Self; + + fn sub(self, rhs: i128) -> Self::Output { + self - Self::from(rhs) + } +} + +impl Sub> for i128 { + type Output = Expr; + + fn sub(self, rhs: Expr) -> Self::Output { + Expr::from(self) - rhs + } +} + +impl Neg for Expr { + type Output = Self; + + fn neg(mut self) -> Self::Output { + for term in &mut self.terms { + term.coefficient = -term.coefficient; + } + self + } +} + +impl Neg for &Expr { + type Output = Expr; + + fn neg(self) -> Self::Output { + -(*self).clone() + } +} + +impl Mul for Expr { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + if self.is_zero() || rhs.is_zero() { + return Self::zero(); + } + + let mut terms = Vec::with_capacity(self.terms.len() * rhs.terms.len()); + for lhs_term in self.terms { + for rhs_term in &rhs.terms { + let mut factors = lhs_term.factors.clone(); + factors.extend(rhs_term.factors.clone()); + terms.push(Term { + coefficient: lhs_term.coefficient * rhs_term.coefficient, + factors, + }); + } + } + Self { terms } + } +} + +impl Mul<&Expr> for Expr { + type Output = Self; + + fn mul(self, rhs: &Expr) -> Self::Output { + self * rhs.clone() + } +} + +impl Mul> for &Expr { + type Output = Expr; + + fn mul(self, rhs: Expr) -> Self::Output { + (*self).clone() * rhs + } +} + +impl Mul<&Expr> for &Expr { + type Output = Expr; + + fn mul(self, rhs: &Expr) -> Self::Output { + (*self).clone() * rhs + } +} + +impl Mul for Expr { + type Output = Self; + + fn mul(mut self, rhs: i128) -> Self::Output { + let rhs = F::from_i128(rhs); + if rhs.is_zero() { + return Self::zero(); + } + for term in &mut self.terms { + term.coefficient *= rhs; + } + self + } +} + +impl Mul> for i128 { + type Output = Expr; + + fn mul(self, rhs: Expr) -> Self::Output { + rhs * self + } +} + +#[cfg(test)] +mod tests { + use crate::{challenge, constant, opening, Expr}; + use jolt_field::{Fr, FromPrimitiveInt}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + enum Opening { + A, + B, + } + + #[test] + fn expression_ops_build_sum_of_products() { + let expr: Expr = + opening(Opening::A) * opening(Opening::B) + challenge(0) * opening(Opening::A) - 3; + + assert_eq!(expr.terms.len(), 3); + assert_eq!(expr.required_openings(), vec![Opening::A, Opening::B]); + assert_eq!(expr.num_challenges(), 1); + } + + #[test] + fn field_constants_scale_terms() { + let expr: Expr = + opening(Opening::A) * constant(Fr::from_u64(7)) + constant(Fr::from_u64(2)); + + let value = expr.evaluate_without_public(|_| Fr::from_u64(3), |_| Fr::from_u64(0)); + + assert_eq!(value, Fr::from_u64(23)); + } + + #[test] + fn zero_terms_are_canonical_empty_expression() { + let multiplier = i128::from(0); + let expr: Expr = opening(Opening::A) * multiplier; + assert!(expr.is_zero()); + let constant: Expr = constant(Fr::from_u64(0)); + assert_eq!(constant, Expr::zero()); + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/config.rs b/crates/jolt-claims/src/protocols/field_inline/config.rs new file mode 100644 index 0000000000..f935c1c335 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/config.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +pub const FIELD_REGISTERS_LOG_K: usize = 4; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum FieldInlineRepresentation { + NativeFieldElement, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FieldInlineConfig { + pub enabled: bool, + pub field_register_log_k: usize, + pub representation: FieldInlineRepresentation, +} + +impl FieldInlineConfig { + pub const fn disabled() -> Self { + Self { + enabled: false, + field_register_log_k: FIELD_REGISTERS_LOG_K, + representation: FieldInlineRepresentation::NativeFieldElement, + } + } + + pub const fn native_v1() -> Self { + Self { + enabled: true, + field_register_log_k: FIELD_REGISTERS_LOG_K, + representation: FieldInlineRepresentation::NativeFieldElement, + } + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/increments.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/increments.rs new file mode 100644 index 0000000000..e202b9a990 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/increments.rs @@ -0,0 +1,184 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening, public}; + +use super::super::super::{ + FieldInlineChallengeId, FieldInlineCommittedPolynomial, FieldInlineExpr, FieldInlineOpeningId, + FieldInlinePublicId, FieldInlineRelationClaims, FieldInlineRelationId, + FieldRegistersIncClaimReductionChallenge, FieldRegistersIncClaimReductionPublic, +}; +use super::super::dimensions::FieldRegistersTraceDimensions; + +pub fn claim_reduction(dimensions: FieldRegistersTraceDimensions) -> FieldInlineRelationClaims +where + F: RingCore, +{ + let eta = inc_challenge(FieldRegistersIncClaimReductionChallenge::Gamma); + + let input = + opening(field_rd_inc_read_write()) + eta.clone() * opening(field_rd_inc_val_evaluation()); + + let output_coeff = inc_public(FieldRegistersIncClaimReductionPublic::EqReadWrite) + + eta * inc_public(FieldRegistersIncClaimReductionPublic::EqValEvaluation); + let output = output_coeff * opening(field_rd_inc_reduced()); + + FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersIncClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +pub fn claim_reduction_input_openings() -> [FieldInlineOpeningId; 2] { + [field_rd_inc_read_write(), field_rd_inc_val_evaluation()] +} + +pub fn claim_reduction_output_openings() -> [FieldInlineOpeningId; 1] { + [field_rd_inc_reduced()] +} + +pub fn field_rd_inc_read_write_opening() -> FieldInlineOpeningId { + field_rd_inc_read_write() +} + +pub fn field_rd_inc_val_evaluation_opening() -> FieldInlineOpeningId { + field_rd_inc_val_evaluation() +} + +pub fn field_rd_inc_reduced_opening() -> FieldInlineOpeningId { + field_rd_inc_reduced() +} + +fn inc_challenge(id: FieldRegistersIncClaimReductionChallenge) -> FieldInlineExpr +where + F: RingCore, +{ + challenge(FieldInlineChallengeId::from(id)) +} + +fn inc_public(id: FieldRegistersIncClaimReductionPublic) -> FieldInlineExpr +where + F: RingCore, +{ + public(FieldInlinePublicId::from(id)) +} + +fn field_rd_inc_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rd_inc_val_evaluation() -> FieldInlineOpeningId { + FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersValEvaluation, + ) +} + +fn field_rd_inc_reduced() -> FieldInlineOpeningId { + FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersIncClaimReduction, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> FieldRegistersTraceDimensions { + FieldRegistersTraceDimensions::new(5) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() { + let claims = claim_reduction::(dimensions()); + + assert_eq!( + claims.id, + FieldInlineRelationId::FieldRegistersIncClaimReduction + ); + assert_eq!(claims.sumcheck, dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + claim_reduction_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + claim_reduction_output_openings().to_vec() + ); + assert_eq!( + claims.required_challenges(), + vec![FieldInlineChallengeId::from( + FieldRegistersIncClaimReductionChallenge::Gamma + )] + ); + assert_eq!( + claims.required_publics(), + vec![ + FieldInlinePublicId::from(FieldRegistersIncClaimReductionPublic::EqReadWrite), + FieldInlinePublicId::from(FieldRegistersIncClaimReductionPublic::EqValEvaluation), + ] + ); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn claim_reduction_evaluates_like_field_rd_inc_reduction_formula() { + let claims = claim_reduction::(dimensions()); + + let read_write_inc = Fr::from_u64(3); + let val_evaluation_inc = Fr::from_u64(5); + let reduced_inc = Fr::from_u64(7); + let eta = Fr::from_u64(11); + let eq_read_write = Fr::from_u64(13); + let eq_val_evaluation = Fr::from_u64(17); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == field_rd_inc_read_write() => read_write_inc, + id if id == field_rd_inc_val_evaluation() => val_evaluation_inc, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersIncClaimReduction( + FieldRegistersIncClaimReductionChallenge::Gamma, + ) => eta, + _ => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == field_rd_inc_reduced() => reduced_inc, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersIncClaimReduction( + FieldRegistersIncClaimReductionChallenge::Gamma, + ) => eta, + _ => zero, + }, + |id| match *id { + FieldInlinePublicId::FieldRegistersIncClaimReduction( + FieldRegistersIncClaimReductionPublic::EqReadWrite, + ) => eq_read_write, + FieldInlinePublicId::FieldRegistersIncClaimReduction( + FieldRegistersIncClaimReductionPublic::EqValEvaluation, + ) => eq_val_evaluation, + }, + ); + + assert_eq!(input, read_write_inc + eta * val_evaluation_inc); + assert_eq!( + output, + (eq_read_write + eta * eq_val_evaluation) * reduced_inc + ); + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/mod.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/mod.rs new file mode 100644 index 0000000000..95147d9f5a --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/mod.rs @@ -0,0 +1,2 @@ +pub mod increments; +pub mod registers; diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/registers.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/registers.rs new file mode 100644 index 0000000000..15a53bdd78 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/claim_reductions/registers.rs @@ -0,0 +1,213 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening}; + +use super::super::super::{ + FieldInlineChallengeId, FieldInlineExpr, FieldInlineOpeningId, FieldInlineRelationClaims, + FieldInlineRelationId, FieldInlineVirtualPolynomial, FieldRegistersClaimReductionChallenge, +}; +use super::super::dimensions::FieldRegistersTraceDimensions; + +pub fn claim_reduction(dimensions: FieldRegistersTraceDimensions) -> FieldInlineRelationClaims +where + F: RingCore, +{ + let gamma = reduction_challenge(FieldRegistersClaimReductionChallenge::Gamma); + let eq_spartan = reduction_challenge(FieldRegistersClaimReductionChallenge::EqSpartan); + + let input = opening(field_rd_value_spartan()) + + gamma.clone() * opening(field_rs1_value_spartan()) + + gamma.clone().pow(2) * opening(field_rs2_value_spartan()); + + let output = eq_spartan.clone() * opening(field_rd_value_reduced()) + + eq_spartan.clone() * gamma.clone() * opening(field_rs1_value_reduced()) + + eq_spartan * gamma.pow(2) * opening(field_rs2_value_reduced()); + + FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +pub fn claim_reduction_input_openings() -> [FieldInlineOpeningId; 3] { + [ + field_rd_value_spartan(), + field_rs1_value_spartan(), + field_rs2_value_spartan(), + ] +} + +pub fn claim_reduction_output_openings() -> [FieldInlineOpeningId; 3] { + [ + field_rd_value_reduced(), + field_rs1_value_reduced(), + field_rs2_value_reduced(), + ] +} + +fn reduction_challenge(id: FieldRegistersClaimReductionChallenge) -> FieldInlineExpr +where + F: RingCore, +{ + challenge(FieldInlineChallengeId::from(id)) +} + +fn field_rd_value_spartan() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRdValue, + FieldInlineRelationId::FieldRegistersSpartanOuter, + ) +} + +fn field_rs1_value_spartan() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs1Value, + FieldInlineRelationId::FieldRegistersSpartanOuter, + ) +} + +fn field_rs2_value_spartan() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs2Value, + FieldInlineRelationId::FieldRegistersSpartanOuter, + ) +} + +fn field_rd_value_reduced() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRdValue, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +fn field_rs1_value_reduced() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs1Value, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +fn field_rs2_value_reduced() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs2Value, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> FieldRegistersTraceDimensions { + FieldRegistersTraceDimensions::new(5) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() { + let claims = claim_reduction::(dimensions()); + + assert_eq!( + claims.id, + FieldInlineRelationId::FieldRegistersClaimReduction + ); + assert_eq!(claims.sumcheck, dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + claim_reduction_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + claim_reduction_output_openings().to_vec() + ); + assert_eq!( + claims.input.required_challenges, + vec![FieldInlineChallengeId::from( + FieldRegistersClaimReductionChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + FieldInlineChallengeId::from(FieldRegistersClaimReductionChallenge::EqSpartan), + FieldInlineChallengeId::from(FieldRegistersClaimReductionChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + FieldInlineChallengeId::from(FieldRegistersClaimReductionChallenge::Gamma), + FieldInlineChallengeId::from(FieldRegistersClaimReductionChallenge::EqSpartan), + ] + ); + assert_eq!( + claims.challenge_index(FieldInlineChallengeId::from( + FieldRegistersClaimReductionChallenge::EqSpartan + )), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn claim_reduction_evaluates_like_field_register_twist_formula() { + let claims = claim_reduction::(dimensions()); + + let rd_spartan = Fr::from_u64(3); + let rs1_spartan = Fr::from_u64(5); + let rs2_spartan = Fr::from_u64(7); + let rd_reduced = Fr::from_u64(11); + let rs1_reduced = Fr::from_u64(13); + let rs2_reduced = Fr::from_u64(17); + let gamma = Fr::from_u64(19); + let eq_spartan = Fr::from_u64(23); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == field_rd_value_spartan() => rd_spartan, + id if id == field_rs1_value_spartan() => rs1_spartan, + id if id == field_rs2_value_spartan() => rs2_spartan, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersClaimReduction( + FieldRegistersClaimReductionChallenge::Gamma, + ) => gamma, + _ => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == field_rd_value_reduced() => rd_reduced, + id if id == field_rs1_value_reduced() => rs1_reduced, + id if id == field_rs2_value_reduced() => rs2_reduced, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersClaimReduction( + FieldRegistersClaimReductionChallenge::Gamma, + ) => gamma, + FieldInlineChallengeId::FieldRegistersClaimReduction( + FieldRegistersClaimReductionChallenge::EqSpartan, + ) => eq_spartan, + _ => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + rd_spartan + gamma * rs1_spartan + gamma * gamma * rs2_spartan + ); + assert_eq!( + output, + eq_spartan * (rd_reduced + gamma * rs1_reduced + gamma * gamma * rs2_reduced) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/dimensions.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/dimensions.rs new file mode 100644 index 0000000000..97b149a1fd --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/dimensions.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; + +pub const FIELD_REGISTERS_ADDRESS_BITS: usize = super::super::FIELD_REGISTERS_LOG_K; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FieldInlineSumcheckSpec { + pub rounds: usize, + pub degree: usize, +} + +impl FieldInlineSumcheckSpec { + pub const fn boolean(rounds: usize, degree: usize) -> Self { + Self { rounds, degree } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct FieldRegistersTraceDimensions { + log_t: usize, +} + +impl FieldRegistersTraceDimensions { + pub const fn new(log_t: usize) -> Self { + Self { log_t } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn sumcheck(self, degree: usize) -> FieldInlineSumcheckSpec { + FieldInlineSumcheckSpec::boolean(self.log_t, degree) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct FieldRegistersReadWriteDimensions { + log_t: usize, + log_k: usize, + phase1_num_rounds: usize, + phase2_num_rounds: usize, +} + +impl FieldRegistersReadWriteDimensions { + pub const fn new( + log_t: usize, + log_k: usize, + phase1_num_rounds: usize, + phase2_num_rounds: usize, + ) -> Self { + Self { + log_t, + log_k, + phase1_num_rounds, + phase2_num_rounds, + } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn log_k(self) -> usize { + self.log_k + } + + pub const fn phase1_num_rounds(self) -> usize { + self.phase1_num_rounds + } + + pub const fn phase2_num_rounds(self) -> usize { + self.phase2_num_rounds + } + + pub const fn read_write_sumcheck(self) -> FieldInlineSumcheckSpec { + FieldInlineSumcheckSpec::boolean(self.log_t + self.log_k, 3) + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/mod.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/mod.rs new file mode 100644 index 0000000000..c27f833125 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/mod.rs @@ -0,0 +1,4 @@ +pub mod claim_reductions; +pub mod dimensions; +pub mod product; +pub mod registers; diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/product.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/product.rs new file mode 100644 index 0000000000..fd977e828c --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/product.rs @@ -0,0 +1,118 @@ +use jolt_field::RingCore; + +use crate::opening; + +use super::super::{ + FieldInlineOpeningId, FieldInlineRelationClaims, FieldInlineRelationId, + FieldInlineVirtualPolynomial, +}; +use super::dimensions::{FieldInlineSumcheckSpec, FieldRegistersTraceDimensions}; + +pub const fn field_product_sumcheck( + dimensions: FieldRegistersTraceDimensions, +) -> FieldInlineSumcheckSpec { + dimensions.sumcheck(2) +} + +pub fn field_product(dimensions: FieldRegistersTraceDimensions) -> FieldInlineRelationClaims +where + F: RingCore, +{ + FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersProduct, + field_product_sumcheck(dimensions), + opening(field_product_opening()), + opening(field_rs1_value_product()) * opening(field_rs2_value_product()), + ) +} + +pub fn field_product_input_openings() -> [FieldInlineOpeningId; 1] { + [field_product_opening()] +} + +pub fn field_product_output_openings() -> [FieldInlineOpeningId; 2] { + [field_rs1_value_product(), field_rs2_value_product()] +} + +fn field_product_opening() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldProduct, + FieldInlineRelationId::FieldRegistersProduct, + ) +} + +fn field_rs1_value_product() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs1Value, + FieldInlineRelationId::FieldRegistersProduct, + ) +} + +fn field_rs2_value_product() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs2Value, + FieldInlineRelationId::FieldRegistersProduct, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> FieldRegistersTraceDimensions { + FieldRegistersTraceDimensions::new(5) + } + + #[test] + fn field_product_claims_expose_expected_dependencies() { + let claims = field_product::(dimensions()); + + assert_eq!(claims.id, FieldInlineRelationId::FieldRegistersProduct); + assert_eq!(claims.sumcheck, field_product_sumcheck(dimensions())); + assert_eq!( + claims.input.required_openings, + field_product_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + field_product_output_openings().to_vec() + ); + assert!(claims.required_challenges().is_empty()); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 0); + } + + #[test] + fn field_product_claims_evaluate_native_field_product_relation() { + let claims = field_product::(dimensions()); + + let product = Fr::from_u64(35); + let rs1 = Fr::from_u64(5); + let rs2 = Fr::from_u64(7); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == field_product_opening() => product, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == field_rs1_value_product() => rs1, + id if id == field_rs2_value_product() => rs2, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + assert_eq!(input, product); + assert_eq!(output, rs1 * rs2); + assert_eq!(input, output); + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/formulas/registers.rs b/crates/jolt-claims/src/protocols/field_inline/formulas/registers.rs new file mode 100644 index 0000000000..b43e6039aa --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/formulas/registers.rs @@ -0,0 +1,394 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening}; + +use super::super::{ + FieldInlineChallengeId, FieldInlineCommittedPolynomial, FieldInlineExpr, FieldInlineOpeningId, + FieldInlineRelationClaims, FieldInlineRelationId, FieldInlineVirtualPolynomial, + FieldRegistersReadWriteChallenge, FieldRegistersValEvaluationChallenge, +}; +use super::dimensions::{ + FieldInlineSumcheckSpec, FieldRegistersReadWriteDimensions, FieldRegistersTraceDimensions, +}; + +pub const fn read_write_checking_sumcheck( + dimensions: FieldRegistersReadWriteDimensions, +) -> FieldInlineSumcheckSpec { + dimensions.read_write_sumcheck() +} + +pub fn read_write_checking( + dimensions: FieldRegistersReadWriteDimensions, +) -> FieldInlineRelationClaims +where + F: RingCore, +{ + let gamma = read_write_challenge(FieldRegistersReadWriteChallenge::Gamma); + let eq_cycle = read_write_challenge(FieldRegistersReadWriteChallenge::EqCycle); + + let input = opening(field_rd_value_claim()) + + gamma.clone() * opening(field_rs1_value_claim()) + + gamma.clone().pow(2) * opening(field_rs2_value_claim()); + + let output = + eq_cycle.clone() * opening(field_rd_wa_read_write()) * opening(field_rd_inc_read_write()) + + eq_cycle.clone() + * opening(field_rd_wa_read_write()) + * opening(field_registers_val_read_write()) + + eq_cycle.clone() + * gamma.clone() + * opening(field_rs1_ra_read_write()) + * opening(field_registers_val_read_write()) + + eq_cycle + * gamma.pow(2) + * opening(field_rs2_ra_read_write()) + * opening(field_registers_val_read_write()); + + FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersReadWriteChecking, + read_write_checking_sumcheck(dimensions), + input, + output, + ) +} + +pub fn val_evaluation(dimensions: FieldRegistersTraceDimensions) -> FieldInlineRelationClaims +where + F: RingCore, +{ + let input = opening(field_registers_val_read_write()); + let output = val_evaluation_challenge(FieldRegistersValEvaluationChallenge::LtCycle) + * opening(field_rd_inc_val_evaluation()) + * opening(field_rd_wa_val_evaluation()); + + FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersValEvaluation, + dimensions.sumcheck(3), + input, + output, + ) +} + +pub fn read_write_checking_input_openings() -> [FieldInlineOpeningId; 3] { + [ + field_rd_value_claim(), + field_rs1_value_claim(), + field_rs2_value_claim(), + ] +} + +pub fn read_write_checking_output_openings() -> [FieldInlineOpeningId; 5] { + [ + field_registers_val_read_write(), + field_rs1_ra_read_write(), + field_rs2_ra_read_write(), + field_rd_wa_read_write(), + field_rd_inc_read_write(), + ] +} + +pub fn val_evaluation_input_openings() -> [FieldInlineOpeningId; 1] { + [field_registers_val_read_write()] +} + +pub fn val_evaluation_output_openings() -> [FieldInlineOpeningId; 2] { + [field_rd_inc_val_evaluation(), field_rd_wa_val_evaluation()] +} + +fn read_write_challenge(id: FieldRegistersReadWriteChallenge) -> FieldInlineExpr +where + F: RingCore, +{ + challenge(FieldInlineChallengeId::from(id)) +} + +fn val_evaluation_challenge(id: FieldRegistersValEvaluationChallenge) -> FieldInlineExpr +where + F: RingCore, +{ + challenge(FieldInlineChallengeId::from(id)) +} + +fn field_rd_value_claim() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRdValue, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +fn field_rs1_value_claim() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs1Value, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +fn field_rs2_value_claim() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs2Value, + FieldInlineRelationId::FieldRegistersClaimReduction, + ) +} + +fn field_registers_val_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRegistersVal, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rs1_ra_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs1Ra, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rs2_ra_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRs2Ra, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rd_wa_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRdWa, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rd_inc_read_write() -> FieldInlineOpeningId { + FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ) +} + +fn field_rd_inc_val_evaluation() -> FieldInlineOpeningId { + FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersValEvaluation, + ) +} + +fn field_rd_wa_val_evaluation() -> FieldInlineOpeningId { + FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRdWa, + FieldInlineRelationId::FieldRegistersValEvaluation, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn trace_dimensions() -> FieldRegistersTraceDimensions { + FieldRegistersTraceDimensions::new(5) + } + + fn read_write_dimensions() -> FieldRegistersReadWriteDimensions { + FieldRegistersReadWriteDimensions::new(5, 4, 2, 1) + } + + #[test] + fn read_write_claims_expose_expected_dependencies() { + let claims = read_write_checking::(read_write_dimensions()); + + assert_eq!( + claims.id, + FieldInlineRelationId::FieldRegistersReadWriteChecking + ); + assert_eq!( + claims.sumcheck, + read_write_checking_sumcheck(read_write_dimensions()) + ); + assert_eq!( + claims.input.required_openings, + read_write_checking_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + vec![ + field_rd_wa_read_write(), + field_rd_inc_read_write(), + field_registers_val_read_write(), + field_rs1_ra_read_write(), + field_rs2_ra_read_write(), + ] + ); + assert_eq!( + read_write_checking_output_openings(), + [ + field_registers_val_read_write(), + field_rs1_ra_read_write(), + field_rs2_ra_read_write(), + field_rd_wa_read_write(), + field_rd_inc_read_write(), + ] + ); + assert_eq!( + claims.input.required_challenges, + vec![FieldInlineChallengeId::from( + FieldRegistersReadWriteChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + FieldInlineChallengeId::from(FieldRegistersReadWriteChallenge::EqCycle), + FieldInlineChallengeId::from(FieldRegistersReadWriteChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + FieldInlineChallengeId::from(FieldRegistersReadWriteChallenge::Gamma), + FieldInlineChallengeId::from(FieldRegistersReadWriteChallenge::EqCycle), + ] + ); + assert_eq!( + claims.challenge_index(FieldInlineChallengeId::from( + FieldRegistersReadWriteChallenge::EqCycle + )), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn read_write_claims_evaluate_like_field_register_twist_formula() { + let claims = read_write_checking::(read_write_dimensions()); + + let rd_value = Fr::from_u64(3); + let rs1_value = Fr::from_u64(5); + let rs2_value = Fr::from_u64(7); + let val = Fr::from_u64(11); + let rs1_ra = Fr::from_u64(13); + let rs2_ra = Fr::from_u64(17); + let rd_wa = Fr::from_u64(19); + let inc = Fr::from_u64(23); + let gamma = Fr::from_u64(29); + let eq_cycle = Fr::from_u64(31); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == field_rd_value_claim() => rd_value, + id if id == field_rs1_value_claim() => rs1_value, + id if id == field_rs2_value_claim() => rs2_value, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersReadWrite( + FieldRegistersReadWriteChallenge::Gamma, + ) => gamma, + _ => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == field_registers_val_read_write() => val, + id if id == field_rs1_ra_read_write() => rs1_ra, + id if id == field_rs2_ra_read_write() => rs2_ra, + id if id == field_rd_wa_read_write() => rd_wa, + id if id == field_rd_inc_read_write() => inc, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersReadWrite( + FieldRegistersReadWriteChallenge::EqCycle, + ) => eq_cycle, + FieldInlineChallengeId::FieldRegistersReadWrite( + FieldRegistersReadWriteChallenge::Gamma, + ) => gamma, + _ => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + rd_value + gamma * rs1_value + gamma * gamma * rs2_value + ); + assert_eq!( + output, + eq_cycle * (rd_wa * (inc + val) + gamma * rs1_ra * val + gamma * gamma * rs2_ra * val) + ); + } + + #[test] + fn val_evaluation_claims_expose_expected_dependencies() { + let claims = val_evaluation::(trace_dimensions()); + + assert_eq!( + claims.id, + FieldInlineRelationId::FieldRegistersValEvaluation + ); + assert_eq!(claims.sumcheck, trace_dimensions().sumcheck(3)); + assert_eq!( + claims.input.required_openings, + val_evaluation_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + val_evaluation_output_openings().to_vec() + ); + assert_eq!( + claims.output.required_challenges, + vec![FieldInlineChallengeId::from( + FieldRegistersValEvaluationChallenge::LtCycle + )] + ); + assert_eq!( + claims.required_challenges(), + vec![FieldInlineChallengeId::from( + FieldRegistersValEvaluationChallenge::LtCycle + )] + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn val_evaluation_claims_evaluate_like_field_register_twist_formula() { + let claims = val_evaluation::(trace_dimensions()); + + let val = Fr::from_u64(3); + let inc = Fr::from_u64(5); + let wa = Fr::from_u64(7); + let lt_cycle = Fr::from_u64(11); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == field_registers_val_read_write() => val, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == field_rd_inc_val_evaluation() => inc, + id if id == field_rd_wa_val_evaluation() => wa, + _ => zero, + }, + |id| match *id { + FieldInlineChallengeId::FieldRegistersValEvaluation( + FieldRegistersValEvaluationChallenge::LtCycle, + ) => lt_cycle, + _ => zero, + }, + |_| zero, + ); + + assert_eq!(input, val); + assert_eq!(output, lt_cycle * inc * wa); + } +} diff --git a/crates/jolt-claims/src/protocols/field_inline/ids.rs b/crates/jolt-claims/src/protocols/field_inline/ids.rs new file mode 100644 index 0000000000..72907ab4b9 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/ids.rs @@ -0,0 +1,116 @@ +use derive_more::From; +use serde::{Deserialize, Serialize}; + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldInlineRelationId { + FieldRegistersSpartanOuter, + FieldRegistersClaimReduction, + FieldRegistersProduct, + FieldRegistersReadWriteChecking, + FieldRegistersValEvaluation, + FieldRegistersIncClaimReduction, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldRegistersClaimReductionChallenge { + Gamma, + EqSpartan, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldRegistersReadWriteChallenge { + Gamma, + EqCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldRegistersValEvaluationChallenge { + LtCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldRegistersIncClaimReductionChallenge { + Gamma, +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum FieldInlineChallengeId { + FieldRegistersClaimReduction(FieldRegistersClaimReductionChallenge), + FieldRegistersReadWrite(FieldRegistersReadWriteChallenge), + FieldRegistersValEvaluation(FieldRegistersValEvaluationChallenge), + FieldRegistersIncClaimReduction(FieldRegistersIncClaimReductionChallenge), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldInlineCommittedPolynomial { + FieldRdInc, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldInlineVirtualPolynomial { + FieldRs1Value, + FieldRs2Value, + FieldRdValue, + FieldProduct, + FieldRs1Ra, + FieldRs2Ra, + FieldRdWa, + FieldRegistersVal, +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum FieldInlinePolynomialId { + Committed(FieldInlineCommittedPolynomial), + Virtual(FieldInlineVirtualPolynomial), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldInlineOpeningId { + Polynomial { + polynomial: FieldInlinePolynomialId, + relation: FieldInlineRelationId, + }, +} + +impl FieldInlineOpeningId { + pub fn polynomial( + polynomial: impl Into, + relation: FieldInlineRelationId, + ) -> Self { + Self::Polynomial { + polynomial: polynomial.into(), + relation, + } + } + + pub fn committed( + polynomial: FieldInlineCommittedPolynomial, + relation: FieldInlineRelationId, + ) -> Self { + Self::polynomial(polynomial, relation) + } + + pub fn virtual_polynomial( + polynomial: FieldInlineVirtualPolynomial, + relation: FieldInlineRelationId, + ) -> Self { + Self::polynomial(polynomial, relation) + } +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum FieldRegistersIncClaimReductionPublic { + EqReadWrite, + EqValEvaluation, +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum FieldInlinePublicId { + FieldRegistersIncClaimReduction(FieldRegistersIncClaimReductionPublic), +} diff --git a/crates/jolt-claims/src/protocols/field_inline/mod.rs b/crates/jolt-claims/src/protocols/field_inline/mod.rs new file mode 100644 index 0000000000..42b9c1f9d9 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/mod.rs @@ -0,0 +1,21 @@ +pub mod formulas; + +mod config; +mod ids; +mod relation; + +pub use config::{FieldInlineConfig, FieldInlineRepresentation, FIELD_REGISTERS_LOG_K}; +pub use formulas::dimensions::{ + FieldInlineSumcheckSpec, FieldRegistersReadWriteDimensions, FieldRegistersTraceDimensions, +}; +pub use ids::{ + FieldInlineChallengeId, FieldInlineCommittedPolynomial, FieldInlineOpeningId, + FieldInlinePolynomialId, FieldInlinePublicId, FieldInlineRelationId, + FieldInlineVirtualPolynomial, FieldRegistersClaimReductionChallenge, + FieldRegistersIncClaimReductionChallenge, FieldRegistersIncClaimReductionPublic, + FieldRegistersReadWriteChallenge, FieldRegistersValEvaluationChallenge, +}; +pub use relation::{ + FieldInlineConsistencyClaim, FieldInlineExpr, FieldInlineInputClaimExpression, + FieldInlineOutputClaimExpression, FieldInlineProtocolClaims, FieldInlineRelationClaims, +}; diff --git a/crates/jolt-claims/src/protocols/field_inline/relation.rs b/crates/jolt-claims/src/protocols/field_inline/relation.rs new file mode 100644 index 0000000000..312c109848 --- /dev/null +++ b/crates/jolt-claims/src/protocols/field_inline/relation.rs @@ -0,0 +1,254 @@ +use serde::{Deserialize, Serialize}; + +use crate::util::extend_unique; +use crate::{ClaimExpression, ConsistencyClaim, Expr, InputClaimExpression, OutputClaimExpression}; + +use super::{ + FieldInlineChallengeId, FieldInlineOpeningId, FieldInlinePublicId, FieldInlineRelationId, + FieldInlineSumcheckSpec, +}; + +pub type FieldInlineExpr = + Expr; +pub type FieldInlineInputClaimExpression = + InputClaimExpression; +pub type FieldInlineOutputClaimExpression = + OutputClaimExpression; +pub type FieldInlineConsistencyClaim = + ConsistencyClaim; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FieldInlineRelationClaims { + pub id: FieldInlineRelationId, + pub sumcheck: FieldInlineSumcheckSpec, + pub input: FieldInlineInputClaimExpression, + pub output: FieldInlineOutputClaimExpression, + pub consistency: Vec>, +} + +impl FieldInlineRelationClaims { + pub fn new( + id: FieldInlineRelationId, + sumcheck: FieldInlineSumcheckSpec, + input: FieldInlineExpr, + output: FieldInlineExpr, + ) -> Self { + Self { + id, + sumcheck, + input: ClaimExpression::from(input), + output: ClaimExpression::from(output), + consistency: Vec::new(), + } + } + + pub fn with_consistency(mut self, consistency: I) -> Self + where + I: IntoIterator, + C: Into>, + { + self.consistency + .extend(consistency.into_iter().map(Into::into)); + self + } + + pub fn push_consistency(&mut self, consistency: C) + where + C: Into>, + { + self.consistency.push(consistency.into()); + } + + pub fn with_input_challenges(mut self, challenges: I) -> Self + where + I: IntoIterator, + { + self.input.pull_challenges_for_transcript_sync(challenges); + self + } + + pub fn required_openings(&self) -> Vec { + let mut openings = self.input.required_openings.clone(); + extend_unique(&mut openings, &self.output.required_openings); + for consistency in &self.consistency { + extend_unique(&mut openings, &consistency.required_openings()); + } + openings + } + + pub fn required_publics(&self) -> Vec { + let mut publics = self.input.required_publics.clone(); + extend_unique(&mut publics, &self.output.required_publics); + for consistency in &self.consistency { + extend_unique(&mut publics, &consistency.required_publics()); + } + publics + } + + pub fn required_challenges(&self) -> Vec { + let mut challenges = self.input.required_challenges.clone(); + extend_unique(&mut challenges, &self.output.required_challenges); + for consistency in &self.consistency { + extend_unique(&mut challenges, &consistency.required_challenges()); + } + challenges + } + + pub fn challenge_index(&self, id: FieldInlineChallengeId) -> Option { + self.required_challenges() + .iter() + .position(|challenge| *challenge == id) + } + + pub fn num_challenges(&self) -> usize { + self.required_challenges().len() + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct FieldInlineProtocolClaims { + pub relations: Vec>, +} + +impl FieldInlineProtocolClaims { + pub fn new(relations: Vec>) -> Self { + debug_assert_unique_relation_ids(&relations); + Self { relations } + } + + pub fn push(&mut self, relation: FieldInlineRelationClaims) { + self.relations.push(relation); + } + + pub fn iter(&self) -> std::slice::Iter<'_, FieldInlineRelationClaims> { + self.relations.iter() + } + + pub fn relation(&self, id: FieldInlineRelationId) -> Option<&FieldInlineRelationClaims> { + self.relations.iter().find(|relation| relation.id == id) + } + + pub fn len(&self) -> usize { + self.relations.len() + } + + pub fn is_empty(&self) -> bool { + self.relations.is_empty() + } + + pub fn required_openings(&self) -> Vec { + let mut openings = Vec::new(); + for relation in &self.relations { + extend_unique(&mut openings, &relation.required_openings()); + } + openings + } + + pub fn required_publics(&self) -> Vec { + let mut publics = Vec::new(); + for relation in &self.relations { + extend_unique(&mut publics, &relation.required_publics()); + } + publics + } + + pub fn required_challenges(&self) -> Vec { + let mut challenges = Vec::new(); + for relation in &self.relations { + extend_unique(&mut challenges, &relation.required_challenges()); + } + challenges + } +} + +impl<'a, F> IntoIterator for &'a FieldInlineProtocolClaims { + type Item = &'a FieldInlineRelationClaims; + type IntoIter = std::slice::Iter<'a, FieldInlineRelationClaims>; + + fn into_iter(self) -> Self::IntoIter { + self.relations.iter() + } +} + +fn debug_assert_unique_relation_ids(relations: &[FieldInlineRelationClaims]) { + debug_assert!( + relations + .iter() + .enumerate() + .all(|(index, relation)| !relations[..index].iter().any(|prev| prev.id == relation.id)), + "field-inline protocol claims contain duplicate relation IDs" + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::opening; + use jolt_field::Fr; + + use super::super::{FieldInlineCommittedPolynomial, FieldInlineVirtualPolynomial}; + + #[test] + fn protocol_claims_preserve_relation_order_and_deduplicate_dependencies() { + let rd_inc = FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ); + let registers_val = FieldInlineOpeningId::virtual_polynomial( + FieldInlineVirtualPolynomial::FieldRegistersVal, + FieldInlineRelationId::FieldRegistersValEvaluation, + ); + + let read_write: FieldInlineRelationClaims = FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersReadWriteChecking, + FieldInlineSumcheckSpec::boolean(12, 3), + opening(rd_inc), + opening(registers_val), + ); + let val_evaluation: FieldInlineRelationClaims = FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersValEvaluation, + FieldInlineSumcheckSpec::boolean(8, 3), + opening(registers_val), + opening(rd_inc), + ); + let protocol = FieldInlineProtocolClaims::new(vec![read_write, val_evaluation]); + + let relation_ids = protocol + .iter() + .map(|relation| relation.id) + .collect::>(); + + assert_eq!( + relation_ids, + vec![ + FieldInlineRelationId::FieldRegistersReadWriteChecking, + FieldInlineRelationId::FieldRegistersValEvaluation, + ] + ); + assert_eq!( + protocol + .relation(FieldInlineRelationId::FieldRegistersValEvaluation) + .map(|relation| relation.id), + Some(FieldInlineRelationId::FieldRegistersValEvaluation) + ); + assert_eq!(protocol.required_openings(), vec![rd_inc, registers_val]); + } + + #[cfg(debug_assertions)] + #[test] + #[should_panic(expected = "field-inline protocol claims contain duplicate relation IDs")] + fn protocol_claims_reject_duplicate_relation_ids_in_debug() { + let rd_inc = FieldInlineOpeningId::committed( + FieldInlineCommittedPolynomial::FieldRdInc, + FieldInlineRelationId::FieldRegistersReadWriteChecking, + ); + let relation: FieldInlineRelationClaims = FieldInlineRelationClaims::new( + FieldInlineRelationId::FieldRegistersReadWriteChecking, + FieldInlineSumcheckSpec::boolean(12, 3), + opening(rd_inc), + opening(rd_inc), + ); + + let _ = FieldInlineProtocolClaims::new(vec![relation.clone(), relation]); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/booleanity.rs b/crates/jolt-claims/src/protocols/jolt/formulas/booleanity.rs new file mode 100644 index 0000000000..c1e5593e7b --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/booleanity.rs @@ -0,0 +1,232 @@ +use jolt_field::{Field, RingCore}; + +use crate::{challenge, opening, public}; + +use super::super::{ + BooleanityChallenge, BooleanityPublic, JoltChallengeId, JoltExpr, JoltOpeningId, JoltPublicId, + JoltRelationClaims, JoltRelationId, +}; +use super::dimensions::{JoltFormulaPointError, JoltSumcheckSpec}; +use super::ra::JoltRaPolynomialLayout; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BooleanityDimensions { + pub layout: JoltRaPolynomialLayout, + pub log_t: usize, + pub log_k_chunk: usize, +} + +impl BooleanityDimensions { + pub const fn new(layout: JoltRaPolynomialLayout, log_t: usize, log_k_chunk: usize) -> Self { + Self { + layout, + log_t, + log_k_chunk, + } + } + + pub const fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t + self.log_k_chunk, 3) + } + + pub fn opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let expected = self.log_t + self.log_k_chunk; + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let (r_address, r_cycle) = challenges.split_at(self.log_k_chunk); + let r_address = r_address.iter().rev().copied().collect::>(); + let r_cycle = r_cycle.iter().rev().copied().collect::>(); + let opening_point = [r_address.as_slice(), r_cycle.as_slice()].concat(); + + Ok(BooleanityOpeningPoint { + r_address, + r_cycle, + opening_point, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BooleanityOpeningPoint { + pub r_address: Vec, + pub r_cycle: Vec, + pub opening_point: Vec, +} + +pub fn booleanity(dimensions: BooleanityDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = booleanity_challenge(BooleanityChallenge::Gamma); + let eq_address_cycle = booleanity_public(BooleanityPublic::EqAddressCycle); + let mut output = JoltExpr::zero(); + + for (i, opening_id) in booleanity_output_openings(dimensions.layout) + .into_iter() + .enumerate() + { + let ra = opening(opening_id); + output = output + gamma.clone().pow(2 * i) * (ra.clone() * ra.clone() - ra); + } + + JoltRelationClaims::new( + JoltRelationId::Booleanity, + dimensions.sumcheck(), + JoltExpr::zero(), + eq_address_cycle * output, + ) + .with_input_challenges([JoltChallengeId::from(BooleanityChallenge::Gamma)]) +} + +fn booleanity_challenge(id: BooleanityChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn booleanity_public(id: BooleanityPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +pub fn booleanity_output_openings(layout: JoltRaPolynomialLayout) -> Vec { + layout.openings(JoltRelationId::Booleanity).collect() +} + +#[cfg(test)] +mod tests { + use super::super::super::JoltCommittedPolynomial; + use super::super::dimensions::JoltFormulaDimensionsError; + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn layout( + instruction: usize, + bytecode: usize, + ram: usize, + ) -> Result { + JoltRaPolynomialLayout::new(instruction, bytecode, ram) + } + + fn dimensions(layout: JoltRaPolynomialLayout) -> BooleanityDimensions { + BooleanityDimensions::new(layout, 5, 8) + } + + #[test] + fn booleanity_exposes_expected_dependencies() -> Result<(), JoltFormulaDimensionsError> { + let layout = layout(1, 1, 1)?; + let claims = booleanity::(dimensions(layout)); + + assert_eq!(claims.id, JoltRelationId::Booleanity); + assert_eq!(claims.sumcheck, JoltSumcheckSpec::boolean(13, 3)); + assert!(claims.input.required_openings.is_empty()); + assert_eq!( + claims.output.required_openings, + booleanity_output_openings(layout) + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(BooleanityChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from(BooleanityChallenge::Gamma)] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(BooleanityChallenge::Gamma)] + ); + assert_eq!( + claims.required_publics(), + vec![JoltPublicId::from(BooleanityPublic::EqAddressCycle)] + ); + assert_eq!(claims.num_challenges(), 1); + Ok(()) + } + + #[test] + fn booleanity_preserves_gamma_dependency_for_single_polynomial( + ) -> Result<(), JoltFormulaDimensionsError> { + let claims = booleanity::(dimensions(layout(1, 0, 0)?)); + + assert!(claims.output.required_challenges.is_empty()); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(BooleanityChallenge::Gamma)] + ); + Ok(()) + } + + #[test] + fn booleanity_evaluates_like_core_formula() -> Result<(), JoltFormulaDimensionsError> { + let layout = layout(1, 1, 1)?; + let claims = booleanity::(dimensions(layout)); + + let instruction_ra = Fr::from_u64(3); + let bytecode_ra = Fr::from_u64(5); + let ram_ra = Fr::from_u64(7); + let gamma = Fr::from_u64(11); + let eq_address_cycle = Fr::from_u64(13); + let zero = Fr::from_u64(0); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id + == JoltOpeningId::committed( + JoltCommittedPolynomial::InstructionRa(0), + JoltRelationId::Booleanity, + ) => + { + instruction_ra + } + id if id + == JoltOpeningId::committed( + JoltCommittedPolynomial::BytecodeRa(0), + JoltRelationId::Booleanity, + ) => + { + bytecode_ra + } + id if id + == JoltOpeningId::committed( + JoltCommittedPolynomial::RamRa(0), + JoltRelationId::Booleanity, + ) => + { + ram_ra + } + _ => zero, + }, + |id| match *id { + JoltChallengeId::Booleanity(BooleanityChallenge::Gamma) => gamma, + _ => zero, + }, + |id| match *id { + JoltPublicId::Booleanity(BooleanityPublic::EqAddressCycle) => eq_address_cycle, + _ => zero, + }, + ); + + let gamma_2 = gamma * gamma; + let gamma_4 = gamma_2 * gamma_2; + assert_eq!( + output, + eq_address_cycle + * ((instruction_ra * instruction_ra - instruction_ra) + + gamma_2 * (bytecode_ra * bytecode_ra - bytecode_ra) + + gamma_4 * (ram_ra * ram_ra - ram_ra)) + ); + Ok(()) + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/formulas/bytecode.rs new file mode 100644 index 0000000000..d3f9ce3fb6 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/bytecode.rs @@ -0,0 +1,1102 @@ +use jolt_field::{Field, RingCore}; +use jolt_lookup_tables::{InstructionLookupTable, LookupTableKind, XLEN}; +use jolt_poly::{EqPolynomial, IdentityPolynomial, MultilinearEvaluation}; +use jolt_riscv::{ + instructions::Noop, CircuitFlags, Flags, InstructionFlags, InterleavedBitsMarker, + JoltInstruction, JoltInstructionRow, CIRCUIT_FLAGS, NUM_CIRCUIT_FLAGS, +}; + +use crate::{challenge, opening, public}; + +use super::super::{ + BytecodeReadRafChallenge, BytecodeReadRafPublic, JoltChallengeId, JoltCommittedPolynomial, + JoltConsistencyClaim, JoltExpr, JoltOpeningId, JoltPublicId, JoltRelationClaims, + JoltRelationId, JoltVirtualPolynomial, +}; +use super::dimensions::{JoltFormulaPointError, JoltSumcheckSpec}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafDimensions { + log_t: usize, + log_k: usize, + committed_ra_polys: usize, +} + +impl BytecodeReadRafDimensions { + pub const fn new(log_t: usize, log_k: usize, committed_ra_polys: usize) -> Self { + Self { + log_t, + log_k, + committed_ra_polys, + } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn log_k(self) -> usize { + self.log_k + } + + pub const fn num_committed_ra_polys(self) -> usize { + self.committed_ra_polys + } + + pub const fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t + self.log_k, self.committed_ra_polys + 1) + } + + pub fn opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let expected = self.log_k + self.log_t; + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let (r_address, r_cycle) = challenges.split_at(self.log_k); + let r_address = r_address.iter().rev().copied().collect::>(); + let r_cycle = r_cycle.iter().rev().copied().collect::>(); + let opening_point = [r_address.as_slice(), r_cycle.as_slice()].concat(); + + Ok(BytecodeReadRafOpeningPoint { + r_address, + r_cycle, + opening_point, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafOpeningPoint { + pub r_address: Vec, + pub r_cycle: Vec, + pub opening_point: Vec, +} + +pub fn read_raf(dimensions: BytecodeReadRafDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = bytecode_challenge(BytecodeReadRafChallenge::Gamma); + + let input = gamma.clone().pow(7) + + stage1_claim() + + gamma.clone() * stage2_claim() + + gamma.clone().pow(2) * stage3_claim() + + gamma.clone().pow(3) * stage4_claim() + + gamma.clone().pow(4) * stage5_claim::() + + gamma.clone().pow(5) * opening(pc_spartan_outer()) + + gamma.pow(6) * opening(pc_spartan_shift()); + + let gamma = bytecode_challenge(BytecodeReadRafChallenge::Gamma); + let output_coeff = bytecode_public(BytecodeReadRafPublic::StageValue(0)) + + gamma.clone() * bytecode_public(BytecodeReadRafPublic::StageValue(1)) + + gamma.clone().pow(2) * bytecode_public(BytecodeReadRafPublic::StageValue(2)) + + gamma.clone().pow(3) * bytecode_public(BytecodeReadRafPublic::StageValue(3)) + + gamma.clone().pow(4) * bytecode_public(BytecodeReadRafPublic::StageValue(4)) + + gamma.clone().pow(5) * bytecode_public(BytecodeReadRafPublic::SpartanOuterRaf) + + gamma.clone().pow(6) * bytecode_public(BytecodeReadRafPublic::SpartanShiftRaf) + + gamma.pow(7) * bytecode_public(BytecodeReadRafPublic::Entry); + + JoltRelationClaims::new( + JoltRelationId::BytecodeReadRaf, + dimensions.sumcheck(), + input, + output_coeff * bytecode_ra_product(dimensions), + ) + .with_input_challenges([ + JoltChallengeId::from(BytecodeReadRafChallenge::Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage1Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage2Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage3Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage4Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage5Gamma), + ]) + .with_consistency([JoltConsistencyClaim::same_evaluation( + unexpanded_pc_spartan_shift(), + unexpanded_pc_instruction_input(), + )]) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafInputOpenings { + pub spartan_outer: BytecodeReadRafSpartanOuterOpenings, + pub spartan_product: BytecodeReadRafSpartanProductOpenings, + pub instruction_input: BytecodeReadRafInstructionInputOpenings, + pub spartan_shift: BytecodeReadRafSpartanShiftOpenings, + pub registers_read_write: BytecodeReadRafRegistersReadWriteOpenings, + pub registers_val_evaluation: BytecodeReadRafRegistersValEvaluationOpenings, + pub spartan_outer_pc: JoltOpeningId, + pub spartan_shift_pc: JoltOpeningId, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafSpartanOuterOpenings { + pub unexpanded_pc: JoltOpeningId, + pub imm: JoltOpeningId, + pub op_flags: Vec<(CircuitFlags, JoltOpeningId)>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafSpartanProductOpenings { + pub jump: JoltOpeningId, + pub branch: JoltOpeningId, + pub write_lookup_output_to_rd: JoltOpeningId, + pub virtual_instruction: JoltOpeningId, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafInstructionInputOpenings { + pub imm: JoltOpeningId, + pub unexpanded_pc_from_shift: JoltOpeningId, + pub left_operand_is_rs1_value: JoltOpeningId, + pub left_operand_is_pc: JoltOpeningId, + pub right_operand_is_rs2_value: JoltOpeningId, + pub right_operand_is_imm: JoltOpeningId, + pub is_noop_from_shift: JoltOpeningId, + pub virtual_instruction_from_shift: JoltOpeningId, + pub is_first_in_sequence_from_shift: JoltOpeningId, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafSpartanShiftOpenings { + pub unexpanded_pc: JoltOpeningId, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafRegistersReadWriteOpenings { + pub rd_wa: JoltOpeningId, + pub rs1_ra: JoltOpeningId, + pub rs2_ra: JoltOpeningId, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafRegistersValEvaluationOpenings { + pub rd_wa: JoltOpeningId, + pub instruction_raf_flag: JoltOpeningId, + pub lookup_table_flags: Vec<(LookupTableKind, JoltOpeningId)>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafOutputOpenings { + pub bytecode_ra: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BytecodeReadRafPublicValues { + pub stage_values: [F; 5], + pub spartan_outer_raf: F, + pub spartan_shift_raf: F, + pub entry: F, +} + +impl BytecodeReadRafPublicValues { + pub fn value(&self, id: BytecodeReadRafPublic) -> F { + match id { + BytecodeReadRafPublic::StageValue(index) => self + .stage_values + .get(index) + .copied() + .unwrap_or_else(F::zero), + BytecodeReadRafPublic::SpartanOuterRaf => self.spartan_outer_raf, + BytecodeReadRafPublic::SpartanShiftRaf => self.spartan_shift_raf, + BytecodeReadRafPublic::Entry => self.entry, + } + } +} + +pub struct BytecodeReadRafEvaluationInputs<'a, F> { + pub bytecode: &'a [JoltInstructionRow], + pub r_address: &'a [F], + pub r_cycle: &'a [F], + pub stage_cycle_points: [&'a [F]; 5], + pub register_read_write_point: &'a [F], + pub register_val_evaluation_point: &'a [F], + pub entry_bytecode_index: usize, + pub stage1_gammas: &'a [F], + pub stage2_gammas: &'a [F], + pub stage3_gammas: &'a [F], + pub stage4_gammas: &'a [F], + pub stage5_gammas: &'a [F], +} + +pub fn read_raf_public_values( + inputs: BytecodeReadRafEvaluationInputs<'_, F>, +) -> Result, JoltFormulaPointError> +where + F: Field, +{ + require_len(inputs.stage1_gammas, 2 + NUM_CIRCUIT_FLAGS)?; + require_len(inputs.stage2_gammas, 4)?; + require_len(inputs.stage3_gammas, 9)?; + require_len(inputs.stage4_gammas, 3)?; + require_len(inputs.stage5_gammas, 2 + LookupTableKind::::COUNT)?; + + let expected_domain = 1usize << inputs.r_address.len(); + if inputs.bytecode.len() != expected_domain { + return Err(JoltFormulaPointError::EvaluationDomainLengthMismatch { + expected: expected_domain, + got: inputs.bytecode.len(), + }); + } + + let address_eq_evals = EqPolynomial::::evals(inputs.r_address, None); + let register_read_write_eq = EqPolynomial::::evals(inputs.register_read_write_point, None); + let register_val_evaluation_eq = + EqPolynomial::::evals(inputs.register_val_evaluation_point, None); + + let mut stage_values = [F::zero(); 5]; + for (instruction, eq_address) in inputs.bytecode.iter().zip(address_eq_evals) { + let row_values = read_raf_row_values::( + instruction, + ®ister_read_write_eq, + ®ister_val_evaluation_eq, + inputs.stage1_gammas, + inputs.stage2_gammas, + inputs.stage3_gammas, + inputs.stage4_gammas, + inputs.stage5_gammas, + ); + for (stage_value, row_value) in stage_values.iter_mut().zip(row_values) { + *stage_value += row_value * eq_address; + } + } + + for (stage_value, stage_cycle_point) in stage_values.iter_mut().zip(inputs.stage_cycle_points) { + *stage_value *= EqPolynomial::::mle(stage_cycle_point, inputs.r_cycle); + } + + let identity = IdentityPolynomial::new(inputs.r_address.len()).evaluate(inputs.r_address); + let spartan_outer_raf = + identity * EqPolynomial::::mle(inputs.stage_cycle_points[0], inputs.r_cycle); + let spartan_shift_raf = + identity * EqPolynomial::::mle(inputs.stage_cycle_points[2], inputs.r_cycle); + + let entry_bits = (0..inputs.r_address.len()) + .map(|i| { + F::from_u64( + ((inputs.entry_bytecode_index >> (inputs.r_address.len() - 1 - i)) & 1) as u64, + ) + }) + .collect::>(); + let zero_cycle = vec![F::zero(); inputs.r_cycle.len()]; + let entry = EqPolynomial::::mle(&entry_bits, inputs.r_address) + * EqPolynomial::::mle(&zero_cycle, inputs.r_cycle); + + Ok(BytecodeReadRafPublicValues { + stage_values, + spartan_outer_raf, + spartan_shift_raf, + entry, + }) +} + +#[expect( + clippy::too_many_arguments, + reason = "Each gamma slice corresponds to one protocol subexpression." +)] +fn read_raf_row_values( + instruction: &JoltInstructionRow, + register_read_write_eq: &[F], + register_val_evaluation_eq: &[F], + stage1_gammas: &[F], + stage2_gammas: &[F], + stage3_gammas: &[F], + stage4_gammas: &[F], + stage5_gammas: &[F], +) -> [F; 5] +where + F: Field, +{ + let decoded = JoltInstruction::try_from(*instruction) + .unwrap_or(JoltInstruction::Noop(Noop(*instruction))); + let circuit_flags = decoded.circuit_flags(); + let instruction_flags = decoded.instruction_flags(); + + let mut stage1 = F::from_u64(instruction.address as u64); + stage1 += stage1_gammas[1].mul_i128(instruction.operands.imm); + for (index, flag) in CIRCUIT_FLAGS.into_iter().enumerate() { + if circuit_flags[flag] { + stage1 += stage1_gammas[index + 2]; + } + } + + let mut stage2 = F::zero(); + if circuit_flags[CircuitFlags::Jump] { + stage2 += stage2_gammas[0]; + } + if instruction_flags[InstructionFlags::Branch] { + stage2 += stage2_gammas[1]; + } + if circuit_flags[CircuitFlags::WriteLookupOutputToRD] { + stage2 += stage2_gammas[2]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + stage2 += stage2_gammas[3]; + } + + let mut stage3 = F::from_i128(instruction.operands.imm); + stage3 += stage3_gammas[1].mul_u64(instruction.address as u64); + if instruction_flags[InstructionFlags::LeftOperandIsRs1Value] { + stage3 += stage3_gammas[2]; + } + if instruction_flags[InstructionFlags::LeftOperandIsPC] { + stage3 += stage3_gammas[3]; + } + if instruction_flags[InstructionFlags::RightOperandIsRs2Value] { + stage3 += stage3_gammas[4]; + } + if instruction_flags[InstructionFlags::RightOperandIsImm] { + stage3 += stage3_gammas[5]; + } + if instruction_flags[InstructionFlags::IsNoop] { + stage3 += stage3_gammas[6]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + stage3 += stage3_gammas[7]; + } + if circuit_flags[CircuitFlags::IsFirstInSequence] { + stage3 += stage3_gammas[8]; + } + + let stage4 = register_eq(instruction.operands.rd, register_read_write_eq) * stage4_gammas[0] + + register_eq(instruction.operands.rs1, register_read_write_eq) * stage4_gammas[1] + + register_eq(instruction.operands.rs2, register_read_write_eq) * stage4_gammas[2]; + + let mut stage5 = register_eq(instruction.operands.rd, register_val_evaluation_eq); + if !circuit_flags.is_interleaved_operands() { + stage5 += stage5_gammas[1]; + } + if let Some(table) = InstructionLookupTable::::lookup_table(&decoded) { + stage5 += stage5_gammas[2 + table.index()]; + } + + [stage1, stage2, stage3, stage4, stage5] +} + +fn register_eq(register: Option, eq: &[F]) -> F { + register + .and_then(|register| eq.get(register as usize)) + .copied() + .unwrap_or_else(F::zero) +} + +fn require_len(values: &[F], expected: usize) -> Result<(), JoltFormulaPointError> { + if values.len() < expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: values.len(), + }); + } + Ok(()) +} + +pub fn read_raf_input_openings() -> BytecodeReadRafInputOpenings { + BytecodeReadRafInputOpenings { + spartan_outer: BytecodeReadRafSpartanOuterOpenings { + unexpanded_pc: unexpanded_pc_spartan_outer(), + imm: imm_spartan_outer(), + op_flags: CIRCUIT_FLAGS + .into_iter() + .map(|flag| (flag, op_flag_spartan_outer(flag))) + .collect(), + }, + spartan_product: BytecodeReadRafSpartanProductOpenings { + jump: op_flag_product(CircuitFlags::Jump), + branch: instruction_flag_product(InstructionFlags::Branch), + write_lookup_output_to_rd: op_flag_product(CircuitFlags::WriteLookupOutputToRD), + virtual_instruction: op_flag_product(CircuitFlags::VirtualInstruction), + }, + instruction_input: BytecodeReadRafInstructionInputOpenings { + imm: imm_instruction_input(), + unexpanded_pc_from_shift: unexpanded_pc_spartan_shift(), + left_operand_is_rs1_value: instruction_flag_input( + InstructionFlags::LeftOperandIsRs1Value, + ), + left_operand_is_pc: instruction_flag_input(InstructionFlags::LeftOperandIsPC), + right_operand_is_rs2_value: instruction_flag_input( + InstructionFlags::RightOperandIsRs2Value, + ), + right_operand_is_imm: instruction_flag_input(InstructionFlags::RightOperandIsImm), + is_noop_from_shift: instruction_flag_shift(InstructionFlags::IsNoop), + virtual_instruction_from_shift: op_flag_shift(CircuitFlags::VirtualInstruction), + is_first_in_sequence_from_shift: op_flag_shift(CircuitFlags::IsFirstInSequence), + }, + spartan_shift: BytecodeReadRafSpartanShiftOpenings { + unexpanded_pc: unexpanded_pc_spartan_shift(), + }, + registers_read_write: BytecodeReadRafRegistersReadWriteOpenings { + rd_wa: rd_wa_read_write(), + rs1_ra: rs1_ra_read_write(), + rs2_ra: rs2_ra_read_write(), + }, + registers_val_evaluation: BytecodeReadRafRegistersValEvaluationOpenings { + rd_wa: rd_wa_val_evaluation(), + instruction_raf_flag: instruction_raf_flag(), + lookup_table_flags: LookupTableKind::::iter() + .map(|table| (table, lookup_table_flag(table))) + .collect(), + }, + spartan_outer_pc: pc_spartan_outer(), + spartan_shift_pc: pc_spartan_shift(), + } +} + +pub fn read_raf_output_openings( + dimensions: BytecodeReadRafDimensions, +) -> BytecodeReadRafOutputOpenings { + BytecodeReadRafOutputOpenings { + bytecode_ra: (0..dimensions.num_committed_ra_polys()) + .map(bytecode_ra) + .collect(), + } +} + +pub fn read_raf_consistency_openings() -> [(JoltOpeningId, JoltOpeningId); 1] { + [( + unexpanded_pc_spartan_shift(), + unexpanded_pc_instruction_input(), + )] +} + +fn stage1_claim() -> JoltExpr +where + F: RingCore, +{ + let beta = bytecode_challenge(BytecodeReadRafChallenge::Stage1Gamma); + let mut claim = + opening(unexpanded_pc_spartan_outer()) + beta.clone() * opening(imm_spartan_outer()); + + for (i, flag) in CIRCUIT_FLAGS.into_iter().enumerate() { + claim = claim + beta.clone().pow(i + 2) * opening(op_flag_spartan_outer(flag)); + } + + claim +} + +fn stage2_claim() -> JoltExpr +where + F: RingCore, +{ + let beta = bytecode_challenge(BytecodeReadRafChallenge::Stage2Gamma); + + opening(op_flag_product(CircuitFlags::Jump)) + + beta.clone() * opening(instruction_flag_product(InstructionFlags::Branch)) + + beta.clone().pow(2) * opening(op_flag_product(CircuitFlags::WriteLookupOutputToRD)) + + beta.pow(3) * opening(op_flag_product(CircuitFlags::VirtualInstruction)) +} + +fn stage3_claim() -> JoltExpr +where + F: RingCore, +{ + let beta = bytecode_challenge(BytecodeReadRafChallenge::Stage3Gamma); + + opening(imm_instruction_input()) + + beta.clone() * opening(unexpanded_pc_spartan_shift()) + + beta.clone().pow(2) + * opening(instruction_flag_input( + InstructionFlags::LeftOperandIsRs1Value, + )) + + beta.clone().pow(3) * opening(instruction_flag_input(InstructionFlags::LeftOperandIsPC)) + + beta.clone().pow(4) + * opening(instruction_flag_input( + InstructionFlags::RightOperandIsRs2Value, + )) + + beta.clone().pow(5) * opening(instruction_flag_input(InstructionFlags::RightOperandIsImm)) + + beta.clone().pow(6) * opening(instruction_flag_shift(InstructionFlags::IsNoop)) + + beta.clone().pow(7) * opening(op_flag_shift(CircuitFlags::VirtualInstruction)) + + beta.pow(8) * opening(op_flag_shift(CircuitFlags::IsFirstInSequence)) +} + +fn stage4_claim() -> JoltExpr +where + F: RingCore, +{ + let beta = bytecode_challenge(BytecodeReadRafChallenge::Stage4Gamma); + + opening(rd_wa_read_write()) + + beta.clone() * opening(rs1_ra_read_write()) + + beta.pow(2) * opening(rs2_ra_read_write()) +} + +fn stage5_claim() -> JoltExpr +where + F: RingCore, +{ + let beta = bytecode_challenge(BytecodeReadRafChallenge::Stage5Gamma); + let mut claim = + opening(rd_wa_val_evaluation()) + beta.clone() * opening(instruction_raf_flag()); + + for (i, table) in LookupTableKind::::iter().enumerate() { + claim = claim + beta.clone().pow(i + 2) * opening(lookup_table_flag(table)); + } + + claim +} + +fn bytecode_challenge(id: BytecodeReadRafChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn bytecode_public(id: BytecodeReadRafPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn bytecode_ra_product(dimensions: BytecodeReadRafDimensions) -> JoltExpr +where + F: RingCore, +{ + let mut product = JoltExpr::one(); + for i in 0..dimensions.num_committed_ra_polys() { + product = product * opening(bytecode_ra(i)); + } + product +} + +fn unexpanded_pc_spartan_outer() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnexpandedPC, + JoltRelationId::SpartanOuter, + ) +} + +fn imm_spartan_outer() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::Imm, JoltRelationId::SpartanOuter) +} + +fn op_flag_spartan_outer(flag: CircuitFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(flag), + JoltRelationId::SpartanOuter, + ) +} + +fn op_flag_product(flag: CircuitFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(flag), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn instruction_flag_product(flag: InstructionFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(flag), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn imm_instruction_input() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Imm, + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn unexpanded_pc_instruction_input() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnexpandedPC, + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn unexpanded_pc_spartan_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnexpandedPC, + JoltRelationId::SpartanShift, + ) +} + +fn instruction_flag_input(flag: InstructionFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(flag), + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn instruction_flag_shift(flag: InstructionFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(flag), + JoltRelationId::SpartanShift, + ) +} + +fn op_flag_shift(flag: CircuitFlags) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(flag), + JoltRelationId::SpartanShift, + ) +} + +fn rd_wa_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWa, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rs1_ra_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Ra, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rs2_ra_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Ra, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rd_wa_val_evaluation() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWa, + JoltRelationId::RegistersValEvaluation, + ) +} + +fn instruction_raf_flag() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionRafFlag, + JoltRelationId::InstructionReadRaf, + ) +} + +fn lookup_table_flag(table: LookupTableKind) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupTableFlag(table.index()), + JoltRelationId::InstructionReadRaf, + ) +} + +fn pc_spartan_outer() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::PC, JoltRelationId::SpartanOuter) +} + +fn pc_spartan_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::PC, JoltRelationId::SpartanShift) +} + +fn bytecode_ra(index: usize) -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::BytecodeRa(index), + JoltRelationId::BytecodeReadRaf, + ) +} + +#[cfg(test)] +#[expect(clippy::panic)] +mod tests { + use super::*; + use crate::protocols::jolt::{JoltCommittedPolynomial, JoltConsistencyClaim, JoltPolynomialId}; + use jolt_field::{Fr, FromPrimitiveInt}; + use jolt_riscv::{JoltInstructionKind, NormalizedOperands}; + + fn dimensions(num_committed_ra_polys: usize) -> BytecodeReadRafDimensions { + BytecodeReadRafDimensions::new(5, 10, num_committed_ra_polys) + } + + fn gamma_power(gamma: Fr, exponent: usize) -> Fr { + let mut value = Fr::from_u64(1); + for _ in 0..exponent { + value *= gamma; + } + value + } + + fn stage1_openings() -> Vec { + let mut openings = vec![unexpanded_pc_spartan_outer(), imm_spartan_outer()]; + openings.extend(CIRCUIT_FLAGS.into_iter().map(op_flag_spartan_outer)); + openings + } + + fn stage5_lookup_flags() -> Vec { + LookupTableKind::::iter() + .map(lookup_table_flag) + .collect() + } + + #[test] + fn read_raf_opening_point_matches_core_order() { + let point = BytecodeReadRafDimensions::new(3, 2, 1) + .opening_point(&(1..=5).map(Fr::from_u64).collect::>()) + .unwrap_or_else(|error| panic!("bytecode read-raf point should normalize: {error}")); + + assert_eq!(point.r_address, vec![Fr::from_u64(2), Fr::from_u64(1)]); + assert_eq!( + point.r_cycle, + vec![Fr::from_u64(5), Fr::from_u64(4), Fr::from_u64(3)] + ); + assert_eq!( + point.opening_point, + vec![ + Fr::from_u64(2), + Fr::from_u64(1), + Fr::from_u64(5), + Fr::from_u64(4), + Fr::from_u64(3), + ] + ); + } + + #[test] + fn read_raf_helpers_expose_typed_openings() { + let input = read_raf_input_openings(); + let output = read_raf_output_openings(dimensions(2)); + + assert_eq!( + input.spartan_outer.unexpanded_pc, + unexpanded_pc_spartan_outer() + ); + assert_eq!(input.spartan_outer.imm, imm_spartan_outer()); + assert_eq!(input.spartan_outer.op_flags.len(), NUM_CIRCUIT_FLAGS); + assert_eq!( + input.spartan_product.jump, + op_flag_product(CircuitFlags::Jump) + ); + assert_eq!( + input.spartan_product.branch, + instruction_flag_product(InstructionFlags::Branch) + ); + assert_eq!( + input.registers_val_evaluation.lookup_table_flags, + LookupTableKind::::iter() + .map(|table| (table, lookup_table_flag(table))) + .collect::>() + ); + assert_eq!(output.bytecode_ra, vec![bytecode_ra(0), bytecode_ra(1)]); + assert_eq!( + read_raf_consistency_openings(), + [( + unexpanded_pc_spartan_shift(), + unexpanded_pc_instruction_input() + )] + ); + } + + #[test] + fn read_raf_public_values_evaluate_bytecode_rows() { + let bytecode = vec![ + JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADD, + address: 9, + operands: NormalizedOperands { + rs1: Some(0), + rs2: Some(0), + rd: Some(0), + imm: 4, + }, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + }, + JoltInstructionRow::default(), + ]; + let one = Fr::from_u64(1); + let zero = Fr::from_u64(0); + let r_address = [zero]; + let r_cycle = [zero]; + let stage_cycle_points = [&r_cycle[..]; 5]; + let stage1_gammas = vec![one; 2 + NUM_CIRCUIT_FLAGS]; + let stage5_gammas = vec![one; 2 + LookupTableKind::::COUNT]; + let public_values = read_raf_public_values::(BytecodeReadRafEvaluationInputs { + bytecode: &bytecode, + r_address: &r_address, + r_cycle: &r_cycle, + stage_cycle_points, + register_read_write_point: &[], + register_val_evaluation_point: &[], + entry_bytecode_index: 0, + stage1_gammas: &stage1_gammas, + stage2_gammas: &[one; 4], + stage3_gammas: &[one; 9], + stage4_gammas: &[one; 3], + stage5_gammas: &stage5_gammas, + }) + .unwrap_or_else(|error| panic!("bytecode public values should evaluate: {error}")); + + assert_eq!( + public_values.stage_values, + [ + Fr::from_u64(15), + Fr::from_u64(1), + Fr::from_u64(15), + Fr::from_u64(3), + Fr::from_u64(3), + ] + ); + assert_eq!(public_values.spartan_outer_raf, zero); + assert_eq!(public_values.spartan_shift_raf, zero); + assert_eq!(public_values.entry, one); + } + + #[test] + fn read_raf_supports_empty_ra_product() { + let claims = read_raf::(dimensions(0)); + + assert!(!claims.output.required_openings.iter().any(|opening_id| { + matches!( + opening_id, + JoltOpeningId::Polynomial { + polynomial: JoltPolynomialId::Committed(JoltCommittedPolynomial::BytecodeRa(_)), + .. + } + ) + })); + } + + #[test] + fn read_raf_exposes_expected_dependencies() { + let dimensions = dimensions(2); + let claims = read_raf::(dimensions); + + let mut expected_input = stage1_openings(); + expected_input.extend([ + op_flag_product(CircuitFlags::Jump), + instruction_flag_product(InstructionFlags::Branch), + op_flag_product(CircuitFlags::WriteLookupOutputToRD), + op_flag_product(CircuitFlags::VirtualInstruction), + imm_instruction_input(), + unexpanded_pc_spartan_shift(), + instruction_flag_input(InstructionFlags::LeftOperandIsRs1Value), + instruction_flag_input(InstructionFlags::LeftOperandIsPC), + instruction_flag_input(InstructionFlags::RightOperandIsRs2Value), + instruction_flag_input(InstructionFlags::RightOperandIsImm), + instruction_flag_shift(InstructionFlags::IsNoop), + op_flag_shift(CircuitFlags::VirtualInstruction), + op_flag_shift(CircuitFlags::IsFirstInSequence), + rd_wa_read_write(), + rs1_ra_read_write(), + rs2_ra_read_write(), + rd_wa_val_evaluation(), + instruction_raf_flag(), + ]); + expected_input.extend(stage5_lookup_flags()); + expected_input.extend([pc_spartan_outer(), pc_spartan_shift()]); + + assert_eq!(claims.id, JoltRelationId::BytecodeReadRaf); + assert_eq!(claims.sumcheck, JoltSumcheckSpec::boolean(15, 3)); + assert_eq!(claims.input.required_openings, expected_input); + assert_eq!( + claims.output.required_openings, + vec![bytecode_ra(0), bytecode_ra(1)] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(BytecodeReadRafChallenge::Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage1Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage2Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage3Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage4Gamma), + JoltChallengeId::from(BytecodeReadRafChallenge::Stage5Gamma), + ] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(BytecodeReadRafPublic::StageValue(0)), + JoltPublicId::from(BytecodeReadRafPublic::StageValue(1)), + JoltPublicId::from(BytecodeReadRafPublic::StageValue(2)), + JoltPublicId::from(BytecodeReadRafPublic::StageValue(3)), + JoltPublicId::from(BytecodeReadRafPublic::StageValue(4)), + JoltPublicId::from(BytecodeReadRafPublic::SpartanOuterRaf), + JoltPublicId::from(BytecodeReadRafPublic::SpartanShiftRaf), + JoltPublicId::from(BytecodeReadRafPublic::Entry), + ] + ); + assert_eq!( + claims.consistency, + vec![JoltConsistencyClaim::same_evaluation( + unexpanded_pc_spartan_shift(), + unexpanded_pc_instruction_input(), + )] + ); + assert_eq!(claims.num_challenges(), 6); + } + + #[test] + fn read_raf_evaluates_like_core_formula() { + let dimensions = dimensions(2); + let claims = read_raf::(dimensions); + + let gamma = Fr::from_u64(3); + let stage1_gamma = Fr::from_u64(5); + let stage2_gamma = Fr::from_u64(7); + let stage3_gamma = Fr::from_u64(11); + let stage4_gamma = Fr::from_u64(13); + let stage5_gamma = Fr::from_u64(17); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == unexpanded_pc_spartan_outer() => Fr::from_u64(19), + id if id == imm_spartan_outer() => Fr::from_u64(23), + id if id == op_flag_product(CircuitFlags::Jump) => Fr::from_u64(29), + id if id == instruction_flag_product(InstructionFlags::Branch) => Fr::from_u64(31), + id if id == op_flag_product(CircuitFlags::WriteLookupOutputToRD) => { + Fr::from_u64(37) + } + id if id == op_flag_product(CircuitFlags::VirtualInstruction) => Fr::from_u64(41), + id if id == imm_instruction_input() => Fr::from_u64(43), + id if id == unexpanded_pc_spartan_shift() => Fr::from_u64(47), + id if id == instruction_flag_input(InstructionFlags::LeftOperandIsRs1Value) => { + Fr::from_u64(53) + } + id if id == instruction_flag_input(InstructionFlags::LeftOperandIsPC) => { + Fr::from_u64(59) + } + id if id == instruction_flag_input(InstructionFlags::RightOperandIsRs2Value) => { + Fr::from_u64(61) + } + id if id == instruction_flag_input(InstructionFlags::RightOperandIsImm) => { + Fr::from_u64(67) + } + id if id == instruction_flag_shift(InstructionFlags::IsNoop) => Fr::from_u64(71), + id if id == op_flag_shift(CircuitFlags::VirtualInstruction) => Fr::from_u64(73), + id if id == op_flag_shift(CircuitFlags::IsFirstInSequence) => Fr::from_u64(79), + id if id == rd_wa_read_write() => Fr::from_u64(83), + id if id == rs1_ra_read_write() => Fr::from_u64(89), + id if id == rs2_ra_read_write() => Fr::from_u64(97), + id if id == rd_wa_val_evaluation() => Fr::from_u64(101), + id if id == instruction_raf_flag() => Fr::from_u64(103), + id if id == pc_spartan_outer() => Fr::from_u64(107), + id if id == pc_spartan_shift() => Fr::from_u64(109), + JoltOpeningId::Polynomial { + polynomial: JoltPolynomialId::Virtual(JoltVirtualPolynomial::OpFlags(flag)), + relation: JoltRelationId::SpartanOuter, + } => Fr::from_u64(200 + u64::from(flag as u8)), + JoltOpeningId::Polynomial { + polynomial: + JoltPolynomialId::Virtual(JoltVirtualPolynomial::LookupTableFlag(index)), + relation: JoltRelationId::InstructionReadRaf, + } => Fr::from_u64(300 + index as u64), + _ => zero, + }, + |id| match *id { + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Gamma) => gamma, + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Stage1Gamma) => { + stage1_gamma + } + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Stage2Gamma) => { + stage2_gamma + } + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Stage3Gamma) => { + stage3_gamma + } + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Stage4Gamma) => { + stage4_gamma + } + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Stage5Gamma) => { + stage5_gamma + } + _ => zero, + }, + |_| zero, + ); + + let mut stage1 = Fr::from_u64(19) + stage1_gamma * Fr::from_u64(23); + for flag in CIRCUIT_FLAGS { + stage1 += gamma_power(stage1_gamma, usize::from(flag as u8) + 2) + * Fr::from_u64(200 + u64::from(flag as u8)); + } + let stage2 = Fr::from_u64(29) + + stage2_gamma * Fr::from_u64(31) + + gamma_power(stage2_gamma, 2) * Fr::from_u64(37) + + gamma_power(stage2_gamma, 3) * Fr::from_u64(41); + let stage3 = Fr::from_u64(43) + + stage3_gamma * Fr::from_u64(47) + + gamma_power(stage3_gamma, 2) * Fr::from_u64(53) + + gamma_power(stage3_gamma, 3) * Fr::from_u64(59) + + gamma_power(stage3_gamma, 4) * Fr::from_u64(61) + + gamma_power(stage3_gamma, 5) * Fr::from_u64(67) + + gamma_power(stage3_gamma, 6) * Fr::from_u64(71) + + gamma_power(stage3_gamma, 7) * Fr::from_u64(73) + + gamma_power(stage3_gamma, 8) * Fr::from_u64(79); + let stage4 = Fr::from_u64(83) + + stage4_gamma * Fr::from_u64(89) + + gamma_power(stage4_gamma, 2) * Fr::from_u64(97); + let mut stage5 = Fr::from_u64(101) + stage5_gamma * Fr::from_u64(103); + for table in LookupTableKind::::iter() { + stage5 += gamma_power(stage5_gamma, table.index() + 2) + * Fr::from_u64(300 + table.index() as u64); + } + + assert_eq!( + input, + gamma_power(gamma, 7) + + stage1 + + gamma * stage2 + + gamma_power(gamma, 2) * stage3 + + gamma_power(gamma, 3) * stage4 + + gamma_power(gamma, 4) * stage5 + + gamma_power(gamma, 5) * Fr::from_u64(107) + + gamma_power(gamma, 6) * Fr::from_u64(109) + ); + + let stage_values = [ + Fr::from_u64(2), + Fr::from_u64(3), + Fr::from_u64(5), + Fr::from_u64(7), + Fr::from_u64(11), + ]; + let spartan_outer_raf = Fr::from_u64(13); + let spartan_shift_raf = Fr::from_u64(17); + let entry = Fr::from_u64(19); + let bytecode_ra_0 = Fr::from_u64(23); + let bytecode_ra_1 = Fr::from_u64(29); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == bytecode_ra(0) => bytecode_ra_0, + id if id == bytecode_ra(1) => bytecode_ra_1, + _ => zero, + }, + |id| match *id { + JoltChallengeId::BytecodeReadRaf(BytecodeReadRafChallenge::Gamma) => gamma, + _ => zero, + }, + |id| match *id { + JoltPublicId::BytecodeReadRaf(BytecodeReadRafPublic::StageValue(index)) => { + stage_values[index] + } + JoltPublicId::BytecodeReadRaf(BytecodeReadRafPublic::SpartanOuterRaf) => { + spartan_outer_raf + } + JoltPublicId::BytecodeReadRaf(BytecodeReadRafPublic::SpartanShiftRaf) => { + spartan_shift_raf + } + JoltPublicId::BytecodeReadRaf(BytecodeReadRafPublic::Entry) => entry, + _ => zero, + }, + ); + + assert_eq!( + output, + (stage_values[0] + + gamma * stage_values[1] + + gamma_power(gamma, 2) * stage_values[2] + + gamma_power(gamma, 3) * stage_values[3] + + gamma_power(gamma, 4) * stage_values[4] + + gamma_power(gamma, 5) * spartan_outer_raf + + gamma_power(gamma, 6) * spartan_shift_raf + + gamma_power(gamma, 7) * entry) + * bytecode_ra_0 + * bytecode_ra_1 + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/advice.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/advice.rs new file mode 100644 index 0000000000..b35012e7f8 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/advice.rs @@ -0,0 +1,491 @@ +use std::{cmp::min, ops::Range}; + +use jolt_field::{Field, RingCore}; +use jolt_poly::EqPolynomial; + +use crate::{opening, public}; + +use super::super::super::{ + AdviceClaimReductionPublic, JoltAdviceKind, JoltOpeningId, JoltPublicId, JoltRelationClaims, + JoltRelationId, +}; +use super::super::dimensions::{CommitmentMatrixShape, JoltSumcheckSpec, TracePolynomialOrder}; +use super::super::error::JoltFormulaPointError; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdviceClaimReductionLayout { + trace_order: TracePolynomialOrder, + log_t: usize, + log_k_chunk: usize, + main_shape: CommitmentMatrixShape, + advice_shape: CommitmentMatrixShape, + cycle_phase_col_rounds: Range, + cycle_phase_row_rounds: Range, +} + +impl AdviceClaimReductionLayout { + pub fn balanced( + trace_order: TracePolynomialOrder, + log_t: usize, + log_k_chunk: usize, + max_advice_size_bytes: usize, + ) -> Self { + Self::new( + trace_order, + log_t, + log_k_chunk, + CommitmentMatrixShape::balanced(log_k_chunk + log_t), + CommitmentMatrixShape::advice_from_max_bytes(max_advice_size_bytes), + ) + } + + pub fn new( + trace_order: TracePolynomialOrder, + log_t: usize, + log_k_chunk: usize, + main_shape: CommitmentMatrixShape, + advice_shape: CommitmentMatrixShape, + ) -> Self { + let (cycle_phase_col_rounds, cycle_phase_row_rounds) = + cycle_phase_round_schedule(trace_order, log_t, log_k_chunk, main_shape, advice_shape); + Self { + trace_order, + log_t, + log_k_chunk, + main_shape, + advice_shape, + cycle_phase_col_rounds, + cycle_phase_row_rounds, + } + } + + pub const fn trace_order(&self) -> TracePolynomialOrder { + self.trace_order + } + + pub const fn log_t(&self) -> usize { + self.log_t + } + + pub const fn log_k_chunk(&self) -> usize { + self.log_k_chunk + } + + pub const fn main_shape(&self) -> CommitmentMatrixShape { + self.main_shape + } + + pub const fn advice_shape(&self) -> CommitmentMatrixShape { + self.advice_shape + } + + pub fn cycle_phase_col_rounds(&self) -> Range { + self.cycle_phase_col_rounds.clone() + } + + pub fn cycle_phase_row_rounds(&self) -> Range { + self.cycle_phase_row_rounds.clone() + } + + pub fn active_cycle_phase_rounds(&self) -> usize { + self.cycle_phase_col_rounds.len() + self.cycle_phase_row_rounds.len() + } + + pub fn cycle_phase_rounds(&self) -> usize { + if !self.cycle_phase_row_rounds.is_empty() { + self.cycle_phase_row_rounds.end - self.cycle_phase_col_rounds.start + } else { + self.cycle_phase_col_rounds.len() + } + } + + pub fn address_phase_rounds(&self) -> usize { + self.advice_shape + .total_vars() + .saturating_sub(self.active_cycle_phase_rounds()) + } + + pub fn dimensions(&self) -> AdviceClaimReductionDimensions { + AdviceClaimReductionDimensions::new(self.cycle_phase_rounds(), self.address_phase_rounds()) + } + + pub fn cycle_phase_opening_point( + &self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let mut advice_var_challenges = self.cycle_phase_variable_challenges(challenges)?; + advice_var_challenges.reverse(); + Ok(advice_var_challenges) + } + + pub fn cycle_phase_variable_challenges( + &self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let expected = self.cycle_phase_rounds(); + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let mut advice_var_challenges = Vec::with_capacity(self.active_cycle_phase_rounds()); + advice_var_challenges.extend_from_slice(&challenges[self.cycle_phase_col_rounds.clone()]); + advice_var_challenges.extend_from_slice(&challenges[self.cycle_phase_row_rounds.clone()]); + Ok(advice_var_challenges) + } + + pub fn address_phase_opening_point( + &self, + cycle_var_challenges: &[F], + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let expected_cycle = self.active_cycle_phase_rounds(); + if cycle_var_challenges.len() != expected_cycle { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: expected_cycle, + got: cycle_var_challenges.len(), + }); + } + let expected_address = self.address_phase_rounds(); + if challenges.len() != expected_address { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: expected_address, + got: challenges.len(), + }); + } + + let mut point = match self.trace_order { + TracePolynomialOrder::CycleMajor => [cycle_var_challenges, challenges].concat(), + TracePolynomialOrder::AddressMajor => [challenges, cycle_var_challenges].concat(), + }; + point.reverse(); + Ok(point) + } + + pub fn cycle_phase_final_output_scale( + &self, + reference_opening_point: &[F], + challenges: &[F], + ) -> Result { + let opening_point = self.cycle_phase_opening_point(challenges)?; + self.final_output_scale_at(reference_opening_point, &opening_point) + } + + pub fn address_phase_final_output_scale( + &self, + reference_opening_point: &[F], + cycle_var_challenges: &[F], + challenges: &[F], + ) -> Result { + let opening_point = self.address_phase_opening_point(cycle_var_challenges, challenges)?; + self.final_output_scale_at(reference_opening_point, &opening_point) + } + + fn final_output_scale_at( + &self, + reference_opening_point: &[F], + opening_point: &[F], + ) -> Result { + if reference_opening_point.len() != opening_point.len() { + return Err(JoltFormulaPointError::OpeningPointLengthMismatch { + expected: reference_opening_point.len(), + got: opening_point.len(), + }); + } + + Ok( + EqPolynomial::::mle(opening_point, reference_opening_point) + * self.dummy_cycle_phase_scale::(), + ) + } + + fn dummy_cycle_phase_scale(&self) -> F { + let two_inv = F::from_u64(2).inv_or_zero(); + (0..self.dummy_cycle_phase_rounds()).fold(F::one(), |scale, _| scale * two_inv) + } + + pub fn dummy_cycle_phase_rounds(&self) -> usize { + self.cycle_phase_rounds() + .saturating_sub(self.active_cycle_phase_rounds()) + } +} + +fn cycle_phase_round_schedule( + trace_order: TracePolynomialOrder, + log_t: usize, + log_k_chunk: usize, + main_shape: CommitmentMatrixShape, + advice_shape: CommitmentMatrixShape, +) -> (Range, Range) { + match trace_order { + TracePolynomialOrder::CycleMajor => { + let col_binding_rounds = 0..min(log_t, advice_shape.column_vars()); + let row_binding_rounds = min(log_t, main_shape.column_vars()) + ..min(log_t, main_shape.column_vars() + advice_shape.row_vars()); + (col_binding_rounds, row_binding_rounds) + } + TracePolynomialOrder::AddressMajor => { + let col_binding_rounds = 0..advice_shape.column_vars().saturating_sub(log_k_chunk); + let row_binding_rounds = main_shape.column_vars().saturating_sub(log_k_chunk) + ..min( + log_t, + main_shape.column_vars().saturating_sub(log_k_chunk) + advice_shape.row_vars(), + ); + (col_binding_rounds, row_binding_rounds) + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct AdviceClaimReductionDimensions { + cycle_phase_rounds: usize, + address_phase_rounds: usize, +} + +impl AdviceClaimReductionDimensions { + pub const fn new(cycle_phase_rounds: usize, address_phase_rounds: usize) -> Self { + Self { + cycle_phase_rounds, + address_phase_rounds, + } + } + + pub const fn cycle_phase_rounds(self) -> usize { + self.cycle_phase_rounds + } + + pub const fn address_phase_rounds(self) -> usize { + self.address_phase_rounds + } + + pub const fn has_address_phase(self) -> bool { + self.address_phase_rounds > 0 + } + + pub const fn cycle_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.cycle_phase_rounds, 2) + } + + pub const fn address_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.address_phase_rounds, 2) + } +} + +pub fn cycle_phase( + kind: JoltAdviceKind, + dimensions: AdviceClaimReductionDimensions, +) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(ram_val_check_advice_opening(kind)); + let output = if dimensions.has_address_phase() { + opening(cycle_phase_advice_opening(kind)) + } else { + public(JoltPublicId::from(AdviceClaimReductionPublic::FinalScale( + kind, + ))) * opening(final_advice_opening(kind)) + }; + + JoltRelationClaims::new( + JoltRelationId::AdviceClaimReductionCyclePhase, + dimensions.cycle_sumcheck(), + input, + output, + ) +} + +pub fn address_phase( + kind: JoltAdviceKind, + dimensions: AdviceClaimReductionDimensions, +) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(cycle_phase_advice_opening(kind)); + let output = public(JoltPublicId::from(AdviceClaimReductionPublic::FinalScale( + kind, + ))) * opening(final_advice_opening(kind)); + + JoltRelationClaims::new( + JoltRelationId::AdviceClaimReduction, + dimensions.address_sumcheck(), + input, + output, + ) +} + +pub fn cycle_phase_output_openings( + kind: JoltAdviceKind, + dimensions: AdviceClaimReductionDimensions, +) -> Vec { + if dimensions.has_address_phase() { + vec![cycle_phase_advice_opening(kind)] + } else { + vec![final_advice_opening(kind)] + } +} + +pub fn ram_val_check_advice_opening(kind: JoltAdviceKind) -> JoltOpeningId { + advice_opening(kind, JoltRelationId::RamValCheck) +} + +pub fn cycle_phase_advice_opening(kind: JoltAdviceKind) -> JoltOpeningId { + advice_opening(kind, JoltRelationId::AdviceClaimReductionCyclePhase) +} + +pub fn final_advice_opening(kind: JoltAdviceKind) -> JoltOpeningId { + advice_opening(kind, JoltRelationId::AdviceClaimReduction) +} + +fn advice_opening(kind: JoltAdviceKind, relation: JoltRelationId) -> JoltOpeningId { + match kind { + JoltAdviceKind::Trusted => JoltOpeningId::trusted_advice(relation), + JoltAdviceKind::Untrusted => JoltOpeningId::untrusted_advice(relation), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn with_address_phase() -> AdviceClaimReductionDimensions { + AdviceClaimReductionDimensions::new(4, 3) + } + + fn without_address_phase() -> AdviceClaimReductionDimensions { + AdviceClaimReductionDimensions::new(4, 0) + } + + #[test] + fn cycle_phase_with_address_phase_exposes_expected_dependencies() { + let claims = cycle_phase::(JoltAdviceKind::Trusted, with_address_phase()); + + assert_eq!(claims.id, JoltRelationId::AdviceClaimReductionCyclePhase); + assert_eq!(claims.sumcheck, with_address_phase().cycle_sumcheck()); + assert_eq!( + claims.input.required_openings, + vec![ram_val_check_advice_opening(JoltAdviceKind::Trusted)] + ); + assert_eq!( + claims.output.required_openings, + cycle_phase_output_openings(JoltAdviceKind::Trusted, with_address_phase()) + ); + assert!(claims.required_challenges().is_empty()); + assert!(claims.required_publics().is_empty()); + } + + #[test] + fn cycle_phase_without_address_phase_exposes_final_scale() { + let claims = cycle_phase::(JoltAdviceKind::Untrusted, without_address_phase()); + let mut expected_openings = vec![ram_val_check_advice_opening(JoltAdviceKind::Untrusted)]; + expected_openings.extend(cycle_phase_output_openings( + JoltAdviceKind::Untrusted, + without_address_phase(), + )); + + assert_eq!(claims.id, JoltRelationId::AdviceClaimReductionCyclePhase); + assert_eq!(claims.required_openings(), expected_openings); + assert_eq!( + claims.required_publics(), + vec![JoltPublicId::from(AdviceClaimReductionPublic::FinalScale( + JoltAdviceKind::Untrusted + ))] + ); + } + + #[test] + fn address_phase_exposes_expected_dependencies() { + let claims = address_phase::(JoltAdviceKind::Trusted, with_address_phase()); + + assert_eq!(claims.id, JoltRelationId::AdviceClaimReduction); + assert_eq!(claims.sumcheck, with_address_phase().address_sumcheck()); + assert_eq!( + claims.input.required_openings, + vec![cycle_phase_advice_opening(JoltAdviceKind::Trusted)] + ); + assert_eq!( + claims.output.required_openings, + vec![final_advice_opening(JoltAdviceKind::Trusted)] + ); + assert_eq!( + claims.required_publics(), + vec![JoltPublicId::from(AdviceClaimReductionPublic::FinalScale( + JoltAdviceKind::Trusted + ))] + ); + } + + #[test] + fn cycle_phase_without_address_phase_evaluates_like_core_formula() { + let claims = cycle_phase::(JoltAdviceKind::Trusted, without_address_phase()); + + let input_advice = Fr::from_u64(3); + let final_advice_claim = Fr::from_u64(5); + let final_scale = Fr::from_u64(7); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_val_check_advice_opening(JoltAdviceKind::Trusted) => input_advice, + _ => zero, + }, + |_| zero, + |_| zero, + ); + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == final_advice_opening(JoltAdviceKind::Trusted) => final_advice_claim, + _ => zero, + }, + |_| zero, + |id| match *id { + JoltPublicId::AdviceClaimReduction(AdviceClaimReductionPublic::FinalScale( + JoltAdviceKind::Trusted, + )) => final_scale, + _ => zero, + }, + ); + + assert_eq!(input, input_advice); + assert_eq!(output, final_scale * final_advice_claim); + } + + #[test] + fn address_phase_evaluates_like_core_formula() { + let claims = address_phase::(JoltAdviceKind::Untrusted, with_address_phase()); + + let cycle_claim = Fr::from_u64(11); + let final_advice_claim = Fr::from_u64(13); + let final_scale = Fr::from_u64(17); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == cycle_phase_advice_opening(JoltAdviceKind::Untrusted) => cycle_claim, + _ => zero, + }, + |_| zero, + |_| zero, + ); + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == final_advice_opening(JoltAdviceKind::Untrusted) => final_advice_claim, + _ => zero, + }, + |_| zero, + |id| match *id { + JoltPublicId::AdviceClaimReduction(AdviceClaimReductionPublic::FinalScale( + JoltAdviceKind::Untrusted, + )) => final_scale, + _ => zero, + }, + ); + + assert_eq!(input, cycle_claim); + assert_eq!(output, final_scale * final_advice_claim); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/hamming_weight.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/hamming_weight.rs new file mode 100644 index 0000000000..3d37dcf135 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/hamming_weight.rs @@ -0,0 +1,443 @@ +use jolt_field::{Field, RingCore}; + +use crate::{challenge, opening, public}; + +use super::super::super::{ + HammingWeightClaimReductionChallenge, HammingWeightClaimReductionPublic, JoltChallengeId, + JoltExpr, JoltOpeningId, JoltPublicId, JoltRelationClaims, JoltRelationId, + JoltVirtualPolynomial, +}; +use super::super::dimensions::{JoltFormulaPointError, JoltSumcheckSpec}; +use super::super::ra::{JoltRaPolynomial, JoltRaPolynomialLayout}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct HammingWeightClaimReductionDimensions { + pub layout: JoltRaPolynomialLayout, + pub log_k_chunk: usize, +} + +impl HammingWeightClaimReductionDimensions { + pub const fn new(layout: JoltRaPolynomialLayout, log_k_chunk: usize) -> Self { + Self { + layout, + log_k_chunk, + } + } + + pub const fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_k_chunk, 2) + } + + pub fn opening_point( + self, + challenges: &[F], + r_cycle: &[F], + ) -> Result, JoltFormulaPointError> { + if challenges.len() != self.log_k_chunk { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: self.log_k_chunk, + got: challenges.len(), + }); + } + + let mut r_address = challenges.iter().rev().copied().collect::>(); + r_address.extend_from_slice(r_cycle); + Ok(r_address) + } +} + +pub fn claim_reduction( + dimensions: HammingWeightClaimReductionDimensions, +) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = hamming_weight_challenge(HammingWeightClaimReductionChallenge::Gamma); + let mut input = JoltExpr::zero(); + let mut output = JoltExpr::zero(); + + for (i, polynomial) in dimensions.layout.polynomials().enumerate() { + input = input + + gamma.clone().pow(3 * i) * hamming_weight_claim(polynomial) + + gamma.clone().pow(3 * i + 1) * opening(booleanity_claim(polynomial)) + + gamma.clone().pow(3 * i + 2) * opening(virtualization_claim(polynomial)); + + let output_coeff = gamma.clone().pow(3 * i) + + gamma.clone().pow(3 * i + 1) + * hamming_weight_public(HammingWeightClaimReductionPublic::EqBooleanity) + + gamma.clone().pow(3 * i + 2) + * hamming_weight_public(HammingWeightClaimReductionPublic::EqVirtualization(i)); + output = output + output_coeff * opening(reduced_claim(polynomial)); + } + + JoltRelationClaims::new( + JoltRelationId::HammingWeightClaimReduction, + dimensions.sumcheck(), + input, + output, + ) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HammingWeightClaimReductionInputOpenings { + pub ram_hamming_weight: JoltOpeningId, + pub booleanity: Vec, + pub virtualization: Vec, +} + +pub fn claim_reduction_input_openings( + dimensions: HammingWeightClaimReductionDimensions, +) -> HammingWeightClaimReductionInputOpenings { + HammingWeightClaimReductionInputOpenings { + ram_hamming_weight: ram_hamming_weight(), + booleanity: dimensions + .layout + .polynomials() + .map(booleanity_claim) + .collect(), + virtualization: dimensions + .layout + .polynomials() + .map(virtualization_claim) + .collect(), + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HammingWeightClaimReductionOutputOpenings { + pub instruction_ra: Vec, + pub bytecode_ra: Vec, + pub ram_ra: Vec, +} + +impl HammingWeightClaimReductionOutputOpenings { + pub fn all(&self) -> Vec { + self.instruction_ra + .iter() + .chain(&self.bytecode_ra) + .chain(&self.ram_ra) + .copied() + .collect() + } +} + +pub fn claim_reduction_output_openings( + dimensions: HammingWeightClaimReductionDimensions, +) -> HammingWeightClaimReductionOutputOpenings { + let mut instruction_ra = Vec::with_capacity(dimensions.layout.instruction()); + let mut bytecode_ra = Vec::with_capacity(dimensions.layout.bytecode()); + let mut ram_ra = Vec::with_capacity(dimensions.layout.ram()); + + for polynomial in dimensions.layout.polynomials() { + match polynomial { + JoltRaPolynomial::Instruction(_) => instruction_ra.push(reduced_claim(polynomial)), + JoltRaPolynomial::Bytecode(_) => bytecode_ra.push(reduced_claim(polynomial)), + JoltRaPolynomial::Ram(_) => ram_ra.push(reduced_claim(polynomial)), + } + } + + HammingWeightClaimReductionOutputOpenings { + instruction_ra, + bytecode_ra, + ram_ra, + } +} + +fn hamming_weight_challenge(id: HammingWeightClaimReductionChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn hamming_weight_public(id: HammingWeightClaimReductionPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn hamming_weight_claim(polynomial: JoltRaPolynomial) -> JoltExpr +where + F: RingCore, +{ + match polynomial { + JoltRaPolynomial::Instruction(_) | JoltRaPolynomial::Bytecode(_) => JoltExpr::one(), + JoltRaPolynomial::Ram(_) => opening(ram_hamming_weight()), + } +} + +fn booleanity_claim(polynomial: JoltRaPolynomial) -> JoltOpeningId { + polynomial.opening(JoltRelationId::Booleanity) +} + +fn virtualization_claim(polynomial: JoltRaPolynomial) -> JoltOpeningId { + match polynomial { + JoltRaPolynomial::Instruction(_) => JoltOpeningId::committed( + polynomial.committed(), + JoltRelationId::InstructionRaVirtualization, + ), + JoltRaPolynomial::Bytecode(_) => { + JoltOpeningId::committed(polynomial.committed(), JoltRelationId::BytecodeReadRaf) + } + JoltRaPolynomial::Ram(_) => { + JoltOpeningId::committed(polynomial.committed(), JoltRelationId::RamRaVirtualization) + } + } +} + +fn reduced_claim(polynomial: JoltRaPolynomial) -> JoltOpeningId { + polynomial.opening(JoltRelationId::HammingWeightClaimReduction) +} + +fn ram_hamming_weight() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamHammingWeight, + JoltRelationId::RamHammingBooleanity, + ) +} + +#[cfg(test)] +mod tests { + use super::super::super::dimensions::JoltFormulaDimensionsError; + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn layout( + instruction: usize, + bytecode: usize, + ram: usize, + ) -> Result { + JoltRaPolynomialLayout::new(instruction, bytecode, ram) + } + + fn gamma_power(gamma: Fr, exponent: usize) -> Fr { + let mut value = Fr::from_u64(1); + for _ in 0..exponent { + value *= gamma; + } + value + } + + fn dimensions(layout: JoltRaPolynomialLayout) -> HammingWeightClaimReductionDimensions { + HammingWeightClaimReductionDimensions::new(layout, 8) + } + + fn dimensions_with_log_k_chunk( + layout: JoltRaPolynomialLayout, + log_k_chunk: usize, + ) -> HammingWeightClaimReductionDimensions { + HammingWeightClaimReductionDimensions::new(layout, log_k_chunk) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() -> Result<(), JoltFormulaDimensionsError> { + let layout = layout(1, 1, 1)?; + let claims = claim_reduction::(dimensions(layout)); + + let instruction = JoltRaPolynomial::Instruction(0); + let bytecode = JoltRaPolynomial::Bytecode(0); + let ram = JoltRaPolynomial::Ram(0); + + assert_eq!(claims.id, JoltRelationId::HammingWeightClaimReduction); + assert_eq!(claims.sumcheck, JoltSumcheckSpec::boolean(8, 2)); + let input_openings = claim_reduction_input_openings(dimensions(layout)); + assert_eq!(input_openings.ram_hamming_weight, ram_hamming_weight()); + assert_eq!( + input_openings.booleanity, + vec![ + booleanity_claim(instruction), + booleanity_claim(bytecode), + booleanity_claim(ram), + ] + ); + assert_eq!( + input_openings.virtualization, + vec![ + virtualization_claim(instruction), + virtualization_claim(bytecode), + virtualization_claim(ram), + ] + ); + assert_eq!( + claims.input.required_openings, + vec![ + booleanity_claim(instruction), + virtualization_claim(instruction), + booleanity_claim(bytecode), + virtualization_claim(bytecode), + ram_hamming_weight(), + booleanity_claim(ram), + virtualization_claim(ram), + ] + ); + let output_openings = claim_reduction_output_openings(dimensions(layout)); + assert_eq!( + output_openings.instruction_ra, + vec![reduced_claim(instruction)] + ); + assert_eq!(output_openings.bytecode_ra, vec![reduced_claim(bytecode)]); + assert_eq!(output_openings.ram_ra, vec![reduced_claim(ram)]); + assert_eq!( + output_openings.all(), + vec![ + reduced_claim(instruction), + reduced_claim(bytecode), + reduced_claim(ram), + ] + ); + assert_eq!( + claims.output.required_openings, + vec![ + reduced_claim(instruction), + reduced_claim(bytecode), + reduced_claim(ram), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from( + HammingWeightClaimReductionChallenge::Gamma + )] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(HammingWeightClaimReductionPublic::EqBooleanity), + JoltPublicId::from(HammingWeightClaimReductionPublic::EqVirtualization(0)), + JoltPublicId::from(HammingWeightClaimReductionPublic::EqVirtualization(1)), + JoltPublicId::from(HammingWeightClaimReductionPublic::EqVirtualization(2)), + ] + ); + assert_eq!(claims.num_challenges(), 1); + Ok(()) + } + + #[test] + fn opening_point_reverses_address_and_appends_cycle() -> Result<(), Box> + { + let dimensions = dimensions_with_log_k_chunk(layout(1, 1, 1)?, 3); + let point = dimensions.opening_point( + &[Fr::from_u64(2), Fr::from_u64(3), Fr::from_u64(4)], + &[Fr::from_u64(5), Fr::from_u64(6)], + )?; + + assert_eq!( + point, + vec![ + Fr::from_u64(4), + Fr::from_u64(3), + Fr::from_u64(2), + Fr::from_u64(5), + Fr::from_u64(6), + ] + ); + Ok(()) + } + + #[test] + fn claim_reduction_evaluates_like_core_formula() -> Result<(), JoltFormulaDimensionsError> { + let layout = layout(1, 1, 1)?; + let claims = claim_reduction::(dimensions(layout)); + + let instruction = JoltRaPolynomial::Instruction(0); + let bytecode = JoltRaPolynomial::Bytecode(0); + let ram = JoltRaPolynomial::Ram(0); + + let ram_hw = Fr::from_u64(3); + let bool_instruction = Fr::from_u64(5); + let virt_instruction = Fr::from_u64(7); + let bool_bytecode = Fr::from_u64(11); + let virt_bytecode = Fr::from_u64(13); + let bool_ram = Fr::from_u64(17); + let virt_ram = Fr::from_u64(19); + let reduced_instruction = Fr::from_u64(23); + let reduced_bytecode = Fr::from_u64(29); + let reduced_ram = Fr::from_u64(31); + let eq_bool = Fr::from_u64(37); + let eq_virt_instruction = Fr::from_u64(41); + let eq_virt_bytecode = Fr::from_u64(43); + let eq_virt_ram = Fr::from_u64(47); + let gamma = Fr::from_u64(53); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_hamming_weight() => ram_hw, + id if id == booleanity_claim(instruction) => bool_instruction, + id if id == virtualization_claim(instruction) => virt_instruction, + id if id == booleanity_claim(bytecode) => bool_bytecode, + id if id == virtualization_claim(bytecode) => virt_bytecode, + id if id == booleanity_claim(ram) => bool_ram, + id if id == virtualization_claim(ram) => virt_ram, + _ => zero, + }, + |id| match *id { + JoltChallengeId::HammingWeightClaimReduction( + HammingWeightClaimReductionChallenge::Gamma, + ) => gamma, + _ => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == reduced_claim(instruction) => reduced_instruction, + id if id == reduced_claim(bytecode) => reduced_bytecode, + id if id == reduced_claim(ram) => reduced_ram, + _ => zero, + }, + |id| match *id { + JoltChallengeId::HammingWeightClaimReduction( + HammingWeightClaimReductionChallenge::Gamma, + ) => gamma, + _ => zero, + }, + |id| match *id { + JoltPublicId::HammingWeightClaimReduction( + HammingWeightClaimReductionPublic::EqBooleanity, + ) => eq_bool, + JoltPublicId::HammingWeightClaimReduction( + HammingWeightClaimReductionPublic::EqVirtualization(0), + ) => eq_virt_instruction, + JoltPublicId::HammingWeightClaimReduction( + HammingWeightClaimReductionPublic::EqVirtualization(1), + ) => eq_virt_bytecode, + JoltPublicId::HammingWeightClaimReduction( + HammingWeightClaimReductionPublic::EqVirtualization(2), + ) => eq_virt_ram, + _ => zero, + }, + ); + + assert_eq!( + input, + gamma_power(gamma, 0) + + gamma_power(gamma, 1) * bool_instruction + + gamma_power(gamma, 2) * virt_instruction + + gamma_power(gamma, 3) + + gamma_power(gamma, 4) * bool_bytecode + + gamma_power(gamma, 5) * virt_bytecode + + gamma_power(gamma, 6) * ram_hw + + gamma_power(gamma, 7) * bool_ram + + gamma_power(gamma, 8) * virt_ram + ); + assert_eq!( + output, + reduced_instruction + * (gamma_power(gamma, 0) + + gamma_power(gamma, 1) * eq_bool + + gamma_power(gamma, 2) * eq_virt_instruction) + + reduced_bytecode + * (gamma_power(gamma, 3) + + gamma_power(gamma, 4) * eq_bool + + gamma_power(gamma, 5) * eq_virt_bytecode) + + reduced_ram + * (gamma_power(gamma, 6) + + gamma_power(gamma, 7) * eq_bool + + gamma_power(gamma, 8) * eq_virt_ram) + ); + Ok(()) + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/increments.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/increments.rs new file mode 100644 index 0000000000..7a34922e20 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/increments.rs @@ -0,0 +1,236 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening, public}; + +use super::super::super::{ + IncClaimReductionChallenge, IncClaimReductionPublic, JoltChallengeId, JoltCommittedPolynomial, + JoltExpr, JoltOpeningId, JoltPublicId, JoltRelationClaims, JoltRelationId, +}; +use super::super::dimensions::TraceDimensions; + +pub fn claim_reduction(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = inc_challenge(IncClaimReductionChallenge::Gamma); + + let input = opening(ram_inc_read_write()) + + gamma.clone() * opening(ram_inc_val_check()) + + gamma.clone().pow(2) * opening(rd_inc_read_write()) + + gamma.clone().pow(3) * opening(rd_inc_val_evaluation()); + + let ram_output_coeff = inc_public(IncClaimReductionPublic::EqRamReadWrite) + + gamma.clone() * inc_public(IncClaimReductionPublic::EqRamValCheck); + let rd_output_coeff = inc_public(IncClaimReductionPublic::EqRegistersReadWrite) + + gamma.clone() * inc_public(IncClaimReductionPublic::EqRegistersValEvaluation); + let output = ram_output_coeff * opening(ram_inc_reduced()) + + gamma.pow(2) * rd_output_coeff * opening(rd_inc_reduced()); + + JoltRelationClaims::new( + JoltRelationId::IncClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +fn inc_challenge(id: IncClaimReductionChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn inc_public(id: IncClaimReductionPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +pub fn claim_reduction_input_openings() -> [JoltOpeningId; 4] { + [ + ram_inc_read_write(), + ram_inc_val_check(), + rd_inc_read_write(), + rd_inc_val_evaluation(), + ] +} + +pub fn claim_reduction_output_openings() -> [JoltOpeningId; 2] { + [ram_inc_reduced(), rd_inc_reduced()] +} + +pub fn ram_inc_read_write_opening() -> JoltOpeningId { + ram_inc_read_write() +} + +pub fn ram_inc_val_check_opening() -> JoltOpeningId { + ram_inc_val_check() +} + +pub fn rd_inc_read_write_opening() -> JoltOpeningId { + rd_inc_read_write() +} + +pub fn rd_inc_val_evaluation_opening() -> JoltOpeningId { + rd_inc_val_evaluation() +} + +pub fn ram_inc_reduced_opening() -> JoltOpeningId { + ram_inc_reduced() +} + +pub fn rd_inc_reduced_opening() -> JoltOpeningId { + rd_inc_reduced() +} + +fn ram_inc_read_write() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::RamReadWriteChecking, + ) +} + +fn ram_inc_val_check() -> JoltOpeningId { + JoltOpeningId::committed(JoltCommittedPolynomial::RamInc, JoltRelationId::RamValCheck) +} + +fn rd_inc_read_write() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rd_inc_val_evaluation() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersValEvaluation, + ) +} + +fn ram_inc_reduced() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::IncClaimReduction, + ) +} + +fn rd_inc_reduced() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::IncClaimReduction, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() { + let claims = claim_reduction::(dimensions()); + + assert_eq!(claims.id, JoltRelationId::IncClaimReduction); + assert_eq!(claims.sumcheck, dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + claim_reduction_input_openings() + ); + assert_eq!( + claims.output.required_openings, + claim_reduction_output_openings() + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(IncClaimReductionChallenge::Gamma)] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(IncClaimReductionPublic::EqRamReadWrite), + JoltPublicId::from(IncClaimReductionPublic::EqRamValCheck), + JoltPublicId::from(IncClaimReductionPublic::EqRegistersReadWrite), + JoltPublicId::from(IncClaimReductionPublic::EqRegistersValEvaluation), + ] + ); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn claim_reduction_evaluates_like_core_formula() { + let claims = claim_reduction::(dimensions()); + + let ram_rw = Fr::from_u64(3); + let ram_val = Fr::from_u64(5); + let rd_rw = Fr::from_u64(7); + let rd_val = Fr::from_u64(11); + let ram_reduced = Fr::from_u64(13); + let rd_reduced = Fr::from_u64(17); + let eq_ram_rw = Fr::from_u64(19); + let eq_ram_val = Fr::from_u64(23); + let eq_rd_rw = Fr::from_u64(29); + let eq_rd_val = Fr::from_u64(31); + let gamma = Fr::from_u64(37); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_inc_read_write() => ram_rw, + id if id == ram_inc_val_check() => ram_val, + id if id == rd_inc_read_write() => rd_rw, + id if id == rd_inc_val_evaluation() => rd_val, + _ => zero, + }, + |id| match *id { + JoltChallengeId::IncClaimReduction(IncClaimReductionChallenge::Gamma) => gamma, + _ => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_inc_reduced() => ram_reduced, + id if id == rd_inc_reduced() => rd_reduced, + _ => zero, + }, + |id| match *id { + JoltChallengeId::IncClaimReduction(IncClaimReductionChallenge::Gamma) => gamma, + _ => zero, + }, + |id| match *id { + JoltPublicId::IncClaimReduction(IncClaimReductionPublic::EqRamReadWrite) => { + eq_ram_rw + } + JoltPublicId::IncClaimReduction(IncClaimReductionPublic::EqRamValCheck) => { + eq_ram_val + } + JoltPublicId::IncClaimReduction(IncClaimReductionPublic::EqRegistersReadWrite) => { + eq_rd_rw + } + JoltPublicId::IncClaimReduction( + IncClaimReductionPublic::EqRegistersValEvaluation, + ) => eq_rd_val, + _ => zero, + }, + ); + + let gamma_2 = gamma * gamma; + assert_eq!( + input, + ram_rw + gamma * ram_val + gamma_2 * rd_rw + gamma_2 * gamma * rd_val + ); + assert_eq!( + output, + ram_reduced * (eq_ram_rw + gamma * eq_ram_val) + + gamma_2 * rd_reduced * (eq_rd_rw + gamma * eq_rd_val) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/instruction.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/instruction.rs new file mode 100644 index 0000000000..ce61879bc4 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/instruction.rs @@ -0,0 +1,317 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening}; + +use super::super::super::{ + InstructionClaimReductionChallenge, JoltChallengeId, JoltExpr, JoltOpeningId, + JoltRelationClaims, JoltRelationId, JoltVirtualPolynomial, +}; +use super::super::dimensions::TraceDimensions; + +pub fn claim_reduction(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = weighted_claims( + lookup_output_spartan(), + left_lookup_operand_spartan(), + right_lookup_operand_spartan(), + left_instruction_input_spartan(), + right_instruction_input_spartan(), + ); + + let output = reduction_challenge(InstructionClaimReductionChallenge::EqSpartan) + * weighted_claims( + lookup_output_reduced(), + left_lookup_operand_reduced(), + right_lookup_operand_reduced(), + left_instruction_input_reduced(), + right_instruction_input_reduced(), + ); + + JoltRelationClaims::new( + JoltRelationId::InstructionClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +pub fn claim_reduction_output_openings() -> [JoltOpeningId; 5] { + [ + lookup_output_reduced(), + left_lookup_operand_reduced(), + right_lookup_operand_reduced(), + left_instruction_input_reduced(), + right_instruction_input_reduced(), + ] +} + +pub fn claim_reduction_input_openings() -> [JoltOpeningId; 5] { + [ + lookup_output_spartan(), + left_lookup_operand_spartan(), + right_lookup_operand_spartan(), + left_instruction_input_spartan(), + right_instruction_input_spartan(), + ] +} + +fn weighted_claims( + lookup_output: JoltOpeningId, + left_lookup_operand: JoltOpeningId, + right_lookup_operand: JoltOpeningId, + left_instruction_input: JoltOpeningId, + right_instruction_input: JoltOpeningId, +) -> JoltExpr +where + F: RingCore, +{ + let gamma = reduction_challenge(InstructionClaimReductionChallenge::Gamma); + + opening(lookup_output) + + gamma.clone() * opening(left_lookup_operand) + + gamma.clone().pow(2) * opening(right_lookup_operand) + + gamma.clone().pow(3) * opening(left_instruction_input) + + gamma.pow(4) * opening(right_instruction_input) +} + +fn reduction_challenge(id: InstructionClaimReductionChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn lookup_output_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupOutput, + JoltRelationId::SpartanOuter, + ) +} + +fn left_lookup_operand_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftLookupOperand, + JoltRelationId::SpartanOuter, + ) +} + +fn right_lookup_operand_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightLookupOperand, + JoltRelationId::SpartanOuter, + ) +} + +fn left_instruction_input_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftInstructionInput, + JoltRelationId::SpartanOuter, + ) +} + +fn right_instruction_input_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightInstructionInput, + JoltRelationId::SpartanOuter, + ) +} + +fn lookup_output_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupOutput, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn left_lookup_operand_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftLookupOperand, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn right_lookup_operand_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightLookupOperand, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn left_instruction_input_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftInstructionInput, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn right_instruction_input_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightInstructionInput, + JoltRelationId::InstructionClaimReduction, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() { + let claims = claim_reduction::(dimensions()); + + assert_eq!(claims.id, JoltRelationId::InstructionClaimReduction); + assert_eq!(claims.sumcheck, dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + claim_reduction_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + claim_reduction_output_openings().to_vec() + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from( + InstructionClaimReductionChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(InstructionClaimReductionChallenge::EqSpartan), + JoltChallengeId::from(InstructionClaimReductionChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(InstructionClaimReductionChallenge::Gamma), + JoltChallengeId::from(InstructionClaimReductionChallenge::EqSpartan), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from( + InstructionClaimReductionChallenge::EqSpartan + )), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn claim_reduction_evaluates_like_core_formula() { + let claims = claim_reduction::(dimensions()); + + let lookup_spartan = Fr::from_u64(3); + let left_lookup_spartan = Fr::from_u64(5); + let right_lookup_spartan = Fr::from_u64(7); + let left_input_spartan = Fr::from_u64(11); + let right_input_spartan = Fr::from_u64(13); + let lookup_reduced = Fr::from_u64(17); + let left_lookup_reduced = Fr::from_u64(19); + let right_lookup_reduced = Fr::from_u64(23); + let left_input_reduced = Fr::from_u64(29); + let right_input_reduced = Fr::from_u64(31); + let gamma = Fr::from_u64(37); + let eq_spartan = Fr::from_u64(41); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == lookup_output_spartan() => lookup_spartan, + id if id == left_lookup_operand_spartan() => left_lookup_spartan, + id if id == right_lookup_operand_spartan() => right_lookup_spartan, + id if id == left_instruction_input_spartan() => left_input_spartan, + id if id == right_instruction_input_spartan() => right_input_spartan, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionClaimReduction( + InstructionClaimReductionChallenge::Gamma, + ) => gamma, + JoltChallengeId::InstructionClaimReduction( + InstructionClaimReductionChallenge::EqSpartan, + ) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == lookup_output_reduced() => lookup_reduced, + id if id == left_lookup_operand_reduced() => left_lookup_reduced, + id if id == right_lookup_operand_reduced() => right_lookup_reduced, + id if id == left_instruction_input_reduced() => left_input_reduced, + id if id == right_instruction_input_reduced() => right_input_reduced, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionClaimReduction( + InstructionClaimReductionChallenge::Gamma, + ) => gamma, + JoltChallengeId::InstructionClaimReduction( + InstructionClaimReductionChallenge::EqSpartan, + ) => eq_spartan, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + lookup_spartan + + gamma * left_lookup_spartan + + gamma * gamma * right_lookup_spartan + + gamma * gamma * gamma * left_input_spartan + + gamma * gamma * gamma * gamma * right_input_spartan + ); + assert_eq!( + output, + eq_spartan + * (lookup_reduced + + gamma * left_lookup_reduced + + gamma * gamma * right_lookup_reduced + + gamma * gamma * gamma * left_input_reduced + + gamma * gamma * gamma * gamma * right_input_reduced) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/mod.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/mod.rs new file mode 100644 index 0000000000..dcff096c81 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/mod.rs @@ -0,0 +1,5 @@ +pub mod advice; +pub mod hamming_weight; +pub mod increments; +pub mod instruction; +pub mod registers; diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/registers.rs b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/registers.rs new file mode 100644 index 0000000000..ed051160cc --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/claim_reductions/registers.rs @@ -0,0 +1,243 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening}; + +use super::super::super::{ + JoltChallengeId, JoltExpr, JoltOpeningId, JoltRelationClaims, JoltRelationId, + JoltVirtualPolynomial, RegistersClaimReductionChallenge, +}; +use super::super::dimensions::TraceDimensions; + +pub fn claim_reduction(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = reduction_challenge(RegistersClaimReductionChallenge::Gamma); + let eq_spartan = reduction_challenge(RegistersClaimReductionChallenge::EqSpartan); + + let input = opening(rd_write_value_spartan()) + + gamma.clone() * opening(rs1_value_spartan()) + + gamma.clone().pow(2) * opening(rs2_value_spartan()); + + let output = eq_spartan.clone() * opening(rd_write_value_reduced()) + + eq_spartan.clone() * gamma.clone() * opening(rs1_value_reduced()) + + eq_spartan * gamma.pow(2) * opening(rs2_value_reduced()); + + JoltRelationClaims::new( + JoltRelationId::RegistersClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +pub fn claim_reduction_input_openings() -> [JoltOpeningId; 3] { + [ + rd_write_value_spartan(), + rs1_value_spartan(), + rs2_value_spartan(), + ] +} + +pub fn claim_reduction_output_openings() -> [JoltOpeningId; 3] { + [ + rd_write_value_reduced(), + rs1_value_reduced(), + rs2_value_reduced(), + ] +} + +fn reduction_challenge(id: RegistersClaimReductionChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn rd_write_value_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWriteValue, + JoltRelationId::SpartanOuter, + ) +} + +fn rs1_value_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Value, + JoltRelationId::SpartanOuter, + ) +} + +fn rs2_value_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Value, + JoltRelationId::SpartanOuter, + ) +} + +fn rd_write_value_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWriteValue, + JoltRelationId::RegistersClaimReduction, + ) +} + +fn rs1_value_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Value, + JoltRelationId::RegistersClaimReduction, + ) +} + +fn rs2_value_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Value, + JoltRelationId::RegistersClaimReduction, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + #[test] + fn claim_reduction_exposes_expected_dependencies() { + let claims = claim_reduction::(dimensions()); + + assert_eq!(claims.id, JoltRelationId::RegistersClaimReduction); + assert_eq!(claims.sumcheck, dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + claim_reduction_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + claim_reduction_output_openings().to_vec() + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from( + RegistersClaimReductionChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(RegistersClaimReductionChallenge::EqSpartan), + JoltChallengeId::from(RegistersClaimReductionChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(RegistersClaimReductionChallenge::Gamma), + JoltChallengeId::from(RegistersClaimReductionChallenge::EqSpartan), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from( + RegistersClaimReductionChallenge::EqSpartan + )), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn claim_reduction_evaluates_like_core_formula() { + let claims = claim_reduction::(dimensions()); + + let rd_spartan = Fr::from_u64(3); + let rs1_spartan = Fr::from_u64(5); + let rs2_spartan = Fr::from_u64(7); + let rd_reduced = Fr::from_u64(11); + let rs1_reduced = Fr::from_u64(13); + let rs2_reduced = Fr::from_u64(17); + let gamma = Fr::from_u64(19); + let eq_spartan = Fr::from_u64(23); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == rd_write_value_spartan() => rd_spartan, + id if id == rs1_value_spartan() => rs1_spartan, + id if id == rs2_value_spartan() => rs2_spartan, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RegistersClaimReduction( + RegistersClaimReductionChallenge::Gamma, + ) => gamma, + JoltChallengeId::RegistersClaimReduction( + RegistersClaimReductionChallenge::EqSpartan, + ) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == rd_write_value_reduced() => rd_reduced, + id if id == rs1_value_reduced() => rs1_reduced, + id if id == rs2_value_reduced() => rs2_reduced, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RegistersClaimReduction( + RegistersClaimReductionChallenge::Gamma, + ) => gamma, + JoltChallengeId::RegistersClaimReduction( + RegistersClaimReductionChallenge::EqSpartan, + ) => eq_spartan, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + rd_spartan + gamma * rs1_spartan + gamma * gamma * rs2_spartan + ); + assert_eq!( + output, + eq_spartan * (rd_reduced + gamma * rs1_reduced + gamma * gamma * rs2_reduced) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/committed_openings.rs b/crates/jolt-claims/src/protocols/jolt/formulas/committed_openings.rs new file mode 100644 index 0000000000..30e169747b --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/committed_openings.rs @@ -0,0 +1,184 @@ +//! Jolt committed-polynomial opening order used by the final PCS check. + +use jolt_field::Field; + +use super::super::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; +use super::ra::JoltRaPolynomialLayout; + +pub fn proof_commitment_order(layout: JoltRaPolynomialLayout) -> Vec { + final_opening_polynomial_order(layout, false, false) +} + +pub fn final_opening_polynomial_order( + layout: JoltRaPolynomialLayout, + include_trusted_advice: bool, + include_untrusted_advice: bool, +) -> Vec { + let mut polynomials = Vec::with_capacity( + 2 + layout.total() + + usize::from(include_trusted_advice) + + usize::from(include_untrusted_advice), + ); + polynomials.push(JoltCommittedPolynomial::RamInc); + polynomials.push(JoltCommittedPolynomial::RdInc); + polynomials.extend((0..layout.instruction()).map(JoltCommittedPolynomial::InstructionRa)); + polynomials.extend((0..layout.bytecode()).map(JoltCommittedPolynomial::BytecodeRa)); + polynomials.extend((0..layout.ram()).map(JoltCommittedPolynomial::RamRa)); + if include_trusted_advice { + polynomials.push(JoltCommittedPolynomial::TrustedAdvice); + } + if include_untrusted_advice { + polynomials.push(JoltCommittedPolynomial::UntrustedAdvice); + } + polynomials +} + +pub fn final_opening_ids( + layout: JoltRaPolynomialLayout, + include_trusted_advice: bool, + include_untrusted_advice: bool, +) -> Vec { + final_opening_polynomial_order(layout, include_trusted_advice, include_untrusted_advice) + .into_iter() + .map(final_opening_id) + .collect() +} + +pub fn final_opening_id(polynomial: JoltCommittedPolynomial) -> JoltOpeningId { + match polynomial { + JoltCommittedPolynomial::TrustedAdvice => { + JoltOpeningId::trusted_advice(JoltRelationId::AdviceClaimReduction) + } + JoltCommittedPolynomial::UntrustedAdvice => { + JoltOpeningId::untrusted_advice(JoltRelationId::AdviceClaimReduction) + } + polynomial => JoltOpeningId::committed(polynomial, final_opening_relation(polynomial)), + } +} + +pub fn final_opening_relation(polynomial: JoltCommittedPolynomial) -> JoltRelationId { + match polynomial { + JoltCommittedPolynomial::RdInc | JoltCommittedPolynomial::RamInc => { + JoltRelationId::IncClaimReduction + } + JoltCommittedPolynomial::InstructionRa(_) + | JoltCommittedPolynomial::BytecodeRa(_) + | JoltCommittedPolynomial::RamRa(_) => JoltRelationId::HammingWeightClaimReduction, + JoltCommittedPolynomial::TrustedAdvice | JoltCommittedPolynomial::UntrustedAdvice => { + JoltRelationId::AdviceClaimReduction + } + } +} + +pub fn advice_commitment_embedding_scale( + opening_point: &[F], + advice_opening_point: &[F], +) -> F { + opening_point + .iter() + .map(|challenge| { + if advice_opening_point.contains(challenge) { + F::one() + } else { + F::one() - challenge + } + }) + .product() +} + +#[cfg(test)] +mod tests { + #![expect(clippy::panic, reason = "tests fail loudly on unexpected errors")] + + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn layout() -> JoltRaPolynomialLayout { + JoltRaPolynomialLayout::new(2, 1, 2).unwrap_or_else(|error| { + panic!("test layout should be valid: {error}"); + }) + } + + #[test] + fn proof_commitment_order_reuses_final_opening_order() { + assert_eq!( + proof_commitment_order(layout()), + vec![ + JoltCommittedPolynomial::RamInc, + JoltCommittedPolynomial::RdInc, + JoltCommittedPolynomial::InstructionRa(0), + JoltCommittedPolynomial::InstructionRa(1), + JoltCommittedPolynomial::BytecodeRa(0), + JoltCommittedPolynomial::RamRa(0), + JoltCommittedPolynomial::RamRa(1), + ] + ); + } + + #[test] + fn final_opening_order_matches_stage8_rlc_order() { + assert_eq!( + final_opening_polynomial_order(layout(), true, true), + vec![ + JoltCommittedPolynomial::RamInc, + JoltCommittedPolynomial::RdInc, + JoltCommittedPolynomial::InstructionRa(0), + JoltCommittedPolynomial::InstructionRa(1), + JoltCommittedPolynomial::BytecodeRa(0), + JoltCommittedPolynomial::RamRa(0), + JoltCommittedPolynomial::RamRa(1), + JoltCommittedPolynomial::TrustedAdvice, + JoltCommittedPolynomial::UntrustedAdvice, + ] + ); + } + + #[test] + fn final_opening_ids_use_sumcheck_sources() { + assert_eq!( + final_opening_ids(layout(), true, false), + vec![ + JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::IncClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::IncClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::InstructionRa(0), + JoltRelationId::HammingWeightClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::InstructionRa(1), + JoltRelationId::HammingWeightClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::BytecodeRa(0), + JoltRelationId::HammingWeightClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::RamRa(0), + JoltRelationId::HammingWeightClaimReduction, + ), + JoltOpeningId::committed( + JoltCommittedPolynomial::RamRa(1), + JoltRelationId::HammingWeightClaimReduction, + ), + JoltOpeningId::trusted_advice(JoltRelationId::AdviceClaimReduction), + ] + ); + } + + #[test] + fn advice_embedding_scale_selects_variables_outside_advice_point() { + let opening_point = [Fr::from_u64(2), Fr::from_u64(3), Fr::from_u64(5)]; + let advice_point = [Fr::from_u64(3)]; + + assert_eq!( + advice_commitment_embedding_scale(&opening_point, &advice_point), + (Fr::from_u64(1) - Fr::from_u64(2)) * (Fr::from_u64(1) - Fr::from_u64(5)) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/dimensions.rs b/crates/jolt-claims/src/protocols/jolt/formulas/dimensions.rs new file mode 100644 index 0000000000..eb540fd92b --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/dimensions.rs @@ -0,0 +1,894 @@ +use jolt_field::Field; +use serde::{Deserialize, Serialize}; + +pub use super::error::{JoltFormulaDimensionsError, JoltFormulaPointError}; + +use super::{ + bytecode::BytecodeReadRafDimensions, + instruction::{InstructionRaVirtualizationDimensions, InstructionReadRafDimensions}, + ra::JoltRaPolynomialLayout, + ram::RamRaVirtualizationDimensions, +}; + +pub const REGISTER_ADDRESS_BITS: usize = 7; +pub const OUTER_UNISKIP_DOMAIN_SIZE: usize = 10; +pub const OUTER_UNISKIP_FIRST_ROUND_DEGREE: usize = 27; +pub const PRODUCT_UNISKIP_DOMAIN_SIZE: usize = 3; +pub const PRODUCT_UNISKIP_FIRST_ROUND_DEGREE: usize = 6; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JoltSumcheckDomain { + BooleanHypercube, + CenteredInteger { domain_size: usize }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct JoltSumcheckSpec { + pub domain: JoltSumcheckDomain, + pub rounds: usize, + pub degree: usize, +} + +impl JoltSumcheckSpec { + pub const fn boolean(rounds: usize, degree: usize) -> Self { + Self { + domain: JoltSumcheckDomain::BooleanHypercube, + rounds, + degree, + } + } + + pub const fn centered_integer(domain_size: usize, rounds: usize, degree: usize) -> Self { + Self { + domain: JoltSumcheckDomain::CenteredInteger { domain_size }, + rounds, + degree, + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TracePolynomialOrder { + #[default] + CycleMajor, + AddressMajor, +} + +impl TracePolynomialOrder { + pub const fn transcript_scalar(self) -> u64 { + match self { + Self::CycleMajor => 0, + Self::AddressMajor => 1, + } + } + + pub fn commitment_opening_point( + self, + opening_point: &[F], + log_t: usize, + ) -> Result, JoltFormulaPointError> { + match self { + Self::CycleMajor => Ok(opening_point.to_vec()), + Self::AddressMajor => { + if opening_point.len() < log_t { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: log_t, + got: opening_point.len(), + }); + } + let log_k = opening_point.len() - log_t; + let (r_address, r_cycle) = opening_point.split_at(log_k); + Ok([r_cycle, r_address].concat()) + } + } + } + + pub const fn address_cycle_to_index( + self, + address: usize, + cycle: usize, + num_addresses: usize, + num_cycles: usize, + ) -> usize { + match self { + Self::CycleMajor => address * num_cycles + cycle, + Self::AddressMajor => cycle * num_addresses + address, + } + } + + pub const fn index_to_address_cycle( + self, + index: usize, + num_addresses: usize, + num_cycles: usize, + ) -> (usize, usize) { + match self { + Self::CycleMajor => (index / num_cycles, index % num_cycles), + Self::AddressMajor => (index % num_addresses, index / num_addresses), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TraceDimensions { + log_t: usize, +} + +impl TraceDimensions { + pub const fn new(log_t: usize) -> Self { + Self { log_t } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn sumcheck(self, degree: usize) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t, degree) + } + + pub fn cycle_opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + if challenges.len() != self.log_t { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: self.log_t, + got: challenges.len(), + }); + } + + Ok(challenges.iter().rev().copied().collect()) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ReadWriteDimensions { + log_t: usize, + log_k: usize, + phase1_num_rounds: usize, + phase2_num_rounds: usize, +} + +impl ReadWriteDimensions { + pub const fn new( + log_t: usize, + log_k: usize, + phase1_num_rounds: usize, + phase2_num_rounds: usize, + ) -> Self { + Self { + log_t, + log_k, + phase1_num_rounds, + phase2_num_rounds, + } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn log_k(self) -> usize { + self.log_k + } + + pub const fn phase1_num_rounds(self) -> usize { + self.phase1_num_rounds + } + + pub const fn phase2_num_rounds(self) -> usize { + self.phase2_num_rounds + } + + pub const fn phase3_cycle_rounds(self) -> usize { + self.log_t - self.phase1_num_rounds + } + + pub const fn read_write_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t + self.log_k, 3) + } + + pub const fn raf_evaluation_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t + self.log_k - self.phase1_num_rounds, 2) + } + + pub const fn output_check_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t + self.log_k - self.phase1_num_rounds, 3) + } + + pub fn read_write_opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + self.validate_phase_split()?; + let expected = self.log_t + self.log_k; + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let (phase1, rest) = challenges.split_at(self.phase1_num_rounds); + let (phase2, rest) = rest.split_at(self.phase2_num_rounds); + let (phase3_cycle, phase3_address) = rest.split_at(self.log_t - self.phase1_num_rounds); + + let r_cycle = phase3_cycle + .iter() + .rev() + .copied() + .chain(phase1.iter().rev().copied()) + .collect::>(); + let r_address = phase3_address + .iter() + .rev() + .copied() + .chain(phase2.iter().rev().copied()) + .collect::>(); + let opening_point = [r_address.as_slice(), r_cycle.as_slice()].concat(); + + Ok(ReadWriteOpeningPoint { + r_address, + r_cycle, + opening_point, + }) + } + + pub fn address_opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + self.validate_phase_split()?; + let cycle_gap_rounds = self.phase3_cycle_rounds(); + let expected = self.log_k + cycle_gap_rounds; + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let phase3_address_start = self.phase2_num_rounds + cycle_gap_rounds; + let mut address = Vec::with_capacity(self.log_k); + address.extend_from_slice(&challenges[..self.phase2_num_rounds]); + address.extend_from_slice(&challenges[phase3_address_start..]); + address.reverse(); + Ok(address) + } + + const fn validate_phase_split(self) -> Result<(), JoltFormulaPointError> { + if self.phase1_num_rounds > self.log_t || self.phase2_num_rounds > self.log_k { + return Err(JoltFormulaPointError::InvalidReadWritePhaseSplit { + phase1_num_rounds: self.phase1_num_rounds, + log_t: self.log_t, + phase2_num_rounds: self.phase2_num_rounds, + log_k: self.log_k, + }); + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReadWriteOpeningPoint { + pub r_address: Vec, + pub r_cycle: Vec, + pub opening_point: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct JoltReadWriteConfig { + pub ram_rw_phase1_num_rounds: u8, + pub ram_rw_phase2_num_rounds: u8, + pub registers_rw_phase1_num_rounds: u8, + pub registers_rw_phase2_num_rounds: u8, +} + +impl JoltReadWriteConfig { + pub const fn ram_dimensions(self, log_t: usize, ram_log_k: usize) -> ReadWriteDimensions { + ReadWriteDimensions::new( + log_t, + ram_log_k, + self.ram_rw_phase1_num_rounds as usize, + self.ram_rw_phase2_num_rounds as usize, + ) + } + + pub const fn register_dimensions( + self, + log_t: usize, + register_log_k: usize, + ) -> ReadWriteDimensions { + ReadWriteDimensions::new( + log_t, + register_log_k, + self.registers_rw_phase1_num_rounds as usize, + self.registers_rw_phase2_num_rounds as usize, + ) + } + + pub const fn needs_single_advice_opening(self, log_t: usize) -> bool { + self.ram_rw_phase1_num_rounds as usize == log_t + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct CommitmentMatrixShape { + column_vars: usize, + row_vars: usize, +} + +impl CommitmentMatrixShape { + pub const fn new(column_vars: usize, row_vars: usize) -> Self { + Self { + column_vars, + row_vars, + } + } + + pub const fn column_vars(self) -> usize { + self.column_vars + } + + pub const fn row_vars(self) -> usize { + self.row_vars + } + + pub const fn total_vars(self) -> usize { + self.column_vars + self.row_vars + } + + pub const fn balanced(total_vars: usize) -> Self { + let column_vars = total_vars.div_ceil(2); + Self { + column_vars, + row_vars: total_vars - column_vars, + } + } + + pub fn advice_from_max_bytes(max_advice_size_bytes: usize) -> Self { + let words = max_advice_size_bytes / 8; + let len = words.next_power_of_two().max(1); + Self::balanced(log2_power_of_two(len)) + } +} + +fn log2_power_of_two(value: usize) -> usize { + assert!( + value.is_power_of_two(), + "expected a power-of-two dimension, got {value}" + ); + value.trailing_zeros() as usize +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct JoltOneHotDimensions { + pub log_t: usize, + pub instruction_address_bits: usize, + pub bytecode_k: usize, + pub ram_k: usize, + pub committed_chunk_bits: usize, + pub lookup_virtual_chunk_bits: usize, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct JoltOneHotConfig { + pub log_k_chunk: u8, + pub lookups_ra_virtual_log_k_chunk: u8, +} + +impl JoltOneHotConfig { + pub const fn committed_chunk_bits(self) -> usize { + self.log_k_chunk as usize + } + + pub const fn lookup_virtual_chunk_bits(self) -> usize { + self.lookups_ra_virtual_log_k_chunk as usize + } + + pub fn committed_address_chunks(self, r_address: &[F]) -> Vec> { + let chunk_bits = self.committed_chunk_bits(); + if chunk_bits == 0 { + return Vec::new(); + } + + let padding = r_address + .len() + .next_multiple_of(chunk_bits) + .saturating_sub(r_address.len()); + let mut padded = Vec::with_capacity(r_address.len() + padding); + padded.extend((0..padding).map(|_| F::zero())); + padded.extend_from_slice(r_address); + padded + .chunks(chunk_bits) + .map(<[F]>::to_vec) + .collect::>() + } + + pub const fn dimensions( + self, + log_t: usize, + instruction_address_bits: usize, + bytecode_k: usize, + ram_k: usize, + ) -> JoltOneHotDimensions { + JoltOneHotDimensions { + log_t, + instruction_address_bits, + bytecode_k, + ram_k, + committed_chunk_bits: self.committed_chunk_bits(), + lookup_virtual_chunk_bits: self.lookup_virtual_chunk_bits(), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct JoltFormulaDimensions { + pub trace: TraceDimensions, + pub ra_layout: JoltRaPolynomialLayout, + pub instruction_read_raf: InstructionReadRafDimensions, + pub instruction_ra_virtualization: InstructionRaVirtualizationDimensions, + pub bytecode_read_raf: BytecodeReadRafDimensions, + pub ram_ra_virtualization: RamRaVirtualizationDimensions, +} + +impl TryFrom for JoltFormulaDimensions { + type Error = JoltFormulaDimensionsError; + + fn try_from(dimensions: JoltOneHotDimensions) -> Result { + require_nonzero( + dimensions.instruction_address_bits, + "instruction_address_bits", + )?; + require_nonzero(dimensions.bytecode_k, "bytecode_k")?; + require_nonzero(dimensions.ram_k, "ram_k")?; + require_nonzero(dimensions.committed_chunk_bits, "committed_chunk_bits")?; + require_nonzero( + dimensions.lookup_virtual_chunk_bits, + "lookup_virtual_chunk_bits", + )?; + + if dimensions.lookup_virtual_chunk_bits < dimensions.committed_chunk_bits { + return Err(JoltFormulaDimensionsError::InvalidChunkOrder { + committed_chunk_bits: dimensions.committed_chunk_bits, + lookup_virtual_chunk_bits: dimensions.lookup_virtual_chunk_bits, + }); + } + + require_divisible( + "lookup_virtual_chunk_bits", + dimensions.lookup_virtual_chunk_bits, + "committed_chunk_bits", + dimensions.committed_chunk_bits, + )?; + require_divisible( + "instruction_address_bits", + dimensions.instruction_address_bits, + "lookup_virtual_chunk_bits", + dimensions.lookup_virtual_chunk_bits, + )?; + + let instruction_address_bits = dimensions.instruction_address_bits; + let bytecode_log_k = ceil_log_2(dimensions.bytecode_k); + let ram_log_k = ceil_log_2(dimensions.ram_k); + let instruction_d = instruction_address_bits.div_ceil(dimensions.committed_chunk_bits); + let bytecode_d = bytecode_log_k.div_ceil(dimensions.committed_chunk_bits); + let ram_d = ram_log_k.div_ceil(dimensions.committed_chunk_bits); + let virtual_instruction_ra_polys = + instruction_address_bits / dimensions.lookup_virtual_chunk_bits; + let committed_per_virtual = + dimensions.lookup_virtual_chunk_bits / dimensions.committed_chunk_bits; + + Ok(Self { + trace: TraceDimensions::new(dimensions.log_t), + ra_layout: JoltRaPolynomialLayout::try_from((instruction_d, bytecode_d, ram_d))?, + instruction_read_raf: InstructionReadRafDimensions::try_from(( + dimensions.log_t, + instruction_address_bits, + virtual_instruction_ra_polys, + ))?, + instruction_ra_virtualization: InstructionRaVirtualizationDimensions::try_from(( + dimensions.log_t, + virtual_instruction_ra_polys, + committed_per_virtual, + ))?, + bytecode_read_raf: BytecodeReadRafDimensions::new( + dimensions.log_t, + bytecode_log_k, + bytecode_d, + ), + ram_ra_virtualization: RamRaVirtualizationDimensions::new(dimensions.log_t, ram_d), + }) + } +} + +fn require_nonzero(value: usize, name: &'static str) -> Result<(), JoltFormulaDimensionsError> { + if value == 0 { + Err(JoltFormulaDimensionsError::Zero { name }) + } else { + Ok(()) + } +} + +fn require_divisible( + value_name: &'static str, + value: usize, + divisor_name: &'static str, + divisor: usize, +) -> Result<(), JoltFormulaDimensionsError> { + if value.is_multiple_of(divisor) { + Ok(()) + } else { + Err(JoltFormulaDimensionsError::NotDivisible { + value_name, + value, + divisor_name, + divisor, + }) + } +} + +fn ceil_log_2(value: usize) -> usize { + if value <= 1 { + 0 + } else { + usize::BITS as usize - (value - 1).leading_zeros() as usize + } +} + +#[cfg(test)] +mod tests { + #![expect(clippy::panic, reason = "tests fail loudly on unexpected errors")] + + use super::super::claim_reductions::advice::AdviceClaimReductionLayout; + use super::*; + use jolt_field::{Fr, FromPrimitiveInt, Invertible}; + + fn dimensions() -> JoltOneHotDimensions { + JoltOneHotDimensions { + log_t: 20, + instruction_address_bits: 128, + bytecode_k: 1024, + ram_k: 4096, + committed_chunk_bits: 8, + lookup_virtual_chunk_bits: 32, + } + } + + #[test] + fn derives_all_runtime_formula_dimensions() -> Result<(), JoltFormulaDimensionsError> { + let dimensions = JoltFormulaDimensions::try_from(dimensions())?; + + assert_eq!(dimensions.ra_layout.instruction(), 16); + assert_eq!(dimensions.trace.log_t(), 20); + assert_eq!(dimensions.ra_layout.bytecode(), 2); + assert_eq!(dimensions.ra_layout.ram(), 2); + assert_eq!( + dimensions.instruction_read_raf.sumcheck(), + JoltSumcheckSpec::boolean(148, 6) + ); + assert_eq!(dimensions.instruction_read_raf.num_virtual_ra_polys(), 4); + assert_eq!( + dimensions + .instruction_ra_virtualization + .num_committed_per_virtual(), + 4 + ); + assert_eq!( + dimensions + .instruction_ra_virtualization + .num_committed_ra_polys(), + 16 + ); + assert_eq!(dimensions.bytecode_read_raf.num_committed_ra_polys(), 2); + assert_eq!(dimensions.ram_ra_virtualization.num_committed_ra_polys(), 2); + Ok(()) + } + + #[test] + fn supports_zero_bytecode_and_ram_d() -> Result<(), JoltFormulaDimensionsError> { + let dimensions = JoltFormulaDimensions::try_from(JoltOneHotDimensions { + bytecode_k: 1, + ram_k: 1, + ..dimensions() + })?; + + assert_eq!(dimensions.ra_layout.instruction(), 16); + assert_eq!(dimensions.ra_layout.bytecode(), 0); + assert_eq!(dimensions.ra_layout.ram(), 0); + assert_eq!(dimensions.ra_layout.total(), 16); + assert_eq!(dimensions.bytecode_read_raf.num_committed_ra_polys(), 0); + assert_eq!(dimensions.ram_ra_virtualization.num_committed_ra_polys(), 0); + Ok(()) + } + + #[test] + fn trace_dimensions_normalize_cycle_opening_point() { + let challenges = [Fr::from_u64(3), Fr::from_u64(5), Fr::from_u64(7)]; + + assert_eq!( + TraceDimensions::new(3) + .cycle_opening_point(&challenges) + .unwrap_or_else(|err| panic!("cycle point should normalize: {err}")), + vec![Fr::from_u64(7), Fr::from_u64(5), Fr::from_u64(3)] + ); + } + + #[test] + fn trace_polynomial_order_indexes_match_protocol_order() { + assert_eq!( + TracePolynomialOrder::CycleMajor.address_cycle_to_index(3, 4, 10, 20), + 64 + ); + assert_eq!( + TracePolynomialOrder::AddressMajor.address_cycle_to_index(3, 4, 10, 20), + 43 + ); + assert_eq!( + TracePolynomialOrder::CycleMajor.index_to_address_cycle(64, 10, 20), + (3, 4) + ); + assert_eq!( + TracePolynomialOrder::AddressMajor.index_to_address_cycle(43, 10, 20), + (3, 4) + ); + assert_eq!(TracePolynomialOrder::CycleMajor.transcript_scalar(), 0); + assert_eq!(TracePolynomialOrder::AddressMajor.transcript_scalar(), 1); + } + + #[test] + fn commitment_matrix_shapes_follow_balanced_policy() { + let shape = CommitmentMatrixShape::balanced(13); + assert_eq!(shape.column_vars(), 7); + assert_eq!(shape.row_vars(), 6); + assert_eq!(shape.total_vars(), 13); + + assert_eq!( + CommitmentMatrixShape::advice_from_max_bytes(64), + CommitmentMatrixShape::new(2, 1) + ); + assert_eq!( + CommitmentMatrixShape::advice_from_max_bytes(0), + CommitmentMatrixShape::new(0, 0) + ); + } + + #[test] + fn advice_layout_extracts_cycle_phase_variables_without_dory_globals() { + let layout = + AdviceClaimReductionLayout::balanced(TracePolynomialOrder::CycleMajor, 8, 4, 64); + let challenges = (1..=7).map(Fr::from_u64).collect::>(); + + assert_eq!(layout.main_shape(), CommitmentMatrixShape::new(6, 6)); + assert_eq!(layout.advice_shape(), CommitmentMatrixShape::new(2, 1)); + assert_eq!(layout.cycle_phase_col_rounds(), 0..2); + assert_eq!(layout.cycle_phase_row_rounds(), 6..7); + assert_eq!(layout.cycle_phase_rounds(), 7); + assert_eq!(layout.active_cycle_phase_rounds(), 3); + assert_eq!(layout.address_phase_rounds(), 0); + assert_eq!(layout.dummy_cycle_phase_rounds(), 4); + assert_eq!( + layout + .cycle_phase_variable_challenges(&challenges) + .unwrap_or_else(|error| panic!("cycle phase variables should extract: {error}")), + vec![Fr::from_u64(1), Fr::from_u64(2), Fr::from_u64(7)] + ); + assert_eq!( + layout + .cycle_phase_opening_point(&challenges) + .unwrap_or_else(|error| panic!("cycle phase point should normalize: {error}")), + vec![Fr::from_u64(7), Fr::from_u64(2), Fr::from_u64(1)] + ); + } + + #[test] + fn advice_layout_extracts_address_phase_point_without_dory_globals() { + let layout = AdviceClaimReductionLayout::new( + TracePolynomialOrder::CycleMajor, + 8, + 4, + CommitmentMatrixShape::balanced(12), + CommitmentMatrixShape::balanced(8), + ); + let cycle_challenges = (1..=8).map(Fr::from_u64).collect::>(); + let cycle_vars = layout + .cycle_phase_variable_challenges(&cycle_challenges) + .unwrap_or_else(|error| panic!("cycle variables should extract: {error}")); + let address_challenges = [Fr::from_u64(101), Fr::from_u64(102)]; + + assert_eq!(layout.cycle_phase_col_rounds(), 0..4); + assert_eq!(layout.cycle_phase_row_rounds(), 6..8); + assert_eq!(layout.active_cycle_phase_rounds(), 6); + assert_eq!(layout.address_phase_rounds(), 2); + assert_eq!( + layout + .address_phase_opening_point(&cycle_vars, &address_challenges) + .unwrap_or_else(|error| panic!("address phase point should normalize: {error}")), + vec![ + Fr::from_u64(102), + Fr::from_u64(101), + Fr::from_u64(8), + Fr::from_u64(7), + Fr::from_u64(4), + Fr::from_u64(3), + Fr::from_u64(2), + Fr::from_u64(1), + ] + ); + } + + #[test] + fn advice_layout_tracks_address_major_cycle_gap() { + let layout = + AdviceClaimReductionLayout::balanced(TracePolynomialOrder::AddressMajor, 8, 4, 64); + let challenges = (1..=3).map(Fr::from_u64).collect::>(); + let cycle_vars = layout + .cycle_phase_variable_challenges(&challenges) + .unwrap_or_else(|error| panic!("cycle variables should extract: {error}")); + + assert_eq!(layout.cycle_phase_col_rounds(), 0..0); + assert_eq!(layout.cycle_phase_row_rounds(), 2..3); + assert_eq!(layout.cycle_phase_rounds(), 3); + assert_eq!(layout.active_cycle_phase_rounds(), 1); + assert_eq!(layout.address_phase_rounds(), 2); + assert_eq!(layout.dummy_cycle_phase_rounds(), 2); + assert_eq!(cycle_vars, vec![Fr::from_u64(3)]); + assert_eq!( + layout + .address_phase_opening_point(&cycle_vars, &[Fr::from_u64(101), Fr::from_u64(102)]) + .unwrap_or_else(|error| panic!("address phase point should normalize: {error}")), + vec![Fr::from_u64(3), Fr::from_u64(102), Fr::from_u64(101)] + ); + } + + #[test] + fn advice_final_output_scale_includes_cycle_phase_dummy_rounds() { + let layout = + AdviceClaimReductionLayout::balanced(TracePolynomialOrder::AddressMajor, 8, 4, 64); + let challenges = [Fr::from_u64(11), Fr::from_u64(12), Fr::from_u64(1)]; + let opening_point = layout + .cycle_phase_opening_point(&challenges) + .unwrap_or_else(|error| panic!("cycle phase point should normalize: {error}")); + let two_inv = Fr::from_u64(2).inv_or_zero(); + + assert_eq!( + layout + .cycle_phase_final_output_scale(&opening_point, &challenges) + .unwrap_or_else(|error| panic!("final output scale should compute: {error}")), + two_inv * two_inv + ); + } + + #[test] + fn rejects_zero_dimensions() { + assert_eq!( + JoltFormulaDimensions::try_from(JoltOneHotDimensions { + instruction_address_bits: 0, + ..dimensions() + }), + Err(JoltFormulaDimensionsError::Zero { + name: "instruction_address_bits" + }) + ); + assert_eq!( + JoltFormulaDimensions::try_from(JoltOneHotDimensions { + bytecode_k: 0, + ..dimensions() + }), + Err(JoltFormulaDimensionsError::Zero { name: "bytecode_k" }) + ); + } + + #[test] + fn rejects_incompatible_chunks() { + assert_eq!( + JoltFormulaDimensions::try_from(JoltOneHotDimensions { + committed_chunk_bits: 16, + lookup_virtual_chunk_bits: 8, + ..dimensions() + }), + Err(JoltFormulaDimensionsError::InvalidChunkOrder { + committed_chunk_bits: 16, + lookup_virtual_chunk_bits: 8, + }) + ); + assert_eq!( + JoltFormulaDimensions::try_from(JoltOneHotDimensions { + lookup_virtual_chunk_bits: 20, + ..dimensions() + }), + Err(JoltFormulaDimensionsError::NotDivisible { + value_name: "lookup_virtual_chunk_bits", + value: 20, + divisor_name: "committed_chunk_bits", + divisor: 8, + }) + ); + assert_eq!( + JoltFormulaDimensions::try_from(JoltOneHotDimensions { + lookup_virtual_chunk_bits: 48, + ..dimensions() + }), + Err(JoltFormulaDimensionsError::NotDivisible { + value_name: "instruction_address_bits", + value: 128, + divisor_name: "lookup_virtual_chunk_bits", + divisor: 48, + }) + ); + } + + #[test] + fn read_write_dimensions_normalize_full_opening_point() { + let dimensions = ReadWriteDimensions::new(4, 3, 1, 2); + let challenges = (1..=7).map(Fr::from_u64).collect::>(); + + let point = dimensions + .read_write_opening_point(&challenges) + .unwrap_or_else(|error| panic!("read-write opening point should evaluate: {error}")); + + assert_eq!( + point.r_cycle, + vec![ + Fr::from_u64(6), + Fr::from_u64(5), + Fr::from_u64(4), + Fr::from_u64(1) + ] + ); + assert_eq!( + point.r_address, + vec![Fr::from_u64(7), Fr::from_u64(3), Fr::from_u64(2)] + ); + assert_eq!( + point.opening_point, + vec![ + Fr::from_u64(7), + Fr::from_u64(3), + Fr::from_u64(2), + Fr::from_u64(6), + Fr::from_u64(5), + Fr::from_u64(4), + Fr::from_u64(1), + ] + ); + } + + #[test] + fn read_write_dimensions_extract_address_opening_point() { + let dimensions = ReadWriteDimensions::new(4, 3, 1, 2); + let challenges = (10..=15).map(Fr::from_u64).collect::>(); + + assert_eq!( + dimensions + .address_opening_point(&challenges) + .unwrap_or_else(|error| panic!("address opening point should evaluate: {error}")), + vec![Fr::from_u64(15), Fr::from_u64(11), Fr::from_u64(10)] + ); + } + + #[test] + fn read_write_point_helpers_reject_bad_shapes() { + let dimensions = ReadWriteDimensions::new(4, 3, 5, 2); + assert_eq!( + dimensions.read_write_opening_point::(&[]), + Err(JoltFormulaPointError::InvalidReadWritePhaseSplit { + phase1_num_rounds: 5, + log_t: 4, + phase2_num_rounds: 2, + log_k: 3, + }) + ); + + let dimensions = ReadWriteDimensions::new(4, 3, 1, 2); + assert_eq!( + dimensions.address_opening_point::(&[Fr::from_u64(0)]), + Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected: 6, + got: 1, + }) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/error.rs b/crates/jolt-claims/src/protocols/jolt/formulas/error.rs new file mode 100644 index 0000000000..c4c280379b --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/error.rs @@ -0,0 +1,47 @@ +use thiserror::Error; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Error)] +pub enum JoltFormulaPointError { + #[error( + "invalid read-write phase split: phase1 {phase1_num_rounds}/{log_t}, phase2 {phase2_num_rounds}/{log_k}" + )] + InvalidReadWritePhaseSplit { + phase1_num_rounds: usize, + log_t: usize, + phase2_num_rounds: usize, + log_k: usize, + }, + #[error("challenge length mismatch: expected {expected}, got {got}")] + ChallengeLengthMismatch { expected: usize, got: usize }, + #[error("opening point length mismatch: expected {expected}, got {got}")] + OpeningPointLengthMismatch { expected: usize, got: usize }, + #[error("evaluation domain length mismatch: expected {expected}, got {got}")] + EvaluationDomainLengthMismatch { expected: usize, got: usize }, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Error)] +pub enum JoltFormulaDimensionsError { + #[error("{name} must be nonzero")] + Zero { name: &'static str }, + #[error("{name} overflowed")] + Overflow { name: &'static str }, + #[error( + "lookup_virtual_chunk_bits ({lookup_virtual_chunk_bits}) must be >= committed_chunk_bits ({committed_chunk_bits})" + )] + InvalidChunkOrder { + committed_chunk_bits: usize, + lookup_virtual_chunk_bits: usize, + }, + #[error("{value_name} ({value}) must be divisible by {divisor_name} ({divisor})")] + NotDivisible { + value_name: &'static str, + value: usize, + divisor_name: &'static str, + divisor: usize, + }, + #[error("phase1_num_rounds ({phase1_num_rounds}) must be <= log_t ({log_t})")] + InvalidPhaseRounds { + phase1_num_rounds: usize, + log_t: usize, + }, +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/instruction.rs b/crates/jolt-claims/src/protocols/jolt/formulas/instruction.rs new file mode 100644 index 0000000000..751344fc5f --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/instruction.rs @@ -0,0 +1,1254 @@ +use std::num::NonZeroUsize; + +use jolt_field::{Field, RingCore}; +use jolt_lookup_tables::{LookupTableKind, XLEN}; +use jolt_riscv::InstructionFlags; + +use crate::{challenge, opening}; + +use super::super::InstructionRaVirtualizationChallenge; +use super::super::{ + InstructionInputChallenge, InstructionReadRafChallenge, JoltChallengeId, + JoltCommittedPolynomial, JoltConsistencyClaim, JoltExpr, JoltOpeningId, JoltRelationClaims, + JoltRelationId, JoltVirtualPolynomial, +}; +use super::dimensions::{ + JoltFormulaDimensionsError, JoltFormulaPointError, JoltSumcheckSpec, TraceDimensions, +}; + +const INPUT_VIRTUALIZATION_DEGREE: usize = 3; +const READ_RAF_BASE_DEGREE: usize = 2; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct InstructionReadRafDimensions { + log_t: usize, + instruction_address_bits: usize, + num_virtual_ra_polys: NonZeroUsize, +} + +impl InstructionReadRafDimensions { + pub const fn new( + log_t: usize, + instruction_address_bits: usize, + num_virtual_ra_polys: NonZeroUsize, + ) -> Self { + Self { + log_t, + instruction_address_bits, + num_virtual_ra_polys, + } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn instruction_address_bits(self) -> usize { + self.instruction_address_bits + } + + pub fn num_virtual_ra_polys(self) -> usize { + self.num_virtual_ra_polys.get() + } + + pub fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean( + self.instruction_address_bits + self.log_t, + self.num_virtual_ra_polys() + READ_RAF_BASE_DEGREE, + ) + } + + pub fn opening_point( + self, + challenges: &[F], + ) -> Result, JoltFormulaPointError> { + let expected = self.instruction_address_bits + self.log_t; + if challenges.len() != expected { + return Err(JoltFormulaPointError::ChallengeLengthMismatch { + expected, + got: challenges.len(), + }); + } + + let (r_address, r_cycle) = challenges.split_at(self.instruction_address_bits); + let r_cycle = r_cycle.iter().rev().copied().collect::>(); + let r_address = r_address.to_vec(); + let opening_point = [r_address.as_slice(), r_cycle.as_slice()].concat(); + + Ok(InstructionReadRafOpeningPoint { + r_address, + r_cycle, + opening_point, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InstructionReadRafOpeningPoint { + pub r_address: Vec, + pub r_cycle: Vec, + pub opening_point: Vec, +} + +impl TryFrom<(usize, usize, usize)> for InstructionReadRafDimensions { + type Error = JoltFormulaDimensionsError; + + fn try_from( + (log_t, instruction_address_bits, num_virtual_ra_polys): (usize, usize, usize), + ) -> Result { + if instruction_address_bits == 0 { + return Err(JoltFormulaDimensionsError::Zero { + name: "instruction_address_bits", + }); + } + Ok(Self::new( + log_t, + instruction_address_bits, + NonZeroUsize::new(num_virtual_ra_polys).ok_or(JoltFormulaDimensionsError::Zero { + name: "instruction virtual RA polynomial count", + })?, + )) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct InstructionRaVirtualizationDimensions { + log_t: usize, + virtual_ra_polys: NonZeroUsize, + committed_per_virtual: NonZeroUsize, + committed_ra_polys: NonZeroUsize, +} + +impl InstructionRaVirtualizationDimensions { + pub fn new( + log_t: usize, + num_virtual_ra_polys: NonZeroUsize, + num_committed_per_virtual: NonZeroUsize, + ) -> Result { + let num_committed_ra_polys = num_virtual_ra_polys + .get() + .checked_mul(num_committed_per_virtual.get()) + .ok_or(JoltFormulaDimensionsError::Overflow { + name: "instruction committed RA polynomial count", + })?; + Ok(Self { + log_t, + virtual_ra_polys: num_virtual_ra_polys, + committed_per_virtual: num_committed_per_virtual, + committed_ra_polys: NonZeroUsize::new(num_committed_ra_polys).ok_or( + JoltFormulaDimensionsError::Zero { + name: "instruction committed RA polynomial count", + }, + )?, + }) + } + + pub fn num_virtual_ra_polys(self) -> usize { + self.virtual_ra_polys.get() + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub fn num_committed_per_virtual(self) -> usize { + self.committed_per_virtual.get() + } + + pub fn num_committed_ra_polys(self) -> usize { + self.committed_ra_polys.get() + } + + pub fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t, self.num_committed_per_virtual() + 1) + } +} + +impl TryFrom<(usize, usize, usize)> for InstructionRaVirtualizationDimensions { + type Error = JoltFormulaDimensionsError; + + fn try_from( + (log_t, num_virtual_ra_polys, num_committed_per_virtual): (usize, usize, usize), + ) -> Result { + Self::new( + log_t, + NonZeroUsize::new(num_virtual_ra_polys).ok_or(JoltFormulaDimensionsError::Zero { + name: "instruction virtual RA polynomial count", + })?, + NonZeroUsize::new(num_committed_per_virtual).ok_or( + JoltFormulaDimensionsError::Zero { + name: "committed RA polynomials per virtual RA", + }, + )?, + ) + } +} + +pub fn input_virtualization(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(right_instruction_input_product()) + + input_challenge(InstructionInputChallenge::Gamma) + * opening(left_instruction_input_product()); + + let output = input_challenge(InstructionInputChallenge::EqProduct) + * opening(right_operand_is_rs2()) + * opening(rs2_value()) + + input_challenge(InstructionInputChallenge::EqProduct) + * opening(right_operand_is_imm()) + * opening(imm()) + + input_challenge(InstructionInputChallenge::EqProduct) + * input_challenge(InstructionInputChallenge::Gamma) + * opening(left_operand_is_rs1()) + * opening(rs1_value()) + + input_challenge(InstructionInputChallenge::EqProduct) + * input_challenge(InstructionInputChallenge::Gamma) + * opening(left_operand_is_pc()) + * opening(unexpanded_pc()); + + JoltRelationClaims::new( + JoltRelationId::InstructionInputVirtualization, + dimensions.sumcheck(INPUT_VIRTUALIZATION_DEGREE), + input, + output, + ) + .with_consistency([ + JoltConsistencyClaim::same_evaluation( + left_instruction_input_reduced(), + left_instruction_input_product(), + ), + JoltConsistencyClaim::same_evaluation( + right_instruction_input_reduced(), + right_instruction_input_product(), + ), + ]) +} + +pub fn input_virtualization_input_openings() -> [JoltOpeningId; 2] { + [ + right_instruction_input_product(), + left_instruction_input_product(), + ] +} + +pub fn input_virtualization_output_openings() -> [JoltOpeningId; 8] { + [ + right_operand_is_rs2(), + rs2_value(), + right_operand_is_imm(), + imm(), + left_operand_is_rs1(), + rs1_value(), + left_operand_is_pc(), + unexpanded_pc(), + ] +} + +pub fn input_virtualization_consistency_openings() -> [(JoltOpeningId, JoltOpeningId); 2] { + [ + ( + left_instruction_input_reduced(), + left_instruction_input_product(), + ), + ( + right_instruction_input_reduced(), + right_instruction_input_product(), + ), + ] +} + +pub fn read_raf(dimensions: InstructionReadRafDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = read_raf_challenge(InstructionReadRafChallenge::Gamma); + let input = opening(lookup_output_reduced()) + + gamma.clone() * opening(left_lookup_operand_reduced()) + + gamma.pow(2) * opening(right_lookup_operand_reduced()); + + let ra_product = instruction_ra_product(dimensions); + let mut output = JoltExpr::zero(); + + for table in LookupTableKind::::iter() { + output = output + + read_raf_challenge(eq_table_value(table)) + * ra_product.clone() + * opening(lookup_table_flag(table)); + } + + output = output + + read_raf_challenge(InstructionReadRafChallenge::EqRafConstant) * ra_product.clone() + + read_raf_challenge(InstructionReadRafChallenge::EqRafFlag) + * ra_product + * opening(instruction_raf_flag()); + + JoltRelationClaims::new( + JoltRelationId::InstructionReadRaf, + dimensions.sumcheck(), + input, + output, + ) + .with_consistency([JoltConsistencyClaim::same_evaluation( + lookup_output_reduced(), + lookup_output_product(), + )]) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InstructionReadRafOutputOpenings { + pub lookup_table_flags: Vec, + pub instruction_ra: Vec, + pub instruction_raf_flag: JoltOpeningId, +} + +pub fn read_raf_input_openings() -> [JoltOpeningId; 3] { + [ + lookup_output_reduced(), + left_lookup_operand_reduced(), + right_lookup_operand_reduced(), + ] +} + +pub fn read_raf_output_openings( + dimensions: InstructionReadRafDimensions, +) -> InstructionReadRafOutputOpenings { + InstructionReadRafOutputOpenings { + lookup_table_flags: LookupTableKind::::iter() + .map(read_raf_lookup_table_flag_opening) + .collect(), + instruction_ra: (0..dimensions.num_virtual_ra_polys()) + .map(read_raf_instruction_ra_opening) + .collect(), + instruction_raf_flag: read_raf_instruction_raf_flag_opening(), + } +} + +pub fn read_raf_consistency_openings() -> [(JoltOpeningId, JoltOpeningId); 1] { + [(lookup_output_reduced(), lookup_output_product())] +} + +pub fn read_raf_lookup_table_flag_opening(table: LookupTableKind) -> JoltOpeningId { + lookup_table_flag(table) +} + +pub fn read_raf_instruction_ra_opening(index: usize) -> JoltOpeningId { + instruction_ra(index) +} + +pub fn read_raf_instruction_raf_flag_opening() -> JoltOpeningId { + instruction_raf_flag() +} + +pub fn ra_virtualization( + dimensions: InstructionRaVirtualizationDimensions, +) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = ra_virtualization_challenge(InstructionRaVirtualizationChallenge::Gamma); + let input = weighted_instruction_ra_sum(dimensions, gamma.clone()); + + let eq_cycle = ra_virtualization_challenge(InstructionRaVirtualizationChallenge::EqCycle); + let mut output = JoltExpr::zero(); + for virtual_index in 0..dimensions.num_virtual_ra_polys() { + output = output + + eq_cycle.clone() + * gamma.clone().pow(virtual_index) + * committed_instruction_ra_product(dimensions, virtual_index); + } + + JoltRelationClaims::new( + JoltRelationId::InstructionRaVirtualization, + dimensions.sumcheck(), + input, + output, + ) + .with_input_challenges([JoltChallengeId::from( + InstructionRaVirtualizationChallenge::Gamma, + )]) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InstructionRaVirtualizationOutputOpenings { + pub committed_instruction_ra_by_virtual: Vec>, +} + +impl InstructionRaVirtualizationOutputOpenings { + pub fn all(&self) -> Vec { + self.committed_instruction_ra_by_virtual + .iter() + .flatten() + .copied() + .collect() + } +} + +pub fn ra_virtualization_input_openings( + dimensions: InstructionRaVirtualizationDimensions, +) -> Vec { + (0..dimensions.num_virtual_ra_polys()) + .map(ra_virtualization_instruction_ra_opening) + .collect() +} + +pub fn ra_virtualization_output_openings( + dimensions: InstructionRaVirtualizationDimensions, +) -> InstructionRaVirtualizationOutputOpenings { + let committed_instruction_ra_by_virtual = (0..dimensions.num_virtual_ra_polys()) + .map(|virtual_index| { + let start = virtual_index * dimensions.num_committed_per_virtual(); + (start..start + dimensions.num_committed_per_virtual()) + .map(ra_virtualization_committed_instruction_ra_opening) + .collect() + }) + .collect(); + + InstructionRaVirtualizationOutputOpenings { + committed_instruction_ra_by_virtual, + } +} + +pub fn ra_virtualization_instruction_ra_opening(index: usize) -> JoltOpeningId { + instruction_ra(index) +} + +pub fn ra_virtualization_committed_instruction_ra_opening(index: usize) -> JoltOpeningId { + committed_instruction_ra(index) +} + +fn input_challenge(id: InstructionInputChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn read_raf_challenge(id: InstructionReadRafChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn ra_virtualization_challenge(id: InstructionRaVirtualizationChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn eq_table_value(table: LookupTableKind) -> InstructionReadRafChallenge { + InstructionReadRafChallenge::EqTableValue(table.index()) +} + +fn weighted_instruction_ra_sum( + dimensions: InstructionRaVirtualizationDimensions, + gamma: JoltExpr, +) -> JoltExpr +where + F: RingCore, +{ + let mut sum = JoltExpr::zero(); + for i in 0..dimensions.num_virtual_ra_polys() { + sum = sum + gamma.clone().pow(i) * opening(instruction_ra(i)); + } + sum +} + +fn instruction_ra_product(dimensions: InstructionReadRafDimensions) -> JoltExpr +where + F: RingCore, +{ + let mut product = JoltExpr::one(); + for i in 0..dimensions.num_virtual_ra_polys() { + product = product * opening(instruction_ra(i)); + } + product +} + +fn committed_instruction_ra_product( + dimensions: InstructionRaVirtualizationDimensions, + virtual_index: usize, +) -> JoltExpr +where + F: RingCore, +{ + let mut product = JoltExpr::one(); + let start = virtual_index * dimensions.num_committed_per_virtual(); + for i in start..start + dimensions.num_committed_per_virtual() { + product = product * opening(committed_instruction_ra(i)); + } + product +} + +fn left_instruction_input_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftInstructionInput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn right_instruction_input_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightInstructionInput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn left_instruction_input_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftInstructionInput, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn right_instruction_input_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightInstructionInput, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn lookup_output_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupOutput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn lookup_output_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupOutput, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn left_lookup_operand_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftLookupOperand, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn right_lookup_operand_reduced() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightLookupOperand, + JoltRelationId::InstructionClaimReduction, + ) +} + +fn instruction_ra(index: usize) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionRa(index), + JoltRelationId::InstructionReadRaf, + ) +} + +fn committed_instruction_ra(index: usize) -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::InstructionRa(index), + JoltRelationId::InstructionRaVirtualization, + ) +} + +fn lookup_table_flag(table: LookupTableKind) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupTableFlag(table.index()), + JoltRelationId::InstructionReadRaf, + ) +} + +fn instruction_raf_flag() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionRafFlag, + JoltRelationId::InstructionReadRaf, + ) +} + +fn left_operand_is_rs1() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsRs1Value), + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn rs1_value() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Value, + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn left_operand_is_pc() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsPC), + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn unexpanded_pc() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnexpandedPC, + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn right_operand_is_rs2() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsRs2Value), + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn rs2_value() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Value, + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn right_operand_is_imm() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsImm), + JoltRelationId::InstructionInputVirtualization, + ) +} + +fn imm() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Imm, + JoltRelationId::InstructionInputVirtualization, + ) +} + +#[cfg(test)] +#[expect(clippy::panic)] +mod tests { + use super::*; + use crate::protocols::jolt::{JoltConsistencyClaim, JoltPolynomialId}; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn read_raf_dimensions(num_virtual_ra_polys: usize) -> InstructionReadRafDimensions { + InstructionReadRafDimensions::try_from((5, 128, num_virtual_ra_polys)) + .unwrap_or_else(|err| panic!("test read-RAF dimensions should be nonzero: {err}")) + } + + fn ra_virtualization_dimensions( + num_virtual_ra_polys: usize, + num_committed_per_virtual: usize, + ) -> InstructionRaVirtualizationDimensions { + InstructionRaVirtualizationDimensions::try_from(( + 5, + num_virtual_ra_polys, + num_committed_per_virtual, + )) + .unwrap_or_else(|err| panic!("test RA virtualization dimensions should be valid: {err}")) + } + + fn trace_dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + fn lookup_table_flags() -> Vec { + LookupTableKind::::iter() + .map(lookup_table_flag) + .collect() + } + + fn eq_table_value_challenges() -> Vec { + LookupTableKind::::iter() + .map(|table| JoltChallengeId::from(eq_table_value(table))) + .collect() + } + + #[test] + fn input_virtualization_exposes_expected_dependencies() { + let claims = input_virtualization::(trace_dimensions()); + + assert_eq!(claims.id, JoltRelationId::InstructionInputVirtualization); + assert_eq!(claims.sumcheck, trace_dimensions().sumcheck(3)); + assert_eq!( + claims.input.required_openings, + input_virtualization_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + input_virtualization_output_openings().to_vec() + ); + assert_eq!( + claims.consistency, + input_virtualization_consistency_openings() + .into_iter() + .map(|(left, right)| JoltConsistencyClaim::same_evaluation(left, right)) + .collect::>() + ); + assert_eq!( + claims.required_openings(), + vec![ + right_instruction_input_product(), + left_instruction_input_product(), + right_operand_is_rs2(), + rs2_value(), + right_operand_is_imm(), + imm(), + left_operand_is_rs1(), + rs1_value(), + left_operand_is_pc(), + unexpanded_pc(), + left_instruction_input_reduced(), + right_instruction_input_reduced(), + ] + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(InstructionInputChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(InstructionInputChallenge::EqProduct), + JoltChallengeId::from(InstructionInputChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(InstructionInputChallenge::Gamma), + JoltChallengeId::from(InstructionInputChallenge::EqProduct), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from(InstructionInputChallenge::EqProduct)), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn input_virtualization_evaluates_like_core_formula() { + let claims = input_virtualization::(trace_dimensions()); + + let right_input = Fr::from_u64(3); + let left_input = Fr::from_u64(5); + let right_is_rs2 = Fr::from_u64(7); + let rs2 = Fr::from_u64(11); + let right_is_imm = Fr::from_u64(13); + let imm_value = Fr::from_u64(17); + let left_is_rs1 = Fr::from_u64(19); + let rs1 = Fr::from_u64(23); + let left_is_pc = Fr::from_u64(29); + let pc = Fr::from_u64(31); + let gamma = Fr::from_u64(37); + let eq_product = Fr::from_u64(41); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == right_instruction_input_product() => right_input, + id if id == left_instruction_input_product() => left_input, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionInput(InstructionInputChallenge::Gamma) => gamma, + JoltChallengeId::InstructionInput(InstructionInputChallenge::EqProduct) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == right_operand_is_rs2() => right_is_rs2, + id if id == rs2_value() => rs2, + id if id == right_operand_is_imm() => right_is_imm, + id if id == imm() => imm_value, + id if id == left_operand_is_rs1() => left_is_rs1, + id if id == rs1_value() => rs1, + id if id == left_operand_is_pc() => left_is_pc, + id if id == unexpanded_pc() => pc, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionInput(InstructionInputChallenge::Gamma) => gamma, + JoltChallengeId::InstructionInput(InstructionInputChallenge::EqProduct) => { + eq_product + } + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!(input, right_input + gamma * left_input); + assert_eq!( + output, + eq_product + * (right_is_rs2 * rs2 + + right_is_imm * imm_value + + gamma * left_is_rs1 * rs1 + + gamma * left_is_pc * pc) + ); + } + + #[test] + fn read_raf_rejects_empty_dimensions() { + assert!(InstructionReadRafDimensions::try_from((5, 128, 0)).is_err()); + assert!(InstructionReadRafDimensions::try_from((5, 0, 1)).is_err()); + } + + #[test] + fn read_raf_exposes_expected_dependencies() { + let dimensions = read_raf_dimensions(2); + let claims = read_raf::(dimensions); + let table_flags = lookup_table_flags(); + let table_value_challenges = eq_table_value_challenges(); + + assert_eq!(claims.id, JoltRelationId::InstructionReadRaf); + assert_eq!(claims.sumcheck, JoltSumcheckSpec::boolean(133, 4)); + assert_eq!( + claims.input.required_openings, + read_raf_input_openings().to_vec() + ); + let mut expected_output_openings = vec![instruction_ra(0), instruction_ra(1)]; + expected_output_openings.extend(table_flags.iter().copied()); + expected_output_openings.push(instruction_raf_flag()); + assert_eq!(claims.output.required_openings, expected_output_openings); + let output_openings = read_raf_output_openings(dimensions); + assert_eq!( + output_openings.instruction_ra, + vec![instruction_ra(0), instruction_ra(1)] + ); + assert_eq!(output_openings.lookup_table_flags, lookup_table_flags()); + assert_eq!(output_openings.instruction_raf_flag, instruction_raf_flag()); + assert_eq!( + claims.consistency, + read_raf_consistency_openings() + .into_iter() + .map(|(left, right)| JoltConsistencyClaim::same_evaluation(left, right)) + .collect::>() + ); + let mut expected_required_openings = vec![ + lookup_output_reduced(), + left_lookup_operand_reduced(), + right_lookup_operand_reduced(), + instruction_ra(0), + instruction_ra(1), + ]; + expected_required_openings.extend(table_flags); + expected_required_openings.extend([instruction_raf_flag(), lookup_output_product()]); + assert_eq!(claims.required_openings(), expected_required_openings); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(InstructionReadRafChallenge::Gamma)] + ); + let mut expected_output_challenges = table_value_challenges.clone(); + expected_output_challenges.extend([ + JoltChallengeId::from(InstructionReadRafChallenge::EqRafConstant), + JoltChallengeId::from(InstructionReadRafChallenge::EqRafFlag), + ]); + assert_eq!( + claims.output.required_challenges, + expected_output_challenges + ); + let mut expected_required_challenges = + vec![JoltChallengeId::from(InstructionReadRafChallenge::Gamma)]; + expected_required_challenges.extend(table_value_challenges); + expected_required_challenges.extend([ + JoltChallengeId::from(InstructionReadRafChallenge::EqRafConstant), + JoltChallengeId::from(InstructionReadRafChallenge::EqRafFlag), + ]); + assert_eq!(claims.required_challenges(), expected_required_challenges); + assert_eq!( + claims.challenge_index(JoltChallengeId::from( + InstructionReadRafChallenge::EqRafFlag + )), + Some(LookupTableKind::::COUNT + 2) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), LookupTableKind::::COUNT + 3); + } + + #[test] + fn read_raf_opening_point_matches_core_order() { + let dimensions = InstructionReadRafDimensions::try_from((3, 4, 1)) + .unwrap_or_else(|err| panic!("test read-RAF dimensions should be valid: {err}")); + let challenges = (1..=7).map(Fr::from_u64).collect::>(); + + let point = dimensions + .opening_point(&challenges) + .unwrap_or_else(|err| panic!("opening point should normalize: {err}")); + + assert_eq!( + point.r_address, + vec![ + Fr::from_u64(1), + Fr::from_u64(2), + Fr::from_u64(3), + Fr::from_u64(4), + ] + ); + assert_eq!( + point.r_cycle, + vec![Fr::from_u64(7), Fr::from_u64(6), Fr::from_u64(5)] + ); + assert_eq!( + point.opening_point, + vec![ + Fr::from_u64(1), + Fr::from_u64(2), + Fr::from_u64(3), + Fr::from_u64(4), + Fr::from_u64(7), + Fr::from_u64(6), + Fr::from_u64(5), + ] + ); + } + + #[test] + fn read_raf_evaluates_like_core_formula() { + let dimensions = read_raf_dimensions(2); + let claims = read_raf::(dimensions); + + let lookup_output = Fr::from_u64(3); + let left_lookup_operand = Fr::from_u64(5); + let right_lookup_operand = Fr::from_u64(7); + let gamma = Fr::from_u64(11); + let ra_0 = Fr::from_u64(2); + let ra_1 = Fr::from_u64(3); + let table_flags: Vec<_> = (0..LookupTableKind::::COUNT) + .map(|i| Fr::from_u64(i as u64 + 5)) + .collect(); + let table_values: Vec<_> = (0..LookupTableKind::::COUNT) + .map(|i| Fr::from_u64(2 * i as u64 + 13)) + .collect(); + let raf_constant = Fr::from_u64(23); + let raf_flag_coeff = Fr::from_u64(29); + let raf_flag = Fr::from_u64(31); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == lookup_output_reduced() => lookup_output, + id if id == left_lookup_operand_reduced() => left_lookup_operand, + id if id == right_lookup_operand_reduced() => right_lookup_operand, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionReadRaf(InstructionReadRafChallenge::Gamma) => gamma, + JoltChallengeId::InstructionReadRaf( + InstructionReadRafChallenge::EqTableValue(_) + | InstructionReadRafChallenge::EqRafConstant + | InstructionReadRafChallenge::EqRafFlag, + ) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == instruction_ra(0) => ra_0, + id if id == instruction_ra(1) => ra_1, + JoltOpeningId::Polynomial { + polynomial: + JoltPolynomialId::Virtual(JoltVirtualPolynomial::LookupTableFlag(index)), + relation: JoltRelationId::InstructionReadRaf, + } => table_flags[index], + id if id == instruction_raf_flag() => raf_flag, + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionReadRaf(InstructionReadRafChallenge::EqTableValue( + index, + )) => table_values[index], + JoltChallengeId::InstructionReadRaf(InstructionReadRafChallenge::EqRafConstant) => { + raf_constant + } + JoltChallengeId::InstructionReadRaf(InstructionReadRafChallenge::EqRafFlag) => { + raf_flag_coeff + } + JoltChallengeId::InstructionReadRaf(InstructionReadRafChallenge::Gamma) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + lookup_output + gamma * left_lookup_operand + gamma * gamma * right_lookup_operand + ); + let table_sum = table_values + .iter() + .zip(table_flags.iter()) + .fold(zero, |sum, (value, flag)| sum + *value * *flag); + assert_eq!( + output, + ra_0 * ra_1 * (table_sum + raf_constant + raf_flag_coeff * raf_flag) + ); + } + + #[test] + fn ra_virtualization_rejects_invalid_dimensions() { + assert!(InstructionRaVirtualizationDimensions::try_from((5, 0, 1)).is_err()); + assert!(InstructionRaVirtualizationDimensions::try_from((5, 1, 0)).is_err()); + assert!(InstructionRaVirtualizationDimensions::try_from((5, usize::MAX, 2)).is_err()); + } + + #[test] + fn ra_virtualization_exposes_expected_dependencies() { + let dimensions = ra_virtualization_dimensions(3, 2); + let claims = ra_virtualization::(dimensions); + + assert_eq!(claims.id, JoltRelationId::InstructionRaVirtualization); + assert_eq!(claims.sumcheck, JoltSumcheckSpec::boolean(5, 3)); + assert_eq!( + claims.input.required_openings, + ra_virtualization_input_openings(dimensions) + ); + assert_eq!( + claims.output.required_openings, + ra_virtualization_output_openings(dimensions).all() + ); + assert_eq!( + ra_virtualization_output_openings(dimensions).committed_instruction_ra_by_virtual, + vec![ + vec![committed_instruction_ra(0), committed_instruction_ra(1)], + vec![committed_instruction_ra(2), committed_instruction_ra(3)], + vec![committed_instruction_ra(4), committed_instruction_ra(5)], + ] + ); + assert!(claims.consistency.is_empty()); + assert_eq!( + claims.required_openings(), + vec![ + instruction_ra(0), + instruction_ra(1), + instruction_ra(2), + committed_instruction_ra(0), + committed_instruction_ra(1), + committed_instruction_ra(2), + committed_instruction_ra(3), + committed_instruction_ra(4), + committed_instruction_ra(5), + ] + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from( + InstructionRaVirtualizationChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(InstructionRaVirtualizationChallenge::EqCycle), + JoltChallengeId::from(InstructionRaVirtualizationChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(InstructionRaVirtualizationChallenge::Gamma), + JoltChallengeId::from(InstructionRaVirtualizationChallenge::EqCycle), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from( + InstructionRaVirtualizationChallenge::Gamma + )), + Some(0) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn ra_virtualization_preserves_gamma_dependency_for_single_virtual_ra() { + let dimensions = ra_virtualization_dimensions(1, 1); + let claims = ra_virtualization::(dimensions); + + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from( + InstructionRaVirtualizationChallenge::Gamma + )] + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from( + InstructionRaVirtualizationChallenge::EqCycle + )] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(InstructionRaVirtualizationChallenge::Gamma), + JoltChallengeId::from(InstructionRaVirtualizationChallenge::EqCycle), + ] + ); + } + + #[test] + fn ra_virtualization_evaluates_like_core_formula() { + let dimensions = ra_virtualization_dimensions(3, 2); + let claims = ra_virtualization::(dimensions); + + let virtual_ra = [Fr::from_u64(3), Fr::from_u64(5), Fr::from_u64(7)]; + let committed_ra = [ + Fr::from_u64(11), + Fr::from_u64(13), + Fr::from_u64(17), + Fr::from_u64(19), + Fr::from_u64(23), + Fr::from_u64(29), + ]; + let gamma = Fr::from_u64(31); + let eq_cycle = Fr::from_u64(37); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + JoltOpeningId::Polynomial { + polynomial: JoltPolynomialId::Virtual(JoltVirtualPolynomial::InstructionRa(i)), + relation: JoltRelationId::InstructionReadRaf, + } => virtual_ra[i], + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionRaVirtualization( + InstructionRaVirtualizationChallenge::Gamma, + ) => gamma, + JoltChallengeId::InstructionRaVirtualization( + InstructionRaVirtualizationChallenge::EqCycle, + ) => eq_cycle, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + JoltOpeningId::Polynomial { + polynomial: + JoltPolynomialId::Committed(JoltCommittedPolynomial::InstructionRa(i)), + relation: JoltRelationId::InstructionRaVirtualization, + } => committed_ra[i], + _ => zero, + }, + |id| match *id { + JoltChallengeId::InstructionRaVirtualization( + InstructionRaVirtualizationChallenge::Gamma, + ) => gamma, + JoltChallengeId::InstructionRaVirtualization( + InstructionRaVirtualizationChallenge::EqCycle, + ) => eq_cycle, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + virtual_ra[0] + gamma * virtual_ra[1] + gamma * gamma * virtual_ra[2] + ); + assert_eq!( + output, + eq_cycle + * (committed_ra[0] * committed_ra[1] + + gamma * committed_ra[2] * committed_ra[3] + + gamma * gamma * committed_ra[4] * committed_ra[5]) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/mod.rs b/crates/jolt-claims/src/protocols/jolt/formulas/mod.rs new file mode 100644 index 0000000000..d4b6fb65d6 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/mod.rs @@ -0,0 +1,11 @@ +pub mod booleanity; +pub mod bytecode; +pub mod claim_reductions; +pub mod committed_openings; +pub mod dimensions; +pub mod error; +pub mod instruction; +pub mod ra; +pub mod ram; +pub mod registers; +pub mod spartan; diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/ra.rs b/crates/jolt-claims/src/protocols/jolt/formulas/ra.rs new file mode 100644 index 0000000000..898e4a0db7 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/ra.rs @@ -0,0 +1,167 @@ +use super::super::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; +use super::dimensions::JoltFormulaDimensionsError; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum JoltRaPolynomial { + Instruction(usize), + Bytecode(usize), + Ram(usize), +} + +impl JoltRaPolynomial { + pub fn committed(self) -> JoltCommittedPolynomial { + match self { + Self::Instruction(index) => JoltCommittedPolynomial::InstructionRa(index), + Self::Bytecode(index) => JoltCommittedPolynomial::BytecodeRa(index), + Self::Ram(index) => JoltCommittedPolynomial::RamRa(index), + } + } + + pub fn opening(self, relation: JoltRelationId) -> JoltOpeningId { + JoltOpeningId::committed(self.committed(), relation) + } +} + +/// Canonical Jolt RA polynomial ordering used by cross-family reductions. +/// +/// Formulas using this layout iterate `InstructionRa`, then `BytecodeRa`, then `RamRa`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct JoltRaPolynomialLayout { + instruction: usize, + bytecode: usize, + ram: usize, +} + +impl JoltRaPolynomialLayout { + pub fn new( + instruction: usize, + bytecode: usize, + ram: usize, + ) -> Result { + let total = instruction + .checked_add(bytecode) + .and_then(|total| total.checked_add(ram)) + .ok_or(JoltFormulaDimensionsError::Overflow { + name: "Jolt RA polynomial count", + })?; + if total == 0 { + return Err(JoltFormulaDimensionsError::Zero { + name: "Jolt RA polynomial count", + }); + } + Ok(Self { + instruction, + bytecode, + ram, + }) + } + + pub fn instruction(self) -> usize { + self.instruction + } + + pub fn bytecode(self) -> usize { + self.bytecode + } + + pub fn ram(self) -> usize { + self.ram + } + + pub fn total(self) -> usize { + self.instruction + self.bytecode + self.ram + } + + pub fn polynomials(self) -> impl Iterator { + (0..self.instruction) + .map(JoltRaPolynomial::Instruction) + .chain((0..self.bytecode).map(JoltRaPolynomial::Bytecode)) + .chain((0..self.ram).map(JoltRaPolynomial::Ram)) + } + + pub fn committed_polynomials(self) -> impl Iterator { + self.polynomials().map(JoltRaPolynomial::committed) + } + + pub fn openings(self, relation: JoltRelationId) -> impl Iterator { + self.polynomials() + .map(move |polynomial| polynomial.opening(relation)) + } +} + +impl TryFrom<(usize, usize, usize)> for JoltRaPolynomialLayout { + type Error = JoltFormulaDimensionsError; + + fn try_from((instruction, bytecode, ram): (usize, usize, usize)) -> Result { + Self::new(instruction, bytecode, ram) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_empty_layout() { + assert_eq!( + JoltRaPolynomialLayout::new(0, 0, 0), + Err(JoltFormulaDimensionsError::Zero { + name: "Jolt RA polynomial count", + }) + ); + } + + #[test] + fn rejects_overflowing_layout() { + assert_eq!( + JoltRaPolynomialLayout::new(usize::MAX, 1, 0), + Err(JoltFormulaDimensionsError::Overflow { + name: "Jolt RA polynomial count", + }) + ); + assert_eq!( + JoltRaPolynomialLayout::new(usize::MAX - 1, 1, 1), + Err(JoltFormulaDimensionsError::Overflow { + name: "Jolt RA polynomial count", + }) + ); + } + + #[test] + fn records_layout_counts_and_total() -> Result<(), JoltFormulaDimensionsError> { + let layout = JoltRaPolynomialLayout::new(2, 3, 5)?; + + assert_eq!(layout.instruction(), 2); + assert_eq!(layout.bytecode(), 3); + assert_eq!(layout.ram(), 5); + assert_eq!(layout.total(), 10); + Ok(()) + } + + #[test] + fn iterates_canonical_ra_order() -> Result<(), JoltFormulaDimensionsError> { + let layout = JoltRaPolynomialLayout::new(2, 1, 2)?; + + assert_eq!( + layout.polynomials().collect::>(), + vec![ + JoltRaPolynomial::Instruction(0), + JoltRaPolynomial::Instruction(1), + JoltRaPolynomial::Bytecode(0), + JoltRaPolynomial::Ram(0), + JoltRaPolynomial::Ram(1), + ] + ); + assert_eq!( + layout.committed_polynomials().collect::>(), + vec![ + JoltCommittedPolynomial::InstructionRa(0), + JoltCommittedPolynomial::InstructionRa(1), + JoltCommittedPolynomial::BytecodeRa(0), + JoltCommittedPolynomial::RamRa(0), + JoltCommittedPolynomial::RamRa(1), + ] + ); + Ok(()) + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/ram.rs b/crates/jolt-claims/src/protocols/jolt/formulas/ram.rs new file mode 100644 index 0000000000..e884bce229 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/ram.rs @@ -0,0 +1,1354 @@ +use jolt_field::{Field, RingCore}; + +use crate::{challenge, constant, opening, public}; + +use super::super::{ + JoltAdviceKind, JoltChallengeId, JoltCommittedPolynomial, JoltExpr, JoltOpeningId, + JoltPublicId, JoltRelationClaims, JoltRelationId, JoltVirtualPolynomial, + RamHammingBooleanityChallenge, RamOutputCheckPublic, RamRaClaimReductionChallenge, + RamRaClaimReductionPublic, RamRaVirtualizationChallenge, RamRafEvaluationPublic, + RamReadWriteChallenge, RamValCheckChallenge, +}; +use super::dimensions::{JoltSumcheckSpec, ReadWriteDimensions, TraceDimensions}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RamRafEvaluationDimensions(ReadWriteDimensions); + +impl TryFrom for RamRafEvaluationDimensions { + type Error = super::dimensions::JoltFormulaDimensionsError; + + fn try_from(dimensions: ReadWriteDimensions) -> Result { + if dimensions.phase1_num_rounds() > dimensions.log_t() { + return Err(Self::Error::InvalidPhaseRounds { + phase1_num_rounds: dimensions.phase1_num_rounds(), + log_t: dimensions.log_t(), + }); + } + Ok(Self(dimensions)) + } +} + +impl RamRafEvaluationDimensions { + pub const fn read_write(self) -> ReadWriteDimensions { + self.0 + } + + pub const fn phase3_cycle_rounds(self) -> usize { + self.0.phase3_cycle_rounds() + } + + pub const fn sumcheck(self) -> JoltSumcheckSpec { + self.0.raf_evaluation_sumcheck() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RamRaVirtualizationDimensions { + log_t: usize, + committed_ra_polys: usize, +} + +impl RamRaVirtualizationDimensions { + pub const fn new(log_t: usize, committed_ra_polys: usize) -> Self { + Self { + log_t, + committed_ra_polys, + } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn num_committed_ra_polys(self) -> usize { + self.committed_ra_polys + } + + pub const fn sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t, self.committed_ra_polys + 1) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamValCheckInit { + public_eval: F, + advice_contributions: Vec>, +} + +impl RamValCheckInit { + pub fn full(init_eval: F) -> Self { + Self { + public_eval: init_eval, + advice_contributions: Vec::new(), + } + } + + pub fn decomposed(public_eval: F, advice_contributions: I) -> Self + where + I: IntoIterator>, + { + Self { + public_eval, + advice_contributions: advice_contributions.into_iter().collect(), + } + } +} + +impl From for RamValCheckInit { + fn from(value: F) -> Self { + Self::full(value) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RamValCheckAdviceContribution { + pub neg_selector: F, + pub opening: JoltOpeningId, +} + +impl RamValCheckAdviceContribution { + pub fn new(neg_selector: F, opening: JoltOpeningId) -> Self { + Self { + neg_selector, + opening, + } + } + + pub fn untrusted(neg_selector: F) -> Self { + Self::new( + neg_selector, + JoltOpeningId::untrusted_advice(JoltRelationId::RamValCheck), + ) + } + + pub fn trusted(neg_selector: F) -> Self { + Self::new( + neg_selector, + JoltOpeningId::trusted_advice(JoltRelationId::RamValCheck), + ) + } +} + +pub fn read_write_checking(dimensions: ReadWriteDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(ram_read_value()) + + read_write_challenge(RamReadWriteChallenge::Gamma) * opening(ram_write_value()); + + let output = read_write_challenge(RamReadWriteChallenge::EqCycle) + * opening(ram_ra()) + * opening(ram_val()) + + read_write_challenge(RamReadWriteChallenge::EqCycle) + * read_write_challenge(RamReadWriteChallenge::Gamma) + * opening(ram_ra()) + * opening(ram_val()) + + read_write_challenge(RamReadWriteChallenge::EqCycle) + * read_write_challenge(RamReadWriteChallenge::Gamma) + * opening(ram_ra()) + * opening(ram_inc()); + + JoltRelationClaims::new( + JoltRelationId::RamReadWriteChecking, + dimensions.read_write_sumcheck(), + input, + output, + ) +} + +pub const fn val_check_sumcheck(dimensions: TraceDimensions) -> JoltSumcheckSpec { + dimensions.sumcheck(3) +} + +pub fn val_check(dimensions: TraceDimensions, init: RamValCheckInit) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = val_check_challenge(RamValCheckChallenge::Gamma); + let init_eval = ram_val_init_eval(init); + + let input = opening(ram_val()) + gamma.clone() * opening(ram_val_final()) + - (JoltExpr::one() + gamma) * init_eval; + + let output = val_check_challenge(RamValCheckChallenge::LtCyclePlusGamma) + * opening(ram_inc_val_check()) + * opening(ram_ra_val_check()); + + JoltRelationClaims::new( + JoltRelationId::RamValCheck, + val_check_sumcheck(dimensions), + input, + output, + ) +} + +pub fn raf_evaluation(dimensions: RamRafEvaluationDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = + constant(F::pow2(dimensions.phase3_cycle_rounds())) * opening(ram_address_spartan()); + let output = raf_evaluation_public(RamRafEvaluationPublic::UnmapAddress) + * opening(ram_ra_raf_evaluation()); + + JoltRelationClaims::new( + JoltRelationId::RamRafEvaluation, + dimensions.sumcheck(), + input, + output, + ) +} + +pub fn output_check(dimensions: ReadWriteDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let output = output_check_public(RamOutputCheckPublic::EqIoMask) * opening(ram_val_final()) + + output_check_public(RamOutputCheckPublic::NegEqIoMaskValIo); + + JoltRelationClaims::new( + JoltRelationId::RamOutputCheck, + dimensions.output_check_sumcheck(), + JoltExpr::zero(), + output, + ) +} + +pub fn read_write_checking_output_openings() -> [JoltOpeningId; 3] { + [ram_val(), ram_ra(), ram_inc()] +} + +pub fn read_write_checking_input_openings() -> [JoltOpeningId; 2] { + [ram_read_value(), ram_write_value()] +} + +pub fn raf_evaluation_output_openings() -> [JoltOpeningId; 1] { + [ram_ra_raf_evaluation()] +} + +pub fn raf_evaluation_input_openings() -> [JoltOpeningId; 1] { + [ram_address_spartan()] +} + +pub fn output_check_output_openings() -> [JoltOpeningId; 1] { + [ram_val_final()] +} + +pub fn val_check_input_openings() -> [JoltOpeningId; 2] { + [ram_val(), ram_val_final()] +} + +pub fn val_check_output_openings() -> [JoltOpeningId; 2] { + [ram_ra_val_check(), ram_inc_val_check()] +} + +pub fn val_check_advice_opening(kind: JoltAdviceKind) -> JoltOpeningId { + match kind { + JoltAdviceKind::Trusted => JoltOpeningId::trusted_advice(JoltRelationId::RamValCheck), + JoltAdviceKind::Untrusted => JoltOpeningId::untrusted_advice(JoltRelationId::RamValCheck), + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamRafEvaluationPublicValues { + pub unmap_address: F, +} + +impl RamRafEvaluationPublicValues { + pub fn value(&self, id: RamRafEvaluationPublic) -> F { + match id { + RamRafEvaluationPublic::UnmapAddress => self.unmap_address, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamOutputCheckPublicValues { + pub eq_io_mask: F, + pub neg_eq_io_mask_val_io: F, +} + +impl RamOutputCheckPublicValues { + pub fn value(&self, id: RamOutputCheckPublic) -> F { + match id { + RamOutputCheckPublic::EqIoMask => self.eq_io_mask, + RamOutputCheckPublic::NegEqIoMaskValIo => self.neg_eq_io_mask_val_io, + } + } +} + +pub fn ra_claim_reduction(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = ra_claim_reduction_challenge(RamRaClaimReductionChallenge::Gamma); + let input = opening(ram_ra_raf_evaluation()) + + gamma.clone() * opening(ram_ra()) + + gamma.clone().pow(2) * opening(ram_ra_val_check()); + + let output = (ra_claim_reduction_public(RamRaClaimReductionPublic::EqCycleRaf) + + gamma.clone() * ra_claim_reduction_public(RamRaClaimReductionPublic::EqCycleReadWrite) + + gamma.pow(2) * ra_claim_reduction_public(RamRaClaimReductionPublic::EqCycleValCheck)) + * opening(ram_ra_claim_reduction()); + + JoltRelationClaims::new( + JoltRelationId::RamRaClaimReduction, + dimensions.sumcheck(2), + input, + output, + ) +} + +pub fn ra_claim_reduction_input_openings() -> [JoltOpeningId; 3] { + [ram_ra_raf_evaluation(), ram_ra(), ram_ra_val_check()] +} + +pub fn ra_claim_reduction_output_openings() -> [JoltOpeningId; 1] { + [ram_ra_claim_reduction()] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamRaClaimReductionPublicValues { + pub eq_cycle_raf: F, + pub eq_cycle_read_write: F, + pub eq_cycle_val_check: F, +} + +impl RamRaClaimReductionPublicValues { + pub fn value(&self, id: RamRaClaimReductionPublic) -> F { + match id { + RamRaClaimReductionPublic::EqCycleRaf => self.eq_cycle_raf, + RamRaClaimReductionPublic::EqCycleReadWrite => self.eq_cycle_read_write, + RamRaClaimReductionPublic::EqCycleValCheck => self.eq_cycle_val_check, + } + } +} + +pub fn ra_virtualization(dimensions: RamRaVirtualizationDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(ram_ra_claim_reduction()); + let output = ra_virtualization_challenge(RamRaVirtualizationChallenge::EqCycle) + * committed_ram_ra_product(dimensions); + + JoltRelationClaims::new( + JoltRelationId::RamRaVirtualization, + dimensions.sumcheck(), + input, + output, + ) +} + +pub fn hamming_booleanity(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let eq_cycle = hamming_booleanity_challenge(RamHammingBooleanityChallenge::EqCycle); + let h = opening(ram_hamming_weight()); + let output = eq_cycle * (h.clone() * h.clone() - h); + + JoltRelationClaims::new( + JoltRelationId::RamHammingBooleanity, + dimensions.sumcheck(3), + JoltExpr::zero(), + output, + ) +} + +pub fn ra_virtualization_input_openings() -> [JoltOpeningId; 1] { + [ram_ra_claim_reduction()] +} + +pub fn ra_virtualization_output_openings( + dimensions: RamRaVirtualizationDimensions, +) -> Vec { + (0..dimensions.num_committed_ra_polys()) + .map(ra_virtualization_committed_ram_ra_opening) + .collect() +} + +pub fn ra_virtualization_committed_ram_ra_opening(index: usize) -> JoltOpeningId { + committed_ram_ra(index) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamRaVirtualizationChallengeValues { + pub eq_cycle: F, +} + +impl RamRaVirtualizationChallengeValues { + pub fn value(&self, id: RamRaVirtualizationChallenge) -> F { + match id { + RamRaVirtualizationChallenge::EqCycle => self.eq_cycle, + } + } +} + +pub fn hamming_booleanity_output_openings() -> [JoltOpeningId; 1] { + [ram_hamming_weight()] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RamHammingBooleanityChallengeValues { + pub eq_cycle: F, +} + +impl RamHammingBooleanityChallengeValues { + pub fn value(&self, id: RamHammingBooleanityChallenge) -> F { + match id { + RamHammingBooleanityChallenge::EqCycle => self.eq_cycle, + } + } +} + +fn read_write_challenge(id: RamReadWriteChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn val_check_challenge(id: RamValCheckChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn ra_claim_reduction_challenge(id: RamRaClaimReductionChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn ra_virtualization_challenge(id: RamRaVirtualizationChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn hamming_booleanity_challenge(id: RamHammingBooleanityChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn raf_evaluation_public(id: RamRafEvaluationPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn output_check_public(id: RamOutputCheckPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn ra_claim_reduction_public(id: RamRaClaimReductionPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn committed_ram_ra_product(dimensions: RamRaVirtualizationDimensions) -> JoltExpr +where + F: RingCore, +{ + let mut product = JoltExpr::one(); + for index in 0..dimensions.num_committed_ra_polys() { + product = product * opening(committed_ram_ra(index)); + } + product +} + +fn ram_val_init_eval(init: RamValCheckInit) -> JoltExpr +where + F: RingCore, +{ + let mut eval = JoltExpr::constant(init.public_eval); + for contribution in init.advice_contributions { + eval = eval - JoltExpr::constant(contribution.neg_selector) * opening(contribution.opening); + } + eval +} + +fn ram_read_value() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamReadValue, + JoltRelationId::SpartanOuter, + ) +} + +fn ram_write_value() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamWriteValue, + JoltRelationId::SpartanOuter, + ) +} + +fn ram_ra() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamRa, + JoltRelationId::RamReadWriteChecking, + ) +} + +fn ram_val() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamVal, + JoltRelationId::RamReadWriteChecking, + ) +} + +fn ram_val_final() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamValFinal, + JoltRelationId::RamOutputCheck, + ) +} + +fn ram_inc() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::RamReadWriteChecking, + ) +} + +fn ram_inc_val_check() -> JoltOpeningId { + JoltOpeningId::committed(JoltCommittedPolynomial::RamInc, JoltRelationId::RamValCheck) +} + +fn ram_ra_val_check() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::RamRa, JoltRelationId::RamValCheck) +} + +fn ram_address_spartan() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamAddress, + JoltRelationId::SpartanOuter, + ) +} + +fn ram_ra_raf_evaluation() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamRa, + JoltRelationId::RamRafEvaluation, + ) +} + +fn ram_ra_claim_reduction() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamRa, + JoltRelationId::RamRaClaimReduction, + ) +} + +fn committed_ram_ra(index: usize) -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RamRa(index), + JoltRelationId::RamRaVirtualization, + ) +} + +fn ram_hamming_weight() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamHammingWeight, + JoltRelationId::RamHammingBooleanity, + ) +} + +#[cfg(test)] +#[expect(clippy::panic)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn trace_dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + fn read_write_dimensions() -> ReadWriteDimensions { + ReadWriteDimensions::new(5, 4, 2, 1) + } + + fn raf_evaluation_dimensions() -> RamRafEvaluationDimensions { + RamRafEvaluationDimensions::try_from(read_write_dimensions()).unwrap_or_else(|err| { + panic!("test RAM RAF evaluation dimensions should be valid: {err}") + }) + } + + fn ra_virtualization_dimensions(committed_ra_polys: usize) -> RamRaVirtualizationDimensions { + RamRaVirtualizationDimensions::new(5, committed_ra_polys) + } + + #[test] + fn read_write_claims_expose_expected_dependencies() { + let claims = read_write_checking::(read_write_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RamReadWriteChecking); + assert_eq!( + claims.sumcheck, + read_write_dimensions().read_write_sumcheck() + ); + assert_eq!( + claims.input.required_openings, + read_write_checking_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + vec![ram_ra(), ram_val(), ram_inc()] + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(RamReadWriteChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(RamReadWriteChallenge::EqCycle), + JoltChallengeId::from(RamReadWriteChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(RamReadWriteChallenge::Gamma), + JoltChallengeId::from(RamReadWriteChallenge::EqCycle), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from(RamReadWriteChallenge::EqCycle)), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn read_write_claims_evaluate_like_core_formula() { + let claims = read_write_checking::(read_write_dimensions()); + + let read = Fr::from_u64(3); + let write = Fr::from_u64(5); + let ra = Fr::from_u64(7); + let val = Fr::from_u64(11); + let inc = Fr::from_u64(13); + let gamma = Fr::from_u64(17); + let eq = Fr::from_u64(19); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_read_value() => read, + id if id == ram_write_value() => write, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamReadWrite(RamReadWriteChallenge::Gamma) => gamma, + JoltChallengeId::RamReadWrite(RamReadWriteChallenge::EqCycle) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_ra() => ra, + id if id == ram_val() => val, + id if id == ram_inc() => inc, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamReadWrite(RamReadWriteChallenge::EqCycle) => eq, + JoltChallengeId::RamReadWrite(RamReadWriteChallenge::Gamma) => gamma, + JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!(input, read + gamma * write); + assert_eq!(output, eq * ra * (val + gamma * (val + inc))); + } + + #[test] + fn raf_evaluation_rejects_invalid_dimensions() { + assert!( + RamRafEvaluationDimensions::try_from(ReadWriteDimensions::new(3, 4, 4, 0)).is_err() + ); + + let dimensions = raf_evaluation_dimensions(); + assert_eq!(dimensions.phase3_cycle_rounds(), 3); + + let large_dimensions = + RamRafEvaluationDimensions::try_from(ReadWriteDimensions::new(usize::MAX, 0, 0, 0)) + .unwrap_or_else(|err| { + panic!("large RAM RAF evaluation dimensions was rejected: {err}") + }); + assert_eq!(large_dimensions.phase3_cycle_rounds(), usize::MAX); + } + + #[test] + fn raf_evaluation_exposes_expected_dependencies() { + let dimensions = raf_evaluation_dimensions(); + let claims = raf_evaluation::(dimensions); + + assert_eq!(claims.id, JoltRelationId::RamRafEvaluation); + assert_eq!(claims.sumcheck, dimensions.sumcheck()); + assert_eq!( + claims.input.required_openings, + raf_evaluation_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + raf_evaluation_output_openings().to_vec() + ); + assert!(claims.input.required_challenges.is_empty()); + assert!(claims.output.required_challenges.is_empty()); + assert!(claims.required_challenges().is_empty()); + assert_eq!( + claims.output.required_publics, + vec![JoltPublicId::from(RamRafEvaluationPublic::UnmapAddress)] + ); + assert_eq!( + claims.required_publics(), + vec![JoltPublicId::from(RamRafEvaluationPublic::UnmapAddress)] + ); + assert_eq!(claims.num_challenges(), 0); + } + + #[test] + fn raf_evaluation_evaluates_like_core_formula() { + let dimensions = raf_evaluation_dimensions(); + let claims = raf_evaluation::(dimensions); + + let address = Fr::from_u64(7); + let ram_ra = Fr::from_u64(11); + let unmap = Fr::from_u64(13); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_address_spartan() => address, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_ra_raf_evaluation() => ram_ra, + _ => zero, + }, + |_| zero, + |id| match *id { + JoltPublicId::RamRafEvaluation(RamRafEvaluationPublic::UnmapAddress) => unmap, + _ => zero, + }, + ); + + assert_eq!(input, address * Fr::from_u64(8)); + assert_eq!(output, unmap * ram_ra); + } + + #[test] + fn output_check_exposes_expected_dependencies() { + let claims = output_check::(read_write_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RamOutputCheck); + assert_eq!( + claims.sumcheck, + read_write_dimensions().output_check_sumcheck() + ); + assert!(claims.input.required_openings.is_empty()); + assert_eq!( + claims.output.required_openings, + output_check_output_openings().to_vec() + ); + assert!(claims.input.required_challenges.is_empty()); + assert!(claims.output.required_challenges.is_empty()); + assert!(claims.required_challenges().is_empty()); + assert!(claims.input.required_publics.is_empty()); + assert_eq!( + claims.output.required_publics, + vec![ + JoltPublicId::from(RamOutputCheckPublic::EqIoMask), + JoltPublicId::from(RamOutputCheckPublic::NegEqIoMaskValIo), + ] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(RamOutputCheckPublic::EqIoMask), + JoltPublicId::from(RamOutputCheckPublic::NegEqIoMaskValIo), + ] + ); + assert_eq!(claims.required_openings(), vec![ram_val_final()]); + assert_eq!(claims.num_challenges(), 0); + } + + #[test] + fn output_check_evaluates_like_core_formula() { + let claims = output_check::(read_write_dimensions()); + + let val_final = Fr::from_u64(7); + let eq_io_mask = Fr::from_u64(11); + let neg_eq_io_mask_val_io = -Fr::from_u64(13); + let zero = Fr::from_u64(0); + + let input = claims + .input + .expression() + .evaluate(|_| zero, |_| zero, |_| zero); + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_val_final() => val_final, + _ => zero, + }, + |_| zero, + |id| match *id { + JoltPublicId::RamOutputCheck(RamOutputCheckPublic::EqIoMask) => eq_io_mask, + JoltPublicId::RamOutputCheck(RamOutputCheckPublic::NegEqIoMaskValIo) => { + neg_eq_io_mask_val_io + } + _ => zero, + }, + ); + + assert_eq!(input, zero); + assert_eq!(output, eq_io_mask * val_final + neg_eq_io_mask_val_io); + } + + #[test] + fn ra_claim_reduction_exposes_expected_dependencies() { + let claims = ra_claim_reduction::(trace_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RamRaClaimReduction); + assert_eq!(claims.sumcheck, trace_dimensions().sumcheck(2)); + assert_eq!( + claims.input.required_openings, + ra_claim_reduction_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + ra_claim_reduction_output_openings().to_vec() + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(RamRaClaimReductionChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from(RamRaClaimReductionChallenge::Gamma)] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(RamRaClaimReductionChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_publics, + vec![ + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleRaf), + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleReadWrite), + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleValCheck), + ] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleRaf), + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleReadWrite), + JoltPublicId::from(RamRaClaimReductionPublic::EqCycleValCheck), + ] + ); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn ra_claim_reduction_evaluates_like_core_formula() { + let claims = ra_claim_reduction::(trace_dimensions()); + + let raf = Fr::from_u64(3); + let rw = Fr::from_u64(5); + let val = Fr::from_u64(7); + let gamma = Fr::from_u64(11); + let reduced = Fr::from_u64(13); + let eq_raf = Fr::from_u64(17); + let eq_rw = Fr::from_u64(19); + let eq_val = Fr::from_u64(23); + let public_values = RamRaClaimReductionPublicValues { + eq_cycle_raf: eq_raf, + eq_cycle_read_write: eq_rw, + eq_cycle_val_check: eq_val, + }; + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_ra_raf_evaluation() => raf, + id if id == ram_ra() => rw, + id if id == ram_ra_val_check() => val, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamRaClaimReduction(RamRaClaimReductionChallenge::Gamma) => gamma, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_ra_claim_reduction() => reduced, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamRaClaimReduction(RamRaClaimReductionChallenge::Gamma) => gamma, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |id| match *id { + JoltPublicId::RamRaClaimReduction(id) => public_values.value(id), + _ => zero, + }, + ); + + assert_eq!(input, raf + gamma * rw + gamma * gamma * val); + assert_eq!( + output, + (eq_raf + gamma * eq_rw + gamma * gamma * eq_val) * reduced + ); + } + + #[test] + fn ra_virtualization_supports_empty_ra_product() { + let claims = ra_virtualization::(ra_virtualization_dimensions(0)); + + assert!(claims.output.required_openings.is_empty()); + } + + #[test] + fn ra_virtualization_exposes_expected_dependencies() { + let dimensions = ra_virtualization_dimensions(3); + let claims = ra_virtualization::(dimensions); + + assert_eq!(claims.id, JoltRelationId::RamRaVirtualization); + assert_eq!(claims.sumcheck, dimensions.sumcheck()); + assert_eq!( + claims.input.required_openings, + ra_virtualization_input_openings() + ); + assert_eq!( + claims.output.required_openings, + ra_virtualization_output_openings(dimensions) + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from(RamRaVirtualizationChallenge::EqCycle)] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(RamRaVirtualizationChallenge::EqCycle)] + ); + assert!(claims.required_publics().is_empty()); + assert_eq!( + claims.required_openings(), + vec![ + ram_ra_claim_reduction(), + committed_ram_ra(0), + committed_ram_ra(1), + committed_ram_ra(2), + ] + ); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn ra_virtualization_evaluates_like_core_formula() { + let dimensions = ra_virtualization_dimensions(3); + let claims = ra_virtualization::(dimensions); + + let reduced = Fr::from_u64(3); + let committed = [Fr::from_u64(5), Fr::from_u64(7), Fr::from_u64(11)]; + let eq_cycle = Fr::from_u64(13); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_ra_claim_reduction() => reduced, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == committed_ram_ra(0) => committed[0], + id if id == committed_ram_ra(1) => committed[1], + id if id == committed_ram_ra(2) => committed[2], + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamRaVirtualization(RamRaVirtualizationChallenge::EqCycle) => { + eq_cycle + } + _ => zero, + }, + |_| zero, + ); + + assert_eq!(input, reduced); + assert_eq!( + output, + eq_cycle * committed[0] * committed[1] * committed[2] + ); + } + + #[test] + fn hamming_booleanity_exposes_expected_dependencies() { + let claims = hamming_booleanity::(trace_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RamHammingBooleanity); + assert_eq!(claims.sumcheck, trace_dimensions().sumcheck(3)); + assert!(claims.input.required_openings.is_empty()); + assert_eq!( + claims.output.required_openings, + hamming_booleanity_output_openings() + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from( + RamHammingBooleanityChallenge::EqCycle + )] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from( + RamHammingBooleanityChallenge::EqCycle + )] + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn hamming_booleanity_evaluates_like_core_formula() { + let claims = hamming_booleanity::(trace_dimensions()); + + let h = Fr::from_u64(7); + let eq_cycle = Fr::from_u64(11); + let zero = Fr::from_u64(0); + + let input = claims + .input + .expression() + .evaluate(|_| zero, |_| zero, |_| zero); + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_hamming_weight() => h, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamHammingBooleanity(RamHammingBooleanityChallenge::EqCycle) => { + eq_cycle + } + _ => zero, + }, + |_| zero, + ); + + assert_eq!(input, zero); + assert_eq!(output, eq_cycle * (h * h - h)); + } + + #[test] + fn val_check_full_init_exposes_expected_dependencies() { + let claims = val_check::(trace_dimensions(), Fr::from_u64(3).into()); + + assert_eq!(claims.id, JoltRelationId::RamValCheck); + assert_eq!(claims.sumcheck, val_check_sumcheck(trace_dimensions())); + assert_eq!( + claims.input.required_openings, + val_check_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + vec![ram_inc_val_check(), ram_ra_val_check()] + ); + assert_eq!( + val_check_output_openings(), + [ram_ra_val_check(), ram_inc_val_check()] + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(RamValCheckChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from( + RamValCheckChallenge::LtCyclePlusGamma + )] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(RamValCheckChallenge::Gamma), + JoltChallengeId::from(RamValCheckChallenge::LtCyclePlusGamma), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from( + RamValCheckChallenge::LtCyclePlusGamma + )), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn val_check_decomposed_init_exposes_advice_openings() { + let init = RamValCheckInit::decomposed( + Fr::from_u64(3), + [ + RamValCheckAdviceContribution::untrusted(-Fr::from_u64(5)), + RamValCheckAdviceContribution::trusted(-Fr::from_u64(7)), + ], + ); + let claims = val_check::(trace_dimensions(), init); + + assert_eq!( + claims.input.required_openings, + vec![ + ram_val(), + ram_val_final(), + val_check_advice_opening(JoltAdviceKind::Untrusted), + val_check_advice_opening(JoltAdviceKind::Trusted), + ] + ); + assert_eq!( + claims.required_openings(), + vec![ + ram_val(), + ram_val_final(), + val_check_advice_opening(JoltAdviceKind::Untrusted), + val_check_advice_opening(JoltAdviceKind::Trusted), + ram_inc_val_check(), + ram_ra_val_check(), + ] + ); + } + + #[test] + fn val_check_full_init_evaluates_like_core_formula() { + let init_eval = Fr::from_u64(3); + let claims = val_check::(trace_dimensions(), init_eval.into()); + + let val_rw = Fr::from_u64(5); + let val_final = Fr::from_u64(7); + let gamma = Fr::from_u64(11); + let inc = Fr::from_u64(13); + let wa = Fr::from_u64(17); + let lt_plus_gamma = Fr::from_u64(19); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_val() => val_rw, + id if id == ram_val_final() => val_final, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamValCheck(RamValCheckChallenge::Gamma) => gamma, + JoltChallengeId::RamValCheck(RamValCheckChallenge::LtCyclePlusGamma) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == ram_inc_val_check() => inc, + id if id == ram_ra_val_check() => wa, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamValCheck(RamValCheckChallenge::LtCyclePlusGamma) => { + lt_plus_gamma + } + JoltChallengeId::RamValCheck(RamValCheckChallenge::Gamma) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + (val_rw - init_eval) + gamma * (val_final - init_eval) + ); + assert_eq!(output, inc * wa * lt_plus_gamma); + } + + #[test] + fn val_check_decomposed_init_evaluates_like_full_init() { + let public_eval = Fr::from_u64(3); + let untrusted_neg_selector = -Fr::from_u64(5); + let trusted_neg_selector = -Fr::from_u64(7); + let init = RamValCheckInit::decomposed( + public_eval, + [ + RamValCheckAdviceContribution::untrusted(untrusted_neg_selector), + RamValCheckAdviceContribution::trusted(trusted_neg_selector), + ], + ); + let claims = val_check::(trace_dimensions(), init); + + let val_rw = Fr::from_u64(11); + let val_final = Fr::from_u64(13); + let gamma = Fr::from_u64(17); + let untrusted_advice = Fr::from_u64(19); + let trusted_advice = Fr::from_u64(23); + let zero = Fr::from_u64(0); + let init_eval = public_eval + - untrusted_neg_selector * untrusted_advice + - trusted_neg_selector * trusted_advice; + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == ram_val() => val_rw, + id if id == ram_val_final() => val_final, + id if id == JoltOpeningId::untrusted_advice(JoltRelationId::RamValCheck) => { + untrusted_advice + } + id if id == JoltOpeningId::trusted_advice(JoltRelationId::RamValCheck) => { + trusted_advice + } + _ => zero, + }, + |id| match *id { + JoltChallengeId::RamValCheck(RamValCheckChallenge::Gamma) => gamma, + JoltChallengeId::RamValCheck(RamValCheckChallenge::LtCyclePlusGamma) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + (val_rw - init_eval) + gamma * (val_final - init_eval) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/registers.rs b/crates/jolt-claims/src/protocols/jolt/formulas/registers.rs new file mode 100644 index 0000000000..05dec6f030 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/registers.rs @@ -0,0 +1,413 @@ +use jolt_field::RingCore; + +use crate::{challenge, opening}; + +use super::super::{ + JoltChallengeId, JoltCommittedPolynomial, JoltExpr, JoltOpeningId, JoltRelationClaims, + JoltRelationId, JoltVirtualPolynomial, RegistersReadWriteChallenge, + RegistersValEvaluationChallenge, +}; +use super::dimensions::{JoltSumcheckSpec, ReadWriteDimensions, TraceDimensions}; + +pub const fn read_write_checking_sumcheck(dimensions: ReadWriteDimensions) -> JoltSumcheckSpec { + dimensions.read_write_sumcheck() +} + +pub fn read_write_checking(dimensions: ReadWriteDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = read_write_challenge(RegistersReadWriteChallenge::Gamma); + let eq_cycle = read_write_challenge(RegistersReadWriteChallenge::EqCycle); + + let input = opening(rd_write_value_claim()) + + gamma.clone() * opening(rs1_value_claim()) + + gamma.clone().pow(2) * opening(rs2_value_claim()); + + let output = eq_cycle.clone() * opening(rd_wa_read_write()) * opening(rd_inc_read_write()) + + eq_cycle.clone() * opening(rd_wa_read_write()) * opening(registers_val_read_write()) + + eq_cycle.clone() + * gamma.clone() + * opening(rs1_ra_read_write()) + * opening(registers_val_read_write()) + + eq_cycle + * gamma.pow(2) + * opening(rs2_ra_read_write()) + * opening(registers_val_read_write()); + + JoltRelationClaims::new( + JoltRelationId::RegistersReadWriteChecking, + read_write_checking_sumcheck(dimensions), + input, + output, + ) +} + +pub fn val_evaluation(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = opening(registers_val_read_write()); + let output = val_evaluation_challenge(RegistersValEvaluationChallenge::LtCycle) + * opening(rd_inc_val_evaluation()) + * opening(rd_wa_val_evaluation()); + + JoltRelationClaims::new( + JoltRelationId::RegistersValEvaluation, + dimensions.sumcheck(3), + input, + output, + ) +} + +pub fn read_write_checking_input_openings() -> [JoltOpeningId; 3] { + [rd_write_value_claim(), rs1_value_claim(), rs2_value_claim()] +} + +pub fn read_write_checking_output_openings() -> [JoltOpeningId; 5] { + [ + registers_val_read_write(), + rs1_ra_read_write(), + rs2_ra_read_write(), + rd_wa_read_write(), + rd_inc_read_write(), + ] +} + +pub fn val_evaluation_input_openings() -> [JoltOpeningId; 1] { + [registers_val_read_write()] +} + +pub fn val_evaluation_output_openings() -> [JoltOpeningId; 2] { + [rd_inc_val_evaluation(), rd_wa_val_evaluation()] +} + +fn read_write_challenge(id: RegistersReadWriteChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn val_evaluation_challenge(id: RegistersValEvaluationChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn rd_write_value_claim() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWriteValue, + JoltRelationId::RegistersClaimReduction, + ) +} + +fn rs1_value_claim() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Value, + JoltRelationId::RegistersClaimReduction, + ) +} + +fn rs2_value_claim() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Value, + JoltRelationId::RegistersClaimReduction, + ) +} + +fn registers_val_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RegistersVal, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rs1_ra_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs1Ra, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rs2_ra_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::Rs2Ra, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rd_wa_read_write() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWa, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rd_inc_read_write() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersReadWriteChecking, + ) +} + +fn rd_inc_val_evaluation() -> JoltOpeningId { + JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersValEvaluation, + ) +} + +fn rd_wa_val_evaluation() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RdWa, + JoltRelationId::RegistersValEvaluation, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn trace_dimensions() -> TraceDimensions { + TraceDimensions::new(5) + } + + fn read_write_dimensions() -> ReadWriteDimensions { + ReadWriteDimensions::new(5, 7, 2, 1) + } + + #[test] + fn read_write_claims_expose_expected_dependencies() { + let claims = read_write_checking::(read_write_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RegistersReadWriteChecking); + assert_eq!( + claims.sumcheck, + read_write_checking_sumcheck(read_write_dimensions()) + ); + assert_eq!( + claims.input.required_openings, + read_write_checking_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + vec![ + rd_wa_read_write(), + rd_inc_read_write(), + registers_val_read_write(), + rs1_ra_read_write(), + rs2_ra_read_write(), + ] + ); + assert_eq!( + read_write_checking_output_openings(), + [ + registers_val_read_write(), + rs1_ra_read_write(), + rs2_ra_read_write(), + rd_wa_read_write(), + rd_inc_read_write(), + ] + ); + assert_eq!( + claims.input.required_challenges, + vec![JoltChallengeId::from(RegistersReadWriteChallenge::Gamma)] + ); + assert_eq!( + claims.output.required_challenges, + vec![ + JoltChallengeId::from(RegistersReadWriteChallenge::EqCycle), + JoltChallengeId::from(RegistersReadWriteChallenge::Gamma), + ] + ); + assert_eq!( + claims.required_challenges(), + vec![ + JoltChallengeId::from(RegistersReadWriteChallenge::Gamma), + JoltChallengeId::from(RegistersReadWriteChallenge::EqCycle), + ] + ); + assert_eq!( + claims.challenge_index(JoltChallengeId::from(RegistersReadWriteChallenge::EqCycle)), + Some(1) + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 2); + } + + #[test] + fn read_write_claims_evaluate_like_core_formula() { + let claims = read_write_checking::(read_write_dimensions()); + + let rd_write_value = Fr::from_u64(3); + let rs1_value = Fr::from_u64(5); + let rs2_value = Fr::from_u64(7); + let val = Fr::from_u64(11); + let rs1_ra = Fr::from_u64(13); + let rs2_ra = Fr::from_u64(17); + let rd_wa = Fr::from_u64(19); + let inc = Fr::from_u64(23); + let gamma = Fr::from_u64(29); + let eq_cycle = Fr::from_u64(31); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == rd_write_value_claim() => rd_write_value, + id if id == rs1_value_claim() => rs1_value, + id if id == rs2_value_claim() => rs2_value, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RegistersReadWrite(RegistersReadWriteChallenge::Gamma) => gamma, + JoltChallengeId::RegistersReadWrite(RegistersReadWriteChallenge::EqCycle) + | JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersValEvaluation(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == registers_val_read_write() => val, + id if id == rs1_ra_read_write() => rs1_ra, + id if id == rs2_ra_read_write() => rs2_ra, + id if id == rd_wa_read_write() => rd_wa, + id if id == rd_inc_read_write() => inc, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RegistersReadWrite(RegistersReadWriteChallenge::EqCycle) => { + eq_cycle + } + JoltChallengeId::RegistersReadWrite(RegistersReadWriteChallenge::Gamma) => gamma, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersValEvaluation(_) => zero, + JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!( + input, + rd_write_value + gamma * rs1_value + gamma * gamma * rs2_value + ); + assert_eq!( + output, + eq_cycle * (rd_wa * (inc + val) + gamma * rs1_ra * val + gamma * gamma * rs2_ra * val) + ); + } + + #[test] + fn val_evaluation_claims_expose_expected_dependencies() { + let claims = val_evaluation::(trace_dimensions()); + + assert_eq!(claims.id, JoltRelationId::RegistersValEvaluation); + assert_eq!(claims.sumcheck, trace_dimensions().sumcheck(3)); + assert_eq!( + claims.input.required_openings, + val_evaluation_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + val_evaluation_output_openings().to_vec() + ); + assert_eq!( + claims.output.required_challenges, + vec![JoltChallengeId::from( + RegistersValEvaluationChallenge::LtCycle + )] + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from( + RegistersValEvaluationChallenge::LtCycle + )] + ); + assert!(claims.required_publics().is_empty()); + assert_eq!(claims.num_challenges(), 1); + } + + #[test] + fn val_evaluation_claims_evaluate_like_core_formula() { + let claims = val_evaluation::(trace_dimensions()); + + let val = Fr::from_u64(3); + let inc = Fr::from_u64(5); + let wa = Fr::from_u64(7); + let lt_cycle = Fr::from_u64(11); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == registers_val_read_write() => val, + _ => zero, + }, + |_| zero, + |_| zero, + ); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == rd_inc_val_evaluation() => inc, + id if id == rd_wa_val_evaluation() => wa, + _ => zero, + }, + |id| match *id { + JoltChallengeId::RegistersValEvaluation( + RegistersValEvaluationChallenge::LtCycle, + ) => lt_cycle, + JoltChallengeId::RamReadWrite(_) + | JoltChallengeId::RamValCheck(_) + | JoltChallengeId::RamRaClaimReduction(_) + | JoltChallengeId::RamRaVirtualization(_) + | JoltChallengeId::RamHammingBooleanity(_) + | JoltChallengeId::RegistersReadWrite(_) + | JoltChallengeId::RegistersClaimReduction(_) + | JoltChallengeId::InstructionClaimReduction(_) + | JoltChallengeId::InstructionInput(_) + | JoltChallengeId::InstructionReadRaf(_) + | JoltChallengeId::InstructionRaVirtualization(_) + | JoltChallengeId::Booleanity(_) + | JoltChallengeId::IncClaimReduction(_) + | JoltChallengeId::HammingWeightClaimReduction(_) + | JoltChallengeId::BytecodeReadRaf(_) + | JoltChallengeId::SpartanShift(_) => zero, + }, + |_| zero, + ); + + assert_eq!(input, val); + assert_eq!(output, lt_cycle * inc * wa); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/formulas/spartan.rs b/crates/jolt-claims/src/protocols/jolt/formulas/spartan.rs new file mode 100644 index 0000000000..0885a4d3e3 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/formulas/spartan.rs @@ -0,0 +1,1028 @@ +use std::fmt; + +use jolt_field::{Field, RingCore}; +use jolt_poly::{ + lagrange::{centered_lagrange_evals, centered_lagrange_kernel, CenteredIntegerDomainError}, + EqPolynomial, +}; +use jolt_riscv::{CircuitFlags, InstructionFlags}; + +use crate::{challenge, opening, public}; + +use super::super::{ + JoltChallengeId, JoltExpr, JoltOpeningId, JoltPublicId, JoltRelationClaims, JoltRelationId, + JoltVirtualPolynomial, SpartanOuterPublic, SpartanProductVirtualizationPublic, + SpartanShiftChallenge, SpartanShiftPublic, +}; +use super::dimensions::{ + JoltSumcheckSpec, TraceDimensions, OUTER_UNISKIP_DOMAIN_SIZE, OUTER_UNISKIP_FIRST_ROUND_DEGREE, + PRODUCT_UNISKIP_DOMAIN_SIZE, PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, +}; + +const OUTER_REMAINDER_DEGREE: usize = 3; +const PRODUCT_REMAINDER_DEGREE: usize = 3; +const SHIFT_DEGREE: usize = 2; +const SPARTAN_OUTER_RV64_ROW_COUNT: usize = 19; +const SPARTAN_OUTER_FIRST_GROUP_ROWS: [usize; OUTER_UNISKIP_DOMAIN_SIZE] = + [1, 2, 3, 4, 5, 6, 11, 14, 17, 18]; +const SPARTAN_OUTER_SECOND_GROUP_ROWS: [usize; 9] = [0, 7, 8, 9, 10, 12, 13, 15, 16]; + +pub const SPARTAN_OUTER_R1CS_INPUTS: [JoltVirtualPolynomial; 35] = [ + JoltVirtualPolynomial::LeftInstructionInput, + JoltVirtualPolynomial::RightInstructionInput, + JoltVirtualPolynomial::Product, + JoltVirtualPolynomial::ShouldBranch, + JoltVirtualPolynomial::PC, + JoltVirtualPolynomial::UnexpandedPC, + JoltVirtualPolynomial::Imm, + JoltVirtualPolynomial::RamAddress, + JoltVirtualPolynomial::Rs1Value, + JoltVirtualPolynomial::Rs2Value, + JoltVirtualPolynomial::RdWriteValue, + JoltVirtualPolynomial::RamReadValue, + JoltVirtualPolynomial::RamWriteValue, + JoltVirtualPolynomial::LeftLookupOperand, + JoltVirtualPolynomial::RightLookupOperand, + JoltVirtualPolynomial::NextUnexpandedPC, + JoltVirtualPolynomial::NextPC, + JoltVirtualPolynomial::NextIsVirtual, + JoltVirtualPolynomial::NextIsFirstInSequence, + JoltVirtualPolynomial::LookupOutput, + JoltVirtualPolynomial::ShouldJump, + JoltVirtualPolynomial::OpFlags(CircuitFlags::AddOperands), + JoltVirtualPolynomial::OpFlags(CircuitFlags::SubtractOperands), + JoltVirtualPolynomial::OpFlags(CircuitFlags::MultiplyOperands), + JoltVirtualPolynomial::OpFlags(CircuitFlags::Load), + JoltVirtualPolynomial::OpFlags(CircuitFlags::Store), + JoltVirtualPolynomial::OpFlags(CircuitFlags::Jump), + JoltVirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), + JoltVirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + JoltVirtualPolynomial::OpFlags(CircuitFlags::Assert), + JoltVirtualPolynomial::OpFlags(CircuitFlags::DoNotUpdateUnexpandedPC), + JoltVirtualPolynomial::OpFlags(CircuitFlags::Advice), + JoltVirtualPolynomial::OpFlags(CircuitFlags::IsCompressed), + JoltVirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), + JoltVirtualPolynomial::OpFlags(CircuitFlags::IsLastInSequence), +]; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SpartanOuterClaimError { + InvalidUniskipDomain(CenteredIntegerDomainError), + ChallengeLengthMismatch { expected: usize, got: usize }, + LinearFormLengthMismatch { expected: usize, got: usize }, + UnsupportedR1csInput { variable: JoltVirtualPolynomial }, +} + +impl fmt::Display for SpartanOuterClaimError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidUniskipDomain(error) => write!(f, "{error}"), + Self::ChallengeLengthMismatch { expected, got } => { + write!( + f, + "challenge length mismatch: expected {expected}, got {got}" + ) + } + Self::LinearFormLengthMismatch { expected, got } => { + write!( + f, + "linear form length mismatch: expected {expected}, got {got}" + ) + } + Self::UnsupportedR1csInput { variable } => { + write!(f, "unsupported Spartan outer R1CS input {variable:?}") + } + } + } +} + +impl std::error::Error for SpartanOuterClaimError {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpartanOuterDimensions { + log_t: usize, + variables: Vec, + include_linear_terms: bool, + include_constant_term: bool, +} + +impl SpartanOuterDimensions { + pub fn new( + log_t: usize, + variables: Vec, + include_linear_terms: bool, + include_constant_term: bool, + ) -> Option { + if variables.is_empty() { + return None; + } + Some(Self { + log_t, + variables, + include_linear_terms, + include_constant_term, + }) + } + + pub fn variables(&self) -> &[JoltVirtualPolynomial] { + &self.variables + } + + pub fn log_t(&self) -> usize { + self.log_t + } + + pub fn include_linear_terms(&self) -> bool { + self.include_linear_terms + } + + pub fn include_constant_term(&self) -> bool { + self.include_constant_term + } + + pub const fn uniskip_sumcheck(&self) -> JoltSumcheckSpec { + JoltSumcheckSpec::centered_integer( + OUTER_UNISKIP_DOMAIN_SIZE, + 1, + OUTER_UNISKIP_FIRST_ROUND_DEGREE, + ) + } + + pub const fn remainder_sumcheck(&self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(1 + self.log_t, OUTER_REMAINDER_DEGREE) + } + + pub fn rv64(log_t: usize) -> Self { + Self { + log_t, + variables: SPARTAN_OUTER_R1CS_INPUTS.to_vec(), + include_linear_terms: true, + include_constant_term: true, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpartanOuterLinearForms { + pub az_coefficients: Vec, + pub bz_coefficients: Vec, + pub az_constant: F, + pub bz_constant: F, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpartanOuterRemainderPlan { + variables: Vec, + include_linear_terms: bool, + include_constant_term: bool, +} + +impl SpartanOuterRemainderPlan { + pub fn from_dimensions(dimensions: &SpartanOuterDimensions) -> Self { + Self { + variables: dimensions.variables().to_vec(), + include_linear_terms: dimensions.include_linear_terms(), + include_constant_term: dimensions.include_constant_term(), + } + } + + pub fn variables(&self) -> &[JoltVirtualPolynomial] { + &self.variables + } + + pub fn r1cs_input_indices(&self) -> Result, SpartanOuterClaimError> { + self.variables + .iter() + .copied() + .map(spartan_outer_r1cs_input_index) + .collect() + } + + pub fn row_weights( + &self, + r0: F, + r_stream: F, + ) -> Result, SpartanOuterClaimError> { + let lagrange_weights = centered_lagrange_evals(OUTER_UNISKIP_DOMAIN_SIZE, r0) + .map_err(SpartanOuterClaimError::InvalidUniskipDomain)?; + let mut weights = vec![F::zero(); SPARTAN_OUTER_RV64_ROW_COUNT]; + + for (index, &row) in SPARTAN_OUTER_FIRST_GROUP_ROWS.iter().enumerate() { + weights[row] += (F::one() - r_stream) * lagrange_weights[index]; + } + for (index, &row) in SPARTAN_OUTER_SECOND_GROUP_ROWS + .iter() + .take(OUTER_UNISKIP_DOMAIN_SIZE) + .enumerate() + { + weights[row] += r_stream * lagrange_weights[index]; + } + + Ok(weights) + } + + pub fn tau_kernel( + &self, + tau: &[F], + r0: F, + remainder_challenges: &[F], + ) -> Result { + let expected = remainder_challenges.len() + 1; + if tau.len() != expected { + return Err(SpartanOuterClaimError::ChallengeLengthMismatch { + expected, + got: tau.len(), + }); + } + + let tau_high = tau[tau.len() - 1]; + let tau_high_bound_r0 = centered_lagrange_kernel(OUTER_UNISKIP_DOMAIN_SIZE, tau_high, r0) + .map_err(SpartanOuterClaimError::InvalidUniskipDomain)?; + let mut reversed_challenges = remainder_challenges.to_vec(); + reversed_challenges.reverse(); + Ok(tau_high_bound_r0 * EqPolynomial::::mle(&tau[..tau.len() - 1], &reversed_challenges)) + } + + pub fn public_claims( + &self, + tau_kernel: F, + linear_forms: &SpartanOuterLinearForms, + ) -> Result, SpartanOuterClaimError> { + let expected = self.variables.len(); + check_linear_form_len(expected, linear_forms.az_coefficients.len())?; + check_linear_form_len(expected, linear_forms.bz_coefficients.len())?; + + let mut claims = Vec::with_capacity(expected * expected + 2 * expected + 1); + for left in 0..expected { + for right in 0..expected { + claims.push(( + SpartanOuterPublic::QuadraticCoefficient { left, right }, + tau_kernel + * linear_forms.az_coefficients[left] + * linear_forms.bz_coefficients[right], + )); + } + } + + if self.include_linear_terms { + for index in 0..expected { + let claim = linear_forms.az_coefficients[index] * linear_forms.bz_constant + + linear_forms.az_constant * linear_forms.bz_coefficients[index]; + claims.push(( + SpartanOuterPublic::LinearCoefficient(index), + tau_kernel * claim, + )); + } + } + + if self.include_constant_term { + claims.push(( + SpartanOuterPublic::ConstantCoefficient, + tau_kernel * linear_forms.az_constant * linear_forms.bz_constant, + )); + } + + Ok(claims) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SpartanProductDimensions { + log_t: usize, +} + +impl SpartanProductDimensions { + pub const fn new(log_t: usize) -> Self { + Self { log_t } + } + + pub const fn log_t(self) -> usize { + self.log_t + } + + pub const fn uniskip_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::centered_integer( + PRODUCT_UNISKIP_DOMAIN_SIZE, + 1, + PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, + ) + } + + pub const fn remainder_sumcheck(self) -> JoltSumcheckSpec { + JoltSumcheckSpec::boolean(self.log_t, PRODUCT_REMAINDER_DEGREE) + } +} + +pub fn outer_uniskip(dimensions: &SpartanOuterDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + JoltRelationClaims::new( + JoltRelationId::SpartanOuter, + dimensions.uniskip_sumcheck(), + JoltExpr::zero(), + opening(outer_uniskip_opening()), + ) +} + +pub fn outer_remainder(dimensions: &SpartanOuterDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let mut output = JoltExpr::zero(); + + for (left_index, left_variable) in dimensions.variables().iter().copied().enumerate() { + for (right_index, right_variable) in dimensions.variables().iter().copied().enumerate() { + output = output + + public(JoltPublicId::from( + SpartanOuterPublic::QuadraticCoefficient { + left: left_index, + right: right_index, + }, + )) * opening(outer_opening(left_variable)) + * opening(outer_opening(right_variable)); + } + } + + if dimensions.include_linear_terms() { + for (index, variable) in dimensions.variables().iter().copied().enumerate() { + output = output + + public(JoltPublicId::from(SpartanOuterPublic::LinearCoefficient( + index, + ))) * opening(outer_opening(variable)); + } + } + + if dimensions.include_constant_term() { + output = output + public(JoltPublicId::from(SpartanOuterPublic::ConstantCoefficient)); + } + + JoltRelationClaims::new( + JoltRelationId::SpartanOuter, + dimensions.remainder_sumcheck(), + opening(outer_uniskip_opening()), + output, + ) +} + +pub fn outer_opening(polynomial: JoltVirtualPolynomial) -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(polynomial, JoltRelationId::SpartanOuter) +} + +pub fn outer_uniskip_opening() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::UnivariateSkip) +} + +pub fn product_uniskip(dimensions: SpartanProductDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let input = product_uniskip_weight(0) * opening(product_outer_opening()) + + product_uniskip_weight(1) * opening(product_should_branch_outer_opening()) + + product_uniskip_weight(2) * opening(product_should_jump_outer_opening()); + + JoltRelationClaims::new( + JoltRelationId::SpartanProductVirtualization, + dimensions.uniskip_sumcheck(), + input, + opening(product_uniskip_opening()), + ) +} + +pub fn product_remainder(dimensions: SpartanProductDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let left = product_weight(0) * opening(left_instruction_input_product()) + + product_weight(1) * opening(lookup_output_product()) + + product_weight(2) * opening(jump_flag_product()); + let right = product_weight(0) * opening(right_instruction_input_product()) + + product_weight(1) * opening(branch_flag_product()) + + product_weight(2) * (JoltExpr::one() - opening(next_is_noop_product())); + + JoltRelationClaims::new( + JoltRelationId::SpartanProductVirtualization, + dimensions.remainder_sumcheck(), + opening(product_uniskip_opening()), + product_tau_kernel() * left * right, + ) +} + +pub fn product_remainder_output_openings() -> [JoltOpeningId; 8] { + [ + left_instruction_input_product(), + right_instruction_input_product(), + jump_flag_product(), + write_lookup_output_to_rd_product(), + lookup_output_product(), + branch_flag_product(), + next_is_noop_product(), + virtual_instruction_product(), + ] +} + +pub fn shift_input_openings() -> [JoltOpeningId; 5] { + [ + next_unexpanded_pc_outer(), + next_pc_outer(), + next_is_virtual_outer(), + next_is_first_in_sequence_outer(), + next_is_noop_product(), + ] +} + +pub fn shift_output_openings() -> [JoltOpeningId; 5] { + [ + unexpanded_pc_shift(), + pc_shift(), + is_virtual_shift(), + is_first_in_sequence_shift(), + is_noop_shift(), + ] +} + +pub fn shift(dimensions: TraceDimensions) -> JoltRelationClaims +where + F: RingCore, +{ + let gamma = shift_challenge(SpartanShiftChallenge::Gamma); + let input = opening(next_unexpanded_pc_outer()) + + gamma.clone() * opening(next_pc_outer()) + + gamma.clone().pow(2) * opening(next_is_virtual_outer()) + + gamma.clone().pow(3) * opening(next_is_first_in_sequence_outer()) + + gamma.clone().pow(4) * (JoltExpr::one() - opening(next_is_noop_product())); + + let output = shift_public(SpartanShiftPublic::EqPlusOneOuter) + * (opening(unexpanded_pc_shift()) + + gamma.clone() * opening(pc_shift()) + + gamma.clone().pow(2) * opening(is_virtual_shift()) + + gamma.clone().pow(3) * opening(is_first_in_sequence_shift())) + + shift_public(SpartanShiftPublic::EqPlusOneProduct) + * gamma.pow(4) + * (JoltExpr::one() - opening(is_noop_shift())); + + JoltRelationClaims::new( + JoltRelationId::SpartanShift, + dimensions.sumcheck(SHIFT_DEGREE), + input, + output, + ) +} + +fn shift_challenge(id: SpartanShiftChallenge) -> JoltExpr +where + F: RingCore, +{ + challenge(JoltChallengeId::from(id)) +} + +fn shift_public(id: SpartanShiftPublic) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from(id)) +} + +fn check_linear_form_len(expected: usize, got: usize) -> Result<(), SpartanOuterClaimError> { + if got == expected { + Ok(()) + } else { + Err(SpartanOuterClaimError::LinearFormLengthMismatch { expected, got }) + } +} + +fn spartan_outer_r1cs_input_index( + variable: JoltVirtualPolynomial, +) -> Result { + SPARTAN_OUTER_R1CS_INPUTS + .iter() + .position(|candidate| *candidate == variable) + .ok_or(SpartanOuterClaimError::UnsupportedR1csInput { variable }) +} + +fn product_weight(index: usize) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from( + SpartanProductVirtualizationPublic::LagrangeWeight(index), + )) +} + +fn product_uniskip_weight(index: usize) -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from( + SpartanProductVirtualizationPublic::UniskipLagrangeWeight(index), + )) +} + +fn product_tau_kernel() -> JoltExpr +where + F: RingCore, +{ + public(JoltPublicId::from( + SpartanProductVirtualizationPublic::TauKernel, + )) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SpartanProductPublicValues { + pub lagrange_weights: [F; PRODUCT_UNISKIP_DOMAIN_SIZE], + pub tau_kernel: F, +} + +impl SpartanProductPublicValues { + pub fn value(&self, id: SpartanProductVirtualizationPublic) -> Option { + match id { + SpartanProductVirtualizationPublic::UniskipLagrangeWeight(index) + | SpartanProductVirtualizationPublic::LagrangeWeight(index) => { + self.lagrange_weights.get(index).copied() + } + SpartanProductVirtualizationPublic::TauKernel => Some(self.tau_kernel), + } + } +} + +pub fn product_uniskip_opening() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnivariateSkip, + JoltRelationId::SpartanProductVirtualization, + ) +} + +pub fn product_outer_opening() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::Product) +} + +pub fn product_should_branch_outer_opening() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::ShouldBranch) +} + +pub fn product_should_jump_outer_opening() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::ShouldJump) +} + +fn left_instruction_input_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LeftInstructionInput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn right_instruction_input_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RightInstructionInput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn lookup_output_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::LookupOutput, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn jump_flag_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(CircuitFlags::Jump), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn write_lookup_output_to_rd_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn branch_flag_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::Branch), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn next_is_noop_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::NextIsNoop, + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn virtual_instruction_product() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + JoltRelationId::SpartanProductVirtualization, + ) +} + +fn next_unexpanded_pc_outer() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::NextUnexpandedPC) +} + +fn next_pc_outer() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::NextPC) +} + +fn next_is_virtual_outer() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::NextIsVirtual) +} + +fn next_is_first_in_sequence_outer() -> JoltOpeningId { + outer_opening(JoltVirtualPolynomial::NextIsFirstInSequence) +} + +fn unexpanded_pc_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::UnexpandedPC, + JoltRelationId::SpartanShift, + ) +} + +fn pc_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::PC, JoltRelationId::SpartanShift) +} + +fn is_virtual_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + JoltRelationId::SpartanShift, + ) +} + +fn is_first_in_sequence_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), + JoltRelationId::SpartanShift, + ) +} + +fn is_noop_shift() -> JoltOpeningId { + JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::InstructionFlags(InstructionFlags::IsNoop), + JoltRelationId::SpartanShift, + ) +} + +#[cfg(test)] +#[expect(clippy::panic, clippy::unwrap_used)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + fn gamma_power(gamma: Fr, exponent: usize) -> Fr { + let mut value = Fr::from_u64(1); + for _ in 0..exponent { + value *= gamma; + } + value + } + + fn outer_dimensions() -> SpartanOuterDimensions { + match SpartanOuterDimensions::new( + 8, + vec![ + JoltVirtualPolynomial::PC, + JoltVirtualPolynomial::LookupOutput, + ], + true, + true, + ) { + Some(dimensions) => dimensions, + None => panic!("test Spartan outer dimensions should be valid"), + } + } + + #[test] + fn outer_dimensions_rejects_empty_variables() { + assert_eq!( + SpartanOuterDimensions::new(8, Vec::new(), false, false), + None + ); + } + + #[test] + fn outer_remainder_exposes_expected_dependencies() { + let dimensions = outer_dimensions(); + let claims = outer_remainder::(&dimensions); + + assert_eq!(claims.id, JoltRelationId::SpartanOuter); + assert_eq!(claims.sumcheck, dimensions.remainder_sumcheck()); + assert_eq!( + claims.input.required_openings, + vec![outer_uniskip_opening()] + ); + assert_eq!( + claims.output.required_openings, + vec![ + outer_opening(JoltVirtualPolynomial::PC), + outer_opening(JoltVirtualPolynomial::LookupOutput), + ] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(SpartanOuterPublic::QuadraticCoefficient { left: 0, right: 0 }), + JoltPublicId::from(SpartanOuterPublic::QuadraticCoefficient { left: 0, right: 1 }), + JoltPublicId::from(SpartanOuterPublic::QuadraticCoefficient { left: 1, right: 0 }), + JoltPublicId::from(SpartanOuterPublic::QuadraticCoefficient { left: 1, right: 1 }), + JoltPublicId::from(SpartanOuterPublic::LinearCoefficient(0)), + JoltPublicId::from(SpartanOuterPublic::LinearCoefficient(1)), + JoltPublicId::from(SpartanOuterPublic::ConstantCoefficient), + ] + ); + assert!(claims.required_challenges().is_empty()); + } + + #[test] + fn outer_split_claims_are_connected() { + let dimensions = outer_dimensions(); + let first = outer_uniskip::(&dimensions); + let remainder = outer_remainder::(&dimensions); + + assert_eq!(first.sumcheck, dimensions.uniskip_sumcheck()); + assert_eq!(first.output, remainder.input); + } + + #[test] + fn default_outer_dimensions_match_r1cs_input_catalog() { + let dimensions = SpartanOuterDimensions::rv64(8); + + assert_eq!(dimensions.log_t(), 8); + assert_eq!(dimensions.variables(), &SPARTAN_OUTER_R1CS_INPUTS); + assert!(dimensions.include_linear_terms()); + assert!(dimensions.include_constant_term()); + } + + #[test] + fn outer_remainder_plan_maps_variables_to_r1cs_inputs() { + let dimensions = outer_dimensions(); + let plan = SpartanOuterRemainderPlan::from_dimensions(&dimensions); + + assert_eq!(plan.r1cs_input_indices(), Ok(vec![4, 19])); + + let unsupported = + SpartanOuterDimensions::new(8, vec![JoltVirtualPolynomial::NextIsNoop], true, true) + .unwrap(); + assert!(matches!( + SpartanOuterRemainderPlan::from_dimensions(&unsupported).r1cs_input_indices(), + Err(SpartanOuterClaimError::UnsupportedR1csInput { + variable: JoltVirtualPolynomial::NextIsNoop + }) + )); + } + + #[test] + fn outer_remainder_plan_computes_group_row_weights() { + let plan = SpartanOuterRemainderPlan::from_dimensions(&outer_dimensions()); + let r0 = Fr::from_i64(-4); + + let first_group = plan.row_weights(r0, Fr::from_u64(0)).unwrap(); + assert_eq!(first_group[1], Fr::from_u64(1)); + assert_eq!(first_group[0], Fr::from_u64(0)); + + let second_group = plan.row_weights(r0, Fr::from_u64(1)).unwrap(); + assert_eq!(second_group[0], Fr::from_u64(1)); + assert_eq!(second_group[1], Fr::from_u64(0)); + } + + #[test] + fn outer_remainder_plan_computes_tau_kernel() { + let plan = SpartanOuterRemainderPlan::from_dimensions(&outer_dimensions()); + let tau = [Fr::from_u64(0), Fr::from_u64(0), Fr::from_i64(-4)]; + let challenges = [Fr::from_u64(0), Fr::from_u64(0)]; + + assert_eq!( + plan.tau_kernel(&tau, Fr::from_i64(-4), &challenges), + Ok(Fr::from_u64(1)) + ); + } + + #[test] + fn outer_remainder_plan_expands_public_coefficients() { + let plan = SpartanOuterRemainderPlan::from_dimensions(&outer_dimensions()); + let linear_forms = SpartanOuterLinearForms { + az_coefficients: vec![Fr::from_u64(2), Fr::from_u64(3)], + bz_coefficients: vec![Fr::from_u64(5), Fr::from_u64(7)], + az_constant: Fr::from_u64(11), + bz_constant: Fr::from_u64(13), + }; + + let claims = plan.public_claims(Fr::from_u64(17), &linear_forms).unwrap(); + + assert_eq!( + claims, + vec![ + ( + SpartanOuterPublic::QuadraticCoefficient { left: 0, right: 0 }, + Fr::from_u64(170), + ), + ( + SpartanOuterPublic::QuadraticCoefficient { left: 0, right: 1 }, + Fr::from_u64(238), + ), + ( + SpartanOuterPublic::QuadraticCoefficient { left: 1, right: 0 }, + Fr::from_u64(255), + ), + ( + SpartanOuterPublic::QuadraticCoefficient { left: 1, right: 1 }, + Fr::from_u64(357), + ), + (SpartanOuterPublic::LinearCoefficient(0), Fr::from_u64(1377),), + (SpartanOuterPublic::LinearCoefficient(1), Fr::from_u64(1972),), + (SpartanOuterPublic::ConstantCoefficient, Fr::from_u64(2431),), + ] + ); + } + + #[test] + fn product_uniskip_exposes_expected_dependencies() { + let dimensions = SpartanProductDimensions::new(7); + let claims = product_uniskip::(dimensions); + + assert_eq!(claims.id, JoltRelationId::SpartanProductVirtualization); + assert_eq!(claims.sumcheck, dimensions.uniskip_sumcheck()); + assert_eq!( + claims.input.required_openings, + vec![ + product_outer_opening(), + product_should_branch_outer_opening(), + product_should_jump_outer_opening() + ] + ); + assert_eq!( + claims.output.required_openings, + vec![product_uniskip_opening()] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(SpartanProductVirtualizationPublic::UniskipLagrangeWeight(0)), + JoltPublicId::from(SpartanProductVirtualizationPublic::UniskipLagrangeWeight(1)), + JoltPublicId::from(SpartanProductVirtualizationPublic::UniskipLagrangeWeight(2)), + ] + ); + } + + #[test] + fn product_split_claims_are_connected() { + let dimensions = SpartanProductDimensions::new(7); + let first = product_uniskip::(dimensions); + let remainder = product_remainder::(dimensions); + + assert_eq!(remainder.sumcheck, dimensions.remainder_sumcheck()); + assert_eq!(first.output, remainder.input); + } + + #[test] + fn product_remainder_evaluates_like_core_formula() { + let claims = product_remainder::(SpartanProductDimensions::new(7)); + + let left_input = Fr::from_u64(2); + let lookup_output = Fr::from_u64(3); + let jump = Fr::from_u64(5); + let right_input = Fr::from_u64(7); + let branch = Fr::from_u64(11); + let next_is_noop = Fr::from_u64(13); + let weights = [Fr::from_u64(17), Fr::from_u64(19), Fr::from_u64(23)]; + let tau_kernel = Fr::from_u64(29); + let zero = Fr::from_u64(0); + + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == left_instruction_input_product() => left_input, + id if id == lookup_output_product() => lookup_output, + id if id == jump_flag_product() => jump, + id if id == right_instruction_input_product() => right_input, + id if id == branch_flag_product() => branch, + id if id == next_is_noop_product() => next_is_noop, + _ => zero, + }, + |_| zero, + |id| match *id { + JoltPublicId::SpartanProductVirtualization( + SpartanProductVirtualizationPublic::LagrangeWeight(index), + ) => weights[index], + JoltPublicId::SpartanProductVirtualization( + SpartanProductVirtualizationPublic::TauKernel, + ) => tau_kernel, + _ => zero, + }, + ); + + assert_eq!( + output, + tau_kernel + * (weights[0] * left_input + weights[1] * lookup_output + weights[2] * jump) + * (weights[0] * right_input + + weights[1] * branch + + weights[2] * (Fr::from_u64(1) - next_is_noop)) + ); + } + + #[test] + fn shift_exposes_expected_dependencies() { + let dimensions = TraceDimensions::new(5); + let claims = shift::(dimensions); + + assert_eq!(claims.id, JoltRelationId::SpartanShift); + assert_eq!(claims.sumcheck, dimensions.sumcheck(SHIFT_DEGREE)); + assert_eq!( + claims.input.required_openings, + shift_input_openings().to_vec() + ); + assert_eq!( + claims.output.required_openings, + shift_output_openings().to_vec() + ); + assert_eq!( + claims.required_challenges(), + vec![JoltChallengeId::from(SpartanShiftChallenge::Gamma)] + ); + assert_eq!( + claims.required_publics(), + vec![ + JoltPublicId::from(SpartanShiftPublic::EqPlusOneOuter), + JoltPublicId::from(SpartanShiftPublic::EqPlusOneProduct), + ] + ); + } + + #[test] + fn shift_evaluates_like_core_formula() { + let claims = shift::(TraceDimensions::new(5)); + + let next_unexpanded_pc = Fr::from_u64(3); + let next_pc = Fr::from_u64(5); + let next_virtual = Fr::from_u64(7); + let next_first = Fr::from_u64(11); + let next_noop = Fr::from_u64(13); + let unexpanded_pc = Fr::from_u64(17); + let pc = Fr::from_u64(19); + let is_virtual = Fr::from_u64(23); + let is_first = Fr::from_u64(29); + let is_noop = Fr::from_u64(31); + let gamma = Fr::from_u64(37); + let eq_outer = Fr::from_u64(41); + let eq_product = Fr::from_u64(43); + let zero = Fr::from_u64(0); + + let input = claims.input.expression().evaluate( + |id| match *id { + id if id == next_unexpanded_pc_outer() => next_unexpanded_pc, + id if id == next_pc_outer() => next_pc, + id if id == next_is_virtual_outer() => next_virtual, + id if id == next_is_first_in_sequence_outer() => next_first, + id if id == next_is_noop_product() => next_noop, + _ => zero, + }, + |id| match *id { + JoltChallengeId::SpartanShift(SpartanShiftChallenge::Gamma) => gamma, + _ => zero, + }, + |_| zero, + ); + let output = claims.output.expression().evaluate( + |id| match *id { + id if id == unexpanded_pc_shift() => unexpanded_pc, + id if id == pc_shift() => pc, + id if id == is_virtual_shift() => is_virtual, + id if id == is_first_in_sequence_shift() => is_first, + id if id == is_noop_shift() => is_noop, + _ => zero, + }, + |id| match *id { + JoltChallengeId::SpartanShift(SpartanShiftChallenge::Gamma) => gamma, + _ => zero, + }, + |id| match *id { + JoltPublicId::SpartanShift(SpartanShiftPublic::EqPlusOneOuter) => eq_outer, + JoltPublicId::SpartanShift(SpartanShiftPublic::EqPlusOneProduct) => eq_product, + _ => zero, + }, + ); + + assert_eq!( + input, + next_unexpanded_pc + + gamma * next_pc + + gamma_power(gamma, 2) * next_virtual + + gamma_power(gamma, 3) * next_first + + gamma_power(gamma, 4) * (Fr::from_u64(1) - next_noop) + ); + assert_eq!( + output, + eq_outer + * (unexpanded_pc + + gamma * pc + + gamma_power(gamma, 2) * is_virtual + + gamma_power(gamma, 3) * is_first) + + eq_product * gamma_power(gamma, 4) * (Fr::from_u64(1) - is_noop) + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/ids.rs b/crates/jolt-claims/src/protocols/jolt/ids.rs new file mode 100644 index 0000000000..07fba0bf19 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/ids.rs @@ -0,0 +1,380 @@ +use derive_more::From; +use jolt_riscv::{CircuitFlags, InstructionFlags}; +use serde::{Deserialize, Serialize}; + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum JoltRelationId { + SpartanOuter, + SpartanProductVirtualization, + SpartanShift, + InstructionClaimReduction, + InstructionInputVirtualization, + InstructionReadRaf, + InstructionRaVirtualization, + RamReadWriteChecking, + RamRafEvaluation, + RamOutputCheck, + RamValCheck, + RamRaClaimReduction, + RamHammingBooleanity, + RamRaVirtualization, + RegistersClaimReduction, + RegistersReadWriteChecking, + RegistersValEvaluation, + BytecodeReadRaf, + Booleanity, + AdviceClaimReductionCyclePhase, + AdviceClaimReduction, + IncClaimReduction, + HammingWeightClaimReduction, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamReadWriteChallenge { + Gamma, + EqCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamValCheckChallenge { + Gamma, + LtCyclePlusGamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamRaClaimReductionChallenge { + Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamRaVirtualizationChallenge { + EqCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamHammingBooleanityChallenge { + EqCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamRafEvaluationPublic { + UnmapAddress, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamOutputCheckPublic { + EqIoMask, + NegEqIoMaskValIo, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RamRaClaimReductionPublic { + EqCycleRaf, + EqCycleReadWrite, + EqCycleValCheck, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum BooleanityChallenge { + Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum BooleanityPublic { + EqAddressCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum IncClaimReductionChallenge { + Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum IncClaimReductionPublic { + EqRamReadWrite, + EqRamValCheck, + EqRegistersReadWrite, + EqRegistersValEvaluation, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum HammingWeightClaimReductionChallenge { + Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum HammingWeightClaimReductionPublic { + EqBooleanity, + EqVirtualization(usize), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum BytecodeReadRafChallenge { + Gamma, + Stage1Gamma, + Stage2Gamma, + Stage3Gamma, + Stage4Gamma, + Stage5Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum BytecodeReadRafPublic { + StageValue(usize), + SpartanOuterRaf, + SpartanShiftRaf, + Entry, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum JoltAdviceKind { + Trusted, + Untrusted, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum AdviceClaimReductionPublic { + FinalScale(JoltAdviceKind), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum SpartanShiftChallenge { + Gamma, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum SpartanShiftPublic { + EqPlusOneOuter, + EqPlusOneProduct, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum SpartanProductVirtualizationPublic { + UniskipLagrangeWeight(usize), + LagrangeWeight(usize), + TauKernel, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum SpartanOuterPublic { + QuadraticCoefficient { left: usize, right: usize }, + LinearCoefficient(usize), + ConstantCoefficient, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RegistersReadWriteChallenge { + Gamma, + EqCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RegistersValEvaluationChallenge { + LtCycle, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum RegistersClaimReductionChallenge { + Gamma, + EqSpartan, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum InstructionClaimReductionChallenge { + Gamma, + EqSpartan, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum InstructionInputChallenge { + Gamma, + EqProduct, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum InstructionReadRafChallenge { + Gamma, + EqTableValue(usize), + EqRafConstant, + EqRafFlag, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum InstructionRaVirtualizationChallenge { + Gamma, + EqCycle, +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum JoltChallengeId { + RamReadWrite(RamReadWriteChallenge), + RamValCheck(RamValCheckChallenge), + RamRaClaimReduction(RamRaClaimReductionChallenge), + RamRaVirtualization(RamRaVirtualizationChallenge), + RamHammingBooleanity(RamHammingBooleanityChallenge), + Booleanity(BooleanityChallenge), + IncClaimReduction(IncClaimReductionChallenge), + HammingWeightClaimReduction(HammingWeightClaimReductionChallenge), + BytecodeReadRaf(BytecodeReadRafChallenge), + SpartanShift(SpartanShiftChallenge), + RegistersReadWrite(RegistersReadWriteChallenge), + RegistersValEvaluation(RegistersValEvaluationChallenge), + RegistersClaimReduction(RegistersClaimReductionChallenge), + InstructionClaimReduction(InstructionClaimReductionChallenge), + InstructionInput(InstructionInputChallenge), + InstructionReadRaf(InstructionReadRafChallenge), + InstructionRaVirtualization(InstructionRaVirtualizationChallenge), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum JoltCommittedPolynomial { + RdInc, + RamInc, + InstructionRa(usize), + BytecodeRa(usize), + RamRa(usize), + TrustedAdvice, + UntrustedAdvice, +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum JoltVirtualPolynomial { + PC, + UnexpandedPC, + NextPC, + NextUnexpandedPC, + NextIsNoop, + NextIsVirtual, + NextIsFirstInSequence, + LeftLookupOperand, + RightLookupOperand, + LeftInstructionInput, + RightInstructionInput, + Product, + ShouldJump, + ShouldBranch, + Rd, + Imm, + Rs1Value, + Rs2Value, + RdWriteValue, + Rs1Ra, + Rs2Ra, + RdWa, + LookupOutput, + InstructionRaf, + InstructionRafFlag, + InstructionRa(usize), + RegistersVal, + RamAddress, + RamRa, + RamReadValue, + RamWriteValue, + RamVal, + RamValInit, + RamValFinal, + RamHammingWeight, + UnivariateSkip, + OpFlags(CircuitFlags), + InstructionFlags(InstructionFlags), + LookupTableFlag(usize), +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum JoltPolynomialId { + Committed(JoltCommittedPolynomial), + Virtual(JoltVirtualPolynomial), +} + +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize)] +pub enum JoltOpeningId { + Polynomial { + polynomial: JoltPolynomialId, + relation: JoltRelationId, + }, + UntrustedAdvice { + relation: JoltRelationId, + }, + TrustedAdvice { + relation: JoltRelationId, + }, +} + +impl JoltOpeningId { + pub fn polynomial(polynomial: impl Into, relation: JoltRelationId) -> Self { + Self::Polynomial { + polynomial: polynomial.into(), + relation, + } + } + + pub fn committed(polynomial: JoltCommittedPolynomial, relation: JoltRelationId) -> Self { + Self::polynomial(polynomial, relation) + } + + pub fn virtual_polynomial(polynomial: JoltVirtualPolynomial, relation: JoltRelationId) -> Self { + Self::polynomial(polynomial, relation) + } + + pub fn untrusted_advice(relation: JoltRelationId) -> Self { + Self::UntrustedAdvice { relation } + } + + pub fn trusted_advice(relation: JoltRelationId) -> Self { + Self::TrustedAdvice { relation } + } +} + +#[derive( + Hash, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Serialize, Deserialize, From, +)] +pub enum JoltPublicId { + TraceLength, + PaddedTraceLength, + BytecodeLength, + MemorySize, + RamRafEvaluation(RamRafEvaluationPublic), + RamOutputCheck(RamOutputCheckPublic), + RamRaClaimReduction(RamRaClaimReductionPublic), + Booleanity(BooleanityPublic), + IncClaimReduction(IncClaimReductionPublic), + HammingWeightClaimReduction(HammingWeightClaimReductionPublic), + BytecodeReadRaf(BytecodeReadRafPublic), + AdviceClaimReduction(AdviceClaimReductionPublic), + SpartanShift(SpartanShiftPublic), + SpartanProductVirtualization(SpartanProductVirtualizationPublic), + SpartanOuter(SpartanOuterPublic), + #[from(ignore)] + PublicInput(usize), + #[from(ignore)] + PublicOutput(usize), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn opening_constructors_preserve_stage_context() { + let relation = JoltRelationId::RamReadWriteChecking; + + assert_eq!( + JoltOpeningId::committed(JoltCommittedPolynomial::RamInc, relation), + JoltOpeningId::Polynomial { + polynomial: JoltPolynomialId::Committed(JoltCommittedPolynomial::RamInc), + relation, + } + ); + assert_eq!( + JoltOpeningId::virtual_polynomial(JoltVirtualPolynomial::RamVal, relation), + JoltOpeningId::Polynomial { + polynomial: JoltPolynomialId::Virtual(JoltVirtualPolynomial::RamVal), + relation, + } + ); + } +} diff --git a/crates/jolt-claims/src/protocols/jolt/mod.rs b/crates/jolt-claims/src/protocols/jolt/mod.rs new file mode 100644 index 0000000000..e9284ee0e5 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/mod.rs @@ -0,0 +1,31 @@ +pub mod formulas; + +mod ids; +mod relation; + +pub use formulas::{ + claim_reductions::advice::{AdviceClaimReductionDimensions, AdviceClaimReductionLayout}, + dimensions::{ + CommitmentMatrixShape, JoltFormulaDimensions, JoltOneHotConfig, JoltOneHotDimensions, + JoltReadWriteConfig, JoltSumcheckDomain, JoltSumcheckSpec, ReadWriteDimensions, + TraceDimensions, TracePolynomialOrder, + }, + error::{JoltFormulaDimensionsError, JoltFormulaPointError}, +}; +pub use ids::{ + AdviceClaimReductionPublic, BooleanityChallenge, BooleanityPublic, BytecodeReadRafChallenge, + BytecodeReadRafPublic, HammingWeightClaimReductionChallenge, HammingWeightClaimReductionPublic, + IncClaimReductionChallenge, IncClaimReductionPublic, InstructionClaimReductionChallenge, + InstructionInputChallenge, InstructionRaVirtualizationChallenge, InstructionReadRafChallenge, + JoltAdviceKind, JoltChallengeId, JoltCommittedPolynomial, JoltOpeningId, JoltPolynomialId, + JoltPublicId, JoltRelationId, JoltVirtualPolynomial, RamHammingBooleanityChallenge, + RamOutputCheckPublic, RamRaClaimReductionChallenge, RamRaClaimReductionPublic, + RamRaVirtualizationChallenge, RamRafEvaluationPublic, RamReadWriteChallenge, + RamValCheckChallenge, RegistersClaimReductionChallenge, RegistersReadWriteChallenge, + RegistersValEvaluationChallenge, SpartanOuterPublic, SpartanProductVirtualizationPublic, + SpartanShiftChallenge, SpartanShiftPublic, +}; +pub use relation::{ + JoltConsistencyClaim, JoltExpr, JoltInputClaimExpression, JoltOutputClaimExpression, + JoltProtocolClaims, JoltRelationClaims, +}; diff --git a/crates/jolt-claims/src/protocols/jolt/relation.rs b/crates/jolt-claims/src/protocols/jolt/relation.rs new file mode 100644 index 0000000000..812f3fdee5 --- /dev/null +++ b/crates/jolt-claims/src/protocols/jolt/relation.rs @@ -0,0 +1,317 @@ +use serde::{Deserialize, Serialize}; + +use crate::util::extend_unique; +use crate::{ClaimExpression, ConsistencyClaim, Expr, InputClaimExpression, OutputClaimExpression}; + +use super::{JoltChallengeId, JoltOpeningId, JoltPublicId, JoltRelationId, JoltSumcheckSpec}; + +pub type JoltExpr = Expr; +pub type JoltInputClaimExpression = + InputClaimExpression; +pub type JoltOutputClaimExpression = + OutputClaimExpression; +pub type JoltConsistencyClaim = + ConsistencyClaim; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct JoltRelationClaims { + pub id: JoltRelationId, + pub sumcheck: JoltSumcheckSpec, + pub input: JoltInputClaimExpression, + pub output: JoltOutputClaimExpression, + /// Extra equality constraints that are part of the relation's soundness contract. + /// + /// Consumers must enforce these in the same constraint system as the input and output + /// claims; treating them as metadata can leave alternate opening chains unbound. + pub consistency: Vec>, +} + +impl JoltRelationClaims { + pub fn new( + id: JoltRelationId, + sumcheck: JoltSumcheckSpec, + input: JoltExpr, + output: JoltExpr, + ) -> Self { + Self { + id, + sumcheck, + input: ClaimExpression::from(input), + output: ClaimExpression::from(output), + consistency: Vec::new(), + } + } + + pub fn with_consistency(mut self, consistency: I) -> Self + where + I: IntoIterator, + C: Into>, + { + self.consistency + .extend(consistency.into_iter().map(Into::into)); + self + } + + pub fn push_consistency(&mut self, consistency: C) + where + C: Into>, + { + self.consistency.push(consistency.into()); + } + + pub fn with_input_challenges(mut self, challenges: I) -> Self + where + I: IntoIterator, + { + self.input.pull_challenges_for_transcript_sync(challenges); + self + } + + pub fn required_openings(&self) -> Vec { + let mut openings = self.input.required_openings.clone(); + extend_unique(&mut openings, &self.output.required_openings); + for consistency in &self.consistency { + extend_unique(&mut openings, &consistency.required_openings()); + } + openings + } + + pub fn required_publics(&self) -> Vec { + let mut publics = self.input.required_publics.clone(); + extend_unique(&mut publics, &self.output.required_publics); + for consistency in &self.consistency { + extend_unique(&mut publics, &consistency.required_publics()); + } + publics + } + + pub fn required_challenges(&self) -> Vec { + let mut challenges = self.input.required_challenges.clone(); + extend_unique(&mut challenges, &self.output.required_challenges); + for consistency in &self.consistency { + extend_unique(&mut challenges, &consistency.required_challenges()); + } + challenges + } + + pub fn challenge_index(&self, id: JoltChallengeId) -> Option { + self.required_challenges() + .iter() + .position(|challenge| *challenge == id) + } + + pub fn num_challenges(&self) -> usize { + self.required_challenges().len() + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct JoltProtocolClaims { + pub relations: Vec>, +} + +impl JoltProtocolClaims { + pub fn new(relations: Vec>) -> Self { + debug_assert_unique_relation_ids(&relations); + Self { relations } + } + + pub fn push(&mut self, relation: JoltRelationClaims) { + self.relations.push(relation); + } + + pub fn iter(&self) -> std::slice::Iter<'_, JoltRelationClaims> { + self.relations.iter() + } + + pub fn relation(&self, id: JoltRelationId) -> Option<&JoltRelationClaims> { + self.relations.iter().find(|relation| relation.id == id) + } + + pub fn len(&self) -> usize { + self.relations.len() + } + + pub fn is_empty(&self) -> bool { + self.relations.is_empty() + } + + pub fn required_openings(&self) -> Vec { + let mut openings = Vec::new(); + for relation in &self.relations { + extend_unique(&mut openings, &relation.required_openings()); + } + openings + } + + pub fn required_publics(&self) -> Vec { + let mut publics = Vec::new(); + for relation in &self.relations { + extend_unique(&mut publics, &relation.required_publics()); + } + publics + } + + pub fn required_challenges(&self) -> Vec { + let mut challenges = Vec::new(); + for relation in &self.relations { + extend_unique(&mut challenges, &relation.required_challenges()); + } + challenges + } +} + +impl<'a, F> IntoIterator for &'a JoltProtocolClaims { + type Item = &'a JoltRelationClaims; + type IntoIter = std::slice::Iter<'a, JoltRelationClaims>; + + fn into_iter(self) -> Self::IntoIter { + self.relations.iter() + } +} + +fn debug_assert_unique_relation_ids(relations: &[JoltRelationClaims]) { + debug_assert!( + relations + .iter() + .enumerate() + .all(|(index, relation)| !relations[..index].iter().any(|prev| prev.id == relation.id)), + "Jolt protocol claims contain duplicate relation IDs" + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{challenge, constant, opening, public}; + use jolt_field::{Fr, FromPrimitiveInt}; + + use super::super::{JoltCommittedPolynomial, JoltVirtualPolynomial, RamReadWriteChallenge}; + + #[test] + fn relation_claims_capture_expression_metadata() { + let ram_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::RamReadWriteChecking, + ); + let ram_val = JoltOpeningId::virtual_polynomial( + JoltVirtualPolynomial::RamVal, + JoltRelationId::RamReadWriteChecking, + ); + let rd_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersReadWriteChecking, + ); + + let gamma = JoltChallengeId::from(RamReadWriteChallenge::Gamma); + let sumcheck = JoltSumcheckSpec::boolean(8, 3); + + let input = opening(ram_inc) + challenge(gamma) * public(JoltPublicId::TraceLength); + let output = opening(ram_val) + constant(Fr::from_u64(7)); + let relation = JoltRelationClaims::new( + JoltRelationId::RamReadWriteChecking, + sumcheck, + input, + output, + ) + .with_consistency([JoltConsistencyClaim::same_evaluation(rd_inc, ram_val)]); + + assert_eq!(relation.id, JoltRelationId::RamReadWriteChecking); + assert_eq!(relation.sumcheck, sumcheck); + assert_eq!(relation.required_openings(), vec![ram_inc, ram_val, rd_inc]); + assert_eq!(relation.required_publics(), vec![JoltPublicId::TraceLength]); + assert_eq!(relation.required_challenges(), vec![gamma]); + assert_eq!(relation.challenge_index(gamma), Some(0)); + assert_eq!(relation.num_challenges(), 1); + assert_eq!( + relation.consistency, + vec![JoltConsistencyClaim::same_evaluation(rd_inc, ram_val)] + ); + } + + #[test] + fn relation_claims_carry_sumcheck_metadata() { + let ram_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::RamReadWriteChecking, + ); + let sumcheck = JoltSumcheckSpec::boolean(8, 3); + + let relation: JoltRelationClaims = JoltRelationClaims::new( + JoltRelationId::RamReadWriteChecking, + sumcheck, + opening(ram_inc), + opening(ram_inc), + ); + + assert_eq!(relation.sumcheck, sumcheck); + } + + #[test] + fn protocol_claims_preserve_relation_order_and_deduplicate_dependencies() { + let rd_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersReadWriteChecking, + ); + let ram_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RamInc, + JoltRelationId::RamReadWriteChecking, + ); + + let registers: JoltRelationClaims = JoltRelationClaims::new( + JoltRelationId::RegistersReadWriteChecking, + JoltSumcheckSpec::boolean(12, 3), + opening(rd_inc) + public(JoltPublicId::PaddedTraceLength), + opening(rd_inc), + ); + let ram: JoltRelationClaims = JoltRelationClaims::new( + JoltRelationId::RamReadWriteChecking, + JoltSumcheckSpec::boolean(20, 3), + opening(ram_inc) + public(JoltPublicId::PaddedTraceLength), + opening(ram_inc), + ); + let protocol = JoltProtocolClaims::new(vec![registers, ram]); + + let relation_ids = protocol + .iter() + .map(|relation| relation.id) + .collect::>(); + + assert_eq!( + relation_ids, + vec![ + JoltRelationId::RegistersReadWriteChecking, + JoltRelationId::RamReadWriteChecking, + ] + ); + assert_eq!( + protocol + .relation(JoltRelationId::RamReadWriteChecking) + .map(|relation| relation.id), + Some(JoltRelationId::RamReadWriteChecking) + ); + assert_eq!(protocol.required_openings(), vec![rd_inc, ram_inc]); + assert_eq!( + protocol.required_publics(), + vec![JoltPublicId::PaddedTraceLength] + ); + } + + #[cfg(debug_assertions)] + #[test] + #[should_panic(expected = "Jolt protocol claims contain duplicate relation IDs")] + fn protocol_claims_reject_duplicate_relation_ids_in_debug() { + let rd_inc = JoltOpeningId::committed( + JoltCommittedPolynomial::RdInc, + JoltRelationId::RegistersReadWriteChecking, + ); + let relation: JoltRelationClaims = JoltRelationClaims::new( + JoltRelationId::RegistersReadWriteChecking, + JoltSumcheckSpec::boolean(12, 3), + opening(rd_inc), + opening(rd_inc), + ); + + let _ = JoltProtocolClaims::new(vec![relation.clone(), relation]); + } +} diff --git a/crates/jolt-claims/src/protocols/mod.rs b/crates/jolt-claims/src/protocols/mod.rs new file mode 100644 index 0000000000..c2f769e377 --- /dev/null +++ b/crates/jolt-claims/src/protocols/mod.rs @@ -0,0 +1,2 @@ +pub mod field_inline; +pub mod jolt; diff --git a/crates/jolt-claims/src/util.rs b/crates/jolt-claims/src/util.rs new file mode 100644 index 0000000000..ae47b43564 --- /dev/null +++ b/crates/jolt-claims/src/util.rs @@ -0,0 +1,7 @@ +pub(crate) fn extend_unique(target: &mut Vec, values: &[T]) { + for value in values { + if !target.contains(value) { + target.push(value.clone()); + } + } +} diff --git a/crates/jolt-crypto/Cargo.toml b/crates/jolt-crypto/Cargo.toml index 44efb83a7b..247760ff72 100644 --- a/crates/jolt-crypto/Cargo.toml +++ b/crates/jolt-crypto/Cargo.toml @@ -29,11 +29,13 @@ bn254 = [ "dep:num-bigint", "dep:num-integer", "dep:num-traits", - "dep:rayon", + "parallel", ] +parallel = ["dep:rayon", "jolt-poly/parallel"] [dependencies] jolt-field = { path = "../jolt-field" } +jolt-poly = { path = "../jolt-poly", default-features = false } jolt-transcript = { path = "../jolt-transcript" } serde = { workspace = true, features = ["derive", "alloc"] } rand_core = { workspace = true } diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index 94e2acfa39..2727b5fd67 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -1,9 +1,16 @@ -use std::fmt::Debug; +use std::{ + error::Error, + fmt::{self, Debug}, +}; -use jolt_field::Field; +use jolt_field::{AdditiveAccumulator, Field, RingAccumulator, WithAccumulator}; +use jolt_poly::EqPolynomial; use jolt_transcript::AppendToTranscript; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(feature = "parallel")] +const PAR_THRESHOLD: usize = 1024; + /// Base commitment abstraction: defines only the output type. /// /// This is the root of the commitment trait hierarchy, shared by both @@ -11,7 +18,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// polynomial commitment schemes (`jolt_openings::CommitmentScheme`). /// The `Output` associated type is the single piece of connective tissue /// between these different levels of abstraction. -pub trait Commitment { +pub trait Commitment: Clone + Debug + Eq + Send + Sync + 'static { /// The commitment value (e.g., a group element, a Merkle root, a lattice vector). type Output: Clone + Debug + Eq + Send + Sync + 'static + Serialize + DeserializeOwned; } @@ -21,10 +28,11 @@ pub trait Commitment { /// Extends [`Commitment`] with the ability to commit to a vector of field /// elements with a blinding factor. Uses `Self::Output` from the supertrait /// as the commitment value type. -pub trait VectorCommitment: Commitment + Clone + Send + Sync + 'static -where - Self::Output: Copy + Default + AppendToTranscript + Serialize + for<'de> Deserialize<'de>, +pub trait VectorCommitment: + Commitment { + type Field: Field; + /// Transparent setup parameters (generators, public parameters, etc.). type Setup: Clone + Send + Sync; @@ -38,18 +46,172 @@ where /// /// May panic if `values.len()` exceeds [`Self::capacity()`]. #[must_use] - fn commit(setup: &Self::Setup, values: &[F], blinding: &F) -> Self::Output; + fn commit(setup: &Self::Setup, values: &[Self::Field], blinding: &Self::Field) -> Self::Output; /// Returns `true` if `commitment` opens to `(values, blinding)`. #[must_use] - fn verify( + fn verify( setup: &Self::Setup, commitment: &Self::Output, - values: &[F], - blinding: &F, + values: &[Self::Field], + blinding: &Self::Field, ) -> bool; + + /// Opens a row-major matrix of committed rows at `(row_point, entry_point)`. + /// + /// Missing entries at the end of `flattened_rows` are treated as zero. + /// Callers must either pass exactly `row_count * row_len` entries or commit + /// each row with the same trailing zero-padding convention; otherwise + /// verification rejects with [`VectorOpeningError::CommitmentMismatch`]. + fn open_committed_rows( + flattened_rows: &[Self::Field], + row_blindings: &[Self::Field], + row_len: usize, + row_point: &[Self::Field], + entry_point: &[Self::Field], + ) -> Result<(VectorCommitmentOpening, Self::Field), VectorOpeningError> + where + ::Accumulator: RingAccumulator, + { + let row_count = point_len_to_basis_len(row_point.len())?; + validate_row_len(row_len, entry_point.len())?; + let max_len = row_count + .checked_mul(row_len) + .ok_or(VectorOpeningError::DimensionOverflow)?; + if flattened_rows.len() > max_len { + return Err(VectorOpeningError::FlattenedRowsTooLong { + max: max_len, + got: flattened_rows.len(), + }); + } + if row_blindings.len() != row_count { + return Err(VectorOpeningError::RowBlindingsLengthMismatch { + expected: row_count, + got: row_blindings.len(), + }); + } + + let row_weights = EqPolynomial::new(row_point.to_vec()).evaluations(); + let entry_weights = EqPolynomial::new(entry_point.to_vec()).evaluations(); + let combined_vector = combine_rows(flattened_rows, row_len, &row_weights, max_len); + let combined_blinding = inner_product(row_blindings, &row_weights); + + let evaluation = inner_product(&combined_vector, &entry_weights); + Ok(( + VectorCommitmentOpening { + combined_vector, + combined_blinding, + }, + evaluation, + )) + } + + /// Verifies a row-combined opening and returns the evaluation at `entry_point`. + /// + fn verify_committed_rows( + setup: &Self::Setup, + row_commitments: &[Self::Output], + row_point: &[Self::Field], + entry_point: &[Self::Field], + opening: &VectorCommitmentOpening, + ) -> Result + where + Self::Output: HomomorphicCommitment, + ::Accumulator: RingAccumulator, + { + let row_count = point_len_to_basis_len(row_point.len())?; + if row_commitments.len() != row_count { + return Err(VectorOpeningError::RowCommitmentsLengthMismatch { + expected: row_count, + got: row_commitments.len(), + }); + } + + let row_len = opening.combined_vector.len(); + validate_row_len(row_len, entry_point.len())?; + let capacity = Self::capacity(setup); + if row_len > capacity { + return Err(VectorOpeningError::CommitmentCapacityExceeded { capacity, row_len }); + } + + let row_weights = EqPolynomial::new(row_point.to_vec()).evaluations(); + let combined_commitment = combine_commitments(row_commitments, &row_weights); + if !Self::verify( + setup, + &combined_commitment, + &opening.combined_vector, + &opening.combined_blinding, + ) { + return Err(VectorOpeningError::CommitmentMismatch); + } + + let entry_weights = EqPolynomial::new(entry_point.to_vec()).evaluations(); + Ok(inner_product(&opening.combined_vector, &entry_weights)) + } +} + +/// Opening data for a linear combination of committed rows. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct VectorCommitmentOpening { + pub combined_vector: Vec, + pub combined_blinding: F, } +/// Errors returned by committed-row opening helpers. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VectorOpeningError { + RowLenZero, + RowLenNotPowerOfTwo { row_len: usize }, + PointTooLarge { point_len: usize }, + EntryPointLengthMismatch { expected: usize, got: usize }, + RowBlindingsLengthMismatch { expected: usize, got: usize }, + RowCommitmentsLengthMismatch { expected: usize, got: usize }, + FlattenedRowsTooLong { max: usize, got: usize }, + CommitmentCapacityExceeded { capacity: usize, row_len: usize }, + DimensionOverflow, + CommitmentMismatch, +} + +impl fmt::Display for VectorOpeningError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::RowLenZero => write!(f, "row length must be non-zero"), + Self::RowLenNotPowerOfTwo { row_len } => { + write!(f, "row length {row_len} is not a power of two") + } + Self::PointTooLarge { point_len } => { + write!( + f, + "point length {point_len} does not fit in usize dimensions" + ) + } + Self::EntryPointLengthMismatch { expected, got } => write!( + f, + "entry point length mismatch: expected {expected}, got {got}" + ), + Self::RowBlindingsLengthMismatch { expected, got } => write!( + f, + "row blinding count mismatch: expected {expected}, got {got}" + ), + Self::RowCommitmentsLengthMismatch { expected, got } => write!( + f, + "row commitment count mismatch: expected {expected}, got {got}" + ), + Self::FlattenedRowsTooLong { max, got } => { + write!(f, "flattened row data has length {got}, maximum is {max}") + } + Self::CommitmentCapacityExceeded { capacity, row_len } => write!( + f, + "row length {row_len} exceeds commitment capacity {capacity}" + ), + Self::DimensionOverflow => write!(f, "vector opening dimensions overflow usize"), + Self::CommitmentMismatch => write!(f, "combined vector commitment does not match rows"), + } + } +} + +impl Error for VectorOpeningError {} + /// Additive homomorphism on commitment values over a scalar field `F`. /// /// Captures the ability to linearly combine two commitments without @@ -65,7 +227,11 @@ where /// Blanket-implemented for [`JoltGroup`](crate::JoltGroup) over any field /// (via `scalar_mul` + addition). Non-group commitment types (e.g., lattice /// vectors) can implement this trait directly for their native scalar field. -pub trait HomomorphicCommitment: Clone { +pub trait HomomorphicCommitment: Clone + Default { + /// Computes `c1 + c2`. + #[must_use] + fn add(c1: &Self, c2: &Self) -> Self; + /// Computes `c1 + scalar * c2`. #[must_use] fn linear_combine(c1: &Self, c2: &Self, scalar: &F) -> Self; @@ -83,7 +249,166 @@ pub trait DeriveSetup { fn derive(source: &Source, capacity: usize) -> Self; } +fn validate_row_len(row_len: usize, entry_point_len: usize) -> Result<(), VectorOpeningError> { + if row_len == 0 { + return Err(VectorOpeningError::RowLenZero); + } + if !row_len.is_power_of_two() { + return Err(VectorOpeningError::RowLenNotPowerOfTwo { row_len }); + } + let expected = row_len.trailing_zeros() as usize; + if entry_point_len != expected { + return Err(VectorOpeningError::EntryPointLengthMismatch { + expected, + got: entry_point_len, + }); + } + Ok(()) +} + +fn point_len_to_basis_len(point_len: usize) -> Result { + if point_len >= usize::BITS as usize { + return Err(VectorOpeningError::PointTooLarge { point_len }); + } + Ok(1usize << point_len) +} + +#[cfg(feature = "parallel")] +fn combine_rows( + flattened_rows: &[F], + row_len: usize, + row_weights: &[F], + max_len: usize, +) -> Vec +where + ::Accumulator: RingAccumulator, +{ + let mut combined_vector = vec![F::zero(); row_len]; + + if max_len >= PAR_THRESHOLD { + use rayon::prelude::*; + + combined_vector + .par_iter_mut() + .enumerate() + .for_each(|(entry_index, combined_entry)| { + let mut acc = ::Accumulator::default(); + for (row_index, row_weight) in row_weights.iter().copied().enumerate() { + if let Some(value) = flattened_rows.get(row_index * row_len + entry_index) { + acc.fmadd(row_weight, *value); + } + } + *combined_entry = acc.reduce(); + }); + } else { + for (entry_index, combined_entry) in combined_vector.iter_mut().enumerate() { + let mut acc = ::Accumulator::default(); + for (row_index, row_weight) in row_weights.iter().copied().enumerate() { + if let Some(value) = flattened_rows.get(row_index * row_len + entry_index) { + acc.fmadd(row_weight, *value); + } + } + *combined_entry = acc.reduce(); + } + } + + combined_vector +} + +#[cfg(not(feature = "parallel"))] +fn combine_rows( + flattened_rows: &[F], + row_len: usize, + row_weights: &[F], + _max_len: usize, +) -> Vec +where + ::Accumulator: RingAccumulator, +{ + let mut combined_vector = vec![F::zero(); row_len]; + + for (entry_index, combined_entry) in combined_vector.iter_mut().enumerate() { + let mut acc = ::Accumulator::default(); + for (row_index, row_weight) in row_weights.iter().copied().enumerate() { + if let Some(value) = flattened_rows.get(row_index * row_len + entry_index) { + acc.fmadd(row_weight, *value); + } + } + *combined_entry = acc.reduce(); + } + + combined_vector +} + +fn inner_product(lhs: &[F], rhs: &[F]) -> F +where + ::Accumulator: RingAccumulator, +{ + #[cfg(feature = "parallel")] + { + if lhs.len() >= PAR_THRESHOLD { + use rayon::prelude::*; + + return lhs + .par_iter() + .zip(rhs.par_iter()) + .fold( + ::Accumulator::default, + |mut acc, (left, right)| { + acc.fmadd(*left, *right); + acc + }, + ) + .reduce( + ::Accumulator::default, + |mut left, right| { + left.merge(right); + left + }, + ) + .reduce(); + } + } + + let mut acc = ::Accumulator::default(); + for (left, right) in lhs.iter().zip(rhs.iter()) { + acc.fmadd(*left, *right); + } + acc.reduce() +} + +fn combine_commitments(commitments: &[C], weights: &[F]) -> C +where + F: Field, + C: HomomorphicCommitment + Copy + Send + Sync, +{ + #[cfg(feature = "parallel")] + { + if commitments.len() >= PAR_THRESHOLD { + use rayon::prelude::*; + + return commitments + .par_iter() + .zip(weights.par_iter()) + .map(|(commitment, weight)| C::linear_combine(&C::default(), commitment, weight)) + .reduce(C::default, |left, right| C::add(&left, &right)); + } + } + + commitments + .iter() + .zip(weights.iter()) + .fold(C::default(), |acc, (commitment, weight)| { + C::linear_combine(&acc, commitment, weight) + }) +} + impl HomomorphicCommitment for G { + #[inline] + fn add(c1: &G, c2: &G) -> G { + *c1 + c2 + } + #[inline] fn linear_combine(c1: &G, c2: &G, scalar: &F) -> G { *c1 + c2.scalar_mul(scalar) diff --git a/crates/jolt-crypto/src/ec/bn254/gt.rs b/crates/jolt-crypto/src/ec/bn254/gt.rs index befa42df4c..603089517e 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -139,6 +139,11 @@ impl AppendToTranscript for Bn254GT { buf.reverse(); transcript.append_bytes(&buf); } + + fn transcript_payload_len(&self) -> Option { + use ark_serialize::CanonicalSerialize; + Some(self.0.uncompressed_size() as u64) + } } impl JoltGroup for Bn254GT { diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index 29ccc507ab..38f713e6b9 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -132,11 +132,10 @@ macro_rules! impl_jolt_group_wrapper { impl ::jolt_transcript::AppendToTranscript for $wrapper { fn append_to_transcript(&self, transcript: &mut T) { use ::ark_serialize::CanonicalSerialize; - let mut buf = Vec::with_capacity(self.0.uncompressed_size()); + let mut buf = Vec::with_capacity(self.0.compressed_size()); self.0 - .serialize_uncompressed(&mut buf) + .serialize_compressed(&mut buf) .expect(concat!(stringify!($wrapper), " serialization cannot fail")); - buf.reverse(); transcript.append_bytes(&buf); } } @@ -203,7 +202,7 @@ use jolt_field::Field; use crate::PairingGroup; /// BN254 pairing-friendly curve. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Bn254; impl Bn254 { @@ -273,3 +272,47 @@ pub(crate) fn field_to_fr(f: &F) -> ark_bn254::Fr { } ark_bn254::Fr::from_le_bytes_mod_order(&bytes) } + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may fail loudly")] +mod tests { + use ark_serialize::CanonicalSerialize; + use jolt_field::Fr; + use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; + + use super::Bn254; + + #[test] + fn g1_transcript_encoding_uses_compressed_commitment_bytes() { + let point = Bn254::g1_generator(); + let mut actual = Blake2bTranscript::::new(b"test"); + point.append_to_transcript(&mut actual); + + let mut expected = Blake2bTranscript::::new(b"test"); + let mut bytes = Vec::new(); + point + .0 + .serialize_compressed(&mut bytes) + .expect("serialize G1"); + expected.append_bytes(&bytes); + + assert_eq!(actual.state(), expected.state()); + } + + #[test] + fn g2_transcript_encoding_uses_compressed_commitment_bytes() { + let point = Bn254::g2_generator(); + let mut actual = Blake2bTranscript::::new(b"test"); + point.append_to_transcript(&mut actual); + + let mut expected = Blake2bTranscript::::new(b"test"); + let mut bytes = Vec::new(); + point + .0 + .serialize_compressed(&mut bytes) + .expect("serialize G2"); + expected.append_bytes(&bytes); + + assert_eq!(actual.state(), expected.state()); + } +} diff --git a/crates/jolt-crypto/src/ec/pairing.rs b/crates/jolt-crypto/src/ec/pairing.rs index 07e56269dc..ab9c164262 100644 --- a/crates/jolt-crypto/src/ec/pairing.rs +++ b/crates/jolt-crypto/src/ec/pairing.rs @@ -1,4 +1,5 @@ use jolt_field::Field; +use std::fmt::Debug; use super::group::JoltGroup; @@ -11,7 +12,7 @@ use super::group::JoltGroup; /// G1, G2, and GT all implement `JoltGroup` (additive notation). GT uses /// additive notation for uniformity, even though the underlying operation /// is Fq12 multiplication. See `Bn254GT` for the mapping. -pub trait PairingGroup: Clone + Sync + Send + 'static { +pub trait PairingGroup: Clone + Debug + Eq + Sync + Send + 'static { /// Scalar field for G1 and G2 (e.g., BN254 Fr). type ScalarField: Field; type G1: JoltGroup; diff --git a/crates/jolt-crypto/src/ec/pedersen.rs b/crates/jolt-crypto/src/ec/pedersen.rs index af5d3105a7..0054a3fe23 100644 --- a/crates/jolt-crypto/src/ec/pedersen.rs +++ b/crates/jolt-crypto/src/ec/pedersen.rs @@ -1,4 +1,4 @@ -use jolt_field::Field; +use jolt_field::Fr; use serde::{Deserialize, Serialize}; use super::group::JoltGroup; @@ -13,7 +13,8 @@ const EMPTY_GENERATORS_MSG: &str = "Pedersen setup requires at least one message /// This provides a blanket `VectorCommitment` implementation for any group /// that implements `JoltGroup`, so concrete backends (BN254, etc.) inherit /// Pedersen commitments. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] pub struct Pedersen { _marker: std::marker::PhantomData, } @@ -66,6 +67,7 @@ impl Commitment for Pedersen { } impl VectorCommitment for Pedersen { + type Field = Fr; type Setup = PedersenSetup; #[inline] @@ -85,7 +87,7 @@ impl VectorCommitment for Pedersen { /// # Panics /// /// Panics if `values.len() > Self::capacity(setup)`. - fn commit(setup: &Self::Setup, values: &[F], blinding: &F) -> G { + fn commit(setup: &Self::Setup, values: &[Fr], blinding: &Fr) -> G { assert!( values.len() <= setup.message_generators.len(), "values length ({}) exceeds generator count ({})", @@ -97,7 +99,7 @@ impl VectorCommitment for Pedersen { msg + blind } - fn verify(setup: &Self::Setup, commitment: &G, values: &[F], blinding: &F) -> bool { + fn verify(setup: &Self::Setup, commitment: &G, values: &[Fr], blinding: &Fr) -> bool { *commitment == Self::commit(setup, values, blinding) } } diff --git a/crates/jolt-crypto/src/lib.rs b/crates/jolt-crypto/src/lib.rs index b8f78c6863..2cdf92fc3e 100644 --- a/crates/jolt-crypto/src/lib.rs +++ b/crates/jolt-crypto/src/lib.rs @@ -9,7 +9,10 @@ pub mod ec; pub use ec::{JoltGroup, PairingGroup, Pedersen, PedersenSetup}; mod commitment; -pub use commitment::{Commitment, DeriveSetup, HomomorphicCommitment, VectorCommitment}; +pub use commitment::{ + Commitment, DeriveSetup, HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening, + VectorOpeningError, +}; #[cfg(feature = "bn254")] pub use ec::bn254::{Bn254, Bn254G1, Bn254G2, Bn254GT}; diff --git a/crates/jolt-crypto/tests/coverage.rs b/crates/jolt-crypto/tests/coverage.rs index 3dca4b1230..6d59088099 100644 --- a/crates/jolt-crypto/tests/coverage.rs +++ b/crates/jolt-crypto/tests/coverage.rs @@ -75,7 +75,9 @@ fn g2_msm_multiple_random() { let naive: Bn254G2 = points .iter() .zip(scalars.iter()) - .fold(Bn254G2::identity(), |acc, (p, s)| acc + p.scalar_mul(s)); + .fold(::identity(), |acc, (p, s)| { + acc + p.scalar_mul(s) + }); assert_eq!(msm_result, naive); } @@ -91,7 +93,7 @@ fn g2_associativity() { #[test] fn g2_neg() { let g = Bn254::g2_generator(); - assert_eq!(g + (-g), Bn254G2::identity()); + assert_eq!(g + (-g), ::identity()); assert!((-g + g).is_identity()); } @@ -123,7 +125,7 @@ fn gt_debug_format_contains_type_name() { #[test] fn gt_identity_is_identity() { - let id = Bn254GT::identity(); + let id = ::identity(); assert!(id.is_identity()); assert!(!gt_element().is_identity()); } @@ -519,7 +521,7 @@ fn g2_scalar_mul_consistency_with_repeated_add() { let n = 7u64; let scalar = Fr::from_u64(n); let via_scalar_mul = g.scalar_mul(&scalar); - let mut via_add = Bn254G2::identity(); + let mut via_add = ::identity(); for _ in 0..n { via_add += g; } @@ -532,7 +534,7 @@ fn gt_scalar_mul_consistency_with_repeated_add() { let n = 5u64; let scalar = Fr::from_u64(n); let via_scalar_mul = e.scalar_mul(&scalar); - let mut via_add = Bn254GT::identity(); + let mut via_add = ::identity(); for _ in 0..n { via_add += e; } diff --git a/crates/jolt-crypto/tests/pedersen.rs b/crates/jolt-crypto/tests/pedersen.rs index b1d3201b43..03828379c2 100644 --- a/crates/jolt-crypto/tests/pedersen.rs +++ b/crates/jolt-crypto/tests/pedersen.rs @@ -1,7 +1,13 @@ //! Pedersen commitment scheme tests over BN254 G1. -use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; +#![expect(clippy::unwrap_used, reason = "tests should fail loudly")] + +use jolt_crypto::{ + Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, + VectorOpeningError, +}; use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_poly::EqPolynomial; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -12,6 +18,43 @@ fn deterministic_setup(count: usize) -> PedersenSetup { PedersenSetup::new(message_generators, blinding_generator) } +fn mle_eval(flattened_rows: &[Fr], row_len: usize, row_point: &[Fr], entry_point: &[Fr]) -> Fr { + let row_weights = EqPolynomial::new(row_point.to_vec()).evaluations(); + let entry_weights = EqPolynomial::new(entry_point.to_vec()).evaluations(); + let mut result = Fr::from_u64(0); + for (row_index, row_weight) in row_weights.iter().copied().enumerate() { + let row_offset = row_index * row_len; + for (entry_index, entry_weight) in entry_weights.iter().copied().enumerate() { + if let Some(value) = flattened_rows.get(row_offset + entry_index) { + result += row_weight * entry_weight * *value; + } + } + } + result +} + +fn row_commitments( + setup: &PedersenSetup, + flattened_rows: &[Fr], + row_len: usize, + row_blindings: &[Fr], +) -> Vec { + row_blindings + .iter() + .enumerate() + .map(|(row_index, blinding)| { + let row_offset = row_index * row_len; + let mut row = vec![Fr::from_u64(0); row_len]; + for (entry_index, row_entry) in row.iter_mut().enumerate() { + if let Some(value) = flattened_rows.get(row_offset + entry_index) { + *row_entry = *value; + } + } + Pedersen::::commit(setup, &row, blinding) + }) + .collect() +} + #[test] fn commit_verify_roundtrip() { let setup = deterministic_setup(4); @@ -177,3 +220,325 @@ fn partial_values_uses_prefix_generators() { &blinding )); } + +#[test] +fn committed_rows_opening_roundtrip_returns_mle_eval() { + let setup = deterministic_setup(4); + let mut rng = ChaCha20Rng::seed_from_u64(10); + + let row_len = 4; + let row_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let entry_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let flattened_rows: Vec = (0..16).map(|_| Fr::random(&mut rng)).collect(); + let row_blindings: Vec = (0..4).map(|_| Fr::random(&mut rng)).collect(); + let row_commitments = row_commitments(&setup, &flattened_rows, row_len, &row_blindings); + + let (opening, opened_eval) = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + row_len, + &row_point, + &entry_point, + ) + .unwrap(); + let verified_eval = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap(); + + let expected = mle_eval(&flattened_rows, row_len, &row_point, &entry_point); + assert_eq!(opened_eval, expected); + assert_eq!(verified_eval, expected); +} + +#[test] +fn committed_rows_opening_accepts_zero_padded_flattened_rows() { + let setup = deterministic_setup(4); + let mut rng = ChaCha20Rng::seed_from_u64(11); + + let row_len = 4; + let row_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let entry_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let flattened_rows: Vec = (0..10).map(|_| Fr::random(&mut rng)).collect(); + let row_blindings: Vec = (0..4).map(|_| Fr::random(&mut rng)).collect(); + let row_commitments = row_commitments(&setup, &flattened_rows, row_len, &row_blindings); + + let (opening, opened_eval) = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + row_len, + &row_point, + &entry_point, + ) + .unwrap(); + let verified_eval = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap(); + + let expected = mle_eval(&flattened_rows, row_len, &row_point, &entry_point); + assert_eq!(opened_eval, expected); + assert_eq!(verified_eval, expected); +} + +#[test] +fn committed_rows_opening_handles_single_row_scalar_entry() { + let setup = deterministic_setup(1); + let value = Fr::from_u64(42); + let blinding = Fr::from_u64(17); + let flattened_rows = [value]; + let row_blindings = [blinding]; + let row_commitments = [Pedersen::::commit( + &setup, + &flattened_rows, + &blinding, + )]; + + let (opening, opened_eval) = + Pedersen::::open_committed_rows(&flattened_rows, &row_blindings, 1, &[], &[]) + .unwrap(); + let verified_eval = + Pedersen::::verify_committed_rows(&setup, &row_commitments, &[], &[], &opening) + .unwrap(); + + assert_eq!(opened_eval, value); + assert_eq!(verified_eval, value); +} + +#[test] +fn committed_rows_opening_rejects_tampered_combined_vector() { + let setup = deterministic_setup(4); + let mut rng = ChaCha20Rng::seed_from_u64(12); + + let row_len = 4; + let row_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let entry_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let flattened_rows: Vec = (0..16).map(|_| Fr::random(&mut rng)).collect(); + let row_blindings: Vec = (0..4).map(|_| Fr::random(&mut rng)).collect(); + let row_commitments = row_commitments(&setup, &flattened_rows, row_len, &row_blindings); + let (mut opening, _) = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + row_len, + &row_point, + &entry_point, + ) + .unwrap(); + + opening.combined_vector[0] += Fr::from_u64(1); + let err = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap_err(); + assert!(matches!(err, VectorOpeningError::CommitmentMismatch)); +} + +#[test] +fn committed_rows_opening_rejects_tampered_blinding() { + let setup = deterministic_setup(4); + let mut rng = ChaCha20Rng::seed_from_u64(13); + + let row_len = 4; + let row_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let entry_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let flattened_rows: Vec = (0..16).map(|_| Fr::random(&mut rng)).collect(); + let row_blindings: Vec = (0..4).map(|_| Fr::random(&mut rng)).collect(); + let row_commitments = row_commitments(&setup, &flattened_rows, row_len, &row_blindings); + let (mut opening, _) = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + row_len, + &row_point, + &entry_point, + ) + .unwrap(); + + opening.combined_blinding += Fr::from_u64(1); + let err = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap_err(); + assert!(matches!(err, VectorOpeningError::CommitmentMismatch)); +} + +#[test] +fn committed_rows_opening_rejects_wrong_row_commitment() { + let setup = deterministic_setup(4); + let mut rng = ChaCha20Rng::seed_from_u64(14); + + let row_len = 4; + let row_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let entry_point: Vec = (0..2).map(|_| Fr::random(&mut rng)).collect(); + let flattened_rows: Vec = (0..16).map(|_| Fr::random(&mut rng)).collect(); + let row_blindings: Vec = (0..4).map(|_| Fr::random(&mut rng)).collect(); + let mut row_commitments = row_commitments(&setup, &flattened_rows, row_len, &row_blindings); + row_commitments[0] = row_commitments[1]; + let (opening, _) = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + row_len, + &row_point, + &entry_point, + ) + .unwrap(); + + let err = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap_err(); + assert!(matches!(err, VectorOpeningError::CommitmentMismatch)); +} + +#[test] +fn committed_rows_opening_rejects_invalid_dimensions() { + let row_point = [Fr::from_u64(2), Fr::from_u64(3)]; + let entry_point = [Fr::from_u64(5), Fr::from_u64(7)]; + let flattened_rows = vec![Fr::from_u64(1); 16]; + let row_blindings = vec![Fr::from_u64(9); 4]; + + let err = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + 0, + &row_point, + &entry_point, + ) + .unwrap_err(); + assert!(matches!(err, VectorOpeningError::RowLenZero)); + + let err = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + 3, + &row_point, + &entry_point, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::RowLenNotPowerOfTwo { row_len: 3 } + )); + + let err = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + 4, + &row_point, + &entry_point[..1], + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::EntryPointLengthMismatch { + expected: 2, + got: 1 + } + )); + + let err = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings[..3], + 4, + &row_point, + &entry_point, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::RowBlindingsLengthMismatch { + expected: 4, + got: 3 + } + )); + + let err = Pedersen::::open_committed_rows( + &[Fr::from_u64(1); 17], + &row_blindings, + 4, + &row_point, + &entry_point, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::FlattenedRowsTooLong { max: 16, got: 17 } + )); + + let err = Pedersen::::open_committed_rows( + &flattened_rows, + &row_blindings, + 4, + &vec![Fr::from_u64(1); usize::BITS as usize], + &entry_point, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::PointTooLarge { point_len } if point_len == usize::BITS as usize + )); +} + +#[test] +fn committed_rows_verifier_rejects_invalid_dimensions() { + let setup = deterministic_setup(2); + let row_point = [Fr::from_u64(2), Fr::from_u64(3)]; + let entry_point = [Fr::from_u64(5), Fr::from_u64(7)]; + let row_commitments = vec![Bn254G1::identity(); 3]; + let opening = VectorCommitmentOpening { + combined_vector: vec![Fr::from_u64(1); 4], + combined_blinding: Fr::from_u64(9), + }; + + let err = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::RowCommitmentsLengthMismatch { + expected: 4, + got: 3 + } + )); + + let row_commitments = vec![Bn254G1::identity(); 4]; + let err = Pedersen::::verify_committed_rows( + &setup, + &row_commitments, + &row_point, + &entry_point, + &opening, + ) + .unwrap_err(); + assert!(matches!( + err, + VectorOpeningError::CommitmentCapacityExceeded { + capacity: 2, + row_len: 4 + } + )); +} diff --git a/crates/jolt-dory/src/scheme.rs b/crates/jolt-dory/src/scheme.rs index dc6491b36a..63526d87a8 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -78,7 +78,7 @@ pub(crate) fn ark_to_jolt_g1(ark: ArkG1) -> Bn254G1 { unsafe { std::mem::transmute(ark) } } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct DoryScheme; impl DoryScheme { @@ -216,6 +216,15 @@ impl CommitmentScheme for DoryScheme { let ark_commitment = jolt_gt_to_ark(&commitment.0); let mut dory_transcript = JoltToDoryTranscript::new(transcript); + if proof.0.e2.is_some() + || proof.0.y_com.is_some() + || proof.0.sigma1_proof.is_some() + || proof.0.sigma2_proof.is_some() + || proof.0.scalar_product_proof.is_some() + { + return Err(OpeningsError::VerificationFailed); + } + dory::verify::( ark_commitment, ark_eval, @@ -342,13 +351,18 @@ impl ZkOpeningScheme for DoryScheme { proof: &Self::Proof, setup: &Self::VerifierSetup, transcript: &mut impl Transcript, - ) -> Result<(), OpeningsError> { + ) -> Result { let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); // In ZK mode dory::verify reads the evaluation commitment from `proof.y_com`, // so the caller-side eval is unused here. let dummy_eval = ::zero(); let ark_commitment = jolt_gt_to_ark(&commitment.0); let mut dory_transcript = JoltToDoryTranscript::new(transcript); + let hiding_commitment = proof + .0 + .y_com + .map(ark_to_jolt_g1) + .ok_or(OpeningsError::VerificationFailed)?; dory::verify::( ark_commitment, @@ -358,7 +372,22 @@ impl ZkOpeningScheme for DoryScheme { setup.0.clone().into_inner(), &mut dory_transcript, ) - .map_err(|_| OpeningsError::VerificationFailed) + .map_err(|_| OpeningsError::VerificationFailed)?; + + Ok(hiding_commitment) + } + + fn bind_zk_opening_inputs( + transcript: &mut impl Transcript, + point: &[Self::Field], + hiding_commitment: &Self::HidingCommitment, + ) { + transcript.append(&LabelWithCount(b"dory_opening_point", point.len() as u64)); + for p in point { + p.append_to_transcript(transcript); + } + transcript.append(&Label(b"dory_eval_commitment")); + hiding_commitment.append_to_transcript(transcript); } } diff --git a/crates/jolt-dory/src/transcript.rs b/crates/jolt-dory/src/transcript.rs index e769681913..6d2ee4f055 100644 --- a/crates/jolt-dory/src/transcript.rs +++ b/crates/jolt-dory/src/transcript.rs @@ -1,10 +1,8 @@ //! Bridges the `jolt-transcript` framework into dory-pcs's `DoryTranscript` trait. //! //! Prover/verifier parity within `jolt-dory` is by construction: both sides -//! traverse this adapter. Cross-framework parity with `jolt-core`'s adapter -//! or with dory-pcs's reference `Blake2bTranscript` is NOT guaranteed — -//! `jolt-transcript` prepends a per-absorb domain-tag byte and squeezes -//! 16-byte challenges, neither of which the other frameworks do. +//! traverse this adapter. The surrounding Jolt transcript is responsible for +//! matching the core Fiat-Shamir byte layout before this adapter is entered. #![expect( clippy::expect_used, @@ -65,7 +63,7 @@ impl> DoryTranscript for JoltToDoryTranscript<'_, } fn challenge_scalar(&mut self, _label: &[u8]) -> ArkFr { - let challenge: Fr = self.transcript.challenge(); + let challenge: Fr = self.transcript.challenge_scalar(); jolt_fr_to_ark(&challenge) } diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index c0e720f8cb..bdce8d8fc5 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -20,6 +20,13 @@ pub const MAX_SERIALIZED_PROOF_ROUNDS: usize = 64; #[derive(Clone, Debug, PartialEq, Eq)] pub struct DoryCommitment(pub Bn254GT); +impl Default for DoryCommitment { + #[inline] + fn default() -> Self { + Self(Bn254GT::default()) + } +} + impl Serialize for DoryCommitment { fn serialize(&self, serializer: S) -> Result { self.0.serialize(serializer) @@ -39,18 +46,29 @@ impl AppendToTranscript for DoryCommitment { fn append_to_transcript(&self, transcript: &mut T) { self.0.append_to_transcript(transcript); } + + fn transcript_payload_len(&self) -> Option { + self.0.transcript_payload_len() + } } impl HomomorphicCommitment for DoryCommitment { + #[inline] + fn add(c1: &Self, c2: &Self) -> Self { + Self(>::add(&c1.0, &c2.0)) + } + #[inline] fn linear_combine(c1: &Self, c2: &Self, scalar: &F) -> Self { Self(HomomorphicCommitment::linear_combine(&c1.0, &c2.0, scalar)) } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct DoryProof(pub ArkDoryProof); +impl Eq for DoryProof {} + impl Serialize for DoryProof { fn serialize(&self, serializer: S) -> Result { canonical_serialize(&self.0, serializer) diff --git a/crates/jolt-dory/tests/commit_open_verify.rs b/crates/jolt-dory/tests/commit_open_verify.rs index 1b14dae377..60aed67c93 100644 --- a/crates/jolt-dory/tests/commit_open_verify.rs +++ b/crates/jolt-dory/tests/commit_open_verify.rs @@ -147,12 +147,14 @@ fn streaming_zk_commitment_is_blinded_and_verifies() { ); let mut pt = Blake2bTranscript::new(b"stream-zk"); - let (proof, _eval_com, _blind) = + let (proof, eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt); let mut vt = Blake2bTranscript::new(b"stream-zk"); - DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) - .expect("streaming ZK commitment must verify"); + let verified_eval_com = + DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) + .expect("streaming ZK commitment must verify"); + assert_eq!(verified_eval_com, eval_com); } #[test] @@ -354,12 +356,14 @@ fn zk_round_trip>(num_vars: usize, seed: u64, labe ::commit_zk(poly.evaluations(), &prover_setup); let mut pt = T::new(label); - let (proof, _eval_com, _blind) = + let (proof, eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt); let mut vt = T::new(label); - DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) - .expect("ZK round-trip verification must succeed"); + let verified_eval_com = + DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) + .expect("ZK round-trip verification must succeed"); + assert_eq!(verified_eval_com, eval_com); } #[test] @@ -376,6 +380,33 @@ fn zk_round_trip_both_transcripts() { zk_round_trip::(num_vars, 1200, b"zk-keccak-rt"); } +#[test] +fn transparent_verify_rejects_zk_opening_proof() { + let num_vars = 4; + let mut rng = ChaCha20Rng::seed_from_u64(1301); + + let prover_setup = DoryScheme::setup_prover(num_vars); + let verifier_setup = DoryScheme::setup_verifier(num_vars); + let poly = Polynomial::::random(num_vars, &mut rng); + let point: Vec = (0..num_vars) + .map(|_| ::random(&mut rng)) + .collect(); + let eval = poly.evaluate(&point); + let (commitment, hint) = + ::commit_zk(poly.evaluations(), &prover_setup); + + let mut pt = Blake2bTranscript::new(b"zk-proof-transparent-verify"); + let (proof, _eval_com, _blind) = + DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt); + + let mut vt = Blake2bTranscript::new(b"zk-proof-transparent-verify"); + let result = DoryScheme::verify(&commitment, &point, eval, &proof, &verifier_setup, &mut vt); + assert!( + result.is_err(), + "transparent verification must reject ZK opening proofs" + ); +} + #[test] fn zk_wrong_commitment_rejected() { let num_vars = 3; @@ -474,7 +505,7 @@ fn zk_combined_commitment_and_hint_verify() { let eval = weighted_poly.evaluate(&point); let mut pt = Blake2bTranscript::new(b"zk-combined"); - let (proof, _eval_com, _blind) = DoryScheme::open_zk( + let (proof, eval_com, _blind) = DoryScheme::open_zk( &weighted_poly, &point, eval, @@ -484,7 +515,7 @@ fn zk_combined_commitment_and_hint_verify() { ); let mut vt = Blake2bTranscript::new(b"zk-combined"); - DoryScheme::verify_zk( + let verified_eval_com = DoryScheme::verify_zk( &combined_commitment, &point, &proof, @@ -492,6 +523,7 @@ fn zk_combined_commitment_and_hint_verify() { &mut vt, ) .expect("combined ZK commitment and hint must verify"); + assert_eq!(verified_eval_com, eval_com); } #[test] diff --git a/crates/jolt-field/src/arkworks/bn254.rs b/crates/jolt-field/src/arkworks/bn254.rs index df579d70ad..d31096ca7f 100644 --- a/crates/jolt-field/src/arkworks/bn254.rs +++ b/crates/jolt-field/src/arkworks/bn254.rs @@ -358,7 +358,27 @@ impl ReducingBytes for Fr { impl TranscriptChallenge for Fr { #[inline] fn from_challenge_bytes(bytes: &[u8]) -> Self { - Fr::from_le_bytes_mod_order(bytes) + let mut buf = [0u8; 16]; + let len = bytes.len().min(buf.len()); + buf[..len].copy_from_slice(&bytes[..len]); + let value = u128::from_le_bytes(buf); + let low = value as u64; + // Top 3 bits of high limb are zeroed to ensure value < BN254 modulus. + let high = ((value >> 64) as u64) & (u64::MAX >> 3); + let Some(inner) = InnerFr::from_bigint_unchecked(ark_ff::BigInt::new([0, 0, low, high])) + else { + unreachable!("masked 125-bit shifted challenge fits in BN254 Fr") + }; + Fr(inner) + } + + #[inline] + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + let mut buf = bytes.to_vec(); + // Scalar challenges match the legacy transcript convention: digest bytes + // are interpreted as a big-endian integer before reduction. + buf.reverse(); + Fr::from_le_bytes_mod_order(&buf) } } diff --git a/crates/jolt-field/src/ring_core.rs b/crates/jolt-field/src/ring_core.rs index 5c28b820d5..5a540eb8de 100644 --- a/crates/jolt-field/src/ring_core.rs +++ b/crates/jolt-field/src/ring_core.rs @@ -30,4 +30,23 @@ pub trait RingCore: fn square(&self) -> Self { *self * *self } + + #[inline] + fn pow2(exponent: usize) -> Self { + let mut result = Self::one(); + let mut base = Self::one() + Self::one(); + let mut remaining = exponent; + + while remaining > 0 { + if remaining % 2 == 1 { + result *= base; + } + remaining /= 2; + if remaining > 0 { + base = base.square(); + } + } + + result + } } diff --git a/crates/jolt-field/src/transcript_challenge.rs b/crates/jolt-field/src/transcript_challenge.rs index 9fd5a4d2e8..4fcdc15e8a 100644 --- a/crates/jolt-field/src/transcript_challenge.rs +++ b/crates/jolt-field/src/transcript_challenge.rs @@ -4,4 +4,9 @@ pub trait TranscriptChallenge: { /// Constructs a challenge from transcript bytes. fn from_challenge_bytes(bytes: &[u8]) -> Self; + + /// Constructs a non-optimized scalar challenge from transcript bytes. + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self::from_challenge_bytes(bytes) + } } diff --git a/crates/jolt-hyperkzg/Cargo.toml b/crates/jolt-hyperkzg/Cargo.toml new file mode 100644 index 0000000000..9986c5f33f --- /dev/null +++ b/crates/jolt-hyperkzg/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "jolt-hyperkzg" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "HyperKZG multilinear polynomial commitment scheme for the Jolt zkVM" + +[lints] +workspace = true + +[dependencies] +jolt-crypto = { path = "../jolt-crypto" } +jolt-field = { path = "../jolt-field" } +jolt-poly = { path = "../jolt-poly" } +jolt-transcript = { path = "../jolt-transcript" } +jolt-openings = { path = "../jolt-openings" } +serde = { workspace = true, features = ["derive"] } +tracing.workspace = true +num-traits = { workspace = true } +rayon = { workspace = true } +thiserror = { workspace = true } +rand_core = { workspace = true, features = ["getrandom"] } +rand_chacha = { workspace = true } + +[dev-dependencies] +jolt-field = { path = "../jolt-field", features = ["bn254"] } +criterion = { workspace = true } +rand = { workspace = true } + +[[bench]] +name = "hyperkzg" +harness = false + +[package.metadata.cargo-machete] +ignored = ["rand_core", "rand_chacha", "rand"] diff --git a/crates/jolt-hyperkzg/README.md b/crates/jolt-hyperkzg/README.md new file mode 100644 index 0000000000..a71a555d61 --- /dev/null +++ b/crates/jolt-hyperkzg/README.md @@ -0,0 +1,49 @@ +# jolt-hyperkzg + +HyperKZG multilinear polynomial commitment scheme for the Jolt zkVM. + +Part of the [Jolt](https://github.com/a16z/jolt) zkVM. + +## Overview + +HyperKZG reduces multilinear polynomial commitments to univariate KZG using the Gemini transformation ([section 2.4.2](https://eprint.iacr.org/2022/420.pdf)), operating directly on evaluation-form polynomials (no FFT/interpolation). + +This crate is generic over `PairingGroup` from `jolt-crypto` and implements `CommitmentScheme` and `AdditivelyHomomorphic` from `jolt-openings`. + +### Protocol + +1. **Commit** — MSM of evaluations against SRS G1 powers. +2. **Open** (Gemini reduction) — fold the multilinear polynomial `ℓ-1` times producing intermediate commitments, derive challenge `r`, batch KZG open at `[r, -r, r²]`. +3. **Verify** — evaluation consistency check, then batch KZG pairing check. + +## Public API + +- **`HyperKZGScheme

`** — Main entry point. Implements `CommitmentScheme` and `AdditivelyHomomorphic`. +- **`HyperKZGCommitment

`** — A commitment (G1 point). +- **`HyperKZGProof

`** — Opening proof containing intermediate commitments and evaluations. +- **`HyperKZGProverSetup

`** / **`HyperKZGVerifierSetup

`** — Structured reference strings. + +### Submodules + +- **`kzg`** — Univariate KZG primitives (commit, open, batch verify). +- **`error`** — Error types. + +## Dependency Position + +``` +jolt-field ─┐ +jolt-crypto ─┤ +jolt-poly ─┼─► jolt-hyperkzg +jolt-transcript ─┤ +jolt-openings ─┘ +``` + +Used by `jolt-zkvm`. + +## Feature Flags + +This crate has no feature flags. + +## License + +MIT diff --git a/crates/jolt-hyperkzg/benches/hyperkzg.rs b/crates/jolt-hyperkzg/benches/hyperkzg.rs new file mode 100644 index 0000000000..27c587e741 --- /dev/null +++ b/crates/jolt-hyperkzg/benches/hyperkzg.rs @@ -0,0 +1,178 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +use jolt_crypto::Bn254; +use jolt_field::{Fr, RandomSampling}; +use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; +use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; +use jolt_poly::Polynomial; +use jolt_transcript::Transcript; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; + +type TestScheme = HyperKZGScheme; + +fn make_setup(max_degree: usize) -> (HyperKZGProverSetup, HyperKZGVerifierSetup) { + let mut rng = ChaCha20Rng::seed_from_u64(0xbe0c); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let pk = TestScheme::setup(&mut rng, max_degree, g1, g2); + let vk = TestScheme::verifier_setup(&pk); + (pk, vk) +} + +fn bench_commit(c: &mut Criterion) { + let mut group = c.benchmark_group("hyperkzg_commit"); + for num_vars in [8, 10, 12, 14] { + let n = 1 << num_vars; + let (pk, _) = make_setup(n); + let _ = group.bench_with_input( + BenchmarkId::from_parameter(num_vars), + &num_vars, + |b, &nv| { + b.iter_batched( + || { + let mut rng = ChaCha20Rng::seed_from_u64(0); + Polynomial::::random(nv, &mut rng) + }, + |poly| TestScheme::commit(poly.evaluations(), &pk), + criterion::BatchSize::SmallInput, + ); + }, + ); + } + group.finish(); +} + +fn bench_open(c: &mut Criterion) { + let mut group = c.benchmark_group("hyperkzg_open"); + for num_vars in [8, 10, 12, 14] { + let n = 1 << num_vars; + let (pk, _) = make_setup(n); + let _ = group.bench_with_input( + BenchmarkId::from_parameter(num_vars), + &num_vars, + |b, &nv| { + b.iter_batched( + || { + let mut rng = ChaCha20Rng::seed_from_u64(0); + let poly = Polynomial::::random(nv, &mut rng); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + (poly, point, eval) + }, + |(poly, point, eval)| { + let mut transcript = jolt_transcript::Blake2bTranscript::new(b"bench-open"); + ::open( + &poly, + &point, + eval, + &pk, + None, + &mut transcript, + ) + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + } + group.finish(); +} + +fn bench_verify(c: &mut Criterion) { + let mut group = c.benchmark_group("hyperkzg_verify"); + for num_vars in [8, 10, 12, 14] { + let n = 1 << num_vars; + let (pk, vk) = make_setup(n); + let _ = group.bench_with_input( + BenchmarkId::from_parameter(num_vars), + &num_vars, + |b, &nv| { + b.iter_batched( + || { + let mut rng = ChaCha20Rng::seed_from_u64(0); + let poly = Polynomial::::random(nv, &mut rng); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + let mut transcript = + jolt_transcript::Blake2bTranscript::new(b"bench-verify"); + let proof = ::open( + &poly, + &point, + eval, + &pk, + None, + &mut transcript, + ); + (commitment, point, eval, proof) + }, + |(commitment, point, eval, proof)| { + let mut transcript = + jolt_transcript::Blake2bTranscript::new(b"bench-verify"); + ::verify( + &commitment, + &point, + eval, + &proof, + &vk, + &mut transcript, + ) + }, + criterion::BatchSize::SmallInput, + ); + }, + ); + } + group.finish(); +} + +fn bench_combine(c: &mut Criterion) { + let mut group = c.benchmark_group("hyperkzg_combine"); + for count in [2, 4, 8, 16] { + let num_vars = 10; + let n = 1 << num_vars; + let (pk, _) = make_setup(n); + let mut rng = ChaCha20Rng::seed_from_u64(0); + + let commitments: Vec<_> = (0..count) + .map(|_| { + let poly = Polynomial::::random(num_vars, &mut rng); + let (c, ()) = TestScheme::commit(poly.evaluations(), &pk); + c + }) + .collect(); + let scalars: Vec = (0..count).map(|_| Fr::random(&mut rng)).collect(); + + let _ = group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| { + b.iter(|| TestScheme::combine(&commitments, &scalars)); + }); + } + group.finish(); +} + +fn bench_setup(c: &mut Criterion) { + let mut group = c.benchmark_group("hyperkzg_setup"); + for num_vars in [8, 10, 12] { + let n = 1 << num_vars; + let _ = group.bench_with_input(BenchmarkId::from_parameter(num_vars), &num_vars, |b, _| { + b.iter(|| { + let mut rng = ChaCha20Rng::seed_from_u64(0xbe0c); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + TestScheme::setup(&mut rng, n, g1, g2) + }); + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_setup, + bench_commit, + bench_open, + bench_verify, + bench_combine, +); +criterion_main!(benches); diff --git a/crates/jolt-hyperkzg/fuzz/Cargo.toml b/crates/jolt-hyperkzg/fuzz/Cargo.toml new file mode 100644 index 0000000000..dc2b94ca42 --- /dev/null +++ b/crates/jolt-hyperkzg/fuzz/Cargo.toml @@ -0,0 +1,36 @@ +[workspace] + +[package] +name = "jolt-hyperkzg-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +jolt-hyperkzg = { path = ".." } +jolt-crypto = { path = "../../jolt-crypto" } +jolt-field = { path = "../../jolt-field", features = ["bn254"] } +jolt-openings = { path = "../../jolt-openings" } +jolt-poly = { path = "../../jolt-poly" } +jolt-transcript = { path = "../../jolt-transcript" } +rand_chacha = "0.3" +rand_core = "0.6" + +[[bin]] +name = "commit_open_verify" +path = "fuzz_targets/commit_open_verify.rs" +doc = false + +[[bin]] +name = "tampered_proof" +path = "fuzz_targets/tampered_proof.rs" +doc = false + +[[bin]] +name = "wrong_eval" +path = "fuzz_targets/wrong_eval.rs" +doc = false diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs new file mode 100644 index 0000000000..51c8079b54 --- /dev/null +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/commit_open_verify.rs @@ -0,0 +1,45 @@ +#![no_main] + +//! Fuzz: random polynomial + random point must always commit-open-verify successfully. + +use jolt_crypto::Bn254; +use jolt_field::{Field, Fr}; +use jolt_hyperkzg::HyperKZGScheme; +use jolt_openings::CommitmentScheme; +use jolt_poly::Polynomial; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use libfuzzer_sys::fuzz_target; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; + +type TestScheme = HyperKZGScheme; + +fuzz_target!(|data: &[u8]| { + if data.len() < 8 { + return; + } + + let seed = u64::from_le_bytes(data[..8].try_into().unwrap()); + let num_vars = (data.len() % 4) + 1; // 1..=4 variables + let n = 1usize << num_vars; + + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let pk = TestScheme::setup(&mut rng, n, g1, g2); + let vk = TestScheme::verifier_setup(&pk); + + let poly = Polynomial::::random(num_vars, &mut rng); + let point: Vec = (0..num_vars).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut pt = Blake2bTranscript::new(b"fuzz"); + let proof = + ::open(&poly, &point, eval, &pk, None, &mut pt); + + let mut vt = Blake2bTranscript::new(b"fuzz"); + ::verify(&commitment, &point, eval, &proof, &vk, &mut vt) + .expect("valid proof must verify"); +}); diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs new file mode 100644 index 0000000000..1363e2487c --- /dev/null +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/tampered_proof.rs @@ -0,0 +1,90 @@ +#![no_main] + +//! Fuzz: tamper with proof fields and verify that verification rejects. +//! +//! We generate a valid proof, then corrupt either an evaluation entry, +//! an intermediate commitment, or a witness commitment using fuzzer-chosen bytes. + +use jolt_crypto::{Bn254, JoltGroup}; +use jolt_field::{Field, Fr}; +use jolt_hyperkzg::HyperKZGScheme; +use jolt_openings::CommitmentScheme; +use jolt_poly::Polynomial; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use libfuzzer_sys::fuzz_target; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; + +type TestScheme = HyperKZGScheme; + +fuzz_target!(|data: &[u8]| { + if data.len() < 10 { + return; + } + + let num_vars = 3; + let n = 1usize << num_vars; + + let mut rng = ChaCha20Rng::seed_from_u64(0xfade); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let pk = TestScheme::setup(&mut rng, n, g1, g2); + let vk = TestScheme::verifier_setup(&pk); + + let poly = Polynomial::::random(num_vars, &mut rng); + let point: Vec = (0..num_vars).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut pt = Blake2bTranscript::new(b"fuzz-tamper"); + let proof = + ::open(&poly, &point, eval, &pk, None, &mut pt); + + let mut tampered = proof.clone(); + + match data[0] % 3 { + 0 => { + // Tamper evaluation entries (exercises folding consistency checks) + let tamper_row = (data[1] as usize) % tampered.v.len(); + let tamper_col = (data[2] as usize) % tampered.v[tamper_row].len(); + let tamper_val = Fr::from_bytes(&data[3..]); + if tamper_val == proof.v[tamper_row][tamper_col] { + return; + } + tampered.v[tamper_row][tamper_col] = tamper_val; + } + 1 => { + // Tamper intermediate commitments (exercises pairing check) + if tampered.com.is_empty() { + return; + } + let idx = (data[1] as usize) % tampered.com.len(); + let scalar = Fr::from_bytes(&data[2..]); + tampered.com[idx] = tampered.com[idx].scalar_mul(&scalar); + if tampered.com[idx] == proof.com[idx] { + return; + } + } + _ => { + // Tamper witness commitments (exercises pairing check) + let idx = (data[1] as usize) % tampered.w.len(); + let scalar = Fr::from_bytes(&data[2..]); + tampered.w[idx] = tampered.w[idx].scalar_mul(&scalar); + if tampered.w[idx] == proof.w[idx] { + return; + } + } + } + + let mut vt = Blake2bTranscript::new(b"fuzz-tamper"); + let result = ::verify( + &commitment, + &point, + eval, + &tampered, + &vk, + &mut vt, + ); + assert!(result.is_err(), "tampered proof must be rejected"); +}); diff --git a/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs b/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs new file mode 100644 index 0000000000..4c15d1af2f --- /dev/null +++ b/crates/jolt-hyperkzg/fuzz/fuzz_targets/wrong_eval.rs @@ -0,0 +1,59 @@ +#![no_main] + +//! Fuzz: claim a wrong evaluation and verify that verification rejects. +//! +//! The prover generates a valid proof for the correct evaluation. The +//! verifier checks against a fuzzer-derived wrong evaluation. Must reject. + +use jolt_crypto::Bn254; +use jolt_field::{Field, Fr}; +use jolt_hyperkzg::HyperKZGScheme; +use jolt_openings::CommitmentScheme; +use jolt_poly::Polynomial; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use libfuzzer_sys::fuzz_target; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; + +type TestScheme = HyperKZGScheme; + +fuzz_target!(|data: &[u8]| { + if data.len() < 32 { + return; + } + + let num_vars = 3; + let n = 1usize << num_vars; + + let mut rng = ChaCha20Rng::seed_from_u64(0xface); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let pk = TestScheme::setup(&mut rng, n, g1, g2); + let vk = TestScheme::verifier_setup(&pk); + + let poly = Polynomial::::random(num_vars, &mut rng); + let point: Vec = (0..num_vars).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let wrong_eval = Fr::from_bytes(data); + if wrong_eval == eval { + return; + } + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut pt = Blake2bTranscript::new(b"fuzz-wrong-eval"); + let proof = + ::open(&poly, &point, eval, &pk, None, &mut pt); + + let mut vt = Blake2bTranscript::new(b"fuzz-wrong-eval"); + let result = ::verify( + &commitment, + &point, + wrong_eval, + &proof, + &vk, + &mut vt, + ); + assert!(result.is_err(), "wrong evaluation must be rejected"); +}); diff --git a/crates/jolt-hyperkzg/fuzz/rust-toolchain.toml b/crates/jolt-hyperkzg/fuzz/rust-toolchain.toml new file mode 100644 index 0000000000..5d56faf9ae --- /dev/null +++ b/crates/jolt-hyperkzg/fuzz/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/crates/jolt-hyperkzg/src/error.rs b/crates/jolt-hyperkzg/src/error.rs new file mode 100644 index 0000000000..bc6df8af9c --- /dev/null +++ b/crates/jolt-hyperkzg/src/error.rs @@ -0,0 +1,26 @@ +//! Error types for HyperKZG operations. + +/// Errors produced by the HyperKZG commitment scheme. +#[derive(Debug, thiserror::Error)] +pub enum HyperKZGError { + #[error("SRS too small: have {have} powers, need {need}")] + SrsTooSmall { have: usize, need: usize }, + + #[error("expected {expected} intermediate commitments, got {got}")] + WrongCommitmentCount { expected: usize, got: usize }, + + #[error("each evaluation row must have {expected} entries")] + WrongEvaluationWidth { expected: usize }, + + #[error("polynomial must have at least 1 variable")] + EmptyPoint, + + #[error("folding consistency check failed at level {level}")] + FoldingConsistencyFailed { level: usize }, + + #[error("batch KZG pairing check failed")] + PairingCheckFailed, + + #[error("degenerate Fiat-Shamir challenge: r = 0")] + DegenerateChallenge, +} diff --git a/crates/jolt-hyperkzg/src/kzg.rs b/crates/jolt-hyperkzg/src/kzg.rs new file mode 100644 index 0000000000..00dcff0ea1 --- /dev/null +++ b/crates/jolt-hyperkzg/src/kzg.rs @@ -0,0 +1,257 @@ +//! Univariate KZG primitives: commit, witness polynomial, batch open/verify. +//! +//! These are the building blocks consumed by the HyperKZG protocol. +//! All operations are generic over `P: PairingGroup`. + +use jolt_crypto::{JoltGroup, PairingGroup}; +use jolt_field::Field; +use jolt_transcript::{AppendToTranscript, Transcript}; +use num_traits::{One, Zero}; + +use crate::error::HyperKZGError; +use crate::types::{HyperKZGProverSetup, HyperKZGVerifierSetup}; + +/// Commits to a polynomial (given as evaluation/coefficient vector) using MSM against SRS G1 powers. +pub(crate) fn kzg_commit( + coeffs: &[P::ScalarField], + setup: &HyperKZGProverSetup

, +) -> Result { + if setup.g1_powers.len() < coeffs.len() { + return Err(HyperKZGError::SrsTooSmall { + have: setup.g1_powers.len(), + need: coeffs.len(), + }); + } + Ok(P::G1::msm(&setup.g1_powers[..coeffs.len()], coeffs)) +} + +/// Computes the KZG witness polynomial `h(x) = f(x) / (x - u)`. +/// +/// Uses Horner's method in reverse: `h[i-1] = f[i] + h[i] * u`. +/// The remainder is `f(u)`, but we don't need it since the verifier +/// can derive it from the evaluation vectors. +pub(crate) fn compute_witness_polynomial(f: &[F], u: F) -> Vec { + let d = f.len(); + if d <= 1 { + return vec![]; + } + let mut h = vec![F::zero(); d - 1]; + h[d - 2] = f[d - 1]; + for i in (1..d - 1).rev() { + h[i - 1] = f[i] + h[i] * u; + } + h +} + +/// Evaluates a polynomial (in evaluation/coefficient form) at a point. +/// +/// Standard Horner evaluation: `f(u) = f[0] + f[1]*u + f[2]*u^2 + ...` +pub(crate) fn eval_univariate(coeffs: &[F], u: F) -> F { + let mut result = F::zero(); + let mut power = F::one(); + for &c in coeffs { + result += c * power; + power *= u; + } + result +} + +/// Batch KZG opening: commits to witness polynomials for each evaluation point. +/// +/// Given polynomials `f[0..k]` and evaluation points `u[0..t]`, computes: +/// - `v[i][j]` = f_j(u_i) for all i, j +/// - Linear combination `B = sum_j q^j * f_j` using Fiat-Shamir challenge +/// - Witness commitments `w[i]` = commit(B(x) / (x - u_i)) +/// +/// Returns `(w, v)`. +pub(crate) fn kzg_open_batch( + f: &[Vec], + u: &[P::ScalarField; 3], + setup: &HyperKZGProverSetup

, + transcript: &mut T, +) -> ([P::G1; 3], [Vec; 3]) +where + P: PairingGroup, + T: Transcript, + P::ScalarField: AppendToTranscript, + P::G1: AppendToTranscript, +{ + let k = f.len(); + + // Compute evaluations v[t][j] = f_j(u_t) + let v: [Vec; 3] = + (*u).map(|ui| f.iter().map(|fj| eval_univariate(fj, ui)).collect()); + + // Absorb all evaluations into transcript + for row in &v { + for val in row { + transcript.append(val); + } + } + + // Derive batching challenge and compute powers q, q^2, ..., q^{k-1} + let q: P::ScalarField = transcript.challenge(); + let q_powers = challenge_powers(q, k); + + // B(x) = sum_j q^j * f_j(x) + let poly_len = f[0].len(); + let mut b_poly = vec![P::ScalarField::zero(); poly_len]; + for (fj, &qj) in f.iter().zip(q_powers.iter()) { + for (b, &c) in b_poly.iter_mut().zip(fj.iter()) { + *b += qj * c; + } + } + + // Compute witness polynomials and commit + let w: [P::G1; 3] = (*u).map(|ui| { + let h = compute_witness_polynomial::(&b_poly, ui); + P::G1::msm(&setup.g1_powers[..h.len()], &h) + }); + + // Absorb witness commitments and mirror the verifier's `d_0` challenge + // to keep prover/verifier transcripts in sync. + for wi in &w { + transcript.append(wi); + } + let _d_0: P::ScalarField = transcript.challenge(); + + (w, v) +} + +/// Batch KZG verification: checks that commitments open correctly at all points. +/// +/// Optimized for the t=3 case used by HyperKZG. The pairing check verifies: +/// `e(L, g2) == e(R, beta_g2)` +pub(crate) fn kzg_verify_batch( + vk: &HyperKZGVerifierSetup

, + com: &[P::G1], + wit: &[P::G1; 3], + u: &[P::ScalarField; 3], + v: &[Vec; 3], + transcript: &mut T, +) -> bool +where + P: PairingGroup, + T: Transcript, + P::ScalarField: AppendToTranscript, + P::G1: AppendToTranscript, +{ + let k = com.len(); + + if v.iter().any(|row| row.len() != k) { + return false; + } + + // Absorb evaluations + for row in v { + for val in row { + transcript.append(val); + } + } + + let q: P::ScalarField = transcript.challenge(); + let q_powers = challenge_powers(q, k); + + // Absorb witness commitments + for wi in wit { + transcript.append(wi); + } + let d_0: P::ScalarField = transcript.challenge(); + let d_1 = d_0 * d_0; + + // q_power_multiplier = 1 + d_0 + d_1 + let q_power_multiplier = P::ScalarField::one() + d_0 + d_1; + let q_powers_multiplied: Vec = + q_powers.iter().map(|qp| *qp * q_power_multiplier).collect(); + + // B(u_i) = sum_j q^j * v[i][j] + let b_u: Vec = v + .iter() + .map(|v_i| { + v_i.iter() + .zip(q_powers.iter()) + .map(|(&a, &b)| a * b) + .fold(P::ScalarField::zero(), |acc, x| acc + x) + }) + .collect(); + + // L = MSM over [C_0..C_{k-1}, W_0, W_1, W_2, g1] with scalars + // [q_powers_multiplied, u_0, u_1*d_0, u_2*d_1, -(b_u[0] + d_0*b_u[1] + d_1*b_u[2])] + let mut bases = Vec::with_capacity(k + 4); + bases.extend_from_slice(&com[..k]); + bases.push(wit[0]); + bases.push(wit[1]); + bases.push(wit[2]); + bases.push(vk.g1); + + let mut scalars = Vec::with_capacity(k + 4); + scalars.extend_from_slice(&q_powers_multiplied[..k]); + scalars.push(u[0]); + scalars.push(u[1] * d_0); + scalars.push(u[2] * d_1); + scalars.push(-(b_u[0] + d_0 * b_u[1] + d_1 * b_u[2])); + + let lhs = P::G1::msm(&bases, &scalars); + + // R = W[0] + d_0*W[1] + d_1*W[2] + let rhs = wit[0] + wit[1].scalar_mul(&d_0) + wit[2].scalar_mul(&d_1); + + // e(L, g2) * e(-R, beta_g2) == identity + let result = P::multi_pairing(&[lhs, -rhs], &[vk.g2, vk.beta_g2]); + result.is_identity() +} + +/// Computes `[1, c, c^2, ..., c^{n-1}]`. +pub(crate) fn challenge_powers(c: F, n: usize) -> Vec { + let mut powers = Vec::with_capacity(n); + let mut cur = F::one(); + for _ in 0..n { + powers.push(cur); + cur *= c; + } + powers +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + use num_traits::Zero; + + #[test] + fn witness_polynomial_division() { + // f(x) = 1 + 2x + 3x^2 + 4x^3 + // f(2) = 1 + 4 + 12 + 32 = 49 + // h(x) = f(x)/(x-2), so f(x) = (x-2)*h(x) + f(2) + let f = vec![ + Fr::from_u64(1), + Fr::from_u64(2), + Fr::from_u64(3), + Fr::from_u64(4), + ]; + let u = Fr::from_u64(2); + let h = compute_witness_polynomial::(&f, u); + + // Verify: (x-u)*h(x) + f(u) should reconstruct f(x) + for x_val in [0u64, 1, 3, 5, 100] { + let x = Fr::from_u64(x_val); + let fx = eval_univariate(&f, x); + let hx = eval_univariate(&h, x); + let fu = eval_univariate(&f, u); + assert_eq!(fx, (x - u) * hx + fu); + } + } + + #[test] + fn eval_univariate_at_zero() { + let f = vec![Fr::from_u64(42), Fr::from_u64(7), Fr::from_u64(3)]; + assert_eq!(eval_univariate(&f, Fr::zero()), Fr::from_u64(42)); + } + + #[test] + fn eval_univariate_linear() { + // f(x) = 3 + 5x, f(2) = 13 + let f = vec![Fr::from_u64(3), Fr::from_u64(5)]; + assert_eq!(eval_univariate(&f, Fr::from_u64(2)), Fr::from_u64(13)); + } +} diff --git a/crates/jolt-hyperkzg/src/lib.rs b/crates/jolt-hyperkzg/src/lib.rs new file mode 100644 index 0000000000..532f02d01e --- /dev/null +++ b/crates/jolt-hyperkzg/src/lib.rs @@ -0,0 +1,28 @@ +//! HyperKZG multilinear polynomial commitment scheme. +//! +//! HyperKZG reduces multilinear polynomial commitments to univariate KZG using +//! the Gemini transformation (section 2.4.2 of ), +//! operating directly on evaluation-form polynomials (no FFT/interpolation). +//! +//! This crate is generic over `PairingGroup` from `jolt-crypto` and implements +//! the `CommitmentScheme` and `AdditivelyHomomorphic` traits from `jolt-openings`. +//! +//! # Protocol overview +//! +//! 1. **Commit**: MSM of evaluations against SRS G1 powers (treating the +//! multilinear evaluation table as univariate coefficients). +//! 2. **Open** (Gemini reduction): +//! - Phase 1: Fold the multilinear polynomial `ell - 1` times, producing +//! intermediate polynomial commitments. +//! - Phase 2: Derive challenge `r` and evaluation points `[r, -r, r^2]`. +//! - Phase 3: Batch KZG opening of all intermediate polynomials at three points. +//! 3. **Verify**: Check evaluation consistency across the three evaluation vectors, +//! then batch KZG pairing check. + +pub mod error; +pub mod kzg; +pub mod scheme; +pub mod types; + +pub use scheme::HyperKZGScheme; +pub use types::{HyperKZGCommitment, HyperKZGProof, HyperKZGProverSetup, HyperKZGVerifierSetup}; diff --git a/crates/jolt-hyperkzg/src/scheme.rs b/crates/jolt-hyperkzg/src/scheme.rs new file mode 100644 index 0000000000..426038f4a5 --- /dev/null +++ b/crates/jolt-hyperkzg/src/scheme.rs @@ -0,0 +1,668 @@ +//! HyperKZG commitment scheme implementing `jolt-openings` traits. +//! +//! [`HyperKZGScheme`] is generic over `P: PairingGroup` — instantiate with +//! `Bn254` for the concrete BN254 curve. + +#![expect( + clippy::expect_used, + reason = "KZG operations return Result for API symmetry; with a correctly-sized SRS and well-formed inputs these errors are unreachable" +)] + +use std::marker::PhantomData; + +use jolt_crypto::{Commitment, DeriveSetup, JoltGroup, PairingGroup, PedersenSetup}; +use jolt_field::{FromPrimitiveInt, RandomSampling}; +use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError}; +use jolt_poly::Polynomial; +use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; +use num_traits::{One, Zero}; +use rayon::prelude::*; + +use crate::error::HyperKZGError; +use crate::kzg::{self, kzg_open_batch, kzg_verify_batch}; +use crate::types::{HyperKZGCommitment, HyperKZGProof, HyperKZGProverSetup, HyperKZGVerifierSetup}; + +/// HyperKZG multilinear polynomial commitment scheme. +/// +/// Generic over `P: PairingGroup`. Implements [`CommitmentScheme`] and +/// [`AdditivelyHomomorphic`] from `jolt-openings`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HyperKZGScheme { + _phantom: PhantomData

, +} + +impl HyperKZGScheme

+where + P::ScalarField: AppendToTranscript, + P::G1: AppendToTranscript, +{ + /// Generates an SRS from a random generator and secret scalar. + /// + /// `max_degree` is the maximum polynomial length (number of evaluations). + /// The SRS will contain `max_degree + 1` G1 powers and 2 G2 powers. + pub fn setup( + rng: &mut R, + max_degree: usize, + g1: P::G1, + g2: P::G2, + ) -> HyperKZGProverSetup

{ + let beta = P::ScalarField::random(rng); + Self::setup_from_secret(beta, max_degree, g1, g2) + } + + /// Generates SRS from a known secret. + /// + /// WARNING: this is only appropriate for deterministic tests or trusted + /// setup tooling that destroys `beta`; anyone who knows `beta` can break + /// KZG binding. + pub fn setup_from_secret( + beta: P::ScalarField, + max_degree: usize, + g1: P::G1, + g2: P::G2, + ) -> HyperKZGProverSetup

{ + let mut g1_powers = Vec::with_capacity(max_degree + 1); + let mut cur = g1; + for _ in 0..=max_degree { + g1_powers.push(cur); + cur = cur.scalar_mul(&beta); + } + + let g2_powers = vec![g2, g2.scalar_mul(&beta)]; + + HyperKZGProverSetup { + g1_powers, + g2_powers, + } + } + + /// Phase 1 of the HyperKZG protocol: fold the multilinear polynomial. + /// + /// Given polynomial $P$ with $2^\ell$ evaluations and opening point + /// $x = (x_1, \ldots, x_\ell)$, produces $\ell$ polynomials + /// $P_0 = P, P_1, \ldots, P_{\ell-1}$ where each $P_i$ has half + /// the length of $P_{i-1}$. + /// + /// The folding relation is: + /// $P_i[j] = (1 - x_{\ell-i}) \cdot P_{i-1}[2j] + x_{\ell-i} \cdot P_{i-1}[2j+1]$ + fn fold_polynomials( + evals: &[P::ScalarField], + point: &[P::ScalarField], + ) -> Vec> { + let ell = point.len(); + let mut polys = Vec::with_capacity(ell); + polys.push(evals.to_vec()); + + for i in 0..ell - 1 { + let prev = &polys[i]; + let half = prev.len() / 2; + let xi = point[ell - i - 1]; + let mut pi = vec![P::ScalarField::zero(); half]; + pi.par_iter_mut().enumerate().for_each(|(j, pj)| { + *pj = prev[2 * j] + xi * (prev[2 * j + 1] - prev[2 * j]); + }); + polys.push(pi); + } + + polys + } + + /// Full HyperKZG opening proof. + #[tracing::instrument(skip_all, name = "HyperKZG::open")] + pub fn open>( + setup: &HyperKZGProverSetup

, + evals: &[P::ScalarField], + point: &[P::ScalarField], + transcript: &mut T, + ) -> Result, HyperKZGError> { + let ell = point.len(); + if ell == 0 { + return Err(HyperKZGError::EmptyPoint); + } + let n = evals.len(); + assert_eq!(n, 1 << ell, "evaluation count must be 2^ell"); + + // Phase 1: fold + let polys = Self::fold_polynomials(evals, point); + assert_eq!(polys.len(), ell); + assert_eq!(polys[ell - 1].len(), 2); + + // Commit to intermediate polynomials (skip polys[0] — already committed) + let com: Vec = polys[1..] + .par_iter() + .map(|p| kzg::kzg_commit::

(p, setup).expect("SRS large enough for intermediate")) + .collect(); + + // Phase 2: derive challenge r + for c in &com { + transcript.append(c); + } + let r: P::ScalarField = transcript.challenge(); + let u = [r, -r, r * r]; + + // Phase 3: batch open all polynomials at the three points + let (w, v) = kzg_open_batch::(&polys, &u, setup, transcript); + + Ok(HyperKZGProof { com, w, v }) + } + + /// HyperKZG verification. + #[tracing::instrument(skip_all, name = "HyperKZG::verify")] + pub fn verify>( + vk: &HyperKZGVerifierSetup

, + commitment: &HyperKZGCommitment

, + point: &[P::ScalarField], + claimed_eval: &P::ScalarField, + proof: &HyperKZGProof

, + transcript: &mut T, + ) -> Result<(), HyperKZGError> { + let ell = point.len(); + if ell == 0 { + return Err(HyperKZGError::EmptyPoint); + } + + if proof.com.len() + 1 != ell { + return Err(HyperKZGError::WrongCommitmentCount { + expected: ell - 1, + got: proof.com.len(), + }); + } + + // Validate inner evaluation widths before mutating the transcript. + let v = &proof.v; + if v[0].len() != ell || v[1].len() != ell || v[2].len() != ell { + return Err(HyperKZGError::WrongEvaluationWidth { expected: ell }); + } + + // Absorb intermediate commitments + for c in &proof.com { + transcript.append(c); + } + let r: P::ScalarField = transcript.challenge(); + + if r.is_zero() { + return Err(HyperKZGError::DegenerateChallenge); + } + + // Prepend the original commitment as C_0 + let mut com = Vec::with_capacity(ell); + com.push(commitment.point); + com.extend_from_slice(&proof.com); + + let u = [r, -r, r * r]; + + let ypos = &v[0]; // evaluations at r + let yneg = &v[1]; // evaluations at -r + let mut y_sq = v[2].clone(); // evaluations at r^2 + y_sq.push(*claimed_eval); + + // Consistency check: the folding relation must hold across evaluations + // + // For each level i, the polynomial P_i is defined by: + // P_i(x) = (1 - x_{ell-i}) * P_{i-1,even}(x) + x_{ell-i} * P_{i-1,odd}(x) + // + // This implies: + // 2*r * P_{i+1}(r^2) = r * (1 - x_{ell-i-1}) * (P_i(r) + P_i(-r)) + // + x_{ell-i-1} * (P_i(r) - P_i(-r)) + let two = P::ScalarField::from_u64(2); + for i in 0..ell { + let lhs = two * r * y_sq[i + 1]; + let rhs = r * (P::ScalarField::one() - point[ell - i - 1]) * (ypos[i] + yneg[i]) + + point[ell - i - 1] * (ypos[i] - yneg[i]); + if lhs != rhs { + return Err(HyperKZGError::FoldingConsistencyFailed { level: i }); + } + } + + // Batch KZG pairing check + if !kzg_verify_batch::(vk, &com, &proof.w, &u, &proof.v, transcript) { + return Err(HyperKZGError::PairingCheckFailed); + } + + Ok(()) + } +} + +/// # Security note +/// +/// Uses KZG SRS powers as Pedersen generators — Pedersen binding shares the +/// KZG trapdoor `beta`. Both are sound once `beta` is destroyed, but the two +/// schemes do not have independent security assumptions. +impl DeriveSetup> for PedersenSetup { + fn derive(source: &HyperKZGProverSetup

, capacity: usize) -> Self { + assert!( + source.g1_powers.len() > capacity, + "SRS has {} G1 powers, need at least {} (capacity + 1 for blinding)", + source.g1_powers.len(), + capacity + 1, + ); + let message_generators = source.g1_powers[..capacity].to_vec(); + let blinding_generator = source.g1_powers[capacity]; + PedersenSetup::new(message_generators, blinding_generator) + } +} + +impl Commitment for HyperKZGScheme

{ + type Output = HyperKZGCommitment

; +} + +impl CommitmentScheme for HyperKZGScheme

+where + P::ScalarField: AppendToTranscript, + P::G1: AppendToTranscript, +{ + type Field = P::ScalarField; + type Proof = HyperKZGProof

; + type ProverSetup = HyperKZGProverSetup

; + type VerifierSetup = HyperKZGVerifierSetup

; + type Polynomial = Polynomial; + type OpeningHint = (); + type SetupParams = (usize, P::G1, P::G2); + + fn setup( + (max_num_vars, g1, g2): Self::SetupParams, + ) -> (Self::ProverSetup, Self::VerifierSetup) { + let mut rng = rand_core::OsRng; + let max_degree = 1usize << max_num_vars; + let prover = HyperKZGScheme::setup(&mut rng, max_degree, g1, g2); + let verifier = Self::verifier_setup(&prover); + (prover, verifier) + } + + fn verifier_setup(prover_setup: &Self::ProverSetup) -> Self::VerifierSetup { + HyperKZGVerifierSetup::from(prover_setup) + } + + fn commit + ?Sized>( + poly: &S, + setup: &Self::ProverSetup, + ) -> (Self::Output, Self::OpeningHint) { + // HyperKZG always works on dense evaluations. + let mut evaluations = Vec::with_capacity(1 << poly.num_vars()); + poly.for_each_row(poly.num_vars(), &mut |_, row| { + evaluations.extend_from_slice(row); + }); + let point = kzg::kzg_commit::

(&evaluations, setup) + .expect("SRS must be large enough for the polynomial"); + (HyperKZGCommitment { point }, ()) + } + + fn open( + poly: &Self::Polynomial, + point: &[Self::Field], + _eval: Self::Field, + setup: &Self::ProverSetup, + _hint: Option, + transcript: &mut impl Transcript, + ) -> Self::Proof { + Self::open(setup, poly.evaluations(), point, transcript) + .expect("HyperKZG open should not fail with valid inputs") + } + + fn verify( + commitment: &Self::Output, + point: &[Self::Field], + eval: Self::Field, + proof: &Self::Proof, + setup: &Self::VerifierSetup, + transcript: &mut impl Transcript, + ) -> Result<(), OpeningsError> { + Self::verify(setup, commitment, point, &eval, proof, transcript) + .map_err(|_| OpeningsError::VerificationFailed) + } + + fn bind_opening_inputs( + transcript: &mut impl Transcript, + point: &[Self::Field], + eval: &Self::Field, + ) { + transcript.append(&LabelWithCount( + b"hyperkzg_opening_point", + point.len() as u64, + )); + for p in point { + p.append_to_transcript(transcript); + } + transcript.append(&Label(b"hyperkzg_opening_eval")); + eval.append_to_transcript(transcript); + } +} + +impl AdditivelyHomomorphic for HyperKZGScheme

+where + P::ScalarField: AppendToTranscript, + P::G1: AppendToTranscript, +{ + fn combine(commitments: &[Self::Output], scalars: &[Self::Field]) -> Self::Output { + assert_eq!(commitments.len(), scalars.len()); + let bases: Vec = commitments.iter().map(|c| c.point).collect(); + HyperKZGCommitment { + point: P::G1::msm(&bases, scalars), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jolt_crypto::Bn254; + use jolt_field::Fr; + use jolt_poly::Polynomial; + use jolt_transcript::Blake2bTranscript; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + type TestScheme = HyperKZGScheme; + + fn test_setup(max_degree: usize) -> (HyperKZGProverSetup, HyperKZGVerifierSetup) { + let mut rng = ChaCha20Rng::seed_from_u64(0xdead_beef); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let prover = TestScheme::setup(&mut rng, max_degree, g1, g2); + let verifier = TestScheme::verifier_setup(&prover); + (prover, verifier) + } + + #[test] + fn commit_open_verify_roundtrip() { + for ell in [2, 3, 4, 6, 8] { + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(ell as u64); + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut prover_transcript = Blake2bTranscript::new(b"test"); + let proof = ::open( + &poly, + &point, + eval, + &pk, + None, + &mut prover_transcript, + ); + + let mut verifier_transcript = Blake2bTranscript::new(b"test"); + let result = ::verify( + &commitment, + &point, + eval, + &proof, + &vk, + &mut verifier_transcript, + ); + assert!(result.is_ok(), "ell={ell}: verification failed: {result:?}"); + } + } + + #[test] + fn wrong_eval_rejects() { + let ell = 4; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(42); + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + let wrong_eval = eval + Fr::from_u64(1); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut prover_transcript = Blake2bTranscript::new(b"test-bad"); + let proof = ::open( + &poly, + &point, + eval, + &pk, + None, + &mut prover_transcript, + ); + + let mut verifier_transcript = Blake2bTranscript::new(b"test-bad"); + let result = ::verify( + &commitment, + &point, + wrong_eval, + &proof, + &vk, + &mut verifier_transcript, + ); + assert!(result.is_err(), "wrong evaluation should be rejected"); + } + + #[test] + fn missing_intermediate_commitment_rejects() { + let ell = 4; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(43); + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut prover_transcript = Blake2bTranscript::new(b"test-missing-com"); + let mut proof = ::open( + &poly, + &point, + eval, + &pk, + None, + &mut prover_transcript, + ); + let _ = proof.com.pop(); + + let mut verifier_transcript = Blake2bTranscript::new(b"test-missing-com"); + let result = TestScheme::verify( + &vk, + &commitment, + &point, + &eval, + &proof, + &mut verifier_transcript, + ); + assert!(matches!( + result, + Err(HyperKZGError::WrongCommitmentCount { .. }) + )); + } + + #[test] + fn trait_setup_uses_fresh_randomness() { + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + + let (_pk1, vk1) = ::setup((4, g1, g2)); + let (_pk2, vk2) = ::setup((4, g1, g2)); + + assert_ne!(vk1.beta_g2, vk2.beta_g2); + } + + #[test] + fn tampered_proof_rejects() { + let ell = 4; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(99); + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut prover_transcript = Blake2bTranscript::new(b"test-tamper"); + let mut proof = ::open( + &poly, + &point, + eval, + &pk, + None, + &mut prover_transcript, + ); + + // Tamper with proof: swap v[0] and v[1] + let v1 = proof.v[1].clone(); + proof.v[0].clone_from(&v1); + + let mut verifier_transcript = Blake2bTranscript::new(b"test-tamper"); + let result = ::verify( + &commitment, + &point, + eval, + &proof, + &vk, + &mut verifier_transcript, + ); + assert!(result.is_err(), "tampered proof should be rejected"); + } + + #[test] + fn combine_is_homomorphic() { + let ell = 3; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(300); + let (pk, _vk) = test_setup(n); + + let poly_a = Polynomial::::random(ell, &mut rng); + let poly_b = Polynomial::::random(ell, &mut rng); + + let (ca, ()) = TestScheme::commit(poly_a.evaluations(), &pk); + let (cb, ()) = TestScheme::commit(poly_b.evaluations(), &pk); + + let sum_evals: Vec = poly_a + .evaluations() + .iter() + .zip(poly_b.evaluations().iter()) + .map(|(a, b)| *a + *b) + .collect(); + let (c_sum_direct, ()) = TestScheme::commit(&sum_evals, &pk); + + let c_sum_combined = TestScheme::combine(&[ca, cb], &[Fr::from_u64(1), Fr::from_u64(1)]); + + assert_eq!( + c_sum_direct, c_sum_combined, + "combine([1,1]) must match commitment to sum" + ); + } + + #[test] + fn combine_with_scalars() { + let ell = 3; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(400); + let (pk, _vk) = test_setup(n); + + let poly_a = Polynomial::::random(ell, &mut rng); + let poly_b = Polynomial::::random(ell, &mut rng); + let s_a = Fr::random(&mut rng); + let s_b = Fr::random(&mut rng); + + let (ca, ()) = TestScheme::commit(poly_a.evaluations(), &pk); + let (cb, ()) = TestScheme::commit(poly_b.evaluations(), &pk); + + let combined_evals: Vec = poly_a + .evaluations() + .iter() + .zip(poly_b.evaluations().iter()) + .map(|(a, b)| s_a * *a + s_b * *b) + .collect(); + let (c_direct, ()) = TestScheme::commit(&combined_evals, &pk); + + let c_combined = TestScheme::combine(&[ca, cb], &[s_a, s_b]); + + assert_eq!(c_direct, c_combined); + } + + #[test] + fn open_verify_with_random_points() { + let mut rng = ChaCha20Rng::seed_from_u64(0xcafe); + + for _ in 0..5 { + let ell = 4; + let n = 1 << ell; + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut pt = Blake2bTranscript::new(b"rand-test"); + let proof = + ::open(&poly, &point, eval, &pk, None, &mut pt); + + let mut vt = Blake2bTranscript::new(b"rand-test"); + ::verify( + &commitment, + &point, + eval, + &proof, + &vk, + &mut vt, + ) + .expect("random instance should verify"); + } + } + + #[test] + fn extract_vc_setup_produces_valid_pedersen() { + use jolt_crypto::{Pedersen, VectorCommitment}; + + let n = 1 << 4; + let (pk, _vk) = test_setup(n); + + let capacity = 5; + let vc_setup = PedersenSetup::::derive(&pk, capacity); + + assert_eq!( + as VectorCommitment>::capacity(&vc_setup), + capacity, + ); + + // Commit and verify a small vector. + let values = vec![Fr::one(), Fr::from_u64(2), Fr::from_u64(3)]; + let blinding = Fr::from_u64(42); + let commitment = as VectorCommitment>::commit( + &vc_setup, &values, &blinding, + ); + assert!( + as VectorCommitment>::verify( + &vc_setup, + &commitment, + &values, + &blinding, + ) + ); + } + + #[test] + fn trivial_polynomial() { + // 1-variable polynomial: [a, b] + let ell = 1; + let n = 1 << ell; + let mut rng = ChaCha20Rng::seed_from_u64(777); + let (pk, vk) = test_setup(n); + + let poly = Polynomial::::random(ell, &mut rng); + let point: Vec = (0..ell).map(|_| Fr::random(&mut rng)).collect(); + let eval = poly.evaluate(&point); + + let (commitment, ()) = TestScheme::commit(poly.evaluations(), &pk); + + let mut pt = Blake2bTranscript::new(b"trivial"); + let proof = ::open(&poly, &point, eval, &pk, None, &mut pt); + + let mut vt = Blake2bTranscript::new(b"trivial"); + ::verify(&commitment, &point, eval, &proof, &vk, &mut vt) + .expect("trivial polynomial should verify"); + } +} diff --git a/crates/jolt-hyperkzg/src/types.rs b/crates/jolt-hyperkzg/src/types.rs new file mode 100644 index 0000000000..55ba4e0848 --- /dev/null +++ b/crates/jolt-hyperkzg/src/types.rs @@ -0,0 +1,125 @@ +//! Commitment, proof, and setup types for HyperKZG. +//! +//! All types are generic over `P: PairingGroup` — no arkworks leakage. + +use jolt_crypto::{HomomorphicCommitment, JoltGroup, PairingGroup}; +use serde::{Deserialize, Serialize}; + +/// Commitment to a multilinear polynomial: a single G1 element. +#[derive(Serialize, Deserialize)] +#[serde(bound( + serialize = "P::G1: Serialize", + deserialize = "P::G1: for<'a> Deserialize<'a>" +))] +pub struct HyperKZGCommitment { + pub(crate) point: P::G1, +} + +impl Copy for HyperKZGCommitment

{} + +#[expect( + clippy::expl_impl_clone_on_copy, + reason = "explicit impl is required because PairingGroup is not bounded by Clone" +)] +impl Clone for HyperKZGCommitment

{ + fn clone(&self) -> Self { + *self + } +} + +impl std::fmt::Debug for HyperKZGCommitment

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HyperKZGCommitment") + .field("point", &self.point) + .finish() + } +} + +impl PartialEq for HyperKZGCommitment

{ + fn eq(&self, other: &Self) -> bool { + self.point == other.point + } +} + +impl Eq for HyperKZGCommitment

{} + +impl HomomorphicCommitment for HyperKZGCommitment

{ + #[inline] + fn add(c1: &Self, c2: &Self) -> Self { + Self { + point: >::add(&c1.point, &c2.point), + } + } + + #[inline] + fn linear_combine(c1: &Self, c2: &Self, scalar: &F) -> Self { + Self { + point: HomomorphicCommitment::linear_combine(&c1.point, &c2.point, scalar), + } + } +} + +impl Default for HyperKZGCommitment

{ + fn default() -> Self { + Self { + point: ::identity(), + } + } +} + +/// Opening proof for the HyperKZG protocol. +/// +/// - `com`: intermediate polynomial commitments from the Gemini folding (ell - 1 elements) +/// - `w`: KZG witness commitments for the three evaluation points `[r, -r, r^2]` +/// - `v`: evaluations of all intermediate polynomials at the three points +/// (`v[t][k]` = polynomial k evaluated at point t) +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound( + serialize = "P::G1: Serialize, P::ScalarField: Serialize", + deserialize = "P::G1: for<'a> Deserialize<'a>, P::ScalarField: for<'a> Deserialize<'a>" +))] +pub struct HyperKZGProof { + pub com: Vec, + pub w: [P::G1; 3], + pub v: [Vec; 3], +} + +/// Prover setup: SRS G1 and G2 powers. +/// +/// G1 powers: `[g1, beta * g1, beta^2 * g1, ..., beta^n * g1]` +/// G2 powers: `[g2, beta * g2]` (only two needed for KZG verification). +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound( + serialize = "P::G1: Serialize, P::G2: Serialize", + deserialize = "P::G1: for<'a> Deserialize<'a>, P::G2: for<'a> Deserialize<'a>" +))] +pub struct HyperKZGProverSetup { + pub(crate) g1_powers: Vec, + pub(crate) g2_powers: Vec, +} + +/// Verifier setup: the four G1/G2 elements needed for pairing checks. +/// +/// - `g1`: generator $g$ +/// - `g2`: generator $h$ +/// - `beta_g2`: $\beta \cdot h$ (for KZG pairing check) +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[serde(bound( + serialize = "P::G1: Serialize, P::G2: Serialize", + deserialize = "P::G1: for<'a> Deserialize<'a>, P::G2: for<'a> Deserialize<'a>" +))] +pub struct HyperKZGVerifierSetup { + pub(crate) g1: P::G1, + pub(crate) g2: P::G2, + pub(crate) beta_g2: P::G2, +} + +impl From<&HyperKZGProverSetup

> for HyperKZGVerifierSetup

{ + fn from(prover: &HyperKZGProverSetup

) -> Self { + Self { + g1: prover.g1_powers[0], + g2: prover.g2_powers[0], + beta_g2: prover.g2_powers[1], + } + } +} diff --git a/crates/jolt-hyperkzg/tests/commit_open_verify.rs b/crates/jolt-hyperkzg/tests/commit_open_verify.rs new file mode 100644 index 0000000000..52a8376880 --- /dev/null +++ b/crates/jolt-hyperkzg/tests/commit_open_verify.rs @@ -0,0 +1,224 @@ +//! Integration tests for HyperKZG commit → open → verify pipeline with BN254. + +#![expect(clippy::expect_used, reason = "tests may panic on assertion failures")] + +use jolt_crypto::Bn254; +use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; +use jolt_hyperkzg::{HyperKZGProverSetup, HyperKZGScheme, HyperKZGVerifierSetup}; +use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme}; +use jolt_poly::Polynomial; +use jolt_transcript::{Blake2bTranscript, Transcript}; +use rand_chacha::ChaCha20Rng; +use rand_core::SeedableRng; + +type KzgPCS = HyperKZGScheme; + +fn make_setup(max_degree: usize) -> (HyperKZGProverSetup, HyperKZGVerifierSetup) { + let mut rng = ChaCha20Rng::seed_from_u64(0xdead_beef); + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let pk = KzgPCS::setup(&mut rng, max_degree, g1, g2); + let vk = KzgPCS::verifier_setup(&pk); + (pk, vk) +} + +fn commit_open_verify( + poly: &Polynomial, + point: &[Fr], + pk: &HyperKZGProverSetup, + vk: &HyperKZGVerifierSetup, + label: &'static [u8], +) { + let eval = poly.evaluate(point); + let (commitment, ()) = ::commit(poly.evaluations(), pk); + + let mut t_p = Blake2bTranscript::new(label); + let proof = ::open(poly, point, eval, pk, None, &mut t_p); + + let mut t_v = Blake2bTranscript::new(label); + ::verify(&commitment, point, eval, &proof, vk, &mut t_v) + .expect("verification should succeed"); +} + +// Basic roundtrip for various polynomial sizes + +#[test] +fn roundtrip_num_vars_1_to_8() { + let mut rng = ChaCha20Rng::seed_from_u64(1000); + for nv in 1..=8 { + let (pk, vk) = make_setup(1 << nv); + let poly = Polynomial::::random(nv, &mut rng); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + commit_open_verify(&poly, &point, &pk, &vk, b"kzg-sizes"); + } +} + +// Edge cases + +/// All-zero polynomial commits to identity point and still verifies. +#[test] +fn zero_polynomial_roundtrip() { + let nv = 3; + let (pk, vk) = make_setup(1 << nv); + let poly = Polynomial::::zeros(nv); + let point = vec![Fr::from_u64(42); nv]; + commit_open_verify(&poly, &point, &pk, &vk, b"kzg-zero"); +} + +/// Single-variable polynomial (2 evaluations). +#[test] +fn single_variable_polynomial() { + let mut rng = ChaCha20Rng::seed_from_u64(2000); + let (pk, vk) = make_setup(2); + let poly = Polynomial::::random(1, &mut rng); + let point = vec![Fr::random(&mut rng)]; + commit_open_verify(&poly, &point, &pk, &vk, b"kzg-single-var"); +} + +/// Constant polynomial (all evaluations are the same value). +#[test] +fn constant_polynomial() { + let nv = 3; + let (pk, vk) = make_setup(1 << nv); + let val = Fr::from_u64(42); + let poly = Polynomial::new(vec![val; 1 << nv]); + let mut rng = ChaCha20Rng::seed_from_u64(2001); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + commit_open_verify(&poly, &point, &pk, &vk, b"kzg-constant"); +} + +// Wrong evaluation rejection + +#[test] +fn wrong_eval_rejected() { + let mut rng = ChaCha20Rng::seed_from_u64(3000); + let nv = 4; + let (pk, vk) = make_setup(1 << nv); + let poly = Polynomial::::random(nv, &mut rng); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + + let correct_eval = poly.evaluate(&point); + let wrong_eval = correct_eval + Fr::from_u64(1); + let (commitment, ()) = ::commit(poly.evaluations(), &pk); + + // Prover opens with correct eval + let mut t_p = Blake2bTranscript::new(b"kzg-wrong"); + let proof = + ::open(&poly, &point, correct_eval, &pk, None, &mut t_p); + + // Verifier checks with wrong eval + let mut t_v = Blake2bTranscript::new(b"kzg-wrong"); + let result = ::verify( + &commitment, + &point, + wrong_eval, + &proof, + &vk, + &mut t_v, + ); + assert!(result.is_err(), "wrong evaluation must be rejected"); +} + +// Homomorphic properties + +/// combine([C_a, C_b], [1, 1]) == commit(a + b). +#[test] +fn homomorphic_sum() { + let mut rng = ChaCha20Rng::seed_from_u64(4000); + let nv = 4; + let (pk, vk) = make_setup(1 << nv); + let a = Polynomial::::random(nv, &mut rng); + let b = Polynomial::::random(nv, &mut rng); + + let (com_a, ()) = ::commit(a.evaluations(), &pk); + let (com_b, ()) = ::commit(b.evaluations(), &pk); + let combined_com = ::combine( + &[com_a, com_b], + &[Fr::from_u64(1), Fr::from_u64(1)], + ); + + let sum_poly = a + b; + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + let eval = sum_poly.evaluate(&point); + + let mut t_p = Blake2bTranscript::new(b"kzg-homo"); + let proof = ::open(&sum_poly, &point, eval, &pk, None, &mut t_p); + + let mut t_v = Blake2bTranscript::new(b"kzg-homo"); + ::verify(&combined_com, &point, eval, &proof, &vk, &mut t_v) + .expect("homomorphic sum must verify"); +} + +/// combine with arbitrary scalars: s_a·C_a + s_b·C_b == commit(s_a·a + s_b·b). +#[test] +fn homomorphic_weighted_combination() { + let mut rng = ChaCha20Rng::seed_from_u64(4001); + let nv = 3; + let (pk, vk) = make_setup(1 << nv); + let a = Polynomial::::random(nv, &mut rng); + let b = Polynomial::::random(nv, &mut rng); + let s_a = Fr::random(&mut rng); + let s_b = Fr::random(&mut rng); + + let (com_a, ()) = ::commit(a.evaluations(), &pk); + let (com_b, ()) = ::commit(b.evaluations(), &pk); + let combined_com = ::combine(&[com_a, com_b], &[s_a, s_b]); + + let weighted_poly = a * s_a + b * s_b; + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + let eval = weighted_poly.evaluate(&point); + + let mut t_p = Blake2bTranscript::new(b"kzg-weighted"); + let proof = + ::open(&weighted_poly, &point, eval, &pk, None, &mut t_p); + + let mut t_v = Blake2bTranscript::new(b"kzg-weighted"); + ::verify(&combined_com, &point, eval, &proof, &vk, &mut t_v) + .expect("weighted combination must verify"); +} + +// Deterministic setup + +#[test] +fn deterministic_setup_from_secret() { + let g1 = Bn254::g1_generator(); + let g2 = Bn254::g2_generator(); + let beta = Fr::from_u64(12345); + + let pk1 = KzgPCS::setup_from_secret(beta, 16, g1, g2); + let pk2 = KzgPCS::setup_from_secret(beta, 16, g1, g2); + let _vk1 = KzgPCS::verifier_setup(&pk1); + let vk2 = KzgPCS::verifier_setup(&pk2); + + // Same setup yields same commitments + let poly = Polynomial::new(vec![Fr::from_u64(1), Fr::from_u64(2)]); + let (com1, ()) = ::commit(poly.evaluations(), &pk1); + let (com2, ()) = ::commit(poly.evaluations(), &pk2); + assert_eq!( + com1, com2, + "deterministic setups must produce same commitments" + ); + + // Verify with either setup + let point = vec![Fr::from_u64(7)]; + let eval = poly.evaluate(&point); + let mut t = Blake2bTranscript::new(b"det-setup"); + let proof = ::open(&poly, &point, eval, &pk1, None, &mut t); + let mut t = Blake2bTranscript::new(b"det-setup"); + ::verify(&com1, &point, eval, &proof, &vk2, &mut t) + .expect("cross-setup verification must work"); +} + +// Property test: random polynomials always verify + +#[test] +fn property_random_polynomials_always_verify() { + for seed in 5000..5010 { + let mut rng = ChaCha20Rng::seed_from_u64(seed); + let nv = 2 + (seed as usize % 5); // 2..6 + let (pk, vk) = make_setup(1 << nv); + let poly = Polynomial::::random(nv, &mut rng); + let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); + commit_open_verify(&poly, &point, &pk, &vk, b"kzg-property"); + } +} diff --git a/crates/jolt-lookup-tables/Cargo.toml b/crates/jolt-lookup-tables/Cargo.toml index 5f0ada2e12..f8eae366db 100644 --- a/crates/jolt-lookup-tables/Cargo.toml +++ b/crates/jolt-lookup-tables/Cargo.toml @@ -13,11 +13,14 @@ workspace = true [dependencies] jolt-field = { workspace = true } -jolt-riscv = { workspace = true } +jolt-riscv = { workspace = true, features = ["serialization"] } serde = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } [dev-dependencies] -jolt-riscv = { workspace = true, features = ["test-utils"] } +jolt-core = { workspace = true, default-features = false, features = [ + "minimal", +] } +jolt-riscv = { workspace = true, features = ["serialization", "test-utils"] } rand = { workspace = true } tracer = { workspace = true, features = ["std", "test-utils"] } diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/addi.rs b/crates/jolt-lookup-tables/src/instructions/riscv/addi.rs index 6e39bd91f3..d4a8604221 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/addi.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/addi.rs @@ -19,7 +19,7 @@ impl LookupQuery for Addi { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/andi.rs b/crates/jolt-lookup-tables/src/instructions/riscv/andi.rs index 67229a8ce9..b77b651b64 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/andi.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/andi.rs @@ -10,7 +10,7 @@ impl LookupQuery for AndI { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/auipc.rs b/crates/jolt-lookup-tables/src/instructions/riscv/auipc.rs index 29a5d51fae..057bafce85 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/auipc.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/auipc.rs @@ -18,9 +18,9 @@ impl LookupQuery for Auipc { fn to_instruction_inputs(&self) -> (u64, i128) { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( - Into::::into(self.0.instruction()).address as u64 + Into::::into(self.0.instruction()).address as u64 & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/ebreak.rs b/crates/jolt-lookup-tables/src/instructions/riscv/ebreak.rs index cb23095631..a2558c3e02 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/ebreak.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/ebreak.rs @@ -1,10 +1,7 @@ -use crate::traits::impl_lookup_table; use crate::traits::LookupQuery; use jolt_riscv::instructions::Ebreak; use jolt_riscv::JoltCycle; -impl_lookup_table!(Ebreak, None); - impl LookupQuery for Ebreak { fn to_instruction_inputs(&self) -> (u64, i128) { (0, 0) diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/ecall.rs b/crates/jolt-lookup-tables/src/instructions/riscv/ecall.rs index 7231d47398..0bfbd78f7d 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/ecall.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/ecall.rs @@ -1,10 +1,7 @@ -use crate::traits::impl_lookup_table; use crate::traits::LookupQuery; use jolt_riscv::instructions::Ecall; use jolt_riscv::JoltCycle; -impl_lookup_table!(Ecall, None); - impl LookupQuery for Ecall { fn to_instruction_inputs(&self) -> (u64, i128) { (0, 0) diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/jal.rs b/crates/jolt-lookup-tables/src/instructions/riscv/jal.rs index 200b7d8e7c..c5397a786a 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/jal.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/jal.rs @@ -18,9 +18,9 @@ impl LookupQuery for Jal { fn to_instruction_inputs(&self) -> (u64, i128) { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( - Into::::into(self.0.instruction()).address as u64 + Into::::into(self.0.instruction()).address as u64 & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/jalr.rs b/crates/jolt-lookup-tables/src/instructions/riscv/jalr.rs index 11311ce534..1e58f68dcc 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/jalr.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/jalr.rs @@ -19,7 +19,7 @@ impl LookupQuery for Jalr { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/lui.rs b/crates/jolt-lookup-tables/src/instructions/riscv/lui.rs index 3a3c77c1b3..df845edec8 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/lui.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/lui.rs @@ -14,7 +14,7 @@ impl LookupQuery for Lui { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( 0, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, @@ -23,7 +23,7 @@ impl LookupQuery for Lui { fn to_lookup_output(&self) -> u64 { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm as u64 & mask diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/ori.rs b/crates/jolt-lookup-tables/src/instructions/riscv/ori.rs index f180b53cdb..fe643f9aec 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/ori.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/ori.rs @@ -10,7 +10,7 @@ impl LookupQuery for OrI { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/slti.rs b/crates/jolt-lookup-tables/src/instructions/riscv/slti.rs index 8e0e6fd527..3eec8d5d60 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/slti.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/slti.rs @@ -10,7 +10,7 @@ impl LookupQuery for SltI { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/sltiu.rs b/crates/jolt-lookup-tables/src/instructions/riscv/sltiu.rs index 3826a2b8d3..9ad162b086 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/sltiu.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/sltiu.rs @@ -10,7 +10,7 @@ impl LookupQuery for SltIU { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/riscv/xori.rs b/crates/jolt-lookup-tables/src/instructions/riscv/xori.rs index 17cb343175..9f845385e6 100644 --- a/crates/jolt-lookup-tables/src/instructions/riscv/xori.rs +++ b/crates/jolt-lookup-tables/src/instructions/riscv/xori.rs @@ -10,7 +10,7 @@ impl LookupQuery for XorI { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/test.rs b/crates/jolt-lookup-tables/src/instructions/test.rs index 9495546e4d..6505fc2938 100644 --- a/crates/jolt-lookup-tables/src/instructions/test.rs +++ b/crates/jolt-lookup-tables/src/instructions/test.rs @@ -2,8 +2,7 @@ use std::any::TypeId; -use jolt_riscv::NormalizedInstruction; -use jolt_riscv::{Flags, InstructionFlags, JoltCycle, JoltInstruction}; +use jolt_riscv::{Flags, InstructionFlags, JoltCycle, JoltInstructionRowData}; use rand::prelude::*; use tracer::emulator::{cpu::Cpu, terminal::DummyTerminal}; use tracer::instruction::{jal::JAL, jalr::JALR, Cycle, RISCVCycle, RISCVTrace}; @@ -16,15 +15,19 @@ use crate::{InstructionLookupTable, LookupQuery, XLEN}; /// tuple-struct constructor as `construct`. #[doc(hidden)] #[expect(clippy::unwrap_used)] -pub fn materialize_entry_test_fn(construct: impl Fn(C) -> T) -where - T: InstructionLookupTable + LookupQuery + core::fmt::Debug, +pub fn materialize_entry_test_fn( + cycle_wrapper: impl Fn(C) -> T, + instr_wrapper: impl Fn(C::Instruction) -> I, +) where + T: LookupQuery + core::fmt::Debug, C: JoltCycle, + I: InstructionLookupTable, { let mut rng = StdRng::seed_from_u64(12345); for _ in 0..10_000 { - let cycle: T = construct(C::random(&mut rng)); - let table = cycle.lookup_table().unwrap(); + let raw = C::random(&mut rng); + let table = instr_wrapper(raw.instruction()).lookup_table().unwrap(); + let cycle: T = cycle_wrapper(raw); assert_eq!( cycle.to_lookup_output(), table.materialize_entry(cycle.to_lookup_index()), @@ -55,13 +58,13 @@ pub fn instruction_inputs_match_constraint_fn( ) where C: JoltCycle, T: LookupQuery + core::fmt::Debug, - I: JoltInstruction + Flags, + I: JoltInstructionRowData + Flags, { let mut rng = StdRng::seed_from_u64(12345); for _ in 0..10_000 { let raw: C = C::random(&mut rng); let instr = raw.instruction(); - let normalized: NormalizedInstruction = instr.into(); + let normalized = instr.jolt_instruction_row(); let unexpanded_pc = normalized.address as u64; let imm = normalized.operands.imm; let flags = instr_wrapper(instr).instruction_flags(); @@ -122,7 +125,7 @@ where for _ in 0..10_000 { let raw: C = C::random(&mut rng); let instr = raw.instruction(); - let normalized: NormalizedInstruction = instr.into(); + let normalized = instr.jolt_instruction_row(); let rs1_idx = normalized.operands.rs1; let rs2_idx = normalized.operands.rs2; let rd_idx = normalized.operands.rd; @@ -184,7 +187,8 @@ macro_rules! materialize_entry_test { $crate::instructions::test::materialize_entry_test_fn::< $jolt>, tracer::instruction::RISCVCycle<$tracer>, - >($jolt) + $jolt<$tracer>, + >($jolt, $jolt) }; } diff --git a/crates/jolt-lookup-tables/src/instructions/virt/assert_halfword_alignment.rs b/crates/jolt-lookup-tables/src/instructions/virt/assert_halfword_alignment.rs index 5e02a9a217..ccc34f8d9c 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/assert_halfword_alignment.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/assert_halfword_alignment.rs @@ -10,7 +10,7 @@ impl LookupQuery for AssertHalfwordAlignm let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/assert_word_alignment.rs b/crates/jolt-lookup-tables/src/instructions/virt/assert_word_alignment.rs index a89475cc8d..0e23b810e7 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/assert_word_alignment.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/assert_word_alignment.rs @@ -10,7 +10,7 @@ impl LookupQuery for AssertWordAlignment< let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/movsign.rs b/crates/jolt-lookup-tables/src/instructions/virt/movsign.rs index 034d4bfa63..eabc0c06f3 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/movsign.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/movsign.rs @@ -10,7 +10,7 @@ impl LookupQuery for MovSign { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/virt/muli.rs b/crates/jolt-lookup-tables/src/instructions/virt/muli.rs index 0719868bfb..c9469e5e8e 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/muli.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/muli.rs @@ -10,7 +10,7 @@ impl LookupQuery for MulI { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( self.0.rs1_val().unwrap_or(0) & mask, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/virt/pow2i.rs b/crates/jolt-lookup-tables/src/instructions/virt/pow2i.rs index d3325c45a2..5ecf71d243 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/pow2i.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/pow2i.rs @@ -10,7 +10,7 @@ impl LookupQuery for Pow2I { let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( 0, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/virt/pow2iw.rs b/crates/jolt-lookup-tables/src/instructions/virt/pow2iw.rs index 05c19adbd1..27461ac7ac 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/pow2iw.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/pow2iw.rs @@ -9,7 +9,7 @@ impl LookupQuery for Pow2IW { fn to_instruction_inputs(&self) -> (u64, i128) { ( 0, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/rotri.rs b/crates/jolt-lookup-tables/src/instructions/virt/rotri.rs index f68608f392..6aa1a44955 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/rotri.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/rotri.rs @@ -9,7 +9,7 @@ impl LookupQuery for VirtualRotri { fn to_instruction_inputs(&self) -> (u64, i128) { ( self.0.rs1_val().unwrap_or(0), - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/rotriw.rs b/crates/jolt-lookup-tables/src/instructions/virt/rotriw.rs index 1010ba0306..0f4abd3412 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/rotriw.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/rotriw.rs @@ -9,7 +9,7 @@ impl LookupQuery for VirtualRotriw { fn to_instruction_inputs(&self) -> (u64, i128) { ( self.0.rs1_val().unwrap_or(0), - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/shift_right_bitmaski.rs b/crates/jolt-lookup-tables/src/instructions/virt/shift_right_bitmaski.rs index 57ab7bc293..811e7eadfc 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/shift_right_bitmaski.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/shift_right_bitmaski.rs @@ -10,7 +10,7 @@ impl LookupQuery for VirtualShiftRightBit let mask = (1u128 << XLEN).wrapping_sub(1) as u64; ( 0, - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm & mask as i128, diff --git a/crates/jolt-lookup-tables/src/instructions/virt/srai.rs b/crates/jolt-lookup-tables/src/instructions/virt/srai.rs index bf19e315aa..729b432a99 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/srai.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/srai.rs @@ -9,7 +9,7 @@ impl LookupQuery for VirtualSrai { fn to_instruction_inputs(&self) -> (u64, i128) { ( self.0.rs1_val().unwrap_or(0), - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/instructions/virt/srli.rs b/crates/jolt-lookup-tables/src/instructions/virt/srli.rs index 977b3190c6..676ca5d82d 100644 --- a/crates/jolt-lookup-tables/src/instructions/virt/srli.rs +++ b/crates/jolt-lookup-tables/src/instructions/virt/srli.rs @@ -9,7 +9,7 @@ impl LookupQuery for VirtualSrli { fn to_instruction_inputs(&self) -> (u64, i128) { ( self.0.rs1_val().unwrap_or(0), - Into::::into(self.0.instruction()) + Into::::into(self.0.instruction()) .operands .imm, ) diff --git a/crates/jolt-lookup-tables/src/tables/mod.rs b/crates/jolt-lookup-tables/src/tables/mod.rs index d6b629cac9..5f5395eb6d 100644 --- a/crates/jolt-lookup-tables/src/tables/mod.rs +++ b/crates/jolt-lookup-tables/src/tables/mod.rs @@ -39,7 +39,6 @@ pub mod unsigned_less_than; pub mod unsigned_less_than_equal; pub mod upper_word; pub mod valid_div0; -pub mod valid_signed_remainder; pub mod valid_unsigned_remainder; pub mod virtual_change_divisor; pub mod virtual_change_divisor_w; @@ -78,7 +77,6 @@ use unsigned_less_than::UnsignedLessThanTable; use unsigned_less_than_equal::UnsignedLessThanEqualTable; use upper_word::UpperWordTable; use valid_div0::ValidDiv0Table; -use valid_signed_remainder::ValidSignedRemainderTable; use valid_unsigned_remainder::ValidUnsignedRemainderTable; use virtual_change_divisor::VirtualChangeDivisorTable; use virtual_change_divisor_w::VirtualChangeDivisorWTable; @@ -97,8 +95,22 @@ use xor::XorTable; /// Each variant carries the corresponding zero-sized table marker. Instructions /// declare which table they use via /// [`InstructionLookupTable::lookup_table`](crate::InstructionLookupTable::lookup_table). +/// +/// Variant indices match `jolt-core::LookupTables` so lookup-table flags in +/// core-produced proofs can be interpreted without an adapter. #[expect(clippy::unsafe_derive_deserialize)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, strum::EnumCount)] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + strum::EnumCount, + strum::EnumIter, +)] #[repr(u8)] pub enum LookupTableKind { RangeCheck(RangeCheckTable), @@ -108,32 +120,31 @@ pub enum LookupTableKind { Or(OrTable), Xor(XorTable), Equal(EqualTable), + SignedGreaterThanEqual(SignedGreaterThanEqualTable), + UnsignedGreaterThanEqual(UnsignedGreaterThanEqualTable), NotEqual(NotEqualTable), SignedLessThan(SignedLessThanTable), UnsignedLessThan(UnsignedLessThanTable), - SignedGreaterThanEqual(SignedGreaterThanEqualTable), - UnsignedGreaterThanEqual(UnsignedGreaterThanEqualTable), - UnsignedLessThanEqual(UnsignedLessThanEqualTable), + SignMask(SignMaskTable), UpperWord(UpperWordTable), + UnsignedLessThanEqual(UnsignedLessThanEqualTable), + ValidUnsignedRemainder(ValidUnsignedRemainderTable), + ValidDiv0(ValidDiv0Table), + HalfwordAlignment(HalfwordAlignmentTable), + WordAlignment(WordAlignmentTable), LowerHalfWord(LowerHalfWordTable), SignExtendHalfWord(SignExtendHalfWordTable), - SignMask(SignMaskTable), Pow2(Pow2Table), Pow2W(Pow2WTable), ShiftRightBitmask(ShiftRightBitmaskTable), + VirtualRev8W(VirtualRev8WTable), VirtualSRL(VirtualSRLTable), VirtualSRA(VirtualSRATable), VirtualROTR(VirtualROTRTable), VirtualROTRW(VirtualROTRWTable), - ValidDiv0(ValidDiv0Table), - ValidUnsignedRemainder(ValidUnsignedRemainderTable), - ValidSignedRemainder(ValidSignedRemainderTable), VirtualChangeDivisor(VirtualChangeDivisorTable), VirtualChangeDivisorW(VirtualChangeDivisorWTable), - HalfwordAlignment(HalfwordAlignmentTable), - WordAlignment(WordAlignmentTable), MulUNoOverflow(MulUNoOverflowTable), - VirtualRev8W(VirtualRev8WTable), VirtualXORROT32(VirtualXORROTTable), VirtualXORROT24(VirtualXORROTTable), VirtualXORROT16(VirtualXORROTTable), @@ -158,32 +169,31 @@ macro_rules! dispatch { Self::Or($t) => $expr, Self::Xor($t) => $expr, Self::Equal($t) => $expr, + Self::SignedGreaterThanEqual($t) => $expr, + Self::UnsignedGreaterThanEqual($t) => $expr, Self::NotEqual($t) => $expr, Self::SignedLessThan($t) => $expr, Self::UnsignedLessThan($t) => $expr, - Self::SignedGreaterThanEqual($t) => $expr, - Self::UnsignedGreaterThanEqual($t) => $expr, - Self::UnsignedLessThanEqual($t) => $expr, + Self::SignMask($t) => $expr, Self::UpperWord($t) => $expr, + Self::UnsignedLessThanEqual($t) => $expr, + Self::ValidUnsignedRemainder($t) => $expr, + Self::ValidDiv0($t) => $expr, + Self::HalfwordAlignment($t) => $expr, + Self::WordAlignment($t) => $expr, Self::LowerHalfWord($t) => $expr, Self::SignExtendHalfWord($t) => $expr, - Self::SignMask($t) => $expr, Self::Pow2($t) => $expr, Self::Pow2W($t) => $expr, Self::ShiftRightBitmask($t) => $expr, + Self::VirtualRev8W($t) => $expr, Self::VirtualSRL($t) => $expr, Self::VirtualSRA($t) => $expr, Self::VirtualROTR($t) => $expr, Self::VirtualROTRW($t) => $expr, - Self::ValidDiv0($t) => $expr, - Self::ValidUnsignedRemainder($t) => $expr, - Self::ValidSignedRemainder($t) => $expr, Self::VirtualChangeDivisor($t) => $expr, Self::VirtualChangeDivisorW($t) => $expr, - Self::HalfwordAlignment($t) => $expr, - Self::WordAlignment($t) => $expr, Self::MulUNoOverflow($t) => $expr, - Self::VirtualRev8W($t) => $expr, Self::VirtualXORROT32($t) => $expr, Self::VirtualXORROT24($t) => $expr, Self::VirtualXORROT16($t) => $expr, @@ -197,6 +207,12 @@ macro_rules! dispatch { } impl LookupTableKind { + pub const COUNT: usize = ::COUNT; + + pub fn iter() -> ::Iterator { + ::iter() + } + /// Returns the discriminant as a `usize`, suitable for array indexing. #[inline] pub fn index(&self) -> usize { diff --git a/crates/jolt-lookup-tables/src/tables/valid_signed_remainder.rs b/crates/jolt-lookup-tables/src/tables/valid_signed_remainder.rs deleted file mode 100644 index 9f470190fb..0000000000 --- a/crates/jolt-lookup-tables/src/tables/valid_signed_remainder.rs +++ /dev/null @@ -1,119 +0,0 @@ -use jolt_field::Field; -use serde::{Deserialize, Serialize}; - -use crate::challenge_ops::{ChallengeOps, FieldOps}; -use crate::tables::prefixes::{PrefixEval, Prefixes}; -use crate::tables::suffixes::{SuffixEval, Suffixes}; -use crate::tables::PrefixSuffixDecomposition; -use crate::traits::LookupTable; -use crate::uninterleave_bits; - -/// (remainder, divisor) -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct ValidSignedRemainderTable; - -impl LookupTable for ValidSignedRemainderTable { - fn materialize_entry(&self, index: u128) -> u64 { - let (x, y) = uninterleave_bits(index); - // Sign-extend the lower XLEN bits to a full i64 so that `unsigned_abs` - // and the sign-bit comparison agree with XLEN-bit signed semantics. - let shift = 64 - XLEN; - let remainder = ((x as i64) << shift) >> shift; - let divisor = ((y as i64) << shift) >> shift; - if remainder == 0 || divisor == 0 { - 1 - } else { - let remainder_sign = remainder >> (XLEN - 1); - let divisor_sign = divisor >> (XLEN - 1); - (remainder.unsigned_abs() < divisor.unsigned_abs() && remainder_sign == divisor_sign) - .into() - } - } - - fn evaluate_mle(&self, r: &[C]) -> F - where - C: ChallengeOps, - F: Field + FieldOps, - { - let x_sign = r[0]; - let y_sign = r[1]; - - let mut remainder_is_zero = F::one() - r[0]; - let mut divisor_is_zero = F::one() - r[1]; - let mut positive_remainder_equals_divisor = (F::one() - x_sign) * (F::one() - y_sign); - let mut positive_remainder_less_than_divisor = (F::one() - x_sign) * (F::one() - y_sign); - let mut negative_divisor_equals_remainder = x_sign * y_sign; - let mut negative_divisor_greater_than_remainder = x_sign * y_sign; - - for i in 1..XLEN { - let x_i = r[2 * i]; - let y_i = r[2 * i + 1]; - if i == 1 { - positive_remainder_less_than_divisor *= (F::one() - x_i) * y_i; - negative_divisor_greater_than_remainder *= x_i * (F::one() - y_i); - } else { - positive_remainder_less_than_divisor += - positive_remainder_equals_divisor * (F::one() - x_i) * y_i; - negative_divisor_greater_than_remainder += - negative_divisor_equals_remainder * x_i * (F::one() - y_i); - } - positive_remainder_equals_divisor *= x_i * y_i + (F::one() - x_i) * (F::one() - y_i); - negative_divisor_equals_remainder *= x_i * y_i + (F::one() - x_i) * (F::one() - y_i); - remainder_is_zero *= F::one() - x_i; - divisor_is_zero *= F::one() - y_i; - } - - positive_remainder_less_than_divisor - + negative_divisor_greater_than_remainder - + y_sign * remainder_is_zero - + divisor_is_zero - } -} - -impl PrefixSuffixDecomposition for ValidSignedRemainderTable { - fn suffixes(&self) -> &'static [Suffixes] { - &[ - Suffixes::One, - Suffixes::LessThan, - Suffixes::GreaterThan, - Suffixes::LeftOperandIsZero, - Suffixes::RightOperandIsZero, - ] - } - - #[expect(clippy::unwrap_used)] - fn combine(&self, prefixes: &[PrefixEval], suffixes: &[SuffixEval]) -> F { - debug_assert_eq!(self.suffixes().len(), suffixes.len()); - let [one, less_than, greater_than, left_operand_is_zero, right_operand_is_zero] = - suffixes.try_into().unwrap(); - prefixes[Prefixes::RightOperandIsZero] * right_operand_is_zero - + prefixes[Prefixes::PositiveRemainderEqualsDivisor] * less_than - + prefixes[Prefixes::PositiveRemainderLessThanDivisor] * one - + prefixes[Prefixes::NegativeDivisorZeroRemainder] * left_operand_is_zero - + prefixes[Prefixes::NegativeDivisorEqualsRemainder] * greater_than - + prefixes[Prefixes::NegativeDivisorGreaterThanRemainder] * one - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tables::test_utils::{mle_full_hypercube_test, mle_random_test, prefix_suffix_test}; - use crate::XLEN; - use jolt_field::Fr; - - #[test] - fn mle_full_hypercube() { - mle_full_hypercube_test::<8, Fr, ValidSignedRemainderTable<8>>(); - } - - #[test] - fn mle_random() { - mle_random_test::>(); - } - - #[test] - fn prefix_suffix() { - prefix_suffix_test::>(); - } -} diff --git a/crates/jolt-lookup-tables/src/traits.rs b/crates/jolt-lookup-tables/src/traits.rs index 6a42c086b3..27948a5752 100644 --- a/crates/jolt-lookup-tables/src/traits.rs +++ b/crates/jolt-lookup-tables/src/traits.rs @@ -1,6 +1,7 @@ //! Lookup-table-related traits. use jolt_field::Field; +use jolt_riscv::{JoltInstruction, JoltInstructionRowData}; use std::fmt::Debug; use crate::challenge_ops::{ChallengeOps, FieldOps}; @@ -29,9 +30,33 @@ pub trait InstructionLookupTable { fn lookup_table(&self) -> Option>; } +macro_rules! impl_jolt_instruction_lookup_table { + ( + instructions: [$($kind:ident => $variant:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + impl InstructionLookupTable + for JoltInstruction + { + #[inline] + fn lookup_table(&self) -> Option> { + match self { + JoltInstruction::Noop(_) => None, + $( + JoltInstruction::$variant(instruction) => instruction.lookup_table(), + )* + } + } + } + }; +} + +jolt_riscv::for_each_jolt_instruction_kind!(impl_jolt_instruction_lookup_table); + macro_rules! impl_lookup_table { ($instr:ident, Some($table:ident)) => { - impl $crate::traits::InstructionLookupTable for $instr { + impl + $crate::traits::InstructionLookupTable for $instr + { #[inline] fn lookup_table(&self) -> Option<$crate::tables::LookupTableKind> { Some($crate::tables::LookupTableKind::$table( @@ -41,7 +66,9 @@ macro_rules! impl_lookup_table { } }; ($instr:ident, None) => { - impl $crate::traits::InstructionLookupTable for $instr { + impl + $crate::traits::InstructionLookupTable for $instr + { #[inline] fn lookup_table(&self) -> Option<$crate::tables::LookupTableKind> { None @@ -80,3 +107,23 @@ pub trait LookupQuery { /// Computes the output lookup entry for this instruction as a u64. fn to_lookup_output(&self) -> u64; } + +#[cfg(test)] +mod tests { + use super::*; + use jolt_riscv::{ + instructions::{Add, Ld, Noop}, + JoltInstructionRow, + }; + + #[test] + fn aggregate_instruction_dispatches_lookup_table() { + let add = JoltInstruction::Add(Add(JoltInstructionRow::default())); + let load = JoltInstruction::Ld(Ld(JoltInstructionRow::default())); + let noop = JoltInstruction::Noop(Noop(JoltInstructionRow::default())); + + assert!(InstructionLookupTable::<64>::lookup_table(&add).is_some()); + assert!(InstructionLookupTable::<64>::lookup_table(&load).is_none()); + assert!(InstructionLookupTable::<64>::lookup_table(&noop).is_none()); + } +} diff --git a/crates/jolt-lookup-tables/tests/core_lookup_table_abi.rs b/crates/jolt-lookup-tables/tests/core_lookup_table_abi.rs new file mode 100644 index 0000000000..2a0c1be893 --- /dev/null +++ b/crates/jolt-lookup-tables/tests/core_lookup_table_abi.rs @@ -0,0 +1,208 @@ +use jolt_core::zkvm::lookup_table::LookupTables as CoreLookupTables; +use jolt_lookup_tables::tables::LookupTableKind; +use strum::EnumCount; + +const XLEN: usize = 64; + +fn core_index(table: CoreLookupTables) -> usize { + CoreLookupTables::enum_index(&table) +} + +#[test] +fn modular_lookup_table_indices_match_core_abi() { + let cases = [ + ( + LookupTableKind::::RangeCheck(Default::default()).index(), + core_index(CoreLookupTables::::RangeCheck(Default::default())), + ), + ( + LookupTableKind::::RangeCheckAligned(Default::default()).index(), + core_index(CoreLookupTables::::RangeCheckAligned( + Default::default(), + )), + ), + ( + LookupTableKind::::And(Default::default()).index(), + core_index(CoreLookupTables::::And(Default::default())), + ), + ( + LookupTableKind::::Andn(Default::default()).index(), + core_index(CoreLookupTables::::Andn(Default::default())), + ), + ( + LookupTableKind::::Or(Default::default()).index(), + core_index(CoreLookupTables::::Or(Default::default())), + ), + ( + LookupTableKind::::Xor(Default::default()).index(), + core_index(CoreLookupTables::::Xor(Default::default())), + ), + ( + LookupTableKind::::Equal(Default::default()).index(), + core_index(CoreLookupTables::::Equal(Default::default())), + ), + ( + LookupTableKind::::SignedGreaterThanEqual(Default::default()).index(), + core_index(CoreLookupTables::::SignedGreaterThanEqual( + Default::default(), + )), + ), + ( + LookupTableKind::::UnsignedGreaterThanEqual(Default::default()).index(), + core_index(CoreLookupTables::::UnsignedGreaterThanEqual( + Default::default(), + )), + ), + ( + LookupTableKind::::NotEqual(Default::default()).index(), + core_index(CoreLookupTables::::NotEqual(Default::default())), + ), + ( + LookupTableKind::::SignedLessThan(Default::default()).index(), + core_index(CoreLookupTables::::SignedLessThan(Default::default())), + ), + ( + LookupTableKind::::UnsignedLessThan(Default::default()).index(), + core_index(CoreLookupTables::::UnsignedLessThan( + Default::default(), + )), + ), + ( + LookupTableKind::::SignMask(Default::default()).index(), + core_index(CoreLookupTables::::Movsign(Default::default())), + ), + ( + LookupTableKind::::UpperWord(Default::default()).index(), + core_index(CoreLookupTables::::UpperWord(Default::default())), + ), + ( + LookupTableKind::::UnsignedLessThanEqual(Default::default()).index(), + core_index(CoreLookupTables::::LessThanEqual(Default::default())), + ), + ( + LookupTableKind::::ValidUnsignedRemainder(Default::default()).index(), + core_index(CoreLookupTables::::ValidUnsignedRemainder( + Default::default(), + )), + ), + ( + LookupTableKind::::ValidDiv0(Default::default()).index(), + core_index(CoreLookupTables::::ValidDiv0(Default::default())), + ), + ( + LookupTableKind::::HalfwordAlignment(Default::default()).index(), + core_index(CoreLookupTables::::HalfwordAlignment( + Default::default(), + )), + ), + ( + LookupTableKind::::WordAlignment(Default::default()).index(), + core_index(CoreLookupTables::::WordAlignment(Default::default())), + ), + ( + LookupTableKind::::LowerHalfWord(Default::default()).index(), + core_index(CoreLookupTables::::LowerHalfWord(Default::default())), + ), + ( + LookupTableKind::::SignExtendHalfWord(Default::default()).index(), + core_index(CoreLookupTables::::SignExtendHalfWord( + Default::default(), + )), + ), + ( + LookupTableKind::::Pow2(Default::default()).index(), + core_index(CoreLookupTables::::Pow2(Default::default())), + ), + ( + LookupTableKind::::Pow2W(Default::default()).index(), + core_index(CoreLookupTables::::Pow2W(Default::default())), + ), + ( + LookupTableKind::::ShiftRightBitmask(Default::default()).index(), + core_index(CoreLookupTables::::ShiftRightBitmask( + Default::default(), + )), + ), + ( + LookupTableKind::::VirtualRev8W(Default::default()).index(), + core_index(CoreLookupTables::::VirtualRev8W(Default::default())), + ), + ( + LookupTableKind::::VirtualSRL(Default::default()).index(), + core_index(CoreLookupTables::::VirtualSRL(Default::default())), + ), + ( + LookupTableKind::::VirtualSRA(Default::default()).index(), + core_index(CoreLookupTables::::VirtualSRA(Default::default())), + ), + ( + LookupTableKind::::VirtualROTR(Default::default()).index(), + core_index(CoreLookupTables::::VirtualROTR(Default::default())), + ), + ( + LookupTableKind::::VirtualROTRW(Default::default()).index(), + core_index(CoreLookupTables::::VirtualROTRW(Default::default())), + ), + ( + LookupTableKind::::VirtualChangeDivisor(Default::default()).index(), + core_index(CoreLookupTables::::VirtualChangeDivisor( + Default::default(), + )), + ), + ( + LookupTableKind::::VirtualChangeDivisorW(Default::default()).index(), + core_index(CoreLookupTables::::VirtualChangeDivisorW( + Default::default(), + )), + ), + ( + LookupTableKind::::MulUNoOverflow(Default::default()).index(), + core_index(CoreLookupTables::::MulUNoOverflow(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROT32(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROT32(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROT24(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROT24(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROT16(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROT16(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROT63(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROT63(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROTW16(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROTW16( + Default::default(), + )), + ), + ( + LookupTableKind::::VirtualXORROTW12(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROTW12( + Default::default(), + )), + ), + ( + LookupTableKind::::VirtualXORROTW8(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROTW8(Default::default())), + ), + ( + LookupTableKind::::VirtualXORROTW7(Default::default()).index(), + core_index(CoreLookupTables::::VirtualXORROTW7(Default::default())), + ), + ]; + + assert_eq!(CoreLookupTables::::COUNT, cases.len()); + for (modular_index, core_index) in cases { + assert_eq!(modular_index, core_index); + } + assert_eq!( + LookupTableKind::::COUNT, + CoreLookupTables::::COUNT + ); +} diff --git a/crates/jolt-openings/src/claims.rs b/crates/jolt-openings/src/claims.rs index d4b95bf0bd..d4b3d4883b 100644 --- a/crates/jolt-openings/src/claims.rs +++ b/crates/jolt-openings/src/claims.rs @@ -1,20 +1,34 @@ //! Stateless claim types for PCS operations. use jolt_field::Field; -use jolt_poly::Polynomial; +use jolt_poly::{Point, Polynomial, HIGH_TO_LOW}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EvaluationClaim { + pub point: Point, + pub value: F, +} + +impl EvaluationClaim { + pub fn new(point: impl Into>, value: F) -> Self { + Self { + point: point.into(), + value, + } + } +} /// Prover-side opening claim: polynomial, evaluation point, and claimed value. #[derive(Clone, Debug)] -pub struct ProverClaim> { +pub struct ProverOpeningClaim> { pub polynomial: P, - pub point: Vec, - pub eval: F, + pub evaluation: EvaluationClaim, } /// Verifier-side opening claim: commitment, point, and claimed value. #[derive(Clone, Debug)] -pub struct VerifierClaim { +pub struct VerifierOpeningClaim { pub commitment: C, - pub point: Vec, - pub eval: F, + pub evaluation: EvaluationClaim, } diff --git a/crates/jolt-openings/src/lib.rs b/crates/jolt-openings/src/lib.rs index 1a66e5b36e..f38aa1bde6 100644 --- a/crates/jolt-openings/src/lib.rs +++ b/crates/jolt-openings/src/lib.rs @@ -6,8 +6,9 @@ //! //! # Design //! -//! - **Stateless.** No accumulators. Claims are plain data ([`ProverClaim`], -//! [`VerifierClaim`]) collected by the caller in `Vec`s. +//! - **Stateless.** No accumulators. Claims are plain data +//! ([`ProverOpeningClaim`], [`VerifierOpeningClaim`]) collected by the caller +//! in `Vec`s. //! - **Reduction is separate from proving.** [`reduce_prover`] / //! [`reduce_verifier`] transform claims (many → fewer) via RLC. //! The PCS opens the reduced claims. @@ -35,7 +36,7 @@ pub mod mock; mod reduction; mod schemes; -pub use claims::{ProverClaim, VerifierClaim}; +pub use claims::{EvaluationClaim, ProverOpeningClaim, VerifierOpeningClaim}; pub use error::OpeningsError; pub use reduction::{reduce_prover, reduce_verifier, rlc_combine, rlc_combine_scalars}; diff --git a/crates/jolt-openings/src/mock.rs b/crates/jolt-openings/src/mock.rs index 8ff531493c..5d079454f1 100644 --- a/crates/jolt-openings/src/mock.rs +++ b/crates/jolt-openings/src/mock.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use jolt_crypto::Commitment; use jolt_field::Field; use jolt_poly::Polynomial; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; use serde::{Deserialize, Serialize}; use jolt_crypto::HomomorphicCommitment; @@ -13,7 +13,8 @@ use jolt_crypto::HomomorphicCommitment; use crate::error::OpeningsError; use crate::schemes::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] pub struct MockCommitmentScheme(PhantomData); /// Stores the full evaluation table so `combine` is truly homomorphic. @@ -23,7 +24,15 @@ pub struct MockCommitment { evaluations: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] +impl Default for MockCommitment { + fn default() -> Self { + Self { + evaluations: Vec::new(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] pub struct MockProof { evaluations: Vec, @@ -118,6 +127,10 @@ impl CommitmentScheme for MockCommitmentScheme { } impl HomomorphicCommitment for MockCommitment { + fn add(c1: &Self, c2: &Self) -> Self { + Self::linear_combine(c1, c2, &F::one()) + } + fn linear_combine(c1: &Self, c2: &Self, scalar: &F) -> Self { let len = c1.evaluations.len().max(c2.evaluations.len()); let mut result = vec![F::zero(); len]; @@ -190,18 +203,37 @@ impl ZkOpeningScheme for MockCommitmentScheme { fn verify_zk( commitment: &Self::Output, - _point: &[Self::Field], + point: &[Self::Field], proof: &Self::Proof, _setup: &Self::VerifierSetup, _transcript: &mut impl Transcript, - ) -> Result<(), OpeningsError> { + ) -> Result { if commitment.evaluations != proof.evaluations { return Err(OpeningsError::CommitmentMismatch { expected: format!("len={}", commitment.evaluations.len()), actual: format!("len={}", proof.evaluations.len()), }); } - Ok(()) + let poly = Polynomial::new(proof.evaluations.clone()); + Ok(MockHidingCommitment { + eval: poly.evaluate(point), + }) + } + + fn bind_zk_opening_inputs( + transcript: &mut impl Transcript, + point: &[Self::Field], + hiding_commitment: &Self::HidingCommitment, + ) { + transcript.append(&LabelWithCount( + b"mock_zk_opening_point", + point.len() as u64, + )); + for p in point { + p.append_to_transcript(transcript); + } + transcript.append(&Label(b"mock_zk_eval_commitment")); + hiding_commitment.append_to_transcript(transcript); } } @@ -209,7 +241,9 @@ impl ZkOpeningScheme for MockCommitmentScheme { #[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] mod tests { use super::*; - use crate::{reduce_prover, reduce_verifier, ProverClaim, VerifierClaim}; + use crate::{ + reduce_prover, reduce_verifier, EvaluationClaim, ProverOpeningClaim, VerifierOpeningClaim, + }; use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; use jolt_poly::Polynomial; use jolt_transcript::Blake2bTranscript; @@ -315,18 +349,16 @@ mod tests { for (i, (poly, point)) in prover_polys.iter().enumerate() { let eval = poly.evaluate(point); - prover_claims.push(ProverClaim { + prover_claims.push(ProverOpeningClaim { polynomial: Polynomial::new(poly.evaluations().to_vec()), - point: point.clone(), - eval, + evaluation: EvaluationClaim::new(point.clone(), eval), }); let (commitment, ()) = MockPCS::commit(poly.evaluations(), &()); let v_eval = verifier_evals.map_or(eval, |overrides| overrides[i]); - verifier_claims.push(VerifierClaim { + verifier_claims.push(VerifierOpeningClaim { commitment, - point: point.clone(), - eval: v_eval, + evaluation: EvaluationClaim::new(point.clone(), v_eval), }); } @@ -338,8 +370,8 @@ mod tests { .map(|claim| { MockPCS::open( &claim.polynomial, - &claim.point, - claim.eval, + &claim.evaluation.point, + claim.evaluation.value, &(), None, &mut transcript_p, @@ -356,8 +388,8 @@ mod tests { for (claim, proof) in reduced_verifier.iter().zip(proofs.iter()) { MockPCS::verify( &claim.commitment, - &claim.point, - claim.eval, + &claim.evaluation.point, + claim.evaluation.value, proof, &(), &mut transcript_v, @@ -437,20 +469,17 @@ mod tests { let s: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); let claims = vec![ - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(p1.evaluations().to_vec()), - point: r.clone(), - eval: p1.evaluate(&r), + evaluation: EvaluationClaim::new(r.clone(), p1.evaluate(&r)), }, - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(p2.evaluations().to_vec()), - point: r.clone(), - eval: p2.evaluate(&r), + evaluation: EvaluationClaim::new(r.clone(), p2.evaluate(&r)), }, - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(p3.evaluations().to_vec()), - point: s.clone(), - eval: p3.evaluate(&s), + evaluation: EvaluationClaim::new(s.clone(), p3.evaluate(&s)), }, ]; @@ -472,9 +501,10 @@ mod tests { let (proof, eval_com, _blinding) = MockPCS::open_zk(&poly, &point, eval, &(), (), &mut transcript_p); - let _ = eval_com; let mut transcript_v = Blake2bTranscript::new(b"zk-test"); - MockPCS::verify_zk(&commitment, &point, &proof, &(), &mut transcript_v) - .expect("valid ZK proof should verify"); + let verified_eval_com = + MockPCS::verify_zk(&commitment, &point, &proof, &(), &mut transcript_v) + .expect("valid ZK proof should verify"); + assert_eq!(verified_eval_com, eval_com); } } diff --git a/crates/jolt-openings/src/reduction.rs b/crates/jolt-openings/src/reduction.rs index 9cd5402312..1b5a074488 100644 --- a/crates/jolt-openings/src/reduction.rs +++ b/crates/jolt-openings/src/reduction.rs @@ -1,9 +1,10 @@ //! Opening claim reduction via random linear combination (RLC). use jolt_field::Field; +use jolt_poly::{Point, HIGH_TO_LOW}; use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; -use crate::claims::{ProverClaim, VerifierClaim}; +use crate::claims::{EvaluationClaim, ProverOpeningClaim, VerifierOpeningClaim}; use crate::error::OpeningsError; use crate::schemes::AdditivelyHomomorphic; use jolt_crypto::HomomorphicCommitment; @@ -11,16 +12,16 @@ use jolt_crypto::HomomorphicCommitment; /// Groups claims by point, draws ρ per group, combines: p = Σ ρ^i · p_i. #[tracing::instrument(skip_all, name = "reduce_prover")] pub fn reduce_prover>( - claims: Vec>, + claims: Vec>, transcript: &mut T, -) -> Vec> { +) -> Vec> { if claims.is_empty() { return Vec::new(); } transcript.append(&LabelWithCount(b"rlc_claims", claims.len() as u64)); for claim in &claims { - claim.eval.append_to_transcript(transcript); + claim.evaluation.value.append_to_transcript(transcript); } let groups = group_prover_claims_by_point(claims); @@ -33,15 +34,14 @@ pub fn reduce_prover>( .iter() .map(|c| c.polynomial.evaluations()) .collect(); - let evals: Vec = group_claims.iter().map(|c| c.eval).collect(); + let evals: Vec = group_claims.iter().map(|c| c.evaluation.value).collect(); let combined_evals = rlc_combine(&eval_slices, rho); let combined_eval = rlc_combine_scalars(&evals, rho); - reduced.push(ProverClaim { + reduced.push(ProverOpeningClaim { polynomial: combined_evals.into(), - point, - eval: combined_eval, + evaluation: EvaluationClaim::new(point, combined_eval), }); } @@ -55,9 +55,9 @@ pub fn reduce_prover>( )] #[tracing::instrument(skip_all, name = "reduce_verifier")] pub fn reduce_verifier( - claims: Vec>, + claims: Vec>, transcript: &mut T, -) -> Result>, OpeningsError> +) -> Result>, OpeningsError> where PCS: AdditivelyHomomorphic, PCS::Output: HomomorphicCommitment, @@ -69,7 +69,7 @@ where transcript.append(&LabelWithCount(b"rlc_claims", claims.len() as u64)); for claim in &claims { - claim.eval.append_to_transcript(transcript); + claim.evaluation.value.append_to_transcript(transcript); } let groups = group_verifier_claims_by_point(claims); @@ -80,16 +80,15 @@ where let commitments: Vec = group_claims.iter().map(|c| c.commitment.clone()).collect(); - let evals: Vec = group_claims.iter().map(|c| c.eval).collect(); + let evals: Vec = group_claims.iter().map(|c| c.evaluation.value).collect(); let powers = rho_powers(rho, commitments.len()); let combined_commitment = PCS::combine(&commitments, &powers); let combined_eval = rlc_combine_scalars(&evals, rho); - reduced.push(VerifierClaim { + reduced.push(VerifierOpeningClaim { commitment: combined_commitment, - point, - eval: combined_eval, + evaluation: EvaluationClaim::new(point, combined_eval), }); } @@ -134,16 +133,21 @@ fn rho_powers(rho: F, n: usize) -> Vec { .collect() } -type PointGroup = Vec<(Vec, Vec>)>; -type VerifierPointGroup = Vec<(Vec, Vec>)>; +type PointGroup = Vec<(Point, Vec>)>; +type VerifierPointGroup = Vec<(Point, Vec>)>; -fn group_prover_claims_by_point(claims: Vec>) -> PointGroup { +fn group_prover_claims_by_point( + claims: Vec>, +) -> PointGroup { let mut groups: PointGroup = Vec::new(); for claim in claims { - if let Some((_, group)) = groups.iter_mut().find(|(point, _)| *point == claim.point) { + if let Some((_, group)) = groups + .iter_mut() + .find(|(point, _)| *point == claim.evaluation.point) + { group.push(claim); } else { - let point = claim.point.clone(); + let point = claim.evaluation.point.clone(); groups.push((point, vec![claim])); } } @@ -151,14 +155,17 @@ fn group_prover_claims_by_point(claims: Vec>) -> } fn group_verifier_claims_by_point( - claims: Vec>, + claims: Vec>, ) -> VerifierPointGroup { let mut groups: VerifierPointGroup = Vec::new(); for claim in claims { - if let Some((_, group)) = groups.iter_mut().find(|(point, _)| *point == claim.point) { + if let Some((_, group)) = groups + .iter_mut() + .find(|(point, _)| *point == claim.evaluation.point) + { group.push(claim); } else { - let point = claim.point.clone(); + let point = claim.evaluation.point.clone(); groups.push((point, vec![claim])); } } @@ -246,15 +253,13 @@ mod tests { fn group_prover_claims_same_point() { let point = vec![Fr::from_u64(1), Fr::from_u64(2)]; let claims = vec![ - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(vec![Fr::from_u64(10)]), - point: point.clone(), - eval: Fr::from_u64(10), + evaluation: EvaluationClaim::new(point.clone(), Fr::from_u64(10)), }, - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(vec![Fr::from_u64(20)]), - point: point.clone(), - eval: Fr::from_u64(20), + evaluation: EvaluationClaim::new(point.clone(), Fr::from_u64(20)), }, ]; let groups = group_prover_claims_by_point(claims); @@ -265,15 +270,13 @@ mod tests { #[test] fn group_prover_claims_different_points() { let claims = vec![ - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(vec![Fr::from_u64(10)]), - point: vec![Fr::from_u64(1)], - eval: Fr::from_u64(10), + evaluation: EvaluationClaim::new(vec![Fr::from_u64(1)], Fr::from_u64(10)), }, - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(vec![Fr::from_u64(20)]), - point: vec![Fr::from_u64(2)], - eval: Fr::from_u64(20), + evaluation: EvaluationClaim::new(vec![Fr::from_u64(2)], Fr::from_u64(20)), }, ]; let groups = group_prover_claims_by_point(claims); diff --git a/crates/jolt-openings/src/schemes.rs b/crates/jolt-openings/src/schemes.rs index 24cb699e0b..97e68e59aa 100644 --- a/crates/jolt-openings/src/schemes.rs +++ b/crates/jolt-openings/src/schemes.rs @@ -16,9 +16,9 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::error::OpeningsError; /// Commit to f: F^n -> F, then prove f(r) = v for verifier-chosen r. -pub trait CommitmentScheme: Commitment + Clone + Send + Sync + 'static { +pub trait CommitmentScheme: Commitment { type Field: Field; - type Proof: Clone + Send + Sync + Serialize + DeserializeOwned; + type Proof: Clone + Debug + Eq + Send + Sync + 'static + Serialize + DeserializeOwned; type ProverSetup: Clone + Send + Sync; type VerifierSetup: Clone + Send + Sync + Serialize + DeserializeOwned; @@ -124,11 +124,19 @@ pub trait ZkOpeningScheme: CommitmentScheme { transcript: &mut impl Transcript, ) -> (Self::Proof, Self::HidingCommitment, Self::Blind); + /// Verify a ZK opening proof and return the hiding commitment to the + /// evaluation that the proof binds internally. fn verify_zk( commitment: &Self::Output, point: &[Self::Field], proof: &Self::Proof, setup: &Self::VerifierSetup, transcript: &mut impl Transcript, - ) -> Result<(), OpeningsError>; + ) -> Result; + + fn bind_zk_opening_inputs( + transcript: &mut impl Transcript, + point: &[Self::Field], + hiding_commitment: &Self::HidingCommitment, + ); } diff --git a/crates/jolt-openings/tests/reduction.rs b/crates/jolt-openings/tests/reduction.rs index 38f5e5ff09..a91335f186 100644 --- a/crates/jolt-openings/tests/reduction.rs +++ b/crates/jolt-openings/tests/reduction.rs @@ -13,9 +13,12 @@ reason = "tests may panic on assertion failures" )] -use jolt_field::{Fr, FromPrimitiveInt, RandomSampling, ReducingBytes}; +use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; use jolt_openings::mock::MockCommitmentScheme; -use jolt_openings::{reduce_prover, reduce_verifier, CommitmentScheme, ProverClaim, VerifierClaim}; +use jolt_openings::{ + reduce_prover, reduce_verifier, CommitmentScheme, EvaluationClaim, ProverOpeningClaim, + VerifierOpeningClaim, +}; use jolt_poly::Polynomial; use jolt_transcript::{Blake2bTranscript, KeccakTranscript, Transcript}; use rand_chacha::ChaCha20Rng; @@ -36,16 +39,14 @@ fn reduce_open_verify>( for (poly, point) in polys.iter().zip(points.iter()) { let eval = poly.evaluate(point); - prover_claims.push(ProverClaim { + prover_claims.push(ProverOpeningClaim { polynomial: Polynomial::new(poly.evaluations().to_vec()), - point: point.clone(), - eval, + evaluation: EvaluationClaim::new(point.clone(), eval), }); let (commitment, ()) = MockPCS::commit(poly.evaluations(), &()); - verifier_claims.push(VerifierClaim { + verifier_claims.push(VerifierOpeningClaim { commitment, - point: point.clone(), - eval, + evaluation: EvaluationClaim::new(point.clone(), eval), }); } @@ -57,8 +58,8 @@ fn reduce_open_verify>( .map(|c| { MockPCS::open( &c.polynomial, - &c.point, - c.eval, + &c.evaluation.point, + c.evaluation.value, &(), None, &mut transcript_p, @@ -75,8 +76,8 @@ fn reduce_open_verify>( for (claim, proof) in reduced_v.iter().zip(proofs.iter()) { MockPCS::verify( &claim.commitment, - &claim.point, - claim.eval, + &claim.evaluation.point, + claim.evaluation.value, proof, &(), &mut transcript_v, @@ -177,30 +178,26 @@ fn tampered_eval_detected() { let eval_b = poly_b.evaluate(&point); let prover_claims = vec![ - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(poly_a.evaluations().to_vec()), - point: point.clone(), - eval: eval_a, + evaluation: EvaluationClaim::new(point.clone(), eval_a), }, - ProverClaim { + ProverOpeningClaim { polynomial: Polynomial::new(poly_b.evaluations().to_vec()), - point: point.clone(), - eval: eval_b, + evaluation: EvaluationClaim::new(point.clone(), eval_b), }, ]; let (com_a, ()) = MockPCS::commit(poly_a.evaluations(), &()); let (com_b, ()) = MockPCS::commit(poly_b.evaluations(), &()); let verifier_claims = vec![ - VerifierClaim { + VerifierOpeningClaim { commitment: com_a, - point: point.clone(), - eval: eval_a, + evaluation: EvaluationClaim::new(point.clone(), eval_a), }, - VerifierClaim { + VerifierOpeningClaim { commitment: com_b, - point: point.clone(), - eval: eval_b + Fr::from_u64(1), // tampered + evaluation: EvaluationClaim::new(point.clone(), eval_b + Fr::from_u64(1)), }, ]; @@ -211,8 +208,8 @@ fn tampered_eval_detected() { .map(|c| { MockPCS::open( &c.polynomial, - &c.point, - c.eval, + &c.evaluation.point, + c.evaluation.value, &(), None, &mut transcript_p, @@ -228,8 +225,8 @@ fn tampered_eval_detected() { for (claim, proof) in reduced_v.iter().zip(proofs.iter()) { if MockPCS::verify( &claim.commitment, - &claim.point, - claim.eval, + &claim.evaluation.point, + claim.evaluation.value, proof, &(), &mut transcript_v, diff --git a/crates/jolt-poly/Cargo.toml b/crates/jolt-poly/Cargo.toml index abf1497eb4..738aae75ee 100644 --- a/crates/jolt-poly/Cargo.toml +++ b/crates/jolt-poly/Cargo.toml @@ -18,6 +18,7 @@ tracing.workspace = true rayon = { workspace = true, optional = true } rand_core = { workspace = true } num-traits = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] rand = { workspace = true } diff --git a/crates/jolt-poly/src/eq.rs b/crates/jolt-poly/src/eq.rs index 94dfcf44b3..a6891ca6a7 100644 --- a/crates/jolt-poly/src/eq.rs +++ b/crates/jolt-poly/src/eq.rs @@ -6,6 +6,7 @@ use jolt_field::Field; use serde::{Deserialize, Serialize}; use crate::math::Math; +use crate::mle::MleError; use crate::thread::unsafe_allocate_zero_vec; /// Equality polynomial $\widetilde{eq}(x, r) = \prod_{i=1}^{n}(r_i x_i + (1-r_i)(1-x_i))$. @@ -116,6 +117,34 @@ impl EqPolynomial { } } +pub fn try_eq_mle(left: &[F], right: &[F]) -> Result { + if left.len() != right.len() { + return Err(MleError::EqualityArityMismatch { + left: left.len(), + right: right.len(), + }); + } + Ok(EqPolynomial::::mle(left, right)) +} + +pub fn eq_index_msb(point: &[F], index: usize) -> F { + let mut eq = F::one(); + for (position, challenge) in point.iter().enumerate() { + let shift = point.len() - 1 - position; + let bit = if shift < usize::BITS as usize { + (index >> shift) & 1 + } else { + 0 + }; + if bit == 1 { + eq *= *challenge; + } else { + eq *= F::one() - *challenge; + } + } + eq +} + /// Static (point-free) evaluation methods for eq polynomial tables. /// /// These accept challenge or field-element slices and produce materialized diff --git a/crates/jolt-poly/src/identity.rs b/crates/jolt-poly/src/identity.rs index 2cc61284b5..1de0ea798d 100644 --- a/crates/jolt-poly/src/identity.rs +++ b/crates/jolt-poly/src/identity.rs @@ -3,6 +3,59 @@ use jolt_field::Field; use serde::{Deserialize, Serialize}; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum OperandSide { + Left, + Right, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OperandPolynomial { + num_vars: usize, + side: OperandSide, +} + +impl OperandPolynomial { + pub const fn new(num_vars: usize, side: OperandSide) -> Self { + Self { num_vars, side } + } + + pub const fn side(&self) -> OperandSide { + self.side + } +} + +impl crate::MultilinearEvaluation for OperandPolynomial { + fn num_vars(&self) -> usize { + self.num_vars + } + + fn len(&self) -> usize { + 1 << self.num_vars + } + + fn evaluate(&self, point: &[F]) -> F { + assert_eq!( + point.len(), + self.num_vars, + "point dimension must match num_vars" + ); + assert!( + self.num_vars.is_multiple_of(2), + "operand polynomial requires an even number of variables" + ); + + let offset = match self.side { + OperandSide::Left => 0, + OperandSide::Right => 1, + }; + let bits = self.num_vars / 2; + (0..bits).fold(F::zero(), |acc, bit_index| { + acc + point[2 * bit_index + offset].mul_pow_2(bits - 1 - bit_index) + }) + } +} + /// Identity polynomial: $\widetilde{I}(x) = \sum_{i=0}^{2^n - 1} i \cdot \widetilde{eq}(x, i)$. /// /// At each Boolean hypercube point $b \in \{0,1\}^n$, this polynomial evaluates to the @@ -23,23 +76,6 @@ impl IdentityPolynomial { pub fn num_vars(&self) -> usize { self.num_vars } - - /// Evaluates $\widetilde{I}(r) = \sum_{i=1}^{n} r_i \cdot 2^{n-i}$. - /// - /// Time: $O(n)$. No heap allocation. - #[inline] - pub fn evaluate(&self, point: &[F]) -> F { - assert_eq!( - point.len(), - self.num_vars, - "point dimension must match num_vars" - ); - let n = self.num_vars; - point - .iter() - .enumerate() - .fold(F::zero(), |acc, (i, &r_i)| acc + r_i.mul_pow_2(n - 1 - i)) - } } impl crate::MultilinearEvaluation for IdentityPolynomial { @@ -52,13 +88,23 @@ impl crate::MultilinearEvaluation for IdentityPolynomial { } fn evaluate(&self, point: &[F]) -> F { - IdentityPolynomial::evaluate(self, point) + assert_eq!( + point.len(), + self.num_vars, + "point dimension must match num_vars" + ); + let n = self.num_vars; + point + .iter() + .enumerate() + .fold(F::zero(), |acc, (i, &r_i)| acc + r_i.mul_pow_2(n - 1 - i)) } } #[cfg(test)] mod tests { use super::*; + use crate::MultilinearEvaluation; use jolt_field::Fr; use jolt_field::FromPrimitiveInt; use num_traits::{One, Zero}; @@ -89,7 +135,9 @@ mod tests { #[test] fn zero_vars() { let id = IdentityPolynomial::new(0); - assert!(id.evaluate::(&[]).is_zero()); + assert!( + >::evaluate(&id, &[]).is_zero() + ); } #[test] @@ -98,4 +146,25 @@ mod tests { assert!(id.evaluate(&[Fr::zero()]).is_zero()); assert_eq!(id.evaluate(&[Fr::one()]), Fr::one()); } + + #[test] + fn operand_polynomial_splits_interleaved_left_and_right_bits() { + let point = [ + Fr::from_u64(1), + Fr::from_u64(0), + Fr::from_u64(0), + Fr::from_u64(1), + Fr::from_u64(1), + Fr::from_u64(1), + ]; + + assert_eq!( + OperandPolynomial::new(6, OperandSide::Left).evaluate(&point), + Fr::from_u64(5) + ); + assert_eq!( + OperandPolynomial::new(6, OperandSide::Right).evaluate(&point), + Fr::from_u64(3) + ); + } } diff --git a/crates/jolt-poly/src/lagrange.rs b/crates/jolt-poly/src/lagrange.rs index 485e0742f8..e10cf96fcf 100644 --- a/crates/jolt-poly/src/lagrange.rs +++ b/crates/jolt-poly/src/lagrange.rs @@ -4,6 +4,8 @@ //! protocols. All functions are generic over [`Field`] and operate on //! integer-indexed domains (symmetric or arbitrary). +use std::fmt; + use jolt_field::Field; /// Evaluates all Lagrange basis polynomials $L_0(r), \ldots, L_{N-1}(r)$ over @@ -61,6 +63,46 @@ pub fn lagrange_evals(domain_start: i64, domain_size: usize, r: F) -> result } +/// Evaluates all Lagrange basis polynomials over the centered consecutive +/// integer domain used by univariate-skip protocols. +pub fn centered_lagrange_evals( + domain_size: usize, + r: F, +) -> Result, CenteredIntegerDomainError> { + Ok(lagrange_evals( + centered_domain_start(domain_size)?, + domain_size, + r, + )) +} + +pub fn centered_lagrange_evals_array( + r: F, +) -> Result<[F; N], CenteredIntegerDomainError> { + let evals = centered_lagrange_evals(N, r)?; + let mut result = [F::zero(); N]; + for (dst, src) in result.iter_mut().zip(evals) { + *dst = src; + } + Ok(result) +} + +/// Computes `sum_i L_i(x) * L_i(y)` over the centered consecutive integer +/// domain used by univariate-skip protocols. +pub fn centered_lagrange_kernel( + domain_size: usize, + x: F, + y: F, +) -> Result { + let x_evals = centered_lagrange_evals(domain_size, x)?; + let y_evals = centered_lagrange_evals(domain_size, y)?; + Ok(x_evals + .into_iter() + .zip(y_evals) + .map(|(left, right)| left * right) + .sum()) +} + /// Computes power sums $S_k = \sum_{t=-D}^{D} t^k$ for $k = 0, 1, \ldots, \text{num\_powers}-1$ /// over the symmetric integer domain $\{-D, \ldots, D\}$ of size $2D+1$. /// @@ -80,6 +122,90 @@ pub fn symmetric_power_sums(half_width: i64, num_powers: usize) -> Vec { sums } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CenteredIntegerDomainError { + EmptyDomain, + DomainTooLarge { domain_size: usize }, + PowerSumOverflow { domain_size: usize, power: usize }, +} + +impl fmt::Display for CenteredIntegerDomainError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyDomain => write!(f, "centered integer domain must be non-empty"), + Self::DomainTooLarge { domain_size } => { + write!( + f, + "centered integer domain size {domain_size} exceeds i64::MAX" + ) + } + Self::PowerSumOverflow { domain_size, power } => write!( + f, + "centered integer domain size {domain_size} overflowed i128 at power {power}" + ), + } + } +} + +impl std::error::Error for CenteredIntegerDomainError {} + +/// Start of the centered consecutive-integer domain used by core univariate skip. +/// +/// The domain has `domain_size` consecutive integer points +/// `{start, start + 1, ..., start + domain_size - 1}` where +/// `start = -floor((domain_size - 1) / 2)`. +pub fn centered_domain_start(domain_size: usize) -> Result { + if domain_size == 0 { + return Err(CenteredIntegerDomainError::EmptyDomain); + } + if domain_size > i64::MAX as usize { + return Err(CenteredIntegerDomainError::DomainTooLarge { domain_size }); + } + Ok(-(((domain_size - 1) / 2) as i64)) +} + +/// Computes `S_k = sum_t t^k` over the centered consecutive-integer domain. +/// +/// This matches core's univariate-skip window convention for both odd and even +/// domain sizes. For example, size `3` is `{-1, 0, 1}` and size `4` is +/// `{-1, 0, 1, 2}`. +pub fn centered_power_sums( + domain_size: usize, + num_powers: usize, +) -> Result, CenteredIntegerDomainError> { + let start = centered_domain_start(domain_size)?; + let mut sums = vec![0i128; num_powers]; + if num_powers == 0 { + return Ok(sums); + } + + for offset in 0..domain_size { + let offset = i64::try_from(offset) + .map_err(|_| CenteredIntegerDomainError::DomainTooLarge { domain_size })?; + let t = i128::from( + start + .checked_add(offset) + .ok_or(CenteredIntegerDomainError::DomainTooLarge { domain_size })?, + ); + let mut pow = 1i128; + for (power, sum) in sums.iter_mut().enumerate() { + *sum = sum + .checked_add(pow) + .ok_or(CenteredIntegerDomainError::PowerSumOverflow { domain_size, power })?; + if power + 1 < num_powers { + pow = pow + .checked_mul(t) + .ok_or(CenteredIntegerDomainError::PowerSumOverflow { + domain_size, + power: power + 1, + })?; + } + } + } + + Ok(sums) +} + /// Polynomial multiplication in coefficient form. /// /// Given $p(x) = \sum a_i x^i$ and $q(x) = \sum b_j x^j$, returns @@ -155,6 +281,7 @@ pub fn interpolate_to_coeffs(domain_start: i64, values: &[F]) -> Vec(r).unwrap(); + + assert_eq!(evals, lagrange_evals(-2, 5, r)); + assert_eq!(evals_array.as_slice(), evals.as_slice()); + assert_eq!( + centered_lagrange_kernel(5, Fr::from_i64(-1), Fr::from_i64(-1)), + Ok(Fr::one()) + ); + assert_eq!( + centered_lagrange_kernel(5, Fr::from_i64(-1), Fr::from_i64(2)), + Ok(Fr::zero()) + ); + } + + #[test] + fn centered_power_sums_handle_even_and_odd_windows() { + assert_eq!(centered_power_sums(3, 4), Ok(vec![3, 0, 2, 0])); + assert_eq!(centered_power_sums(4, 4), Ok(vec![4, 2, 6, 8])); + } + + #[test] + fn centered_power_sums_reject_invalid_or_overflowing_inputs() { + assert_eq!( + centered_power_sums(0, 2), + Err(CenteredIntegerDomainError::EmptyDomain) + ); + assert!(matches!( + centered_power_sums(10, 100), + Err(CenteredIntegerDomainError::PowerSumOverflow { .. }) + )); + } + #[test] fn poly_mul_basic() { // (1 + 2x) * (3 + x) = 3 + 7x + 2x^2 diff --git a/crates/jolt-poly/src/lib.rs b/crates/jolt-poly/src/lib.rs index 88c2d27055..bfd351e730 100644 --- a/crates/jolt-poly/src/lib.rs +++ b/crates/jolt-poly/src/lib.rs @@ -38,8 +38,8 @@ //! //! # Utility Modules //! -//! - [`lagrange`]: Lagrange interpolation, symmetric power sums, polynomial multiplication, -//! Newton-form interpolation over integer domains +//! - [`lagrange`]: Lagrange interpolation, integer-domain power sums, polynomial +//! multiplication, Newton-form interpolation over integer domains //! - [`math`]: Bit-manipulation utilities on `usize` via the `Math` trait (`pow2`, `log_2`) //! - [`thread`]: `drop_in_background_thread` (rayon) and `unsafe_allocate_zero_vec` (zero-init allocation) @@ -52,18 +52,24 @@ mod identity; pub mod lagrange; mod lt; pub mod math; +mod mle; mod multilinear; mod one_hot; +mod point; pub mod thread; mod univariate; pub use binding::BindingOrder; pub use compressed_univariate::CompressedPoly; pub use dense::Polynomial; -pub use eq::EqPolynomial; +pub use eq::{eq_index_msb, try_eq_mle, EqPolynomial}; pub use eq_plus_one::{EqPlusOnePolynomial, EqPlusOnePrefixSuffix}; -pub use identity::IdentityPolynomial; +pub use identity::{IdentityPolynomial, OperandPolynomial, OperandSide}; pub use lt::LtPolynomial; +pub use mle::{ + block_selector_mle_msb, range_mask_mle_msb, sparse_mle_msb, sparse_segments_mle_msb, MleError, +}; pub use multilinear::{MultilinearBinding, MultilinearEvaluation, MultilinearPoly, RlcSource}; pub use one_hot::OneHotPolynomial; +pub use point::{Endianness, Point, HIGH_TO_LOW, LOW_TO_HIGH}; pub use univariate::{UnivariatePoly, UnivariatePolynomial}; diff --git a/crates/jolt-poly/src/mle.rs b/crates/jolt-poly/src/mle.rs new file mode 100644 index 0000000000..7988001c19 --- /dev/null +++ b/crates/jolt-poly/src/mle.rs @@ -0,0 +1,286 @@ +use jolt_field::Field; +use thiserror::Error; + +use crate::eq_index_msb; + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum MleError { + #[error("equality polynomial arity mismatch: left {left}, right {right}")] + EqualityArityMismatch { left: usize, right: usize }, + #[error("invalid MLE range [{start}, {end})")] + InvalidRange { start: u128, end: u128 }, + #[error("MLE arity {arity} exceeds u128 capacity")] + DomainTooLarge { arity: usize }, + #[error("MLE range end {end} exceeds domain size {domain_size}")] + RangeEndOutOfDomain { end: u128, domain_size: u128 }, + #[error("MLE block has {block_vars} variables but point has arity {arity}")] + BlockVariablesExceedArity { block_vars: usize, arity: usize }, + #[error("MLE block start {start_index} is not aligned to block size {block_size}")] + BlockStartUnaligned { + start_index: usize, + block_size: u128, + }, + #[error("MLE block end {end} exceeds domain size {domain_size}")] + BlockEndOutOfDomain { end: u128, domain_size: u128 }, +} + +pub fn sparse_mle_msb(start_index: usize, values: &[u64], point: &[F]) -> F { + values + .iter() + .enumerate() + .map(|(offset, value)| F::from_u64(*value) * eq_index_msb(point, start_index + offset)) + .sum() +} + +pub fn sparse_segments_mle_msb<'a, F, I>(segments: I, point: &[F]) -> F +where + F: Field, + I: IntoIterator, +{ + segments + .into_iter() + .map(|(start_index, values)| sparse_mle_msb(start_index, values, point)) + .sum() +} + +pub fn block_selector_mle_msb( + start_index: usize, + block_num_vars: usize, + point: &[F], +) -> Result { + if block_num_vars > point.len() { + return Err(MleError::BlockVariablesExceedArity { + block_vars: block_num_vars, + arity: point.len(), + }); + } + if block_num_vars >= usize::BITS as usize { + return Err(MleError::DomainTooLarge { + arity: block_num_vars, + }); + } + + let block_size = 1u128 + .checked_shl(block_num_vars as u32) + .ok_or(MleError::DomainTooLarge { + arity: block_num_vars, + })?; + let domain_size = 1u128 + .checked_shl(point.len() as u32) + .ok_or(MleError::DomainTooLarge { arity: point.len() })?; + let start = start_index as u128; + if !start.is_multiple_of(block_size) { + return Err(MleError::BlockStartUnaligned { + start_index, + block_size, + }); + } + let end = start + .checked_add(block_size) + .ok_or(MleError::DomainTooLarge { arity: point.len() })?; + if end > domain_size { + return Err(MleError::BlockEndOutOfDomain { end, domain_size }); + } + + let selector_point_len = point.len() - block_num_vars; + let block_index = usize::try_from(start / block_size) + .map_err(|_| MleError::DomainTooLarge { arity: point.len() })?; + Ok(eq_index_msb(&point[..selector_point_len], block_index)) +} + +pub fn range_mask_mle_msb( + range_start: u128, + range_end: u128, + point: &[F], +) -> Result { + if range_start >= range_end { + return Err(MleError::InvalidRange { + start: range_start, + end: range_end, + }); + } + let domain_size = 1u128 + .checked_shl(point.len() as u32) + .ok_or(MleError::DomainTooLarge { arity: point.len() })?; + if range_end > domain_size { + return Err(MleError::RangeEndOutOfDomain { + end: range_end, + domain_size, + }); + } + + Ok(less_than_mle_msb(range_end, point) - less_than_mle_msb(range_start, point)) +} + +fn less_than_mle_msb(bound: u128, point: &[F]) -> F { + if Some(bound) == 1u128.checked_shl(point.len() as u32) { + return F::one(); + } + let mut lt_bound = F::zero(); + let mut eq_bound = F::one(); + for (index, challenge) in point.iter().enumerate() { + if msb_bit(bound, point.len(), index) == 1 { + lt_bound += eq_bound * (F::one() - *challenge); + eq_bound *= *challenge; + } else { + eq_bound *= F::one() - *challenge; + } + } + lt_bound +} + +const fn msb_bit(value: u128, len: usize, position: usize) -> u8 { + let shift = len - 1 - position; + if shift < u128::BITS as usize { + ((value >> shift) & 1) as u8 + } else { + 0 + } +} + +#[cfg(test)] +mod tests { + #![expect(clippy::panic, reason = "tests fail loudly on unexpected errors")] + + use super::*; + use crate::{eq_index_msb, try_eq_mle}; + use jolt_field::{Fr, FromPrimitiveInt}; + use num_traits::{One, Zero}; + + #[test] + fn eq_index_uses_msb_order() { + let point = [Fr::from_u64(2), Fr::from_u64(3), Fr::from_u64(5)]; + + assert_eq!( + eq_index_msb(&point, 0b101), + point[0] * (Fr::one() - point[1]) * point[2] + ); + assert_eq!( + eq_index_msb(&point, 0b010), + (Fr::one() - point[0]) * point[1] * (Fr::one() - point[2]) + ); + } + + #[test] + fn checked_eq_mle_rejects_arity_mismatch() { + assert_eq!( + try_eq_mle::(&[Fr::from_u64(1)], &[Fr::from_u64(1), Fr::from_u64(0)]), + Err(MleError::EqualityArityMismatch { left: 1, right: 2 }) + ); + } + + #[test] + fn sparse_mle_matches_explicit_sum() { + let point = [Fr::from_u64(2), Fr::from_u64(3)]; + let values = [7, 11]; + + assert_eq!( + sparse_mle_msb(1, &values, &point), + Fr::from_u64(7) * eq_index_msb(&point, 1) + Fr::from_u64(11) * eq_index_msb(&point, 2) + ); + } + + #[test] + fn sparse_segments_mle_sums_segments() { + let point = [Fr::from_u64(2), Fr::from_u64(3)]; + let left = [7]; + let right = [11]; + + assert_eq!( + sparse_segments_mle_msb([(1, left.as_slice()), (2, right.as_slice())], &point), + sparse_mle_msb(1, &left, &point) + sparse_mle_msb(2, &right, &point) + ); + } + + #[test] + fn block_selector_evaluates_aligned_prefix() { + let point = [Fr::from_u64(2), Fr::from_u64(3), Fr::from_u64(5)]; + + assert_eq!( + block_selector_mle_msb(0b100, 1, &point) + .unwrap_or_else(|error| panic!("selector should evaluate: {error}")), + eq_index_msb(&point[..2], 0b10) + ); + assert_eq!( + block_selector_mle_msb(0, 3, &point) + .unwrap_or_else(|error| panic!("whole-domain selector should evaluate: {error}")), + Fr::one() + ); + } + + #[test] + fn block_selector_rejects_invalid_blocks() { + assert_eq!( + block_selector_mle_msb::(0, 3, &[Fr::zero(), Fr::zero()]), + Err(MleError::BlockVariablesExceedArity { + block_vars: 3, + arity: 2 + }) + ); + assert_eq!( + block_selector_mle_msb::(1, 1, &[Fr::zero(), Fr::zero()]), + Err(MleError::BlockStartUnaligned { + start_index: 1, + block_size: 2 + }) + ); + assert_eq!( + block_selector_mle_msb::(4, 1, &[Fr::zero(), Fr::zero()]), + Err(MleError::BlockEndOutOfDomain { + end: 6, + domain_size: 4 + }) + ); + assert_eq!( + block_selector_mle_msb::( + 0, + usize::BITS as usize, + &vec![Fr::zero(); usize::BITS as usize] + ), + Err(MleError::DomainTooLarge { + arity: usize::BITS as usize + }) + ); + } + + #[test] + fn range_mask_matches_vertex_membership() { + for index in 0..8 { + let point = [ + Fr::from_u64(((index >> 2) & 1) as u64), + Fr::from_u64(((index >> 1) & 1) as u64), + Fr::from_u64((index & 1) as u64), + ]; + let expected = if (2..5).contains(&index) { + Fr::one() + } else { + Fr::zero() + }; + assert_eq!( + range_mask_mle_msb(2, 5, &point) + .unwrap_or_else(|error| panic!("range mask should evaluate: {error}")), + expected + ); + } + } + + #[test] + fn range_mask_rejects_invalid_ranges() { + assert_eq!( + range_mask_mle_msb::(3, 3, &[Fr::zero(), Fr::zero()]), + Err(MleError::InvalidRange { start: 3, end: 3 }) + ); + assert_eq!( + range_mask_mle_msb(0, 4, &[Fr::from_u64(7), Fr::from_u64(11)]) + .unwrap_or_else(|error| panic!("full-domain range should evaluate: {error}")), + Fr::one() + ); + assert_eq!( + range_mask_mle_msb::(0, 5, &[Fr::zero(), Fr::zero()]), + Err(MleError::RangeEndOutOfDomain { + end: 5, + domain_size: 4 + }) + ); + } +} diff --git a/crates/jolt-poly/src/point.rs b/crates/jolt-poly/src/point.rs new file mode 100644 index 0000000000..31b78f8638 --- /dev/null +++ b/crates/jolt-poly/src/point.rs @@ -0,0 +1,130 @@ +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +pub type Endianness = bool; +pub const HIGH_TO_LOW: Endianness = false; +pub const LOW_TO_HIGH: Endianness = true; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Point { + coords: Vec, +} + +impl Point { + pub fn new(coords: impl Into>) -> Self { + Self { + coords: coords.into(), + } + } + + pub fn match_endianness(&self) -> Point + where + F: Clone, + { + let mut coords = self.coords.clone(); + if E != TARGET { + coords.reverse(); + } + Point::::new(coords) + } + + pub fn concat(points: impl IntoIterator) -> Self { + let mut coords = Vec::new(); + for point in points { + coords.extend(point.coords); + } + Self { coords } + } + + pub fn as_slice(&self) -> &[F] { + &self.coords + } + + pub fn into_vec(self) -> Vec { + self.coords + } + + pub fn len(&self) -> usize { + self.coords.len() + } + + pub fn is_empty(&self) -> bool { + self.coords.is_empty() + } +} + +impl Point { + pub fn high_to_low(coords: impl Into>) -> Self { + Self::new(coords) + } +} + +impl Point { + pub fn low_to_high(coords: impl Into>) -> Self { + Self::new(coords) + } +} + +impl From> for Point { + fn from(coords: Vec) -> Self { + Self::new(coords) + } +} + +impl From> for Vec { + fn from(point: Point) -> Self { + point.coords + } +} + +impl AsRef<[F]> for Point { + fn as_ref(&self) -> &[F] { + self.as_slice() + } +} + +impl Deref for Point { + type Target = [F]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl PartialEq> for Point { + fn eq(&self, other: &Vec) -> bool { + self.as_slice() == other.as_slice() + } +} + +#[cfg(test)] +mod tests { + use super::{Point, HIGH_TO_LOW, LOW_TO_HIGH}; + + #[test] + fn match_endianness_reverses_when_order_changes() { + let point = Point::::low_to_high(vec![1, 2, 3]); + let canonical = point.match_endianness::(); + + assert_eq!(canonical.into_vec(), vec![3, 2, 1]); + } + + #[test] + fn match_endianness_preserves_matching_order() { + let point = Point::::high_to_low(vec![1, 2, 3]); + let canonical = point.match_endianness::(); + + assert_eq!(canonical.into_vec(), vec![1, 2, 3]); + } + + #[test] + fn concat_preserves_point_order() { + let point = Point::::concat([ + Point::::high_to_low(vec![1, 2]), + Point::::low_to_high(vec![3, 4]).match_endianness::(), + ]); + + assert_eq!(point.into_vec(), vec![1, 2, 4, 3]); + } +} diff --git a/crates/jolt-poly/tests/integration.rs b/crates/jolt-poly/tests/integration.rs index 5ffa348a39..f9c5981c1e 100644 --- a/crates/jolt-poly/tests/integration.rs +++ b/crates/jolt-poly/tests/integration.rs @@ -7,7 +7,8 @@ use jolt_field::{Fr, FromPrimitiveInt, RandomSampling}; use jolt_poly::{ - EqPolynomial, IdentityPolynomial, MultilinearPoly, Polynomial, RlcSource, UnivariatePoly, + EqPolynomial, IdentityPolynomial, MultilinearEvaluation, MultilinearPoly, Polynomial, + RlcSource, UnivariatePoly, }; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -179,7 +180,7 @@ fn identity_polynomial_boolean_indexing() { } }) .collect(); - let eval = id.evaluate::(&bits); + let eval = id.evaluate(&bits); assert_eq!(eval, Fr::from_u64(idx as u64), "identity at index {idx}"); } } @@ -192,7 +193,7 @@ fn identity_polynomial_random_point() { let id = IdentityPolynomial::new(nv); let point: Vec = (0..nv).map(|_| Fr::random(&mut rng)).collect(); - let eval = id.evaluate::(&point); + let eval = id.evaluate(&point); // Manual: sum_i r_i * 2^(n-1-i) let expected: Fr = point diff --git a/crates/jolt-program/Cargo.toml b/crates/jolt-program/Cargo.toml index 33c66a9c3e..988910d36d 100644 --- a/crates/jolt-program/Cargo.toml +++ b/crates/jolt-program/Cargo.toml @@ -12,14 +12,20 @@ categories = ["cryptography"] workspace = true [features] -default = ["std"] +default = ["std", "serialization"] +serialization = ["dep:ark-serialize", "dep:serde", "jolt-riscv/serialization"] std = ["common/std"] image = ["dep:object"] [dependencies] -ark-serialize.workspace = true +ark-serialize = { workspace = true, optional = true } common = { workspace = true, default-features = false } -jolt-riscv = { workspace = true } +jolt-riscv = { workspace = true, default-features = false } object = { workspace = true, optional = true } -serde.workspace = true +serde = { workspace = true, optional = true } thiserror.workspace = true + +[dev-dependencies] +hex.workspace = true +serde_json = { workspace = true, features = ["std"] } +sha2.workspace = true diff --git a/crates/jolt-program/src/error.rs b/crates/jolt-program/src/error.rs index b3bece4844..d9548cf845 100644 --- a/crates/jolt-program/src/error.rs +++ b/crates/jolt-program/src/error.rs @@ -1,9 +1,13 @@ +use jolt_riscv::SourceInstructionKind; + #[derive(Debug, thiserror::Error)] pub enum ProgramError { #[error("unsupported program architecture: {0}")] UnsupportedArchitecture(&'static str), #[error("malformed program image: {0}")] MalformedImage(&'static str), + #[error("source instruction is not legal in the selected profile: {0:?}")] + IllegalSourceInstruction(SourceInstructionKind), #[error(transparent)] Expansion(#[from] crate::expand::ExpansionError), } diff --git a/crates/jolt-program/src/execution/mod.rs b/crates/jolt-program/src/execution/mod.rs index 046d5abb60..ba4628cfd9 100644 --- a/crates/jolt-program/src/execution/mod.rs +++ b/crates/jolt-program/src/execution/mod.rs @@ -8,6 +8,8 @@ use crate::{ expand::{expand_program, expand_program_with_provider, InlineExpansionProvider}, image::decode_elf, }; +#[cfg(feature = "image")] +use jolt_riscv::{JoltInstructionProfile, JoltInstructionRow, RV64IMAC_JOLT}; pub use backend::{ExecutionBackend, TraceSource}; pub use error::TraceError; @@ -18,8 +20,11 @@ pub use trace::{ #[cfg(feature = "image")] pub fn build_jolt_program(elf_bytes: &[u8]) -> Result { - let image = decode_elf(elf_bytes)?; - let expanded_bytecode = expand_program(image.instructions.iter().copied())?; + let image = decode_elf(elf_bytes, RV64IMAC_JOLT)?; + let expanded_bytecode = expand_program(&image.instructions, RV64IMAC_JOLT)? + .into_iter() + .map(JoltInstructionRow::from) + .collect(); Ok(JoltProgram::from_rv64_image( elf_bytes.to_vec(), expanded_bytecode, @@ -31,10 +36,14 @@ pub fn build_jolt_program(elf_bytes: &[u8]) -> Result pub fn build_jolt_program_with_inline_provider( elf_bytes: &[u8], inline_provider: &mut P, + profile: JoltInstructionProfile, ) -> Result { - let image = decode_elf(elf_bytes)?; + let image = decode_elf(elf_bytes, profile)?; let expanded_bytecode = - expand_program_with_provider(image.instructions.iter().copied(), inline_provider)?; + expand_program_with_provider(&image.instructions, inline_provider, profile)? + .into_iter() + .map(JoltInstructionRow::from) + .collect(); Ok(JoltProgram::from_rv64_image( elf_bytes.to_vec(), expanded_bytecode, @@ -56,8 +65,9 @@ pub fn build_jolt_program_with_inline_provider< >( elf_bytes: &[u8], inline_provider: &mut P, + profile: jolt_riscv::JoltInstructionProfile, ) -> Result { - let _ = (elf_bytes, inline_provider); + let _ = (elf_bytes, inline_provider, profile); Err(ProgramError::MalformedImage( "building a Jolt program from ELF bytes requires the jolt-program image feature", )) diff --git a/crates/jolt-program/src/execution/trace.rs b/crates/jolt-program/src/execution/trace.rs index e913543470..0109d8477b 100644 --- a/crates/jolt-program/src/execution/trace.rs +++ b/crates/jolt-program/src/execution/trace.rs @@ -1,5 +1,5 @@ use common::jolt_device::{JoltDevice, MemoryConfig}; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::JoltInstructionRow; use super::{ExecutionBackend, TraceError, TraceSource}; @@ -13,7 +13,7 @@ use super::{ExecutionBackend, TraceError, TraceSource}; pub struct JoltProgram { elf_bytes: Vec, /// Final Jolt bytecode rows after expanding decoded RV64 instructions. - pub expanded_bytecode: Vec, + pub expanded_bytecode: Vec, /// Initial byte values for memory-backed ELF sections. pub memory_init: Vec<(u64, u8)>, /// End address of the loaded program image. @@ -35,7 +35,7 @@ impl JoltProgram { pub fn from_parts( elf_bytes: Vec, - expanded_bytecode: Vec, + expanded_bytecode: Vec, memory_init: Vec<(u64, u8)>, program_end: u64, entry_address: u64, @@ -57,7 +57,7 @@ impl JoltProgram { #[cfg(feature = "image")] pub fn from_rv64_image( elf_bytes: Vec, - expanded_bytecode: Vec, + expanded_bytecode: Vec, image: crate::image::Rv64ProgramImage, ) -> Self { Self::from_parts( @@ -106,40 +106,64 @@ impl TraceInputs { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct RegisterRead { pub register: u8, pub value: u64, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct RegisterWrite { pub register: u8, pub pre_value: u64, pub post_value: u64, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct RegisterState { pub rs1: Option, pub rs2: Option, pub rd: Option, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct RamRead { pub address: u64, pub value: u64, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct RamWrite { pub address: u64, pub pre_value: u64, pub post_value: u64, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub enum RamAccess { Read(RamRead), Write(RamWrite), @@ -147,14 +171,22 @@ pub enum RamAccess { NoOp, } -#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct MemoryImage { pub bytes: Vec<(u64, u8)>, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct TraceRow { - pub instruction: NormalizedInstruction, + pub instruction: JoltInstructionRow, pub registers: RegisterState, pub ram_access: RamAccess, } diff --git a/crates/jolt-program/src/expand/allocator.rs b/crates/jolt-program/src/expand/allocator.rs index 2df10c55cb..0abbdcae14 100644 --- a/crates/jolt-program/src/expand/allocator.rs +++ b/crates/jolt-program/src/expand/allocator.rs @@ -3,9 +3,10 @@ use common::constants::{RISCV_REGISTER_COUNT, VIRTUAL_REGISTER_COUNT}; use crate::expand::ExpansionError; const NUM_VIRTUAL_REGISTERS: usize = VIRTUAL_REGISTER_COUNT as usize; -const NUM_VIRTUAL_INSTRUCTION_REGISTERS: usize = 8; +pub(super) const NUM_VIRTUAL_INSTRUCTION_REGISTERS: usize = 8; const RISCV_REGISTER_BASE: u8 = RISCV_REGISTER_COUNT; const NUM_RESERVED_VIRTUAL_REGISTERS: usize = 8; +const MAX_RECURSION_DEPTH: usize = 128; const RESERVATION_W_REGISTER: u8 = RISCV_REGISTER_BASE; const RESERVATION_D_REGISTER: u8 = RISCV_REGISTER_BASE + 1; @@ -23,18 +24,65 @@ pub const CSR_MEPC: u16 = 0x341; pub const CSR_MCAUSE: u16 = 0x342; pub const CSR_MTVAL: u16 = 0x343; +pub(super) const fn reservation_w_register() -> u8 { + RESERVATION_W_REGISTER +} + +pub(super) const fn reservation_d_register() -> u8 { + RESERVATION_D_REGISTER +} + +pub(super) const fn trap_handler_register() -> u8 { + TRAP_HANDLER_REGISTER +} + +pub(super) const fn mepc_register() -> u8 { + MEPC_REGISTER +} + +pub(super) const fn mcause_register() -> u8 { + MCAUSE_REGISTER +} + +pub(super) const fn mtval_register() -> u8 { + MTVAL_REGISTER +} + +pub(super) const fn mstatus_register() -> u8 { + MSTATUS_REGISTER +} + +pub(super) fn virtual_register_for_csr(csr_addr: u16) -> Option { + match csr_addr { + CSR_MSTATUS => Some(mstatus_register()), + CSR_MTVEC => Some(trap_handler_register()), + CSR_MSCRATCH => Some(mscratch_register()), + CSR_MEPC => Some(mepc_register()), + CSR_MCAUSE => Some(mcause_register()), + CSR_MTVAL => Some(mtval_register()), + _ => None, + } +} + +pub(super) const fn mscratch_register() -> u8 { + MSCRATCH_REGISTER +} + +/// Virtual register pool partitioned into reserved (CSRs), instruction (per-expansion temps), +/// and inline (provider-allocated) ranges. Also tracks recursion depth. #[derive(Debug, Clone)] pub struct ExpansionAllocator { - allocated: [bool; NUM_VIRTUAL_REGISTERS], - /// Inline-only virtual registers that must be reset before finalizing an inline sequence. - pending_clearing_inline: Vec, + allocated: u128, + pending_clearing_inline: u128, + recursion_depth: usize, } impl ExpansionAllocator { pub const fn new() -> Self { Self { - allocated: [false; NUM_VIRTUAL_REGISTERS], - pending_clearing_inline: Vec::new(), + allocated: 0, + pending_clearing_inline: 0, + recursion_depth: 0, } } @@ -70,18 +118,6 @@ impl ExpansionAllocator { MSTATUS_REGISTER } - pub fn csr_to_virtual_register(&self, csr_addr: u16) -> Option { - match csr_addr { - CSR_MSTATUS => Some(self.mstatus_register()), - CSR_MTVEC => Some(self.trap_handler_register()), - CSR_MSCRATCH => Some(self.mscratch_register()), - CSR_MEPC => Some(self.mepc_register()), - CSR_MCAUSE => Some(self.mcause_register()), - CSR_MTVAL => Some(self.mtval_register()), - _ => None, - } - } - pub fn allocate(&mut self) -> Result { self.allocate_in_range( NUM_RESERVED_VIRTUAL_REGISTERS, @@ -96,31 +132,44 @@ impl ExpansionAllocator { NUM_VIRTUAL_REGISTERS, "inline", )?; - if !self.pending_clearing_inline.contains(®ister) { - self.pending_clearing_inline.push(register); - } + self.pending_clearing_inline |= Self::register_bit(register)?; Ok(register) } pub fn release(&mut self, register: u8) -> Result<(), ExpansionError> { - let index = Self::virtual_index(register)?; - if !self.allocated[index] { + let bit = Self::register_bit(register)?; + if self.allocated & bit == 0 { return Err(ExpansionError::UnallocatedVirtualRegister { register }); } - self.allocated[index] = false; + self.allocated &= !bit; Ok(()) } pub fn take_registers_for_reset(&mut self) -> Result, ExpansionError> { - if self - .allocated - .iter() - .skip(NUM_RESERVED_VIRTUAL_REGISTERS + NUM_VIRTUAL_INSTRUCTION_REGISTERS) - .any(|allocated| *allocated) - { + let inline_mask = Self::range_mask( + NUM_RESERVED_VIRTUAL_REGISTERS + NUM_VIRTUAL_INSTRUCTION_REGISTERS, + NUM_VIRTUAL_REGISTERS, + ); + if self.allocated & inline_mask != 0 { return Err(ExpansionError::InlineRegistersStillAllocated); } - Ok(std::mem::take(&mut self.pending_clearing_inline)) + let pending = self.pending_clearing_inline; + self.pending_clearing_inline = 0; + Ok(Self::registers_in_mask(pending)) + } + + pub(super) fn enter_expansion(&mut self) -> Result<(), ExpansionError> { + if self.recursion_depth == MAX_RECURSION_DEPTH { + return Err(ExpansionError::RecursionDepthExceeded { + max_depth: MAX_RECURSION_DEPTH, + }); + } + self.recursion_depth += 1; + Ok(()) + } + + pub(super) fn exit_expansion(&mut self) { + self.recursion_depth -= 1; } fn allocate_in_range( @@ -130,8 +179,9 @@ impl ExpansionAllocator { pool: &'static str, ) -> Result { for index in start..end { - if !self.allocated[index] { - self.allocated[index] = true; + let bit = 1u128 << index; + if self.allocated & bit == 0 { + self.allocated |= bit; return Ok(RISCV_REGISTER_BASE + index as u8); } } @@ -148,6 +198,25 @@ impl ExpansionAllocator { } Ok(index) } + + fn register_bit(register: u8) -> Result { + Ok(1u128 << Self::virtual_index(register)?) + } + + fn range_mask(start: usize, end: usize) -> u128 { + let len = end - start; + ((1u128 << len) - 1) << start + } + + fn registers_in_mask(mask: u128) -> Vec { + let mut registers = Vec::new(); + for index in 0..NUM_VIRTUAL_REGISTERS { + if mask & (1u128 << index) != 0 { + registers.push(RISCV_REGISTER_BASE + index as u8); + } + } + registers + } } impl Default for ExpansionAllocator { @@ -224,31 +293,21 @@ mod tests { #[test] fn maps_supported_csrs_to_reserved_registers() { - let allocator = ExpansionAllocator::new(); assert_eq!( - allocator.csr_to_virtual_register(CSR_MSTATUS), + virtual_register_for_csr(CSR_MSTATUS), Some(MSTATUS_REGISTER) ); assert_eq!( - allocator.csr_to_virtual_register(CSR_MTVEC), + virtual_register_for_csr(CSR_MTVEC), Some(TRAP_HANDLER_REGISTER) ); assert_eq!( - allocator.csr_to_virtual_register(CSR_MSCRATCH), + virtual_register_for_csr(CSR_MSCRATCH), Some(MSCRATCH_REGISTER) ); - assert_eq!( - allocator.csr_to_virtual_register(CSR_MEPC), - Some(MEPC_REGISTER) - ); - assert_eq!( - allocator.csr_to_virtual_register(CSR_MCAUSE), - Some(MCAUSE_REGISTER) - ); - assert_eq!( - allocator.csr_to_virtual_register(CSR_MTVAL), - Some(MTVAL_REGISTER) - ); - assert_eq!(allocator.csr_to_virtual_register(0x999), None); + assert_eq!(virtual_register_for_csr(CSR_MEPC), Some(MEPC_REGISTER)); + assert_eq!(virtual_register_for_csr(CSR_MCAUSE), Some(MCAUSE_REGISTER)); + assert_eq!(virtual_register_for_csr(CSR_MTVAL), Some(MTVAL_REGISTER)); + assert_eq!(virtual_register_for_csr(0x999), None); } } diff --git a/crates/jolt-program/src/expand/arithmetic/addiw.rs b/crates/jolt-program/src/expand/arithmetic/addiw.rs index af8dd3986d..d1801ccdb8 100644 --- a/crates/jolt-program/src/expand/arithmetic/addiw.rs +++ b/crates/jolt-program/src/expand/arithmetic/addiw.rs @@ -1,22 +1,30 @@ use super::*; +/// Lowers `ADDIW` by doing the addition in the final row universe and then +/// forcing the architectural RV64 word result. +/// +/// RISC-V word arithmetic keeps only the low 32 bits and sign-extends bit 31 +/// back to XLEN. The final `VirtualSignExtendWord` row is therefore part of +/// the source instruction's semantics, not a cleanup step. pub(in crate::expand) fn expand_addiw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_i( - InstructionKind::ADDI, - rd(instruction)?, - rs1(instruction)?, + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(rs1(instruction)?), instruction.operands.imm, - )?; + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/arithmetic/addw.rs b/crates/jolt-program/src/expand/arithmetic/addw.rs index abfea95def..76e49cae70 100644 --- a/crates/jolt-program/src/expand/arithmetic/addw.rs +++ b/crates/jolt-program/src/expand/arithmetic/addw.rs @@ -1,22 +1,30 @@ use super::*; +/// Lowers `ADDW` by emitting a full-width `ADD` followed by word sign +/// extension. +/// +/// The full-width sum may contain arbitrary high bits. `VirtualSignExtendWord` +/// enforces the RV64 word-arithmetic contract that only the low 32-bit result +/// is kept and then sign-extended into the destination register. pub(in crate::expand) fn expand_addw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_r( - InstructionKind::ADD, - rd(instruction)?, - rs1(instruction)?, - rs2(instruction)?, - )?; + JoltInstructionKind::ADD, + reg(rd(instruction)?), + reg(rs1(instruction)?), + reg(rs2(instruction)?), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/arithmetic/mulh.rs b/crates/jolt-program/src/expand/arithmetic/mulh.rs index 7626874e97..63636170c4 100644 --- a/crates/jolt-program/src/expand/arithmetic/mulh.rs +++ b/crates/jolt-program/src/expand/arithmetic/mulh.rs @@ -1,29 +1,63 @@ use super::*; +/// Lowers signed `MULH` to unsigned high multiplication plus sign corrections. +/// +/// `MULHU` gives the high half of the unsigned product. For signed operands, +/// each negative input contributes a correction term equal to the other +/// operand. `VirtualMovsign` materializes either all-ones or zero from each +/// sign bit, so the two extra multiplies add exactly those two's-complement +/// corrections before writing the signed high 64 bits. pub(in crate::expand) fn expand_mulh( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_sx = allocator.allocate()?; - let v_sy = allocator.allocate()?; - let v_tmp = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::VirtualMovsign, v_sx, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::VirtualMovsign, v_sy, rs2(instruction)?, 0)?; - asm.emit_r(InstructionKind::MUL, v_sx, v_sx, rs2(instruction)?)?; - asm.emit_r(InstructionKind::MUL, v_sy, v_sy, rs1(instruction)?)?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_sx = asm.allocate()?; + let v_sy = asm.allocate()?; + let v_tmp = asm.allocate()?; + + asm.emit_i( + JoltInstructionKind::VirtualMovsign, + v_sx.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.emit_i( + JoltInstructionKind::VirtualMovsign, + v_sy.operand(), + reg(rs2(instruction)?), + 0, + ); + asm.emit_r( + JoltInstructionKind::MUL, + v_sx.operand(), + v_sx.operand(), + reg(rs2(instruction)?), + ); + asm.emit_r( + JoltInstructionKind::MUL, + v_sy.operand(), + v_sy.operand(), + reg(rs1(instruction)?), + ); asm.emit_r( - InstructionKind::MULHU, - v_tmp, - rs1(instruction)?, - rs2(instruction)?, - )?; - asm.emit_r(InstructionKind::ADD, v_tmp, v_tmp, v_sx)?; - asm.emit_r(InstructionKind::ADD, rd(instruction)?, v_tmp, v_sy)?; - let sequence = asm.finalize()?; - allocator.release(v_sx)?; - allocator.release(v_sy)?; - allocator.release(v_tmp)?; - Ok(sequence) + JoltInstructionKind::MULHU, + v_tmp.operand(), + reg(rs1(instruction)?), + reg(rs2(instruction)?), + ); + asm.emit_r( + JoltInstructionKind::ADD, + v_tmp.operand(), + v_tmp.operand(), + v_sx.operand(), + ); + asm.emit_r( + JoltInstructionKind::ADD, + reg(rd(instruction)?), + v_tmp.operand(), + v_sy.operand(), + ); + asm.release_many([v_sx, v_sy, v_tmp]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/arithmetic/mulhsu.rs b/crates/jolt-program/src/expand/arithmetic/mulhsu.rs index 503ca47976..918abe7036 100644 --- a/crates/jolt-program/src/expand/arithmetic/mulhsu.rs +++ b/crates/jolt-program/src/expand/arithmetic/mulhsu.rs @@ -1,30 +1,83 @@ use super::*; +/// Lowers signed-by-unsigned `MULHSU` by normalizing the signed operand before +/// reapplying the sign to the high half. +/// +/// The sequence computes the absolute value of `rs1`, multiplies it as an +/// unsigned value by `rs2`, and then applies the two's-complement correction +/// needed when `rs1` was negative. The final `SLTU` detects the carry produced +/// while negating the low half, which must be added into the high half. pub(in crate::expand) fn expand_mulhsu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let v2 = allocator.allocate()?; - let v3 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::VirtualMovsign, v0, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::ANDI, v1, v0, 1)?; - asm.emit_r(InstructionKind::XOR, v2, rs1(instruction)?, v0)?; - asm.emit_r(InstructionKind::ADD, v2, v2, v1)?; - asm.emit_r(InstructionKind::MULHU, v3, v2, rs2(instruction)?)?; - asm.emit_r(InstructionKind::MUL, v2, v2, rs2(instruction)?)?; - asm.emit_r(InstructionKind::XOR, v3, v3, v0)?; - asm.emit_r(InstructionKind::XOR, v2, v2, v0)?; - asm.emit_r(InstructionKind::ADD, v0, v2, v1)?; - asm.emit_r(InstructionKind::SLTU, v0, v0, v2)?; - asm.emit_r(InstructionKind::ADD, rd(instruction)?, v3, v0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - allocator.release(v2)?; - allocator.release(v3)?; - Ok(sequence) + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + let v2 = asm.allocate()?; + let v3 = asm.allocate()?; + + asm.emit_i( + JoltInstructionKind::VirtualMovsign, + v0.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.emit_i(JoltInstructionKind::ANDI, v1.operand(), v0.operand(), 1); + asm.emit_r( + JoltInstructionKind::XOR, + v2.operand(), + reg(rs1(instruction)?), + v0.operand(), + ); + asm.emit_r( + JoltInstructionKind::ADD, + v2.operand(), + v2.operand(), + v1.operand(), + ); + asm.emit_r( + JoltInstructionKind::MULHU, + v3.operand(), + v2.operand(), + reg(rs2(instruction)?), + ); + asm.emit_r( + JoltInstructionKind::MUL, + v2.operand(), + v2.operand(), + reg(rs2(instruction)?), + ); + asm.emit_r( + JoltInstructionKind::XOR, + v3.operand(), + v3.operand(), + v0.operand(), + ); + asm.emit_r( + JoltInstructionKind::XOR, + v2.operand(), + v2.operand(), + v0.operand(), + ); + asm.emit_r( + JoltInstructionKind::ADD, + v0.operand(), + v2.operand(), + v1.operand(), + ); + asm.emit_r( + JoltInstructionKind::SLTU, + v0.operand(), + v0.operand(), + v2.operand(), + ); + asm.emit_r( + JoltInstructionKind::ADD, + reg(rd(instruction)?), + v3.operand(), + v0.operand(), + ); + asm.release_many([v0, v1, v2, v3]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/arithmetic/mulw.rs b/crates/jolt-program/src/expand/arithmetic/mulw.rs index 904f3406bb..f708434c9d 100644 --- a/crates/jolt-program/src/expand/arithmetic/mulw.rs +++ b/crates/jolt-program/src/expand/arithmetic/mulw.rs @@ -1,22 +1,28 @@ use super::*; +/// Lowers `MULW` by multiplying at XLEN and then imposing the RV64 word result. +/// +/// RISC-V defines `MULW` as the low 32 bits of the product sign-extended to +/// 64 bits. The final virtual row is what discards any higher product bits. pub(in crate::expand) fn expand_mulw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_r( - InstructionKind::MUL, - rd(instruction)?, - rs1(instruction)?, - rs2(instruction)?, - )?; + JoltInstructionKind::MUL, + reg(rd(instruction)?), + reg(rs1(instruction)?), + reg(rs2(instruction)?), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/arithmetic/subw.rs b/crates/jolt-program/src/expand/arithmetic/subw.rs index 1919ef70c3..05b4587fe3 100644 --- a/crates/jolt-program/src/expand/arithmetic/subw.rs +++ b/crates/jolt-program/src/expand/arithmetic/subw.rs @@ -1,22 +1,29 @@ use super::*; +/// Lowers `SUBW` by subtracting at XLEN and then sign-extending the low word. +/// +/// This mirrors the architectural RV64 word instruction: high bits from the +/// intermediate full-width subtraction are ignored, and bit 31 determines the +/// final sign extension. pub(in crate::expand) fn expand_subw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_r( - InstructionKind::SUB, - rd(instruction)?, - rs1(instruction)?, - rs2(instruction)?, - )?; + JoltInstructionKind::SUB, + reg(rd(instruction)?), + reg(rs1(instruction)?), + reg(rs2(instruction)?), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/assembler.rs b/crates/jolt-program/src/expand/assembler.rs deleted file mode 100644 index be179f9f19..0000000000 --- a/crates/jolt-program/src/expand/assembler.rs +++ /dev/null @@ -1,301 +0,0 @@ -use common::constants::{RISCV_REGISTER_COUNT, VIRTUAL_INSTRUCTION_RESERVED_REGISTER_COUNT}; -use jolt_riscv::{InstructionKind, NormalizedInstruction, NormalizedOperands}; - -use crate::expand::{allocator::ExpansionAllocator, expand_instruction, ExpansionError}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Value { - Imm(u64), - Reg(u8), -} - -#[derive(Debug)] -pub struct InstrAssembler<'a> { - address: usize, - is_compressed: bool, - has_inline_instr_format: bool, - sequence: Vec, - allocator: &'a mut ExpansionAllocator, -} - -impl<'a> InstrAssembler<'a> { - pub fn new(address: usize, is_compressed: bool, allocator: &'a mut ExpansionAllocator) -> Self { - Self { - address, - is_compressed, - has_inline_instr_format: false, - sequence: Vec::new(), - allocator, - } - } - - pub fn new_inline( - address: usize, - is_compressed: bool, - allocator: &'a mut ExpansionAllocator, - ) -> Self { - Self { - address, - is_compressed, - has_inline_instr_format: true, - sequence: Vec::new(), - allocator, - } - } - - pub fn allocator(&mut self) -> &mut ExpansionAllocator { - self.allocator - } - - pub fn emit( - &mut self, - instruction_kind: InstructionKind, - operands: NormalizedOperands, - ) -> Result<(), ExpansionError> { - if self.has_inline_instr_format { - Self::validate_inline_write_target(operands.rd)?; - } - let instruction = NormalizedInstruction { - instruction_kind, - address: self.address, - operands, - virtual_sequence_remaining: Some(0), - is_first_in_sequence: false, - is_compressed: false, - }; - self.sequence - .extend(expand_instruction(&instruction, self.allocator)?); - Ok(()) - } - - pub fn emit_r( - &mut self, - instruction_kind: InstructionKind, - rd: u8, - rs1: u8, - rs2: u8, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: Some(rs2), - imm: 0, - }, - ) - } - - pub fn emit_i( - &mut self, - instruction_kind: InstructionKind, - rd: u8, - rs1: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: None, - imm, - }, - ) - } - - pub fn emit_s( - &mut self, - instruction_kind: InstructionKind, - rs1: u8, - rs2: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: Some(rs2), - imm, - }, - ) - } - - pub fn emit_b( - &mut self, - instruction_kind: InstructionKind, - rs1: u8, - rs2: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: Some(rs2), - imm, - }, - ) - } - - pub fn emit_j( - &mut self, - instruction_kind: InstructionKind, - rd: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: Some(rd), - rs1: None, - rs2: None, - imm, - }, - ) - } - - pub fn emit_u( - &mut self, - instruction_kind: InstructionKind, - rd: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: Some(rd), - rs1: None, - rs2: None, - imm, - }, - ) - } - - pub fn emit_align( - &mut self, - instruction_kind: InstructionKind, - rs1: u8, - imm: i128, - ) -> Result<(), ExpansionError> { - self.emit( - instruction_kind, - NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: None, - imm, - }, - ) - } - - pub fn finalize(mut self) -> Result, ExpansionError> { - finalize_sequence(&mut self.sequence, self.is_compressed)?; - Ok(self.sequence) - } - - pub fn finalize_inline(mut self) -> Result, ExpansionError> { - for register in self.allocator.take_registers_for_reset()? { - self.emit_i(InstructionKind::ADDI, register, 0, 0)?; - } - self.finalize() - } - - fn validate_inline_write_target(rd: Option) -> Result<(), ExpansionError> { - let Some(register) = rd else { - return Ok(()); - }; - let minimum_register = RISCV_REGISTER_COUNT + VIRTUAL_INSTRUCTION_RESERVED_REGISTER_COUNT; - if register == 0 || register >= minimum_register { - return Ok(()); - } - Err(ExpansionError::InvalidInlineWriteTarget { - register, - minimum_register, - }) - } -} - -fn finalize_sequence( - sequence: &mut [NormalizedInstruction], - is_compressed: bool, -) -> Result<(), ExpansionError> { - if sequence.is_empty() { - return Err(ExpansionError::EmptySequence); - } - - let len = sequence.len(); - for (index, instruction) in sequence.iter_mut().enumerate() { - set_sequence_metadata(instruction, index == 0, Some((len - index - 1) as u16)); - } - if let Some(last) = sequence.last_mut() { - last.is_compressed = is_compressed; - } - Ok(()) -} - -fn set_sequence_metadata( - instruction: &mut NormalizedInstruction, - is_first_in_sequence: bool, - virtual_sequence_remaining: Option, -) { - instruction.is_first_in_sequence = is_first_in_sequence; - instruction.virtual_sequence_remaining = virtual_sequence_remaining; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn finalizes_sequence_metadata() -> Result<(), ExpansionError> { - let mut allocator = ExpansionAllocator::new(); - let mut assembler = InstrAssembler::new(0x8000_0000, true, &mut allocator); - assembler.emit_i(InstructionKind::ADDI, 1, 2, 3)?; - assembler.emit_r(InstructionKind::ADD, 4, 5, 6)?; - - let sequence = assembler.finalize()?; - assert_eq!(sequence[0].virtual_sequence_remaining, Some(1)); - assert!(sequence[0].is_first_in_sequence); - assert!(!sequence[0].is_compressed); - assert_eq!(sequence[1].virtual_sequence_remaining, Some(0)); - assert!(!sequence[1].is_first_in_sequence); - assert!(sequence[1].is_compressed); - Ok(()) - } - - #[test] - fn rejects_inline_write_to_non_inline_register() { - let mut allocator = ExpansionAllocator::new(); - let mut assembler = InstrAssembler::new_inline(0x8000_0000, false, &mut allocator); - - assert!(matches!( - assembler.emit_i(InstructionKind::ADDI, 1, 0, 0), - Err(ExpansionError::InvalidInlineWriteTarget { - register: 1, - minimum_register: 40 - }) - )); - } - - #[test] - fn finalizes_inline_with_reset_instructions() -> Result<(), ExpansionError> { - let mut allocator = ExpansionAllocator::new(); - let register = allocator.allocate_for_inline()?; - allocator.release(register)?; - - let mut assembler = InstrAssembler::new_inline(0x8000_0000, false, &mut allocator); - assembler.emit_i(InstructionKind::ADDI, register, 0, 7)?; - let sequence = assembler.finalize_inline()?; - - assert_eq!(sequence.len(), 2); - assert_eq!(sequence[0].operands.rd, Some(register)); - assert_eq!(sequence[0].operands.imm, 7); - assert_eq!(sequence[1].instruction_kind, InstructionKind::ADDI); - assert_eq!(sequence[1].operands.rd, Some(register)); - assert_eq!(sequence[1].operands.imm, 0); - Ok(()) - } -} diff --git a/crates/jolt-program/src/expand/control_flow/csrrs.rs b/crates/jolt-program/src/expand/control_flow/csrrs.rs index a396f2794c..c938bdda1a 100644 --- a/crates/jolt-program/src/expand/control_flow/csrrs.rs +++ b/crates/jolt-program/src/expand/control_flow/csrrs.rs @@ -1,38 +1,77 @@ use super::*; +/// Lowers `CSRRS` to operations on Jolt's CSR virtual registers. +/// +/// Supported CSRs are represented by reserved virtual registers. The sequence +/// must preserve the Zicsr read-before-write rule, including the special cases: +/// `rs1 = x0` is read-only, `rd = x0` discards the old CSR value, and +/// `rd == rs1` needs a temporary so the source bits are not overwritten before +/// the CSR update. pub(in crate::expand) fn expand_csrrs( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let csr = csr_address(instruction); - let virtual_reg = allocator - .csr_to_virtual_register(csr) - .ok_or(ExpansionError::UnsupportedCsr(csr))?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let virtual_reg = virtual_register_for_csr(csr).ok_or(ExpansionError::UnsupportedCsr(csr))?; + let mut asm = ExpansionBuilder::new(*instruction); + if rs1(instruction)? == 0 { - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, virtual_reg, 0)?; + // Read-only `csrr rd, csr`: copy the CSR virtual register to rd. + asm.emit_i( + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(virtual_reg), + 0, + ); + return asm.finalize(); } else if rd(instruction)? == 0 { + // Set-only `csrs csr, rs1`: update the CSR virtual register and + // deliberately discard the old value. asm.emit_r( - InstructionKind::OR, - virtual_reg, - virtual_reg, - rs1(instruction)?, - )?; + JoltInstructionKind::OR, + reg(virtual_reg), + reg(virtual_reg), + reg(rs1(instruction)?), + ); + return asm.finalize(); } else if rd(instruction)? == rs1(instruction)? { - let temp = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::ADDI, temp, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, virtual_reg, 0)?; - asm.emit_r(InstructionKind::OR, virtual_reg, virtual_reg, temp)?; - asm.allocator().release(temp)?; - } else { - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, virtual_reg, 0)?; + // Preserve rs1 before writing rd with the old CSR value. + let temp = asm.allocate()?; + asm.emit_i( + JoltInstructionKind::ADDI, + temp.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.emit_i( + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(virtual_reg), + 0, + ); asm.emit_r( - InstructionKind::OR, - virtual_reg, - virtual_reg, - rs1(instruction)?, - )?; + JoltInstructionKind::OR, + reg(virtual_reg), + reg(virtual_reg), + temp.operand(), + ); + asm.release(temp); + return asm.finalize(); } + + // General case: rd receives the old CSR value, then the CSR accumulates + // the source bits. + asm.emit_i( + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(virtual_reg), + 0, + ); + asm.emit_r( + JoltInstructionKind::OR, + reg(virtual_reg), + reg(virtual_reg), + reg(rs1(instruction)?), + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/control_flow/csrrw.rs b/crates/jolt-program/src/expand/control_flow/csrrw.rs index 87224c1903..869042f85a 100644 --- a/crates/jolt-program/src/expand/control_flow/csrrw.rs +++ b/crates/jolt-program/src/expand/control_flow/csrrw.rs @@ -1,26 +1,66 @@ use super::*; +/// Lowers `CSRRW` to operations on Jolt's CSR virtual registers. +/// +/// The reserved virtual register for the CSR is the proof-facing source of +/// truth. The sequence preserves the read-before-write swap rule: `rd` +/// receives the old CSR value unless `rd = x0`, and the CSR virtual register +/// receives `rs1`. If `rd == rs1`, a temporary keeps the new CSR value alive +/// while `rd` is overwritten with the old one. pub(in crate::expand) fn expand_csrrw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let csr = csr_address(instruction); - let virtual_reg = allocator - .csr_to_virtual_register(csr) - .ok_or(ExpansionError::UnsupportedCsr(csr))?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let virtual_reg = virtual_register_for_csr(csr).ok_or(ExpansionError::UnsupportedCsr(csr))?; + let mut asm = ExpansionBuilder::new(*instruction); + if rd(instruction)? == 0 { - asm.emit_i(InstructionKind::ADDI, virtual_reg, rs1(instruction)?, 0)?; + // `csrw csr, rs1`: write the CSR and discard the old value. + asm.emit_i( + JoltInstructionKind::ADDI, + reg(virtual_reg), + reg(rs1(instruction)?), + 0, + ); + return asm.finalize(); } else if rd(instruction)? == rs1(instruction)? { - let temp = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::ADDI, temp, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, virtual_reg, 0)?; - asm.emit_i(InstructionKind::ADDI, virtual_reg, temp, 0)?; - asm.allocator().release(temp)?; - } else { - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, virtual_reg, 0)?; - asm.emit_i(InstructionKind::ADDI, virtual_reg, rs1(instruction)?, 0)?; + // Preserve rs1 before rd is overwritten with the old CSR value. + let temp = asm.allocate()?; + asm.emit_i( + JoltInstructionKind::ADDI, + temp.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.emit_i( + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(virtual_reg), + 0, + ); + asm.emit_i( + JoltInstructionKind::ADDI, + reg(virtual_reg), + temp.operand(), + 0, + ); + asm.release(temp); + return asm.finalize(); } + + // General case: copy old CSR to rd, then copy rs1 into the CSR. + asm.emit_i( + JoltInstructionKind::ADDI, + reg(rd(instruction)?), + reg(virtual_reg), + 0, + ); + asm.emit_i( + JoltInstructionKind::ADDI, + reg(virtual_reg), + reg(rs1(instruction)?), + 0, + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/control_flow/ebreak.rs b/crates/jolt-program/src/expand/control_flow/ebreak.rs index 0b26c58670..d4f0aaa622 100644 --- a/crates/jolt-program/src/expand/control_flow/ebreak.rs +++ b/crates/jolt-program/src/expand/control_flow/ebreak.rs @@ -1,13 +1,18 @@ use super::*; +/// Lowers `EBREAK` to a self-jump that terminates the emulator trace. +/// +/// Jolt has no debugger trap target for `EBREAK`. The tracer treats unchanged +/// PC as program termination, so `JAL +0` preserves that observable behavior +/// while still producing a target-legal final row. pub(in crate::expand) fn expand_ebreak( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - let discard = asm.allocator().allocate()?; - asm.emit_j(InstructionKind::JAL, discard, 0)?; - asm.allocator().release(discard)?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let discard = asm.allocate()?; + + asm.emit_j(JoltInstructionKind::JAL, discard.operand(), 0); + asm.release(discard); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/control_flow/ecall.rs b/crates/jolt-program/src/expand/control_flow/ecall.rs index b395220d1e..089d339869 100644 --- a/crates/jolt-program/src/expand/control_flow/ecall.rs +++ b/crates/jolt-program/src/expand/control_flow/ecall.rs @@ -1,35 +1,66 @@ use super::*; +/// Lowers `ECALL` into the M-mode trap-entry sequence used by Jolt. +/// +/// The sequence writes the proof-facing CSR virtual registers (`mepc`, +/// `mcause`, `mtval`, `mstatus`) and then jumps to the virtual `mtvec` +/// register. Jolt's current trap model is M-mode-only with no interrupt +/// hardware, so `mstatus` is written as `MPP=M-mode, MIE=0, MPIE=0` instead of +/// using the full privileged-spec read/modify/write path. pub(in crate::expand) fn expand_ecall( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { const MCAUSE_ECALL_FROM_MMODE: i128 = 11; - let v_trap_handler_reg = allocator.trap_handler_register(); - let vr_mepc = allocator.mepc_register(); - let vr_mcause = allocator.mcause_register(); - let vr_mtval = allocator.mtval_register(); - let vr_mstatus = allocator.mstatus_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let v_trap_handler_reg = trap_handler_register(); + let vr_mepc = mepc_register(); + let vr_mcause = mcause_register(); + let vr_mtval = mtval_register(); + let vr_mstatus = mstatus_register(); - let ecall_addr = asm.allocator().allocate()?; - asm.emit_u(InstructionKind::AUIPC, ecall_addr, 0)?; - asm.emit_i(InstructionKind::ADDI, vr_mepc, ecall_addr, 0)?; - asm.allocator().release(ecall_addr)?; + let mut asm = ExpansionBuilder::new(*instruction); - asm.emit_i(InstructionKind::ADDI, vr_mcause, 0, MCAUSE_ECALL_FROM_MMODE)?; - asm.emit_i(InstructionKind::ADDI, vr_mtval, 0, 0)?; + // AUIPC materializes this ECALL row's PC so mepc points back to the trap + // source, matching the tracer's ECALL trap semantics. + let ecall_addr = asm.allocate()?; + asm.emit_u(JoltInstructionKind::AUIPC, ecall_addr.operand(), 0); + asm.emit_i( + JoltInstructionKind::ADDI, + reg(vr_mepc), + ecall_addr.operand(), + 0, + ); + asm.release(ecall_addr); + // Machine-mode environment call, with no trap value. + asm.emit_i( + JoltInstructionKind::ADDI, + reg(vr_mcause), + reg(0), + MCAUSE_ECALL_FROM_MMODE, + ); + asm.emit_i(JoltInstructionKind::ADDI, reg(vr_mtval), reg(0), 0); - let three = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::ADDI, three, 0, 3)?; - asm.emit_i(InstructionKind::SLLI, vr_mstatus, three, 11)?; - asm.allocator().release(three)?; + // 3 << 11 sets MPP=M-mode and leaves the interrupt-enable bits cleared. + let three = asm.allocate()?; + asm.emit_i(JoltInstructionKind::ADDI, three.operand(), reg(0), 3); + asm.expand_i( + SourceInstructionKind::SLLI, + reg(vr_mstatus), + three.operand(), + 11, + ); + asm.release(three); - let jalr_rd = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::JALR, jalr_rd, v_trap_handler_reg, 0)?; - asm.allocator().release(jalr_rd)?; + // Jump to mtvec. The link register is a temporary because ECALL does not + // expose a return address through the architectural register file. + let jalr_rd = asm.allocate()?; + asm.emit_i( + JoltInstructionKind::JALR, + jalr_rd.operand(), + reg(v_trap_handler_reg), + 0, + ); + asm.release(jalr_rd); asm.finalize() } diff --git a/crates/jolt-program/src/expand/control_flow/mret.rs b/crates/jolt-program/src/expand/control_flow/mret.rs index e56a0869b4..6a29fc763c 100644 --- a/crates/jolt-program/src/expand/control_flow/mret.rs +++ b/crates/jolt-program/src/expand/control_flow/mret.rs @@ -1,14 +1,24 @@ use super::*; +/// Lowers `MRET` to a jump through the proof-facing `mepc` virtual register. +/// +/// Jolt currently models M-mode-only execution with no interrupt hardware. The +/// ZeroOS trampoline restores `mstatus` explicitly before `MRET`, so this +/// source instruction only needs to transfer control to `mepc`. pub(in crate::expand) fn expand_mret( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mepc_vr = allocator.mepc_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - let jalr_rd = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::JALR, jalr_rd, mepc_vr, 0)?; - asm.allocator().release(jalr_rd)?; + instruction: &SourceInstructionRow, +) -> Result { + let mepc_vr = mepc_register(); + let mut asm = ExpansionBuilder::new(*instruction); + let jalr_rd = asm.allocate()?; + + asm.emit_i( + JoltInstructionKind::JALR, + jalr_rd.operand(), + reg(mepc_vr), + 0, + ); + asm.release(jalr_rd); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/division/div.rs b/crates/jolt-program/src/expand/division/div.rs index 8deda139b5..a02c924c30 100644 --- a/crates/jolt-program/src/expand/division/div.rs +++ b/crates/jolt-program/src/expand/division/div.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed 64-bit `DIV` through the shared quotient/remainder verifier. +/// +/// The shared recipe witnesses both quotient and remainder, constrains them +/// against RISC-V signed-division edge cases, and writes the quotient. pub(in crate::expand) fn expand_div( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_signed_div_rem(instruction, allocator, false, false) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_signed_div_rem(instruction, false, false) } diff --git a/crates/jolt-program/src/expand/division/divu.rs b/crates/jolt-program/src/expand/division/divu.rs index d3ae0b7a9a..62f3c0d22a 100644 --- a/crates/jolt-program/src/expand/division/divu.rs +++ b/crates/jolt-program/src/expand/division/divu.rs @@ -1,38 +1,69 @@ use super::*; +/// Lowers unsigned 64-bit `DIVU` by witnessing a quotient and proving it is +/// the unique valid quotient for `(rs1, rs2)`. +/// +/// The sequence accepts the RISC-V divide-by-zero result through +/// `VirtualAssertValidDiv0`. Otherwise it proves `q * divisor` does not +/// overflow, `q * divisor <= dividend`, and the derived remainder is either +/// below the divisor or the divisor is zero. pub(in crate::expand) fn expand_divu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_j(InstructionKind::VirtualAdvice, v0, 0)?; - asm.emit_b( - InstructionKind::VirtualAssertValidDiv0, - rs2(instruction)?, - v0, + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + + // v0 is the quotient supplied by the tracer. The following assertions bind + // it to the architectural unsigned division relation before copying to rd. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + v0.operand(), + 0, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidDiv0, + reg(rs2(instruction)?), + v0.operand(), + 0, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertMulUNoOverflow, + v0.operand(), + reg(rs2(instruction)?), 0, - )?; - asm.emit_b( - InstructionKind::VirtualAssertMulUNoOverflow, - v0, - rs2(instruction)?, + ); + asm.expand_r( + SourceInstructionKind::MUL, + v1.operand(), + v0.operand(), + reg(rs2(instruction)?), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + v1.operand(), + reg(rs1(instruction)?), 0, - )?; - asm.emit_r(InstructionKind::MUL, v1, v0, rs2(instruction)?)?; - asm.emit_b(InstructionKind::VirtualAssertLTE, v1, rs1(instruction)?, 0)?; - asm.emit_r(InstructionKind::SUB, v1, rs1(instruction)?, v1)?; - asm.emit_b( - InstructionKind::VirtualAssertValidUnsignedRemainder, - v1, - rs2(instruction)?, + ); + asm.expand_r( + SourceInstructionKind::SUB, + v1.operand(), + reg(rs1(instruction)?), + v1.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidUnsignedRemainder, + v1.operand(), + reg(rs2(instruction)?), 0, - )?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, v0, 0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - Ok(sequence) + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + v0.operand(), + 0, + ); + asm.release_many([v0, v1]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/division/divuw.rs b/crates/jolt-program/src/expand/division/divuw.rs index f960425c84..05b78866d3 100644 --- a/crates/jolt-program/src/expand/division/divuw.rs +++ b/crates/jolt-program/src/expand/division/divuw.rs @@ -1,8 +1,12 @@ use super::*; +/// Lowers unsigned 32-bit `DIVUW` through the shared word-division verifier. +/// +/// Both inputs are zero-extended to 32 bits, the quotient is proved as an +/// unsigned result, and the final quotient is sign-extended as required by RV64 +/// word instructions. pub(in crate::expand) fn expand_divuw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_unsigned_word_div_rem(instruction, allocator, false) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_unsigned_word_div_rem(instruction, false) } diff --git a/crates/jolt-program/src/expand/division/divw.rs b/crates/jolt-program/src/expand/division/divw.rs index 65e24cad25..397f8807cb 100644 --- a/crates/jolt-program/src/expand/division/divw.rs +++ b/crates/jolt-program/src/expand/division/divw.rs @@ -1,8 +1,12 @@ use super::*; +/// Lowers signed 32-bit `DIVW` through the shared signed division recipe. +/// +/// The recipe sign-extends both operands to their RV64 word values, proves the +/// quotient/remainder relation over those values, and sign-extends the quotient +/// as the final word result. pub(in crate::expand) fn expand_divw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_signed_div_rem(instruction, allocator, true, false) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_signed_div_rem(instruction, true, false) } diff --git a/crates/jolt-program/src/expand/division/rem.rs b/crates/jolt-program/src/expand/division/rem.rs index ed056f67cd..135d594581 100644 --- a/crates/jolt-program/src/expand/division/rem.rs +++ b/crates/jolt-program/src/expand/division/rem.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed 64-bit `REM` through the shared quotient/remainder verifier. +/// +/// The same relation that proves `DIV` also proves the remainder; this wrapper +/// selects the remainder path and copies that value to `rd`. pub(in crate::expand) fn expand_rem( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_signed_div_rem(instruction, allocator, false, true) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_signed_div_rem(instruction, false, true) } diff --git a/crates/jolt-program/src/expand/division/remu.rs b/crates/jolt-program/src/expand/division/remu.rs index 61c16809a9..254745297d 100644 --- a/crates/jolt-program/src/expand/division/remu.rs +++ b/crates/jolt-program/src/expand/division/remu.rs @@ -1,30 +1,62 @@ use super::*; +/// Lowers unsigned 64-bit `REMU` by witnessing the quotient and deriving the +/// remainder. +/// +/// The quotient is never exposed. The sequence proves multiplication does not +/// overflow, subtracts `q * divisor` from the dividend, and checks the result +/// satisfies the unsigned remainder bound, with divisor zero admitted by the +/// virtual assertion. pub(in crate::expand) fn expand_remu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_j(InstructionKind::VirtualAdvice, v0, 0)?; - asm.emit_b( - InstructionKind::VirtualAssertMulUNoOverflow, - v0, - rs2(instruction)?, + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + + // v0 starts as the quotient witness and is then reused for q * divisor and + // finally for the derived remainder. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + v0.operand(), + 0, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertMulUNoOverflow, + v0.operand(), + reg(rs2(instruction)?), + 0, + ); + asm.expand_r( + SourceInstructionKind::MUL, + v0.operand(), + v0.operand(), + reg(rs2(instruction)?), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + v0.operand(), + reg(rs1(instruction)?), 0, - )?; - asm.emit_r(InstructionKind::MUL, v0, v0, rs2(instruction)?)?; - asm.emit_b(InstructionKind::VirtualAssertLTE, v0, rs1(instruction)?, 0)?; - asm.emit_r(InstructionKind::SUB, v0, rs1(instruction)?, v0)?; - asm.emit_b( - InstructionKind::VirtualAssertValidUnsignedRemainder, - v0, - rs2(instruction)?, + ); + asm.expand_r( + SourceInstructionKind::SUB, + v0.operand(), + reg(rs1(instruction)?), + v0.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidUnsignedRemainder, + v0.operand(), + reg(rs2(instruction)?), 0, - )?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, v0, 0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - Ok(sequence) + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + v0.operand(), + 0, + ); + asm.release(v0); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/division/remuw.rs b/crates/jolt-program/src/expand/division/remuw.rs index 153fb094c0..133a7a1bec 100644 --- a/crates/jolt-program/src/expand/division/remuw.rs +++ b/crates/jolt-program/src/expand/division/remuw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned 32-bit `REMUW` through the shared word-division verifier. +/// +/// The recipe proves the unsigned 32-bit quotient/remainder relation and +/// sign-extends the derived remainder to match RV64 word-result semantics. pub(in crate::expand) fn expand_remuw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_unsigned_word_div_rem(instruction, allocator, true) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_unsigned_word_div_rem(instruction, true) } diff --git a/crates/jolt-program/src/expand/division/remw.rs b/crates/jolt-program/src/expand/division/remw.rs index 833b15a5aa..abd8ff0123 100644 --- a/crates/jolt-program/src/expand/division/remw.rs +++ b/crates/jolt-program/src/expand/division/remw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed 32-bit `REMW` through the shared signed division recipe. +/// +/// The recipe works over sign-extended word operands and writes the proved +/// signed remainder, then applies the final RV64 word sign extension. pub(in crate::expand) fn expand_remw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_signed_div_rem(instruction, allocator, true, true) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_signed_div_rem(instruction, true, true) } diff --git a/crates/jolt-program/src/expand/division/shared.rs b/crates/jolt-program/src/expand/division/shared.rs index 9150c75d11..10c8359e6e 100644 --- a/crates/jolt-program/src/expand/division/shared.rs +++ b/crates/jolt-program/src/expand/division/shared.rs @@ -1,196 +1,402 @@ use super::*; +/// Builds the signed `DIV`/`REM` and `DIVW`/`REMW` verifier sequence. +/// +/// The tracer supplies quotient `a2` and remainder data `a3` as advice. This +/// recipe proves the RISC-V signed-division contract around those witnesses: +/// divide-by-zero quotient, signed overflow (`MIN / -1`), product/remainder +/// recomposition, and `abs(remainder) < abs(divisor)` whenever the divisor is +/// nonzero. `word` selects 32-bit operand normalization and final word +/// sign-extension; `remainder_output` selects which proved value is copied to +/// `rd`. pub(in crate::expand) fn expand_signed_div_rem( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, + instruction: &SourceInstructionRow, word: bool, remainder_output: bool, -) -> Result, ExpansionError> { - let a0 = rs1(instruction)?; - let a1 = rs2(instruction)?; - let a2 = allocator.allocate()?; - let a3 = allocator.allocate()?; - let t0 = allocator.allocate()?; - let t1 = allocator.allocate()?; - let (mut t2, mut t3, t4) = if word { - ( - allocator.allocate()?, - allocator.allocate()?, - Some(allocator.allocate()?), - ) +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let a0 = reg(rs1(instruction)?); + let a1 = reg(rs2(instruction)?); + let a2 = asm.allocate()?; + let a3 = asm.allocate()?; + let t0 = asm.allocate()?; + let t1 = asm.allocate()?; + let (word_t2, word_t3) = if word { + (Some(asm.allocate()?), Some(asm.allocate()?)) } else { - (0, 0, None) + (None, None) }; - let dividend = t4.unwrap_or(a0); - let divisor = if word { t3 } else { a1 }; + let t4 = if word { Some(asm.allocate()?) } else { None }; + let dividend: RegisterOperand = t4.map_or(a0, TempId::operand); + let divisor: RegisterOperand = word_t3.map_or(a1, TempId::operand); let shmat = if word { 31 } else { 63 }; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - - asm.emit_j(InstructionKind::VirtualAdvice, a2, 0)?; - asm.emit_j(InstructionKind::VirtualAdvice, a3, 0)?; + // a2 is the quotient witness. a3 participates in the remainder proof and + // later becomes the signed remainder candidate for REM/REMW. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + a2.operand(), + 0, + ); + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + a3.operand(), + 0, + ); if word { - asm.emit_i(InstructionKind::VirtualSignExtendWord, dividend, a0, 0)?; - asm.emit_i(InstructionKind::VirtualSignExtendWord, divisor, a1, 0)?; + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + dividend, + a0, + 0, + ); + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + divisor, + a1, + 0, + ); } - - asm.emit_b(InstructionKind::VirtualAssertValidDiv0, divisor, a2, 0)?; - asm.emit_r( + asm.expand_b( + SourceInstructionKind::VirtualAssertValidDiv0, + divisor, + a2.operand(), + 0, + ); + // RISC-V signed overflow (`MIN / -1`) returns the dividend as the + // quotient and zero remainder. Replacing the divisor by 1 in that one case + // lets the same product/remainder proof cover normal and overflow inputs. + asm.expand_r( if word { - InstructionKind::VirtualChangeDivisorW + SourceInstructionKind::VirtualChangeDivisorW( + jolt_riscv::instructions::VirtualChangeDivisorW(()), + ) } else { - InstructionKind::VirtualChangeDivisor + SourceInstructionKind::VirtualChangeDivisor( + jolt_riscv::instructions::VirtualChangeDivisor(()), + ) }, - t0, + t0.operand(), dividend, divisor, - )?; + ); if word { - asm.emit_i(InstructionKind::VirtualSignExtendWord, t1, a2, 0)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, t1, a2, 0)?; - asm.emit_i(InstructionKind::SRAI, t2, a3, 32)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, t2, 0, 0)?; + // Word mode proves that the quotient is already sign-extended from 32 + // bits and that the supplied remainder data fits in the low word. + let t2 = word_t2.ok_or(ExpansionError::MalformedInstruction("missing word temp"))?; + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + t1.operand(), + a2.operand(), + 0, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + t1.operand(), + a2.operand(), + 0, + ); + asm.expand_i(SourceInstructionKind::SRAI, t2.operand(), a3.operand(), 32); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + t2.operand(), + reg(0), + 0, + ); } else { - asm.emit_r(InstructionKind::MULH, t1, a2, t0)?; - t2 = asm.allocator().allocate()?; - t3 = asm.allocator().allocate()?; - asm.emit_r(InstructionKind::MUL, t2, a2, t0)?; - asm.emit_i(InstructionKind::SRAI, t3, t2, shmat)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, t1, t3, 0)?; + asm.expand_r( + SourceInstructionKind::MULH, + t1.operand(), + a2.operand(), + t0.operand(), + ); + let t2 = asm.allocate()?; + let t3 = asm.allocate()?; + asm.expand_r( + SourceInstructionKind::MUL, + t2.operand(), + a2.operand(), + t0.operand(), + ); + asm.expand_i( + SourceInstructionKind::SRAI, + t3.operand(), + t2.operand(), + shmat, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + t1.operand(), + t3.operand(), + 0, + ); + asm.release_many([t2, t3]); } if word { - asm.emit_i(InstructionKind::SRAI, t2, dividend, shmat)?; - asm.emit_r(InstructionKind::XOR, t3, a3, t2)?; - asm.emit_r(InstructionKind::SUB, t3, t3, t2)?; - asm.emit_r(InstructionKind::MUL, t1, a2, t0)?; - asm.emit_r(InstructionKind::ADD, t1, t1, t3)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, t1, dividend, 0)?; - asm.emit_i(InstructionKind::SRAI, t2, t0, 31)?; - asm.emit_r(InstructionKind::XOR, t1, t0, t2)?; - asm.emit_r(InstructionKind::SUB, t1, t1, t2)?; - asm.emit_b( - InstructionKind::VirtualAssertValidUnsignedRemainder, - a3, - t1, + // Recompose the signed dividend from quotient, adjusted divisor, and + // signed remainder, then bound the absolute remainder by the absolute + // adjusted divisor. + let t2 = word_t2.ok_or(ExpansionError::MalformedInstruction("missing word temp"))?; + let t3 = word_t3.ok_or(ExpansionError::MalformedInstruction("missing word temp"))?; + asm.expand_i(SourceInstructionKind::SRAI, t2.operand(), dividend, shmat); + asm.expand_r( + SourceInstructionKind::XOR, + t3.operand(), + a3.operand(), + t2.operand(), + ); + asm.expand_r( + SourceInstructionKind::SUB, + t3.operand(), + t3.operand(), + t2.operand(), + ); + asm.expand_r( + SourceInstructionKind::MUL, + t1.operand(), + a2.operand(), + t0.operand(), + ); + asm.expand_r( + SourceInstructionKind::ADD, + t1.operand(), + t1.operand(), + t3.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + t1.operand(), + dividend, 0, - )?; - asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - if remainder_output { t3 } else { a2 }, + ); + asm.expand_i(SourceInstructionKind::SRAI, t2.operand(), t0.operand(), 31); + asm.expand_r( + SourceInstructionKind::XOR, + t1.operand(), + t0.operand(), + t2.operand(), + ); + asm.expand_r( + SourceInstructionKind::SUB, + t1.operand(), + t1.operand(), + t2.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidUnsignedRemainder, + a3.operand(), + t1.operand(), 0, - )?; + ); + let output: RegisterOperand = if remainder_output { + t3.operand() + } else { + a2.operand() + }; + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + output, + 0, + ); + asm.release_many([t2, t3]); } else { - asm.emit_i(InstructionKind::SRAI, t1, dividend, shmat)?; - asm.emit_r(InstructionKind::XOR, t3, a3, t1)?; - asm.emit_r(InstructionKind::SUB, t3, t3, t1)?; - asm.emit_r(InstructionKind::ADD, t2, t2, t3)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, t2, a0, 0)?; - asm.emit_i(InstructionKind::SRAI, t1, t0, shmat)?; - asm.emit_r( - InstructionKind::XOR, - if remainder_output { t2 } else { t3 }, - t0, - t1, - )?; + // 64-bit signed division follows the same proof shape as word mode, + // but computes temporary absolute values for the remainder and adjusted + // divisor directly in 64-bit space. + let t2 = asm.allocate()?; + let t3 = asm.allocate()?; let abs_divisor = if remainder_output { t2 } else { t3 }; - asm.emit_r(InstructionKind::SUB, abs_divisor, abs_divisor, t1)?; - asm.emit_b( - InstructionKind::VirtualAssertValidUnsignedRemainder, - a3, - abs_divisor, + asm.expand_i(SourceInstructionKind::SRAI, t1.operand(), dividend, shmat); + asm.expand_r( + SourceInstructionKind::XOR, + t3.operand(), + a3.operand(), + t1.operand(), + ); + asm.expand_r( + SourceInstructionKind::SUB, + t3.operand(), + t3.operand(), + t1.operand(), + ); + asm.expand_r( + SourceInstructionKind::ADD, + t2.operand(), + t2.operand(), + t3.operand(), + ); + asm.expand_b(SourceInstructionKind::VirtualAssertEQ, t2.operand(), a0, 0); + asm.expand_i( + SourceInstructionKind::SRAI, + t1.operand(), + t0.operand(), + shmat, + ); + asm.expand_r( + SourceInstructionKind::XOR, + abs_divisor.operand(), + t0.operand(), + t1.operand(), + ); + asm.expand_r( + SourceInstructionKind::SUB, + abs_divisor.operand(), + abs_divisor.operand(), + t1.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidUnsignedRemainder, + a3.operand(), + abs_divisor.operand(), 0, - )?; - asm.emit_i( - InstructionKind::ADDI, - rd(instruction)?, - if remainder_output { t3 } else { a2 }, + ); + let output: RegisterOperand = if remainder_output { + t3.operand() + } else { + a2.operand() + }; + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + output, 0, - )?; + ); + asm.release_many([t2, t3]); } - let sequence = asm.finalize()?; - allocator.release(a2)?; - allocator.release(a3)?; - allocator.release(t0)?; - allocator.release(t1)?; - allocator.release(t2)?; - allocator.release(t3)?; + asm.release_many([a2, a3, t0, t1]); if let Some(t4) = t4 { - allocator.release(t4)?; + asm.release(t4); } - Ok(sequence) + + asm.finalize() } +/// Builds the unsigned `DIVUW`/`REMUW` verifier sequence. +/// +/// Word unsigned division first zero-extends both source operands. A quotient +/// witness is then constrained by +/// `dividend = quotient * divisor + remainder` with `remainder < divisor`, +/// except that the RISC-V divisor-zero quotient is admitted by +/// `VirtualAssertValidDiv0`. The chosen output is sign-extended as a 32-bit +/// RV64 word result. pub(in crate::expand) fn expand_unsigned_word_div_rem( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, + instruction: &SourceInstructionRow, remainder_output: bool, -) -> Result, ExpansionError> { - let rs1_extended = allocator.allocate()?; - let rs2_extended = allocator.allocate()?; - let quotient = allocator.allocate()?; +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let rs1_extended = asm.allocate()?; + let rs2_extended = asm.allocate()?; + let quotient = asm.allocate()?; let tmp = if remainder_output { quotient } else { - allocator.allocate()? + asm.allocate()? }; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i( - InstructionKind::VirtualZeroExtendWord, - rs1_extended, - rs1(instruction)?, + + // The relation is unsigned 32-bit, so both architectural inputs are + // explicitly normalized before any quotient checks are emitted. + asm.expand_i( + SourceInstructionKind::VirtualZeroExtendWord( + jolt_riscv::instructions::VirtualZeroExtendWord(()), + ), + rs1_extended.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.expand_i( + SourceInstructionKind::VirtualZeroExtendWord( + jolt_riscv::instructions::VirtualZeroExtendWord(()), + ), + rs2_extended.operand(), + reg(rs2(instruction)?), 0, - )?; - asm.emit_i( - InstructionKind::VirtualZeroExtendWord, - rs2_extended, - rs2(instruction)?, + ); + // quotient is advice; tmp is first q * divisor and then the derived + // remainder unless this is the quotient-output path. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + quotient.operand(), 0, - )?; - asm.emit_j(InstructionKind::VirtualAdvice, quotient, 0)?; - asm.emit_b( - InstructionKind::VirtualAssertMulUNoOverflow, - quotient, - rs2_extended, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertMulUNoOverflow, + quotient.operand(), + rs2_extended.operand(), 0, - )?; - asm.emit_r(InstructionKind::MUL, tmp, quotient, rs2_extended)?; - asm.emit_b(InstructionKind::VirtualAssertLTE, tmp, rs1_extended, 0)?; - asm.emit_r(InstructionKind::SUB, tmp, rs1_extended, tmp)?; - asm.emit_b( - InstructionKind::VirtualAssertValidUnsignedRemainder, - tmp, - rs2_extended, + ); + asm.expand_r( + SourceInstructionKind::MUL, + tmp.operand(), + quotient.operand(), + rs2_extended.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + tmp.operand(), + rs1_extended.operand(), 0, - )?; + ); + asm.expand_r( + SourceInstructionKind::SUB, + tmp.operand(), + rs1_extended.operand(), + tmp.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidUnsignedRemainder, + tmp.operand(), + rs2_extended.operand(), + 0, + ); + if remainder_output { - asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - tmp, + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + tmp.operand(), 0, - )?; + ); } else { - asm.emit_i(InstructionKind::VirtualSignExtendWord, tmp, quotient, 0)?; - asm.emit_b( - InstructionKind::VirtualAssertValidDiv0, - rs2_extended, - tmp, + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + tmp.operand(), + quotient.operand(), + 0, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertValidDiv0, + rs2_extended.operand(), + tmp.operand(), 0, - )?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, tmp, 0)?; + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + tmp.operand(), + 0, + ); } - let sequence = asm.finalize()?; - allocator.release(rs1_extended)?; - allocator.release(rs2_extended)?; - allocator.release(quotient)?; + + asm.release_many([rs1_extended, rs2_extended, quotient]); if !remainder_output { - allocator.release(tmp)?; + asm.release(tmp); } - Ok(sequence) + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/error.rs b/crates/jolt-program/src/expand/error.rs index fa94e93078..b83edec7d6 100644 --- a/crates/jolt-program/src/expand/error.rs +++ b/crates/jolt-program/src/expand/error.rs @@ -12,8 +12,24 @@ pub enum ExpansionError { InvalidInlineWriteTarget { register: u8, minimum_register: u8 }, #[error("expansion produced an empty instruction sequence")] EmptySequence, - #[error("malformed normalized instruction: {0}")] + #[error("expansion produced {actual} rows, exceeding the per-source capacity {capacity}")] + CapacityExceeded { actual: usize, capacity: usize }, + #[error("expansion recursion depth exceeded {max_depth}")] + RecursionDepthExceeded { max_depth: usize }, + #[error("temporary expansion register {index} was used before allocation")] + UnallocatedTemporaryRegister { index: usize }, + #[error("temporary expansion register {index} was allocated more than once")] + DuplicateTemporaryRegister { index: usize }, + #[error("temporary expansion register {index} was allocated but not released")] + LeakedTemporaryRegister { index: usize }, + #[error("expansion allocated too many temporary registers: {actual}")] + TooManyTemporaryRegisters { actual: usize }, + #[error("malformed Jolt row: {0}")] MalformedInstruction(&'static str), + #[error("source instruction {0:?} has no direct final Jolt row")] + IllegalSourceInstruction(jolt_riscv::SourceInstructionKind), + #[error("instruction {0:?} is not legal in finalized provider-free bytecode")] + IllegalTargetInstruction(jolt_riscv::JoltInstructionKind), #[error("unsupported CSR 0x{0:03x}")] UnsupportedCsr(u16), #[error("unsupported instruction expansion")] diff --git a/crates/jolt-program/src/expand/fixtures/main_expand_parity_hashes.json b/crates/jolt-program/src/expand/fixtures/main_expand_parity_hashes.json new file mode 100644 index 0000000000..7fb9987448 --- /dev/null +++ b/crates/jolt-program/src/expand/fixtures/main_expand_parity_hashes.json @@ -0,0 +1 @@ +[{"input":{"address":2147483648,"instruction_kind":"rv64.addiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"ADDIW_imm_7_compressed_false","output_sha256":"32eb8135b6f9dbabe85ca657240191750cbbabeb167f91cf14961947403aa68b"},{"input":{"address":2147483652,"instruction_kind":"rv64.addiw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"ADDIW_imm_7_compressed_true","output_sha256":"4f7dca9d6949f6f47df4fdd51e97d3875ea1180f95cbd9f6e6b3575b0bd2516b"},{"input":{"address":2147483656,"instruction_kind":"rv64.addiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"ADDIW_imm_7_rd0","output_sha256":"0a8540287f921cfc9bc91ef662f7844cd6292c5d5ecf710a258348be35c6c801"},{"input":{"address":2147483660,"instruction_kind":"rv64.addw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"ADDW_imm_0_compressed_false","output_sha256":"bd1fedb9566bcf65d9679270fb21bd50f09efacd943a0c4a100c81e0291229a0"},{"input":{"address":2147483664,"instruction_kind":"rv64.addw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"ADDW_imm_0_compressed_true","output_sha256":"a781f668473c1ef3500291c03d0a9497103a24bbb40c62801ae360c8b36b8520"},{"input":{"address":2147483668,"instruction_kind":"rv64.addw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"ADDW_imm_0_rd0","output_sha256":"bfae330f62da52cef32eccd4e489bab2fbfa13c9c65bf14035cb07c4567e36fa"},{"input":{"address":2147483672,"instruction_kind":"rv64.subw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SUBW_imm_0_compressed_false","output_sha256":"f320ea66ecc5c79e235fd9a72c93bbc3fab27122f1a2fd8f27ad10a76c9b9b13"},{"input":{"address":2147483676,"instruction_kind":"rv64.subw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SUBW_imm_0_compressed_true","output_sha256":"69d51e38a8df842700f67963a7398186248c3c5d99f96b691705e2ad57bc0d40"},{"input":{"address":2147483680,"instruction_kind":"rv64.subw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SUBW_imm_0_rd0","output_sha256":"b3c3fae510b2a189580d1ab6172042cde5a72d35dc6f39240b5f614fca4c8b77"},{"input":{"address":2147483684,"instruction_kind":"rv64.mulh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULH_imm_0_compressed_false","output_sha256":"4b4db938916d2ba87064fd7f1f8e7ba21c8ea568f89476b30d4a62d1b4b8ebbb"},{"input":{"address":2147483688,"instruction_kind":"rv64.mulh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULH_imm_0_compressed_true","output_sha256":"0c8237538c2e73d425a068fb5bf2ff4f10a55f3c4516e5250c38dfe22ad67dee"},{"input":{"address":2147483692,"instruction_kind":"rv64.mulh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULH_imm_0_rd0","output_sha256":"ecd056a055bfb1a7cde35ff5558e1daf449a1f55998e6f51af745ce1c7e3b5eb"},{"input":{"address":2147483696,"instruction_kind":"rv64.mulhsu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULHSU_imm_0_compressed_false","output_sha256":"737679d0af913462144bd19e2e04a63d9140dfcf183eb688c3158d662d1ed506"},{"input":{"address":2147483700,"instruction_kind":"rv64.mulhsu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULHSU_imm_0_compressed_true","output_sha256":"b8aef2f3b2aae411adc434348848bf6cad6a968e8c0a74795c2b02eb460a3a25"},{"input":{"address":2147483704,"instruction_kind":"rv64.mulhsu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULHSU_imm_0_rd0","output_sha256":"b897a37d1667f3954e7f34d56379862435d82731a0a7c57dd19a6eff7b303531"},{"input":{"address":2147483708,"instruction_kind":"rv64.mulw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULW_imm_0_compressed_false","output_sha256":"df5ac5c99588ca27e8e892df2cfa3cdbaa4bfd2def848143b15c0fdf2222853b"},{"input":{"address":2147483712,"instruction_kind":"rv64.mulw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULW_imm_0_compressed_true","output_sha256":"0ff4d7975f52e83a298d0babe45ac2c0837528026ff37855da715b27075e7f28"},{"input":{"address":2147483716,"instruction_kind":"rv64.mulw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"MULW_imm_0_rd0","output_sha256":"6dc04ad3023565cf09fcca5253f26497feda1fc05e9d3402024e137ad4c0eae9"},{"input":{"address":2147483720,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_-8_compressed_false","output_sha256":"26fb0653ddb49512b4b4d820cf4584b6684e719fbb6145cab347bd4269667dc6"},{"input":{"address":2147483724,"instruction_kind":"rv64.lb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_-8_compressed_true","output_sha256":"5997effc5a965acf862945b6a309cc81aaa816fbf2b2c3f3a22548ea11da5d88"},{"input":{"address":2147483728,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_-8_rd0","output_sha256":"c3c61ba93b962df9b85f5a3008af04948021cd461c460ba3e861988e2fe28cbf"},{"input":{"address":2147483732,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_0_compressed_false","output_sha256":"6c5fd0bde74c80ecb4cc420842cd6f42513274e1fa33325704324157b517ad18"},{"input":{"address":2147483736,"instruction_kind":"rv64.lb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_0_compressed_true","output_sha256":"9ee9efd0444c28b6bffc269fb9221f78bf9a2efacfdd261a09b1cd17f433f994"},{"input":{"address":2147483740,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_0_rd0","output_sha256":"7ec32c39eca85cb749217a4f2c18c0e1c1e5d2657b6f4aaf73cafe20ce283960"},{"input":{"address":2147483744,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_7_compressed_false","output_sha256":"849f260398aaed889a9b87b0189eb7dd2fc2af7e663a28a4476aef8d374390b4"},{"input":{"address":2147483748,"instruction_kind":"rv64.lb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_7_compressed_true","output_sha256":"98ef3b14008a0ff56c67fc737939db691e0f800a23a8dae3ef5391041692cf6d"},{"input":{"address":2147483752,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_7_rd0","output_sha256":"fde88e48d2849403c3738770f390475014528663b3e01f1f982a9754dcfbcc68"},{"input":{"address":2147483756,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_12_compressed_false","output_sha256":"caab1241ab825cf8ce84205e04ebdd61e2dbcb29b30780350de00e22f4bc4569"},{"input":{"address":2147483760,"instruction_kind":"rv64.lb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_12_compressed_true","output_sha256":"bb95396e7019cb9e14815969cf40d5075a6d424a175900455e5e92a4af31b0f8"},{"input":{"address":2147483764,"instruction_kind":"rv64.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LB_imm_12_rd0","output_sha256":"b30e215c960b57348346221efbd35e939ac8b355f89717670d932f89f9bf98a6"},{"input":{"address":2147483768,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_-8_compressed_false","output_sha256":"1d794719204fb6f53381a9a9c685f953fe88a98e179a64ca6089f38e63122877"},{"input":{"address":2147483772,"instruction_kind":"rv64.lbu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_-8_compressed_true","output_sha256":"a0bc3b6f3e02098e769aa6550fa99ad4715954c38107c2f19ac0936490720f5d"},{"input":{"address":2147483776,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_-8_rd0","output_sha256":"190834cdbc2be918aa030b4e98a4220219e9be071b829bdcd78b3b8e9e340980"},{"input":{"address":2147483780,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_0_compressed_false","output_sha256":"fef6d17754475755b30bca1b3ec7c713454a2627344a84e08794b40dd9289070"},{"input":{"address":2147483784,"instruction_kind":"rv64.lbu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_0_compressed_true","output_sha256":"f1f609656580a8e49fa482b95bee4abafdf953f9423d7aa3e3545ecac1900441"},{"input":{"address":2147483788,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_0_rd0","output_sha256":"50f8d4c554be46f48b43bbf88bf463a053b81c6823b7fc03287fc2c95db3a454"},{"input":{"address":2147483792,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_7_compressed_false","output_sha256":"6dc2a543841884a923bd752f1fc558d6f71152b0a71bd2ecffc14e05cf115a60"},{"input":{"address":2147483796,"instruction_kind":"rv64.lbu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_7_compressed_true","output_sha256":"c31ce4f5655a707a7a7d58e89cad98d5e49b87a0c221ece201d554a3db697a4b"},{"input":{"address":2147483800,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_7_rd0","output_sha256":"4196e299ea6726cc3d1aaec248a5ced10ba99adf14f01a5629cd7d28b82226e4"},{"input":{"address":2147483804,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_12_compressed_false","output_sha256":"448568e0c8dff20616df8f4e7abcdff49c02dc00f89bd53e21e670846c7db4f8"},{"input":{"address":2147483808,"instruction_kind":"rv64.lbu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_12_compressed_true","output_sha256":"6243fa41f0fb2aab4a108ab1fab3d41320f37c46225dd965e0d85efc739b2c32"},{"input":{"address":2147483812,"instruction_kind":"rv64.lbu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LBU_imm_12_rd0","output_sha256":"1fa314763b7400976c1ed1f4c661bc85f5602d05181fc7489801fa4a8faf74ef"},{"input":{"address":2147483816,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_-8_compressed_false","output_sha256":"503f3915141b946e2e119993b80b74d11e52efd9d6586d4b20065bc77ac22810"},{"input":{"address":2147483820,"instruction_kind":"rv64.lh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_-8_compressed_true","output_sha256":"afbb395a7d76b5cb57c1e83a5a2aa4f3eba78738ab43fea499679acd26e302b9"},{"input":{"address":2147483824,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_-8_rd0","output_sha256":"def4582c55c64e60f305ad91972532536be918610809330c594dcf0d5cbe2ffa"},{"input":{"address":2147483828,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_0_compressed_false","output_sha256":"d3788c378b615ce21f8ccb1c838b3259d5d4b46f5f4f8fe8c02b73daedda1d11"},{"input":{"address":2147483832,"instruction_kind":"rv64.lh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_0_compressed_true","output_sha256":"eace1027c1ccd99e104cdeb9ea31c64a4bdc6dfa35f8d8c31039634c6ffd4d4d"},{"input":{"address":2147483836,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_0_rd0","output_sha256":"f45142e422fde60d56c59e4ad5be19b4ced63417616329fbd178062318eda5ed"},{"input":{"address":2147483840,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_7_compressed_false","output_sha256":"037f4b200f1b4e30c8a88141d84d3b48ebc27d1e7132d99c37ff7257bd32f42b"},{"input":{"address":2147483844,"instruction_kind":"rv64.lh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_7_compressed_true","output_sha256":"b82cfbabd41be3b5c677853bfc0dd253980b7af5ef6d0a9288156be080521c63"},{"input":{"address":2147483848,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_7_rd0","output_sha256":"b84612ecde9d0d869a7fdfd6853e31cb34043ca9ad9b11772faace5096ac0f43"},{"input":{"address":2147483852,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_12_compressed_false","output_sha256":"de5530ac0a09ae8cdcca95bbec597e3751d4529f2e9d680678ea0ff224e354f2"},{"input":{"address":2147483856,"instruction_kind":"rv64.lh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_12_compressed_true","output_sha256":"47431511cea5b4bbb9aa17b3c7addf61c9c427e726a5b9aa2bb8f2f6a9caa19f"},{"input":{"address":2147483860,"instruction_kind":"rv64.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LH_imm_12_rd0","output_sha256":"ad609accc2360101151cca7af3fdf67efc3d37bc160b4caa95981be138922941"},{"input":{"address":2147483864,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_-8_compressed_false","output_sha256":"ead3647a79709d6f4aea104b54fc35c610e2630a622c434676ccfff7dd27f726"},{"input":{"address":2147483868,"instruction_kind":"rv64.lhu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_-8_compressed_true","output_sha256":"1c484667243e53d96cc6cb7ec713b49bc187efef517a3aeda5c61a570d242c1a"},{"input":{"address":2147483872,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_-8_rd0","output_sha256":"43c10224b80f64cf6ea0d5606588010b1600b625b6e80b446e4b4f1d5095d260"},{"input":{"address":2147483876,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_0_compressed_false","output_sha256":"298f42274de3f10a95fc9c646fa0c68a6e06d19409ef6c05063a487f5d5286b9"},{"input":{"address":2147483880,"instruction_kind":"rv64.lhu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_0_compressed_true","output_sha256":"237552d99d71b76e06aa5ae599619c12a2eeeacf19c9e523301a3679045464d8"},{"input":{"address":2147483884,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_0_rd0","output_sha256":"c209275a91a643ebd0078035a1cf2d06b95c3f4e01c613fe93c90d23c4324e43"},{"input":{"address":2147483888,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_7_compressed_false","output_sha256":"f83d274835b0a02bf6bf5e76dc3878b3ef6675eb2e45a9144b84d1d7789a2a18"},{"input":{"address":2147483892,"instruction_kind":"rv64.lhu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_7_compressed_true","output_sha256":"ec7584d2afce54389f97b9698e0a01749c49c748d7accaafe0713d420d911af5"},{"input":{"address":2147483896,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_7_rd0","output_sha256":"7695dd8fc4a41975ced98f717d78babec74f39d10f41f32bdbaa02843f95c3ff"},{"input":{"address":2147483900,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_12_compressed_false","output_sha256":"8e947bc7d467b2de7e18c97e470f01edf351508021692e5c53ec637e2cabb9ae"},{"input":{"address":2147483904,"instruction_kind":"rv64.lhu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_12_compressed_true","output_sha256":"f802708322e058d8d27076299e3dac4d19f832e9513d1be9cf40579aaeb28b60"},{"input":{"address":2147483908,"instruction_kind":"rv64.lhu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LHU_imm_12_rd0","output_sha256":"54747031abb7d42d9794a2c074ccc03e3d5647d822f5484139152b9aa00f866e"},{"input":{"address":2147483912,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_-8_compressed_false","output_sha256":"f6afe7068426bc40cef36919457c9b70bdf5adfaa8ed5c2a70165ba7af9fe83a"},{"input":{"address":2147483916,"instruction_kind":"rv64.lw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_-8_compressed_true","output_sha256":"0fb99b58688422fe28d51ebf83ab7ad70234000e9b6e9defbd95b1a81562724c"},{"input":{"address":2147483920,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_-8_rd0","output_sha256":"5ebbf2e15fafde1c6ee816601ad65a0ae39a12257468aca0134094cf260d04b5"},{"input":{"address":2147483924,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_0_compressed_false","output_sha256":"32afac063f169e406e1c8bb54faafe487d3576141d1a70730b36cc1af3a0ee17"},{"input":{"address":2147483928,"instruction_kind":"rv64.lw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_0_compressed_true","output_sha256":"bb375a864fd55c1f93d1fbb17c840b3a1780a4a8815706ff056161b9d9f3daee"},{"input":{"address":2147483932,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_0_rd0","output_sha256":"7b08bce9027a738aab5eff9d1ff4536b61f6cc5a8d58a8fb7ecc7d7ec7ac4dcc"},{"input":{"address":2147483936,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_7_compressed_false","output_sha256":"3d94129ffac0419a2bc070e37af804a19ebc871e539d6df26c343ea7bb9bedfa"},{"input":{"address":2147483940,"instruction_kind":"rv64.lw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_7_compressed_true","output_sha256":"8e0365de1cf9a785c2d5a4ba80c4f7c15e8ab2bb053d1bf2a11d5fd649672b5f"},{"input":{"address":2147483944,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_7_rd0","output_sha256":"97548f809f6a958a088077cdc2d7a7db196481b948dd6efb5c0b70fc3b284f39"},{"input":{"address":2147483948,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_12_compressed_false","output_sha256":"f6fe3aa0d183fc77cf527478e3bc2efd7c790f11f5571b7acfe344fc1e157cbf"},{"input":{"address":2147483952,"instruction_kind":"rv64.lw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_12_compressed_true","output_sha256":"7e34d4e05afdc3567d8f914b7d751c2a73b5056d426c7d17b0a5c92e297ec2bc"},{"input":{"address":2147483956,"instruction_kind":"rv64.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LW_imm_12_rd0","output_sha256":"73486afac48118e112db086e42875c7e5ea01cff6d513c65645af77c4298010d"},{"input":{"address":2147483960,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_-8_compressed_false","output_sha256":"b9e4d0a4f5ba6d6bae2d48a761ba2625d40fe82a32fed0d6e077e990755319b2"},{"input":{"address":2147483964,"instruction_kind":"rv64.lwu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_-8_compressed_true","output_sha256":"78a5de0629f59f2b73443bae7d0d0bf3834094f3830395798e8a51fe6329a5a2"},{"input":{"address":2147483968,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_-8_rd0","output_sha256":"80a26808f5e77460de5a0f64e326fe250033ac8f7aebdc2965de799503450b92"},{"input":{"address":2147483972,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_0_compressed_false","output_sha256":"37549b8ee97e732984318153bf6f98b474baaf0cf9023c2a96537e33197672b4"},{"input":{"address":2147483976,"instruction_kind":"rv64.lwu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_0_compressed_true","output_sha256":"3a694412b52f12858782f6e760a1ed73e5c0e95333852c8fab9111b2fddf968b"},{"input":{"address":2147483980,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_0_rd0","output_sha256":"1c9870409a2523a8d1d14d80f10eae7b087599dda8e5c834317f920f570afd71"},{"input":{"address":2147483984,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_7_compressed_false","output_sha256":"3ae00aab802f95fe6dfd7bbafd1c28aea7901c84afcdfb7474f1d9d5d88da530"},{"input":{"address":2147483988,"instruction_kind":"rv64.lwu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_7_compressed_true","output_sha256":"bb3c556fab6043c2fcd136cdb97d31f12efa273b3b5e1d2903d3c46b4ce45a85"},{"input":{"address":2147483992,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_7_rd0","output_sha256":"c4cdd6960e3c9a07362c019c53d2a5d4b6cd66735c37c7e74abfe450fcc54e7e"},{"input":{"address":2147483996,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_12_compressed_false","output_sha256":"7151ebe794f8d44e8ee715c5eb6885332f6c9469f0a3fb01ea116fb8da8e38cc"},{"input":{"address":2147484000,"instruction_kind":"rv64.lwu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_12_compressed_true","output_sha256":"26fc1e69e129c511045a7d3d824e6761a75f3cf95446a975001981e596c84d23"},{"input":{"address":2147484004,"instruction_kind":"rv64.lwu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LWU_imm_12_rd0","output_sha256":"a37e90a86b0f09bc84e327854c773710f4770f78596a3d00e8412d2feb52ead6"},{"input":{"address":2147484008,"instruction_kind":"jolt.advice.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLB_imm_12_compressed_false","output_sha256":"43f64dcce65a7f9dcb24d5af946db33a0a0b7978af91fe8bcf172d4c51c0a14f"},{"input":{"address":2147484012,"instruction_kind":"jolt.advice.lb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLB_imm_12_compressed_true","output_sha256":"ae4afaa99785be0fcf3c5d3e3522a8a31ac9d3f7bda5096046a51b96b0b4e1fb"},{"input":{"address":2147484016,"instruction_kind":"jolt.advice.lb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLB_imm_12_rd0","output_sha256":"32921efa5e62fc8928946ff0d766c0f5df199ed2a23280ac0763e6591980b213"},{"input":{"address":2147484020,"instruction_kind":"jolt.advice.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLH_imm_12_compressed_false","output_sha256":"fd080e7d1aee04f55c46cc594e09b1326f92a2b510d2d5e3061354e2963d31c7"},{"input":{"address":2147484024,"instruction_kind":"jolt.advice.lh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLH_imm_12_compressed_true","output_sha256":"091764b16008d8f21c29365555c4c5b0d881d0701519095a959045d27f3bbe33"},{"input":{"address":2147484028,"instruction_kind":"jolt.advice.lh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLH_imm_12_rd0","output_sha256":"b97167790353f55f65f180b86d7a5495a22d02156f950274f53aa451ed2afb71"},{"input":{"address":2147484032,"instruction_kind":"jolt.advice.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLW_imm_12_compressed_false","output_sha256":"bc7a420ac9931c2493be244579bc2cee3b80a4025f5c630698c640bb03ba0ff6"},{"input":{"address":2147484036,"instruction_kind":"jolt.advice.lw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLW_imm_12_compressed_true","output_sha256":"150a868cc85c7cc7a428bdd192a5f27eac4c2cd864178cff91c7a07aa270edbd"},{"input":{"address":2147484040,"instruction_kind":"jolt.advice.lw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLW_imm_12_rd0","output_sha256":"8b44eed1a1d7241dd52a6f0b9b3ce2ba92c9d282386dbd56849a473962817f92"},{"input":{"address":2147484044,"instruction_kind":"jolt.advice.ld","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLD_imm_12_compressed_false","output_sha256":"b067bdca6a0208431eabc20871becfafc914fd1927fe16dcdcaceda7a7b74548"},{"input":{"address":2147484048,"instruction_kind":"jolt.advice.ld","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLD_imm_12_compressed_true","output_sha256":"4d6bcc0f69f02d53bd0c6f70e5fc3e152d98a7ec9e5b5a0f0ca561d2b38a0045"},{"input":{"address":2147484052,"instruction_kind":"jolt.advice.ld","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"AdviceLD_imm_12_rd0","output_sha256":"55f25568cf4f01651639af9e09ed073f0554c6fba482e9de47b37f461b5577c3"},{"input":{"address":2147484056,"instruction_kind":"rv64.amoaddd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDD_imm_0_compressed_false","output_sha256":"2bb5d33fb2e34349818eccde7f8061018b8c8851bf6bab6e0cc48704e5478a7a"},{"input":{"address":2147484060,"instruction_kind":"rv64.amoaddd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDD_imm_0_compressed_true","output_sha256":"c21cc6355f4bb64a34d2e66d1d80fa4299bcb9f429fd1068eeb343d3f0670c02"},{"input":{"address":2147484064,"instruction_kind":"rv64.amoaddd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDD_imm_0_rd0","output_sha256":"f5ce12746feb0698e9a230bd9de36e673a8a89fb5bd50cd35c84b4344626baf4"},{"input":{"address":2147484068,"instruction_kind":"rv64.amoandd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDD_imm_0_compressed_false","output_sha256":"f8bc1accb1029af8a1dd1733694311463693732461704d50b1f856e2f5c5f960"},{"input":{"address":2147484072,"instruction_kind":"rv64.amoandd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDD_imm_0_compressed_true","output_sha256":"b9ba373d17c7d85a09e7926c0ab33901d02917568027a592c55418f107a35bc8"},{"input":{"address":2147484076,"instruction_kind":"rv64.amoandd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDD_imm_0_rd0","output_sha256":"816b849b9a75bc99644d3700cefc92593c0d90ae8e45beeaad943b97b9e48b98"},{"input":{"address":2147484080,"instruction_kind":"rv64.amoord","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORD_imm_0_compressed_false","output_sha256":"72c1c8e3b15c75fbb1776e9effe8da6be950238fdcc30907bf3f5382e220db92"},{"input":{"address":2147484084,"instruction_kind":"rv64.amoord","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORD_imm_0_compressed_true","output_sha256":"a803aaa834afde8f6b380bdec25efd0e188969ed51dd8af6b5fde86fa191bd24"},{"input":{"address":2147484088,"instruction_kind":"rv64.amoord","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORD_imm_0_rd0","output_sha256":"77bd272e8fe8310bb410685068cb4275e421b55dff194edeeb433b740c081862"},{"input":{"address":2147484092,"instruction_kind":"rv64.amoxord","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORD_imm_0_compressed_false","output_sha256":"66c61f28e9063a1136eb2a818ba86615a9d6593b12f018c401b0285f2d67db67"},{"input":{"address":2147484096,"instruction_kind":"rv64.amoxord","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORD_imm_0_compressed_true","output_sha256":"e893e8028a7b87566528c42312916af89fdf595e953b53c8f9d009717b9b8a13"},{"input":{"address":2147484100,"instruction_kind":"rv64.amoxord","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORD_imm_0_rd0","output_sha256":"e09ea056b2bf01e7a3aaf836268491acfb1aa05a0dea906d94ccf926391fd821"},{"input":{"address":2147484104,"instruction_kind":"rv64.amoswapd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPD_imm_0_compressed_false","output_sha256":"760c9cfe1c0c8ef45e0fd4cf839e0479fa18e048c3de68182eccb55cf6993e56"},{"input":{"address":2147484108,"instruction_kind":"rv64.amoswapd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPD_imm_0_compressed_true","output_sha256":"6ebda469dfbf85ea5c879b0b0bc4be79c17772647a091951230d9ecb2d5e6420"},{"input":{"address":2147484112,"instruction_kind":"rv64.amoswapd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPD_imm_0_rd0","output_sha256":"3476479937a73478764447ea7db3188eb5af361550e66b0239b5a56a7d13acb6"},{"input":{"address":2147484116,"instruction_kind":"rv64.amomaxd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXD_imm_0_compressed_false","output_sha256":"8e3ff2ce7538d6009f509893cb10b4ea1c1a36bc3906a5c1668d79bec9fba15f"},{"input":{"address":2147484120,"instruction_kind":"rv64.amomaxd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXD_imm_0_compressed_true","output_sha256":"aac7391dae5141fb7132408ae0fbc7231ce9ba541d0ddf272cdda74dce3cc3bf"},{"input":{"address":2147484124,"instruction_kind":"rv64.amomaxd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXD_imm_0_rd0","output_sha256":"8a9e7bc7a834ceb1bdadb614d4f51870402ff688ed21b3e1d5f7747cd6bac048"},{"input":{"address":2147484128,"instruction_kind":"rv64.amomaxud","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUD_imm_0_compressed_false","output_sha256":"ab391d84e038457857a94d59b56405c5f9b12bb2ca6e179f526775febd65ac53"},{"input":{"address":2147484132,"instruction_kind":"rv64.amomaxud","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUD_imm_0_compressed_true","output_sha256":"aab710d9ee59c60a9707a59ae7d2fb4b04ae6a2a03bffa739183f7c8e0fc2310"},{"input":{"address":2147484136,"instruction_kind":"rv64.amomaxud","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUD_imm_0_rd0","output_sha256":"b69701080acd0121a719136c57b54c1c2f2f37e707ce31ad84ff0fdb5646f946"},{"input":{"address":2147484140,"instruction_kind":"rv64.amomind","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMIND_imm_0_compressed_false","output_sha256":"e0b8a120e659d45c82c10c0ad585e19d7f4b76b975daf7d543376dd731967c0e"},{"input":{"address":2147484144,"instruction_kind":"rv64.amomind","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMIND_imm_0_compressed_true","output_sha256":"6640331c647804d50eadd5797128db3f37d65d6ac7927df7c40b63ca3e7d2749"},{"input":{"address":2147484148,"instruction_kind":"rv64.amomind","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMIND_imm_0_rd0","output_sha256":"cdeb6631b5bd63698cf2f38a5c5c0c8394fc23487ecd7c853365c186666c80d3"},{"input":{"address":2147484152,"instruction_kind":"rv64.amominud","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUD_imm_0_compressed_false","output_sha256":"c07032e6536ffb2679ebef81a7c9fff4cfb41f2d008488716528b27632819cac"},{"input":{"address":2147484156,"instruction_kind":"rv64.amominud","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUD_imm_0_compressed_true","output_sha256":"3bc71aa7c8bad2f13b422e1e37f9b5634475337f64f6bc878ec6c67f04307cf3"},{"input":{"address":2147484160,"instruction_kind":"rv64.amominud","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUD_imm_0_rd0","output_sha256":"667676915672a212bade4984dbec38e9c206d79ddc3a56be872596c06efd1103"},{"input":{"address":2147484164,"instruction_kind":"rv64.amoaddw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDW_imm_0_compressed_false","output_sha256":"649c024cc9cdabc94041ff290b16ab49e4c3a27a57e842443e235b4e3a2ef3c9"},{"input":{"address":2147484168,"instruction_kind":"rv64.amoaddw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDW_imm_0_compressed_true","output_sha256":"13b37115989a604c463793894cd30e71b6b53ef88fa783b337ea83196620436d"},{"input":{"address":2147484172,"instruction_kind":"rv64.amoaddw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOADDW_imm_0_rd0","output_sha256":"65ee49bb5ab6983864a4179d964da602144c0318c7d19ea9a0c48f26c4bd03a6"},{"input":{"address":2147484176,"instruction_kind":"rv64.amoandw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDW_imm_0_compressed_false","output_sha256":"82339fae39f327fb448e4278ef36bf3331402c477a9a43b347aa1787346fda23"},{"input":{"address":2147484180,"instruction_kind":"rv64.amoandw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDW_imm_0_compressed_true","output_sha256":"d37f4e70643ab449b04f98390dc36ff513577667cc5f610db9b495a1c1eb14bd"},{"input":{"address":2147484184,"instruction_kind":"rv64.amoandw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOANDW_imm_0_rd0","output_sha256":"cf4fd8d75f0432b51eb25b14554e66e716b38b3ef9f96bd5a69c86892b3d74bd"},{"input":{"address":2147484188,"instruction_kind":"rv64.amoorw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORW_imm_0_compressed_false","output_sha256":"6f2c9f7fef3ad1d16675450c0ed3c9fefea9a97f4f6df675895fcd87da2360d6"},{"input":{"address":2147484192,"instruction_kind":"rv64.amoorw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORW_imm_0_compressed_true","output_sha256":"15efff01e37dd41f7185e87de6a50b86ff36737fff747cd07f52add4ec948e00"},{"input":{"address":2147484196,"instruction_kind":"rv64.amoorw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOORW_imm_0_rd0","output_sha256":"c0c66b74a8d04a4a8b080fd41f154d7252b4da8f94fdec23237cab9b62de4686"},{"input":{"address":2147484200,"instruction_kind":"rv64.amoxorw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORW_imm_0_compressed_false","output_sha256":"69c908b93827575f5a5011feed81b69ea472ea4f577c00bcd431d35370134800"},{"input":{"address":2147484204,"instruction_kind":"rv64.amoxorw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORW_imm_0_compressed_true","output_sha256":"8bfc831bcf8f191ea77d2c30124bd36f59adb9fb2d4801a96c8fcec6eb29f6c4"},{"input":{"address":2147484208,"instruction_kind":"rv64.amoxorw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOXORW_imm_0_rd0","output_sha256":"da83b8b6829bffe43ac01b0ce74942010df86969b463aa497b072b6d224bddae"},{"input":{"address":2147484212,"instruction_kind":"rv64.amoswapw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPW_imm_0_compressed_false","output_sha256":"99871a63005e3c08ebe0d3cf774beb7f8077dd699f0b44156677e4e0a3196b53"},{"input":{"address":2147484216,"instruction_kind":"rv64.amoswapw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPW_imm_0_compressed_true","output_sha256":"c3cfae7428004a22fcc8a94f0c66ace66ab945e7758d927adef2018f1b892ddf"},{"input":{"address":2147484220,"instruction_kind":"rv64.amoswapw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOSWAPW_imm_0_rd0","output_sha256":"98d26c39f82d65132539f32d44a7b823b788834952b461053f28dd2b9ff5d602"},{"input":{"address":2147484224,"instruction_kind":"rv64.amomaxw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXW_imm_0_compressed_false","output_sha256":"91fa14bdb18a5769b99281ed82a43da466f136abe32a949db7e7cd450b7b47af"},{"input":{"address":2147484228,"instruction_kind":"rv64.amomaxw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXW_imm_0_compressed_true","output_sha256":"fc5edb5ef8a369be52e5b61a09666032c6ce11c678d5a34f44767109766c16c2"},{"input":{"address":2147484232,"instruction_kind":"rv64.amomaxw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXW_imm_0_rd0","output_sha256":"8aa337a5c4a8b676e4f0d694d180562d382bfb016159486fccf4311294444ec1"},{"input":{"address":2147484236,"instruction_kind":"rv64.amomaxuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUW_imm_0_compressed_false","output_sha256":"ed77c8c2e597e52a0f733904dd7fad86a3c7a2f7d2703489a3f953349e79ce82"},{"input":{"address":2147484240,"instruction_kind":"rv64.amomaxuw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUW_imm_0_compressed_true","output_sha256":"266b297261bfe0ba811716c72b6e5186ecd643bd3b158cf81e9cc59bddbee3b5"},{"input":{"address":2147484244,"instruction_kind":"rv64.amomaxuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMAXUW_imm_0_rd0","output_sha256":"ca573460754b9cb6a0fa4c7bd4e6c190186eaba7c6ee521347a8ce65c7f57a2d"},{"input":{"address":2147484248,"instruction_kind":"rv64.amominw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINW_imm_0_compressed_false","output_sha256":"105918d524b35a2f0dae9bed040b44dc31f30ba090de0cb20cc7f271d419d862"},{"input":{"address":2147484252,"instruction_kind":"rv64.amominw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINW_imm_0_compressed_true","output_sha256":"7553106c87e22eef22956c77858f6ca301bf227d207e6c14efb62bd2acd3cf48"},{"input":{"address":2147484256,"instruction_kind":"rv64.amominw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINW_imm_0_rd0","output_sha256":"7a3e1f6bd0439774195b4133fa9979904640644c3d6e85c6e002c86fa09689c3"},{"input":{"address":2147484260,"instruction_kind":"rv64.amominuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUW_imm_0_compressed_false","output_sha256":"56807d5a22603248cefb97257f21c1997f7ef6f583c3e693254b3f8a7849fc96"},{"input":{"address":2147484264,"instruction_kind":"rv64.amominuw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUW_imm_0_compressed_true","output_sha256":"8e8725a22805498042a93990b4bcdb7446abcd4efb599a0d889c912319a5e8af"},{"input":{"address":2147484268,"instruction_kind":"rv64.amominuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"AMOMINUW_imm_0_rd0","output_sha256":"797c6da8047993bd52cb50de8b7bb6ca32239a5fc25a52b5303196750b56943e"},{"input":{"address":2147484272,"instruction_kind":"rv64.lrd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRD_imm_0_compressed_false","output_sha256":"495faf0082a4f25c6a97a7bbe42114d56620dc3cb9c403ac8a34a196c45ae0e4"},{"input":{"address":2147484276,"instruction_kind":"rv64.lrd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRD_imm_0_compressed_true","output_sha256":"207a07bdb291bbaad75cb1de790162ba5f2b7d83fc5d5d331cef7c7158cda5bd"},{"input":{"address":2147484280,"instruction_kind":"rv64.lrd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRD_imm_0_rd0","output_sha256":"8dff0eda23a20ba0fe7e08c49e18ecfe4d38b98fe0dff424bcd35d7e6c1fff31"},{"input":{"address":2147484284,"instruction_kind":"rv64.lrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRW_imm_0_compressed_false","output_sha256":"d9a269f4acc663f8639ca43a72cb25790a82841ed7920af0aab47776610c8d6d"},{"input":{"address":2147484288,"instruction_kind":"rv64.lrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRW_imm_0_compressed_true","output_sha256":"ef7e5b1bd120b1642b5f94bd1999c597c14d7ed66bf71a8ace95dc6091651fd6"},{"input":{"address":2147484292,"instruction_kind":"rv64.lrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"LRW_imm_0_rd0","output_sha256":"8173d4ac72b74e1be6ee594c10761d3eb58b49eaeada8386c4476980175dc960"},{"input":{"address":2147484296,"instruction_kind":"rv64.div","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIV_imm_0_compressed_false","output_sha256":"43944af13ba4457f0ffbbb193397d83b066cce481b92984e117203a5867fa0d2"},{"input":{"address":2147484300,"instruction_kind":"rv64.div","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIV_imm_0_compressed_true","output_sha256":"2d77e0b714305e97044e656183359a85eefbcc03b0be82c1bd8a1e808fc8f498"},{"input":{"address":2147484304,"instruction_kind":"rv64.div","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIV_imm_0_rd0","output_sha256":"cb4d3498e3fc436eca167393d5700b5cbd53306326ab87d29e08759e44305ccb"},{"input":{"address":2147484308,"instruction_kind":"rv64.divu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVU_imm_0_compressed_false","output_sha256":"371b75792e074cf9228ce6fd3963cb487a1d178713a82ad1ce37b9896142a8ac"},{"input":{"address":2147484312,"instruction_kind":"rv64.divu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVU_imm_0_compressed_true","output_sha256":"f69230319a20ce9f3b211477790e11abf2f9c66414cac0eb6f54ec2ab414671b"},{"input":{"address":2147484316,"instruction_kind":"rv64.divu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVU_imm_0_rd0","output_sha256":"5183f904e26ac710cf834f25b842324250433fabd4028f617b8ac66eba1cb5e5"},{"input":{"address":2147484320,"instruction_kind":"rv64.divw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVW_imm_0_compressed_false","output_sha256":"64aa2c0d990222bf4567d369213b84a11c0721dc20efc2df4c2246656c2704bf"},{"input":{"address":2147484324,"instruction_kind":"rv64.divw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVW_imm_0_compressed_true","output_sha256":"2a9d889360db27c5994d4d3232642c27949b7b332416d7dc4ba5cea4d5efc1d3"},{"input":{"address":2147484328,"instruction_kind":"rv64.divw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVW_imm_0_rd0","output_sha256":"65b015e787ccc00ecaf5fcb1d37705241165355a94a2b48f22eba3ca91c12d4d"},{"input":{"address":2147484332,"instruction_kind":"rv64.divuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVUW_imm_0_compressed_false","output_sha256":"b40ec7339e795190d43d0ce13a384feb57af1d42e0327e2fafc19fb6d915daad"},{"input":{"address":2147484336,"instruction_kind":"rv64.divuw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVUW_imm_0_compressed_true","output_sha256":"2cc8e3ff46516e6c010122307d17b0749babc056c51907b52130c73801f557f8"},{"input":{"address":2147484340,"instruction_kind":"rv64.divuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"DIVUW_imm_0_rd0","output_sha256":"7fd28dd01196a274f97aaeb23b028e47972479fa600c1cdf843c08739bbb38a2"},{"input":{"address":2147484344,"instruction_kind":"rv64.rem","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REM_imm_0_compressed_false","output_sha256":"2d7877911a299d329126b496f1f08fd1b592175f7c664ab0ccc87bb99428501f"},{"input":{"address":2147484348,"instruction_kind":"rv64.rem","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REM_imm_0_compressed_true","output_sha256":"008f83b8187feb7a16c8cb5c5d481e425cb3eac9291a64f367468eef5e0fcf59"},{"input":{"address":2147484352,"instruction_kind":"rv64.rem","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REM_imm_0_rd0","output_sha256":"292554b300e5cd38e71be3d5153fa92a580e0e4422c869ec90dcccb263412ff5"},{"input":{"address":2147484356,"instruction_kind":"rv64.remu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMU_imm_0_compressed_false","output_sha256":"ffda00a1d7d91327f1cb84254652df60d7cb4ebdfd408f6b759c683d3a8cf1b7"},{"input":{"address":2147484360,"instruction_kind":"rv64.remu","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMU_imm_0_compressed_true","output_sha256":"fd860484f51f7ec1b94a014bfa373d5fef2ee0e08c2424a3d3c0907beda916f4"},{"input":{"address":2147484364,"instruction_kind":"rv64.remu","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMU_imm_0_rd0","output_sha256":"3a16940e25b18b7affe8f4705b551e689c2544da28ac83969873701f8608779f"},{"input":{"address":2147484368,"instruction_kind":"rv64.remw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMW_imm_0_compressed_false","output_sha256":"02b6e0575f7b55dfbe271d1858233330f2232f3c940628c10bfd757a81b3342a"},{"input":{"address":2147484372,"instruction_kind":"rv64.remw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMW_imm_0_compressed_true","output_sha256":"f38bf51a487588b4bc894a169a663329fc8017f70bc25d67985093ad8050186a"},{"input":{"address":2147484376,"instruction_kind":"rv64.remw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMW_imm_0_rd0","output_sha256":"b3b95d862cbcef53002df023d8f9650d49c691ed3e1a9b52e3a19a19ce16d998"},{"input":{"address":2147484380,"instruction_kind":"rv64.remuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMUW_imm_0_compressed_false","output_sha256":"26d6e9d34439a6d48860fc86968ba8ed0768be007b713c4f4809b4502a4845ca"},{"input":{"address":2147484384,"instruction_kind":"rv64.remuw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMUW_imm_0_compressed_true","output_sha256":"be7cdf22f7557a4058a96d8452c16e1ad0a8cd30d5bca2de458fc48025fc56de"},{"input":{"address":2147484388,"instruction_kind":"rv64.remuw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"REMUW_imm_0_rd0","output_sha256":"5a8f0f3b24a5c43e14475106c426d9ee228b10583478bb7d833cb7fa345bf3b3"},{"input":{"address":2147484392,"instruction_kind":"rv64.sb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_-8_compressed_false","output_sha256":"170c9aedbd4b7db981c5a22f3db2cedce84b976733ec593ab320c78a4f2665ce"},{"input":{"address":2147484396,"instruction_kind":"rv64.sb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_-8_compressed_true","output_sha256":"04ed988555084eb037ce8223eb4a4f86c6cf9637ac24536e6e6f782c63b5b711"},{"input":{"address":2147484400,"instruction_kind":"rv64.sb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_0_compressed_false","output_sha256":"f340de29d5fa558fabe01e05312fc8f2a61ed866fe17838e0e6f5e32e7be2a75"},{"input":{"address":2147484404,"instruction_kind":"rv64.sb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_0_compressed_true","output_sha256":"ef07282b591b0f2d20788786d845a16ba1aaf50b1ad8046a482efe3416100db0"},{"input":{"address":2147484408,"instruction_kind":"rv64.sb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_7_compressed_false","output_sha256":"f99bd70fcc5206c243d5ddf476cf3712b6a02b7058b76677c181df8d2e0dbfbb"},{"input":{"address":2147484412,"instruction_kind":"rv64.sb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_7_compressed_true","output_sha256":"bc6b17c9f2016b782ca9afe9c46738b81bacd72d99c6e993e963aaa7b7590703"},{"input":{"address":2147484416,"instruction_kind":"rv64.sb","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_12_compressed_false","output_sha256":"6739b288bac4a2cfe7047aade4118423243417e204cda74db89de76a2e2cda73"},{"input":{"address":2147484420,"instruction_kind":"rv64.sb","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SB_imm_12_compressed_true","output_sha256":"767725cb9e655410249b2f3fe9e06446fe7fa103a0abc58063ad610f1494cf43"},{"input":{"address":2147484424,"instruction_kind":"rv64.scd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCD_imm_0_compressed_false","output_sha256":"aa8bfb31dc83f5746a044215a4a6eea9de120d10b1c129712094d9ecea6d3903"},{"input":{"address":2147484428,"instruction_kind":"rv64.scd","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCD_imm_0_compressed_true","output_sha256":"1419bbc0c09d03ad34e23f80ec6a6c7fc1bd1e5db264deb19bfc1ff3184af832"},{"input":{"address":2147484432,"instruction_kind":"rv64.scd","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCD_imm_0_rd0","output_sha256":"2e96f5102c2e520dda2a39b4d1c376579f76ea6f0a140037a83c5bfe36d7473b"},{"input":{"address":2147484436,"instruction_kind":"rv64.scw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCW_imm_0_compressed_false","output_sha256":"d5c302434fef13d3e0062cbb6ac9bb42ffc527866171d0377fd1cb2b94317c13"},{"input":{"address":2147484440,"instruction_kind":"rv64.scw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCW_imm_0_compressed_true","output_sha256":"f5acc7105ff64eef489be969bee9f84380d3799b556ae6cfc408d4cd6cfa802f"},{"input":{"address":2147484444,"instruction_kind":"rv64.scw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SCW_imm_0_rd0","output_sha256":"d9b4558f0f8811e4b66c45a085a46aa74ba950befd81689ba9bd1708d6538460"},{"input":{"address":2147484448,"instruction_kind":"rv64.sh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_-8_compressed_false","output_sha256":"e018ab93b8d91bae8db8b3052f8a9ac1fd1b3767a0a891b54975bd4ebb1c3456"},{"input":{"address":2147484452,"instruction_kind":"rv64.sh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_-8_compressed_true","output_sha256":"699626f40d2552cecd6cf5c3e01aa2f67ace9bd0c6bf9fabb56cce590cd59960"},{"input":{"address":2147484456,"instruction_kind":"rv64.sh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_0_compressed_false","output_sha256":"5fd6de9f7d76fd5c0296f136617c83f1e38b695137b4db9407f9cbfa46ffc9ad"},{"input":{"address":2147484460,"instruction_kind":"rv64.sh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_0_compressed_true","output_sha256":"ff7c6dd5870c80830003197f69cc76c35384f7fdc344a4fc8c6da77366c37d15"},{"input":{"address":2147484464,"instruction_kind":"rv64.sh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_7_compressed_false","output_sha256":"abd5926d432b47f271a5f953845217249929124ac20c32711b8f04723ed3b45c"},{"input":{"address":2147484468,"instruction_kind":"rv64.sh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_7_compressed_true","output_sha256":"65ef08e4cba5bf72f0320ab7e1087671c2345eeba3cf4f3ec4f846c31bce7d48"},{"input":{"address":2147484472,"instruction_kind":"rv64.sh","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_12_compressed_false","output_sha256":"981b2134c5490496383e24b0a60312fa94c1d041f1c19fc10333a521fd647a94"},{"input":{"address":2147484476,"instruction_kind":"rv64.sh","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SH_imm_12_compressed_true","output_sha256":"383aa3f34364ebde259ce42073b0424e0f162071c5c473c5d27c0eca52a8bc44"},{"input":{"address":2147484480,"instruction_kind":"rv64.sw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_-8_compressed_false","output_sha256":"3e36b24925d6bde044d15cb5d805a17bf3e0f3b18bb21977111bb25a3d087da6"},{"input":{"address":2147484484,"instruction_kind":"rv64.sw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":-8,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_-8_compressed_true","output_sha256":"01edefc6654fa5ca40316654583c9ea10b7d32bffc6c42babcea93ac5f1046d6"},{"input":{"address":2147484488,"instruction_kind":"rv64.sw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_0_compressed_false","output_sha256":"c14039d78be26473d12ae37219f35101f5e91a2cca95067b298a89ca57da02d5"},{"input":{"address":2147484492,"instruction_kind":"rv64.sw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_0_compressed_true","output_sha256":"285f8531caf5b79279314d986c49a9635f46e03385752c640d083f4a44a81606"},{"input":{"address":2147484496,"instruction_kind":"rv64.sw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_7_compressed_false","output_sha256":"4da36a5c2bafc09f9b93b5cac7b78d39313685c5e7b082999994162f6d4bce4e"},{"input":{"address":2147484500,"instruction_kind":"rv64.sw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":7,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_7_compressed_true","output_sha256":"081bce62de8b15d9280677465c9d2b2fb80c2db991854b83ac62410760b871b7"},{"input":{"address":2147484504,"instruction_kind":"rv64.sw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_12_compressed_false","output_sha256":"28740f5d974a41e7b39cd0b82b1a36edaef738168ae393ba9d1eb88eca53dfe7"},{"input":{"address":2147484508,"instruction_kind":"rv64.sw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":12,"rd":null,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SW_imm_12_compressed_true","output_sha256":"4a787e7703369d0ef7baa8e5c333d2212f89f743fdb622494bf92ea1a79b2c02"},{"input":{"address":2147484512,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":768,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_768_compressed_false","output_sha256":"133414373c82541f121480a53a0ff4e37134b77b1203b64c5fd212b445c5ab42"},{"input":{"address":2147484516,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":768,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_768_compressed_true","output_sha256":"f08f6e54ba03f09d95e269b8f36e8ba4cc3d28ccfcb30b01df8c689fd7aeff81"},{"input":{"address":2147484520,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":768,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_768_rd0","output_sha256":"428f9b3215e6eeae1898e4940824b27c48a9764e3621200aff20f1ad9d7938a7"},{"input":{"address":2147484524,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":773,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_773_compressed_false","output_sha256":"b7d05146b56184616ecd4221971d54c737503e045eb748897eccb24737c68f36"},{"input":{"address":2147484528,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":773,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_773_compressed_true","output_sha256":"f94f1a8d661abe8589d44f2fac647e28b4ca0b9eea770f8337be4bf3554a302b"},{"input":{"address":2147484532,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":773,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_773_rd0","output_sha256":"7b983687cff0ef92f27ec1292c2c72195f3b3954f4d6e2bd6bad2c7430607918"},{"input":{"address":2147484536,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":832,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_832_compressed_false","output_sha256":"be0a44d1a0c5057a6d3f90b3b83b5495ede05a4f5a52e9a8b922aee109f2fc7b"},{"input":{"address":2147484540,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":832,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_832_compressed_true","output_sha256":"cb64c4e1e5386ea596913e2bc533b38fc295e2b99cead7d1e0492b681220ab1f"},{"input":{"address":2147484544,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":832,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_832_rd0","output_sha256":"b12f48771546a50f34a7d4a5836a41eaf3ddc1a74556fb079d6d74c7bcb6e827"},{"input":{"address":2147484548,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":833,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_833_compressed_false","output_sha256":"977b63afb204e6ad0d4f58fbe9ebf50d8a901b565e6be91227824e7152ac745d"},{"input":{"address":2147484552,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":833,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_833_compressed_true","output_sha256":"fac7f96b1da91f4ee554c9f0feead5418b78cfa5e68206902aa58c04759ef0d4"},{"input":{"address":2147484556,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":833,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_833_rd0","output_sha256":"ae764128af4fb6864fd09442451f99e2dcbbf6e281ca3c3d9d94b61651d96653"},{"input":{"address":2147484560,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":834,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_834_compressed_false","output_sha256":"a2c81d9e9f70df51c2044575901f7d4dc5b01f037c3d8b8aa2d36e534372ad41"},{"input":{"address":2147484564,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":834,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_834_compressed_true","output_sha256":"665d51f32e97a8bedcd2e2d58a9e901e99a1ef797b9de61779e0e8ef3f3b2c53"},{"input":{"address":2147484568,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":834,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_834_rd0","output_sha256":"b30acdcb1729e88d8986ca0e2bca7288940e2263cc4f0b4e6f792abf04a7d1f3"},{"input":{"address":2147484572,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":835,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_835_compressed_false","output_sha256":"c158bb238f9515843fa9ac16940afb43066a14330868b95881e86a4c71875372"},{"input":{"address":2147484576,"instruction_kind":"rv64.csrrw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":835,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_835_compressed_true","output_sha256":"86e298636877acd5e7ff7468368a3723689630868f4f70cb1543d70a97138a8d"},{"input":{"address":2147484580,"instruction_kind":"rv64.csrrw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":835,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRW_imm_835_rd0","output_sha256":"958098f901892c9c8ca836e21752e64d5a63293092f4c4594064cd64888a3f9e"},{"input":{"address":2147484584,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":768,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_768_compressed_false","output_sha256":"f6f09a674b4ab9d04083787e34d4c9fdd78f5c2573f3a5a19c43d52dc5dc482a"},{"input":{"address":2147484588,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":768,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_768_compressed_true","output_sha256":"e7c5f0f795dbe5988e9dfdc5db221b256fd0405ab54bc4e48ad03bc2a151ea50"},{"input":{"address":2147484592,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":768,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_768_rd0","output_sha256":"2d445eca6a56cacf4a2a93de1b0c4a5c25ed016f3eead50b342900f1e8b875f6"},{"input":{"address":2147484596,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":773,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_773_compressed_false","output_sha256":"d8be2553353733701391a0501823306e4d0a8a6dc24bcd494368a0b95c83c3f1"},{"input":{"address":2147484600,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":773,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_773_compressed_true","output_sha256":"23bca04fb0e9946106a065059bc05f6589e8aebd57165a2f975a5ee9a92edc90"},{"input":{"address":2147484604,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":773,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_773_rd0","output_sha256":"ac41909028f146b7064360672a5b3ef4f65882ce6b896374c9acee6d0efd21a8"},{"input":{"address":2147484608,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":832,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_832_compressed_false","output_sha256":"2395c5a81609ed623d1ad3020cf15f9425797815a2fa4896adb5ecee50d49124"},{"input":{"address":2147484612,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":832,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_832_compressed_true","output_sha256":"df4395ebdc7ecdc23af84c7efeffa66d88cb2ee0d74b4d4c90859e71f9efbc44"},{"input":{"address":2147484616,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":832,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_832_rd0","output_sha256":"892d8226edd977cea783e5ad53adfcfa150619507f5dc50c5eece6650de001b2"},{"input":{"address":2147484620,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":833,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_833_compressed_false","output_sha256":"491c41f068f919023cf75010e459712902ba68cdf9bb9c3dcfff3a4b29b6aff6"},{"input":{"address":2147484624,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":833,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_833_compressed_true","output_sha256":"ceb1b80872c92ebd633a30c6af7b1cd7ade231a836a2abbe60b3d2cee6f3eb3f"},{"input":{"address":2147484628,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":833,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_833_rd0","output_sha256":"74d138c33a935e267bc831a4eee6edf392370ba88b871d10272e870981bc277f"},{"input":{"address":2147484632,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":834,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_834_compressed_false","output_sha256":"151faf7afaba0c55b0a24f54cc53946151567975b07690bf6aff12c786185dea"},{"input":{"address":2147484636,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":834,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_834_compressed_true","output_sha256":"cc373d8a615bbcfbc67eef714a41ff520c9bdede87ea66bc09ddb009f846b66d"},{"input":{"address":2147484640,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":834,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_834_rd0","output_sha256":"7bdd1512682fab33b6666d67ffe8de748fc2e51a3a684d07f38cc198fe4bfba5"},{"input":{"address":2147484644,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":835,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_835_compressed_false","output_sha256":"efc5c14ec1052834cd03dc54e36ad2033d802b720c9f9caec159ca209c76c649"},{"input":{"address":2147484648,"instruction_kind":"rv64.csrrs","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":835,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_835_compressed_true","output_sha256":"abae4155650ca598ff73b18418c8a8e7edcf4842ddf41f13e58a9cc79365a4db"},{"input":{"address":2147484652,"instruction_kind":"rv64.csrrs","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":835,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"CSRRS_imm_835_rd0","output_sha256":"e329268e05037eb1ddda2b31b311398f8b0688b83f22fdcec45e1d64d54f9de8"},{"input":{"address":2147484656,"instruction_kind":"rv64.ebreak","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"EBREAK_imm_0_compressed_false","output_sha256":"436f1b6299f8969a1a67c3f2a3126a9763c6b161c1180df494dcd045840bc1d5"},{"input":{"address":2147484660,"instruction_kind":"rv64.ebreak","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"EBREAK_imm_0_compressed_true","output_sha256":"969122d2adfb7af6a3960bb27327ab57082921ff292624f31a4da475c1b91637"},{"input":{"address":2147484664,"instruction_kind":"rv64.ebreak","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"EBREAK_imm_0_rd0","output_sha256":"c81dcbb3969508d41e8ccf1b78f7a6822403a04435b6c6d9b349460112d574fb"},{"input":{"address":2147484668,"instruction_kind":"rv64.ecall","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"ECALL_imm_0_compressed_false","output_sha256":"8a827dda42c5e473cd97eeb71dce294a0faf68cdad9f9212cd3376aaf6e28bc3"},{"input":{"address":2147484672,"instruction_kind":"rv64.ecall","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"ECALL_imm_0_compressed_true","output_sha256":"5e2bf355e650589506af8af01ddbd735610e5283580c48c8daadec8994ddfd87"},{"input":{"address":2147484676,"instruction_kind":"rv64.ecall","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"ECALL_imm_0_rd0","output_sha256":"2b4cb1bb90eef1df2d9feb5586763b28d9abcbb090387f3e5ad488092e4fbc1a"},{"input":{"address":2147484680,"instruction_kind":"rv64.mret","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"MRET_imm_0_compressed_false","output_sha256":"ad0c6fac2d59f50c1924ae8b882da5c0be0aed09ac5824346185ffbc130f93fc"},{"input":{"address":2147484684,"instruction_kind":"rv64.mret","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"MRET_imm_0_compressed_true","output_sha256":"e74bb8901ac7424da3d4b4823ab19c97bcd57acfe7c49c38eb242b008e33b6cc"},{"input":{"address":2147484688,"instruction_kind":"rv64.mret","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":null,"rs2":null},"virtual_sequence_remaining":null},"name":"MRET_imm_0_rd0","output_sha256":"1612a64d23227c00bb63bf5878436930af246b75e01a6d1fe12cdde02ef3de73"},{"input":{"address":2147484692,"instruction_kind":"rv64.sll","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLL_imm_0_compressed_false","output_sha256":"c13caaefe37a3107205bd7ca4e74be7c612dd2174a76c0620d88070f5bffb30c"},{"input":{"address":2147484696,"instruction_kind":"rv64.sll","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLL_imm_0_compressed_true","output_sha256":"e9f70cd21b8e5d25fe5ec8b43e85f2be84d75e4a463d29927b661b930b2f8ca0"},{"input":{"address":2147484700,"instruction_kind":"rv64.sll","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLL_imm_0_rd0","output_sha256":"e87f68d715fec58c6b3d1481a3cae7b0b25c8ce94d81563e88dc90c2db558b1e"},{"input":{"address":2147484704,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_0_compressed_false","output_sha256":"421d6f9546c9a1ae60e74fd66b82fc0220a65c8f0c551cea20eb87fafcbd950b"},{"input":{"address":2147484708,"instruction_kind":"rv64.slli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_0_compressed_true","output_sha256":"18a6e13ddb5d22d5ab85f0ad557728b958cbdf2550496839eb7077dd919d6f7e"},{"input":{"address":2147484712,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_0_rd0","output_sha256":"04f8b666ba6c937b91f88f089f59f81520c7e260c84bd1d7d801ea9ecf0aa551"},{"input":{"address":2147484716,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_1_compressed_false","output_sha256":"ba494a183516d745438c4a18c53463d1f7a4d9a27fdfaf477b9c4e989edbe57d"},{"input":{"address":2147484720,"instruction_kind":"rv64.slli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_1_compressed_true","output_sha256":"e441243f4d9709396aa5269f863624a3455081bb85309622ad76bec7a5e5ab02"},{"input":{"address":2147484724,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_1_rd0","output_sha256":"771a132d079b1d6c4b6046b4165fb3357ee2542755f45268e4732a58e5e6b2ea"},{"input":{"address":2147484728,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_31_compressed_false","output_sha256":"58c4f9c1fd9bbd4d60a0cd9b4626be6a4cca2020d2d63a90f2602fc6063b08c1"},{"input":{"address":2147484732,"instruction_kind":"rv64.slli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_31_compressed_true","output_sha256":"54e1007cc03fc7923d85ce7f9277a19907a70f4015086610c94d9d2d06a2a8b1"},{"input":{"address":2147484736,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_31_rd0","output_sha256":"d1fae263bc3ff2a46311dd99ee30073dba9ba78fe28fed510120aa941376d638"},{"input":{"address":2147484740,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_32_compressed_false","output_sha256":"c2c6f314d879ad774e98408acbb98da11e288057b6202d83e6717d93b310f2cb"},{"input":{"address":2147484744,"instruction_kind":"rv64.slli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_32_compressed_true","output_sha256":"df8d5451d6ec1ec592770439e82d38885b7e1297aeae571434c9f0310b6c09d9"},{"input":{"address":2147484748,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_32_rd0","output_sha256":"f6d713c294c2d4adc31e6de065400fb6b67e0a30780e616ec6c51cbfc62fdc01"},{"input":{"address":2147484752,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_63_compressed_false","output_sha256":"a84d6781f8178f9df0c99f03b78dae46c60043cd6cccfe095b97869f863e0858"},{"input":{"address":2147484756,"instruction_kind":"rv64.slli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_63_compressed_true","output_sha256":"9f8bc35032e5275501976e5110e7005b75dc0d5305efa9b61b8217d2fd077dfb"},{"input":{"address":2147484760,"instruction_kind":"rv64.slli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLI_imm_63_rd0","output_sha256":"e7d5c357fc880624c1f2fb651b92dee3d0a4b3340dd5817587b3bdc5ced28774"},{"input":{"address":2147484764,"instruction_kind":"rv64.sllw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLLW_imm_0_compressed_false","output_sha256":"e87abe6fd0e5ea37fe9e9c3b36134da3979091496cb9d7a2c5ccefd6439f49af"},{"input":{"address":2147484768,"instruction_kind":"rv64.sllw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLLW_imm_0_compressed_true","output_sha256":"aba5723311395cfb7d2d613072d8f4748368101b19a3af1e4dbfb41d8feeecb2"},{"input":{"address":2147484772,"instruction_kind":"rv64.sllw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SLLW_imm_0_rd0","output_sha256":"ea66d861e50dbb0fe96769317b880581987f81ce5642cb5d5abbd6d33a46ffdd"},{"input":{"address":2147484776,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_0_compressed_false","output_sha256":"deddc57a92a29003e41e670fd7c18c665481164a383c30232c38adfe255630d4"},{"input":{"address":2147484780,"instruction_kind":"rv64.slliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_0_compressed_true","output_sha256":"8a6aef5d2af601843972ede26ac6d78f6552a6c1384ca47efea1e77e06d8baad"},{"input":{"address":2147484784,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_0_rd0","output_sha256":"76783f0569a85b9dc28b19da880d84a4326566630f204fd1ee51dc6d1d59cb24"},{"input":{"address":2147484788,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_1_compressed_false","output_sha256":"b8f9be593ef392ae68d7dca5ee9f774171c71a95a4458d6c47fdfb4914344eed"},{"input":{"address":2147484792,"instruction_kind":"rv64.slliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_1_compressed_true","output_sha256":"38b62847eab22c3684e0e970578fafa3aacec323e360e92e1e1bdf6fbfca8b03"},{"input":{"address":2147484796,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_1_rd0","output_sha256":"55d1c290697d6e3bc55bad0f7c659c8901d8aef399f24c1e7ce610acb588b4fe"},{"input":{"address":2147484800,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_15_compressed_false","output_sha256":"3e561fad1d74cf09a7ae62a30f2a4643b8eca705f94c33d8962d50f1e044df9f"},{"input":{"address":2147484804,"instruction_kind":"rv64.slliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_15_compressed_true","output_sha256":"e6a808acad0c8f3e97d334c199520b43245d7696d52f0baa3126ee24d4ef254b"},{"input":{"address":2147484808,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_15_rd0","output_sha256":"7c5ed536f0ac75bf75772060ece2792d643a777248b14408c71f5c94e2a32c42"},{"input":{"address":2147484812,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_31_compressed_false","output_sha256":"4d312519565408bd667c76ea6608313d65a991243d1b3c73be825004590f3276"},{"input":{"address":2147484816,"instruction_kind":"rv64.slliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_31_compressed_true","output_sha256":"0580fd1856d646a03e171f16dff431055e10fa204b750f5e7e70adc262b9a2bd"},{"input":{"address":2147484820,"instruction_kind":"rv64.slliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SLLIW_imm_31_rd0","output_sha256":"f1636de7bae6dde2149a98346e0178d308e244102a274596851daff0b40ecfe2"},{"input":{"address":2147484824,"instruction_kind":"rv64.srl","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRL_imm_0_compressed_false","output_sha256":"49e5fa5e75bbeaaf5168a193c1edc02a71f3751ef88ef41bbd269ae93bc7afeb"},{"input":{"address":2147484828,"instruction_kind":"rv64.srl","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRL_imm_0_compressed_true","output_sha256":"4029976ec0951caa6637edc6c1d652c9a98d8c3906ad481ed983a8a9f463a413"},{"input":{"address":2147484832,"instruction_kind":"rv64.srl","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRL_imm_0_rd0","output_sha256":"52be936faf684c15de879bfd3e2660b644f0387d7bfbd3023b7a343fce945680"},{"input":{"address":2147484836,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_0_compressed_false","output_sha256":"06db0f6d4dabefcdf6c1227f826539e15a3b797bc8ea6184478428eb70a2f251"},{"input":{"address":2147484840,"instruction_kind":"rv64.srli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_0_compressed_true","output_sha256":"423c1580b91e45cefd3c39fac8f576d2d2000693f20332dd30a85fe49d96bbd9"},{"input":{"address":2147484844,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_0_rd0","output_sha256":"ed5677eb94cfb4142fe768076c5b4781e10a2b765b00d083e103c6d0320765e5"},{"input":{"address":2147484848,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_1_compressed_false","output_sha256":"051bb901970fb8f47ccfef21029a676c787e833cd0edf37d24708250d169a20a"},{"input":{"address":2147484852,"instruction_kind":"rv64.srli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_1_compressed_true","output_sha256":"9179c17a50caf27a59be7e6159e4b45c70f2832c2e424e5092b5ccbfd6e050fe"},{"input":{"address":2147484856,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_1_rd0","output_sha256":"1c8fe27c69a0d54833d86e58d040f7903e4b0a8ec57ccba4c2ac8bc45a6e2ac9"},{"input":{"address":2147484860,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_31_compressed_false","output_sha256":"3eff14ff1fbe69d69b92624ee7f1bb29b751506d0a43a1841e555df661c1472c"},{"input":{"address":2147484864,"instruction_kind":"rv64.srli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_31_compressed_true","output_sha256":"8a75db94a507e989b6771cd924c0bd123ac2ee49f2cf34921132707453781e45"},{"input":{"address":2147484868,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_31_rd0","output_sha256":"0da04b570b2a603f4d1c2cdbb2ca9316806ec2d3a32fbaf6a01e22ccd2e8fb7a"},{"input":{"address":2147484872,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_32_compressed_false","output_sha256":"cc6952a5cd13dca4efb570605a6b69d5855741edd61c2c6272c125c3d5fa577a"},{"input":{"address":2147484876,"instruction_kind":"rv64.srli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_32_compressed_true","output_sha256":"9a21ddb02fc97b45d1ec1daab225f21358f1bb50ecc41b68e8c038e100eca61b"},{"input":{"address":2147484880,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_32_rd0","output_sha256":"00132d5bafe5767730c91af0784a121e032ae25662257c24d977a824eb43bd76"},{"input":{"address":2147484884,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_63_compressed_false","output_sha256":"4ffa7d90943d9305eecb60d6d21b3607276e1c3c93e85fa167debf5510460c92"},{"input":{"address":2147484888,"instruction_kind":"rv64.srli","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_63_compressed_true","output_sha256":"566ef7664c9fbba3577647ab8c07696bdaad37fb9a932d3fcf63510a11413180"},{"input":{"address":2147484892,"instruction_kind":"rv64.srli","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLI_imm_63_rd0","output_sha256":"7d0ac3ac5c127e6e8caf9703d70c79f7be2957f58cede15b4f8b64f74ba48b94"},{"input":{"address":2147484896,"instruction_kind":"rv64.sra","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRA_imm_0_compressed_false","output_sha256":"81398271ac2d970e87d0328888325dc7cc8e2b74928d6fd8b91ae4236a73894b"},{"input":{"address":2147484900,"instruction_kind":"rv64.sra","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRA_imm_0_compressed_true","output_sha256":"4041705eca58cea8efb6938670adac7f5cc021d5817f7a24512adcfb73c5cb3d"},{"input":{"address":2147484904,"instruction_kind":"rv64.sra","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRA_imm_0_rd0","output_sha256":"05c45f03fe7d65441b3a844b5d60b78dd5ec24c794634f4abd6bffe56cbf45d4"},{"input":{"address":2147484908,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_0_compressed_false","output_sha256":"d03b5398c746d7f8ac227d77f91e4e329b12ee23e6a6f8534dcecb3da727052c"},{"input":{"address":2147484912,"instruction_kind":"rv64.srai","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_0_compressed_true","output_sha256":"9289321e181b745d9872c91dce003e2900f0b40dff6c43ae9abc451853306270"},{"input":{"address":2147484916,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_0_rd0","output_sha256":"accad9cc82445c6437b2612bf88a08aaf49d8aea69ae11ca8c978fdd30c64c83"},{"input":{"address":2147484920,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_1_compressed_false","output_sha256":"1ea2e1a108578298ad97e86e10e4b09bcdc8cd622e44f8e4f99473f08ff86270"},{"input":{"address":2147484924,"instruction_kind":"rv64.srai","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_1_compressed_true","output_sha256":"dba15217371465ef66bdb098fe2f95faf84d0973639536d7eab61a90630ae025"},{"input":{"address":2147484928,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_1_rd0","output_sha256":"160982651924fc73c93df5dd0efe37ed10ee9caf791989e588a629eee02d9289"},{"input":{"address":2147484932,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_31_compressed_false","output_sha256":"04dd1c35745cbee72ea576bb5371d83b0060be9bd90aa933f9c87d512b0f2fa2"},{"input":{"address":2147484936,"instruction_kind":"rv64.srai","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_31_compressed_true","output_sha256":"61c82e3c45acb948bbe6ab92811cb46bd63955258c123c8b5daaf9cea0f5538e"},{"input":{"address":2147484940,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_31_rd0","output_sha256":"d33072d6bac759fef58e20fcdf31d16142fec346889bceb568c15f84d9b7901a"},{"input":{"address":2147484944,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_32_compressed_false","output_sha256":"fbea0aad596e36b2c1621616a84ebfc877da92235346c1548269602d39fed862"},{"input":{"address":2147484948,"instruction_kind":"rv64.srai","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":32,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_32_compressed_true","output_sha256":"21b13697bc84e6af9817d2a5dc69e9bbb626ed316604f79ed5b20ac221213e9e"},{"input":{"address":2147484952,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":32,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_32_rd0","output_sha256":"d03180b8619d64caa4cc72282989995ff89c9d64b05c0876a3d64fded5636e06"},{"input":{"address":2147484956,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_63_compressed_false","output_sha256":"1ef0892deb31dc16798db5f40b84a0a582984cb14e63c799949c2f6fde372d2a"},{"input":{"address":2147484960,"instruction_kind":"rv64.srai","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":63,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_63_compressed_true","output_sha256":"d3b5461c4d1e114058003479fbadd6f0914a87220d5a487fa2f799aa7b6ace0e"},{"input":{"address":2147484964,"instruction_kind":"rv64.srai","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":63,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAI_imm_63_rd0","output_sha256":"ce08095771cfa3fda541352115cbb97d74e61ffcdde430f144332d73b0f5ac1d"},{"input":{"address":2147484968,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_0_compressed_false","output_sha256":"155a0b80661ae61c1a73a9fa4c23c5f8c4e50fafe8af0f3183124b5aba5a3c69"},{"input":{"address":2147484972,"instruction_kind":"rv64.srliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_0_compressed_true","output_sha256":"58fb3b0bd6ee0df47f15ce4d5e8263abbdee1702b0c29569eab1422301c9d6e4"},{"input":{"address":2147484976,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_0_rd0","output_sha256":"7985c372d52e00b8d7ce31e27988b40b79839b68bbb7be6ed7cfde29850dc0a0"},{"input":{"address":2147484980,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_1_compressed_false","output_sha256":"a31454e144b8705ac934d7a865f79d1d6a6778e4709f19dbdd8eb48e4cb7afca"},{"input":{"address":2147484984,"instruction_kind":"rv64.srliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_1_compressed_true","output_sha256":"b78714e7808b58c1bb81ffb7db6e3853f86aef8256daa64c1b38d89d2107074d"},{"input":{"address":2147484988,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_1_rd0","output_sha256":"c798ac1b292978ccfe451862fa7b0c92c7b89b11db7aae34c2682cdd75a8e5b0"},{"input":{"address":2147484992,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_15_compressed_false","output_sha256":"ceaa72e3c33745aba9bbe36189ef912d07b89feb006e4e5a41580f31ced2f3c8"},{"input":{"address":2147484996,"instruction_kind":"rv64.srliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_15_compressed_true","output_sha256":"4e55db2988a6bfd1686acf6b795759d59c6783f908ffbe19f0178a32796ffd43"},{"input":{"address":2147485000,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_15_rd0","output_sha256":"90f0d2b16af53700980e3f1dd98c9531379769ae751f4e237a8a1ed01ae0a3e9"},{"input":{"address":2147485004,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_31_compressed_false","output_sha256":"fa7b98e2182cd18fd48bf5d152d853a52053035ad0d6cfceb244f2470501ae09"},{"input":{"address":2147485008,"instruction_kind":"rv64.srliw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_31_compressed_true","output_sha256":"e97e7480bf02701195ddfbd37f4ced2aa12c941c0509de7251a5b3a241e491fb"},{"input":{"address":2147485012,"instruction_kind":"rv64.srliw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRLIW_imm_31_rd0","output_sha256":"ea5000db20305782b1df2fbebd55abdd2d65b4a9a0b107927b396aa34d97cdb9"},{"input":{"address":2147485016,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_0_compressed_false","output_sha256":"a92947f3469c815738a8dcbba11e6a756669e12a116a57ee57b6cd5981c6deed"},{"input":{"address":2147485020,"instruction_kind":"rv64.sraiw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_0_compressed_true","output_sha256":"99a49e50359c886ad6a28639e569bc786e5656d66d7095defb26fd4ae2ae0d8b"},{"input":{"address":2147485024,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_0_rd0","output_sha256":"9bd10684842b05bb242f137b70f7c07edc221bf088717ee4d0a3bd9ebafc7d71"},{"input":{"address":2147485028,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_1_compressed_false","output_sha256":"9813c6ffe3189a0be6b5230ea7af8fb0daa1bea834bac2c8ac3b6d596bd4078e"},{"input":{"address":2147485032,"instruction_kind":"rv64.sraiw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":1,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_1_compressed_true","output_sha256":"198307020966e4446fb9af1957034a5ae00bc245f9312b6f4eab01768b714e64"},{"input":{"address":2147485036,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":1,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_1_rd0","output_sha256":"4edf43f71dfd9d5f3eb65e28a51d5f2d2a76db9b26e13ff266758063a12e1e85"},{"input":{"address":2147485040,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_15_compressed_false","output_sha256":"fb809001e176ca1f02b40c6e2467fe6e2ef4cce1ffd66c390b64a0c81ecf7cdb"},{"input":{"address":2147485044,"instruction_kind":"rv64.sraiw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":15,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_15_compressed_true","output_sha256":"9c68eae2172f6de4965ee3752005f17649a70d6f84b415ab2417f43db22b2c57"},{"input":{"address":2147485048,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":15,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_15_rd0","output_sha256":"4853bd33124b93ac944e23746c11762e81305006156ab709f16d5f6f179019de"},{"input":{"address":2147485052,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_31_compressed_false","output_sha256":"7ce3f3a19bf5d6b599223ef1f607001a69ef8008354825b7ac69996589fef32a"},{"input":{"address":2147485056,"instruction_kind":"rv64.sraiw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":31,"rd":7,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_31_compressed_true","output_sha256":"df1ec14ebaf01cfec84edc88e0aaa173ed515d2a59815467b590ae7b9a0594c3"},{"input":{"address":2147485060,"instruction_kind":"rv64.sraiw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":31,"rd":0,"rs1":5,"rs2":null},"virtual_sequence_remaining":null},"name":"SRAIW_imm_31_rd0","output_sha256":"eaf2ee995a63a5e213ecb48b32722e48d26f2dcb1a958d5d52637c8deee897ca"},{"input":{"address":2147485064,"instruction_kind":"rv64.srlw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRLW_imm_0_compressed_false","output_sha256":"71b4f99ce1bc5f2fc75773b47fb4037ea98e168b9edd758f515123898bc27d04"},{"input":{"address":2147485068,"instruction_kind":"rv64.srlw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRLW_imm_0_compressed_true","output_sha256":"9cbac7dd60643f427141c9da1be45e3a4023940e2640d9a0d263649dad10e03d"},{"input":{"address":2147485072,"instruction_kind":"rv64.srlw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRLW_imm_0_rd0","output_sha256":"178f36ab6ca01b3cfd81122fb53935d5e5a8ed75c686ea0bc20a895a89e1dc5f"},{"input":{"address":2147485076,"instruction_kind":"rv64.sraw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRAW_imm_0_compressed_false","output_sha256":"3cbad9d1bd50492c32a422e54d00d11c1dd3f1243f99553fa2d61db57b1b5145"},{"input":{"address":2147485080,"instruction_kind":"rv64.sraw","is_compressed":true,"is_first_in_sequence":false,"operands":{"imm":0,"rd":7,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRAW_imm_0_compressed_true","output_sha256":"2e185cefe9a22842907227e287c5d8c05f945256f65855628f4f7ba9cb3c810d"},{"input":{"address":2147485084,"instruction_kind":"rv64.sraw","is_compressed":false,"is_first_in_sequence":false,"operands":{"imm":0,"rd":0,"rs1":5,"rs2":6},"virtual_sequence_remaining":null},"name":"SRAW_imm_0_rd0","output_sha256":"53390af3df8dcb88a7f6db82a00efd155d255f63df60971ef9b03e05be1f6f1c"}] diff --git a/crates/jolt-program/src/expand/grammar.rs b/crates/jolt-program/src/expand/grammar.rs new file mode 100644 index 0000000000..c4add144d1 --- /dev/null +++ b/crates/jolt-program/src/expand/grammar.rs @@ -0,0 +1,468 @@ +use jolt_riscv::{JoltInstructionKind, SourceInstructionKind, SourceInstructionRow}; + +use crate::expand::{allocator::NUM_VIRTUAL_INSTRUCTION_REGISTERS, ExpansionError}; + +/// Symbolic register placeholder, resolved to a physical virtual register during materialization. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct TempId(pub(super) u8); + +impl TempId { + pub(super) const fn index(self) -> usize { + self.0 as usize + } + + pub(super) const fn operand(self) -> RegisterOperand { + RegisterOperand::Temp(self) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RegisterOperand { + Register(u8), + Temp(TempId), +} + +pub(super) const fn reg(register: u8) -> RegisterOperand { + RegisterOperand::Register(register) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct TemplateOperands { + pub(super) rd: Option, + pub(super) rs1: Option, + pub(super) rs2: Option, + pub(super) imm: i128, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct InstructionTemplate { + pub(super) instruction_kind: K, + pub(super) operands: TemplateOperands, +} + +pub(super) type RowTemplate = InstructionTemplate; +pub(super) type SourceInstructionRowTemplate = InstructionTemplate; + +impl InstructionTemplate { + pub(super) fn r( + instruction_kind: K, + rd: RegisterOperand, + rs1: RegisterOperand, + rs2: RegisterOperand, + ) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: Some(rs2), + imm: 0, + }, + } + } + + pub(super) fn i( + instruction_kind: K, + rd: RegisterOperand, + rs1: RegisterOperand, + imm: i128, + ) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: None, + imm, + }, + } + } + + pub(super) fn j(instruction_kind: K, rd: RegisterOperand, imm: i128) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: Some(rd), + rs1: None, + rs2: None, + imm, + }, + } + } + + pub(super) fn u(instruction_kind: K, rd: RegisterOperand, imm: i128) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: Some(rd), + rs1: None, + rs2: None, + imm, + }, + } + } + + pub(super) fn b( + instruction_kind: K, + rs1: RegisterOperand, + rs2: RegisterOperand, + imm: i128, + ) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: None, + rs1: Some(rs1), + rs2: Some(rs2), + imm, + }, + } + } + + pub(super) fn s( + instruction_kind: K, + rs1: RegisterOperand, + rs2: RegisterOperand, + imm: i128, + ) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: None, + rs1: Some(rs1), + rs2: Some(rs2), + imm, + }, + } + } + + /// Pseudo-I format for address/alignment assertions that read `rs1` and an + /// immediate offset but do not write `rd`. + pub(super) fn address(instruction_kind: K, rs1: RegisterOperand, imm: i128) -> Self { + Self { + instruction_kind, + operands: TemplateOperands { + rd: None, + rs1: Some(rs1), + rs2: None, + imm, + }, + } + } +} + +/// A single step in a symbolic expansion recipe. +#[derive(Clone, Copy)] +pub(super) enum ExpansionOp { + /// Append this row directly to the output. + Emit(RowTemplate), + /// Recursively expand this row through the full pipeline before appending. + Expand(SourceInstructionRowTemplate), + Allocate(TempId), + Release(TempId), +} + +/// A complete symbolic recipe: source instruction paired with the ops to materialize it. +pub(super) struct ExpandedInstructionSequence { + pub(super) source: SourceInstructionRow, + pub(super) ops: Vec, +} + +/// Builds a symbolic expansion recipe from emit/expand/allocate/release calls. +pub(super) struct ExpansionBuilder { + source: SourceInstructionRow, + ops: Vec, + next_temp: usize, +} + +impl ExpansionBuilder { + pub(super) fn new(source: SourceInstructionRow) -> Self { + Self { + source, + ops: Vec::new(), + next_temp: 0, + } + } + + pub(super) fn allocate(&mut self) -> Result { + if self.next_temp >= NUM_VIRTUAL_INSTRUCTION_REGISTERS { + return Err(ExpansionError::TooManyTemporaryRegisters { + actual: self.next_temp + 1, + }); + } + let temp = TempId(self.next_temp as u8); + self.next_temp += 1; + self.ops.push(ExpansionOp::Allocate(temp)); + Ok(temp) + } + + /// Append an already target-legal row to this source row's output sequence. + /// + /// Use `emit_*` when the row should appear exactly as written in finalized + /// bytecode. Use `expand_*` instead when the row is a source-only helper + /// that must be routed through the central expander first. + pub(super) fn emit_r( + &mut self, + instruction_kind: JoltInstructionKind, + rd: RegisterOperand, + rs1: RegisterOperand, + rs2: RegisterOperand, + ) { + self.emit(RowTemplate::r(instruction_kind, rd, rs1, rs2)); + } + + pub(super) fn emit_i( + &mut self, + instruction_kind: JoltInstructionKind, + rd: RegisterOperand, + rs1: RegisterOperand, + imm: i128, + ) { + self.emit(RowTemplate::i(instruction_kind, rd, rs1, imm)); + } + + pub(super) fn emit_j( + &mut self, + instruction_kind: JoltInstructionKind, + rd: RegisterOperand, + imm: i128, + ) { + self.emit(RowTemplate::j(instruction_kind, rd, imm)); + } + + pub(super) fn emit_u( + &mut self, + instruction_kind: JoltInstructionKind, + rd: RegisterOperand, + imm: i128, + ) { + self.emit(RowTemplate::u(instruction_kind, rd, imm)); + } + + /// Record a source-only helper row that the provider-free materializer must + /// expand before appending its finalized rows to this source-row sequence. + /// + /// Recursive helper expansion always goes through `ExpansionState`, so + /// rd=x0 handling, recursion depth, allocator state, and metadata stamping + /// stay centralized. + pub(super) fn expand_r( + &mut self, + instruction_kind: SourceInstructionKind, + rd: RegisterOperand, + rs1: RegisterOperand, + rs2: RegisterOperand, + ) { + self.expand(SourceInstructionRowTemplate::r( + instruction_kind, + rd, + rs1, + rs2, + )); + } + + pub(super) fn expand_i( + &mut self, + instruction_kind: SourceInstructionKind, + rd: RegisterOperand, + rs1: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::i( + instruction_kind, + rd, + rs1, + imm, + )); + } + + pub(super) fn expand_j( + &mut self, + instruction_kind: SourceInstructionKind, + rd: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::j(instruction_kind, rd, imm)); + } + + pub(super) fn expand_u( + &mut self, + instruction_kind: SourceInstructionKind, + rd: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::u(instruction_kind, rd, imm)); + } + + pub(super) fn expand_b( + &mut self, + instruction_kind: SourceInstructionKind, + rs1: RegisterOperand, + rs2: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::b( + instruction_kind, + rs1, + rs2, + imm, + )); + } + + pub(super) fn expand_s( + &mut self, + instruction_kind: SourceInstructionKind, + rs1: RegisterOperand, + rs2: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::s( + instruction_kind, + rs1, + rs2, + imm, + )); + } + + pub(super) fn expand_address( + &mut self, + instruction_kind: SourceInstructionKind, + rs1: RegisterOperand, + imm: i128, + ) { + self.expand(SourceInstructionRowTemplate::address( + instruction_kind, + rs1, + imm, + )); + } + + pub(super) fn release(&mut self, temp: TempId) { + self.ops.push(ExpansionOp::Release(temp)); + } + + pub(super) fn release_many(&mut self, registers: [TempId; N]) { + for register in registers { + self.release(register); + } + } + + pub(super) fn finalize(self) -> Result { + Ok(ExpandedInstructionSequence { + source: self.source, + ops: self.ops, + }) + } + + fn emit(&mut self, row: RowTemplate) { + self.ops.push(ExpansionOp::Emit(row)); + } + + fn expand(&mut self, row: SourceInstructionRowTemplate) { + self.ops.push(ExpansionOp::Expand(row)); + } +} + +/// Instructions that exist only in decoded source and must be expanded into target-legal sequences. +pub(super) fn is_source_only(instruction_kind: SourceInstructionKind) -> bool { + matches!( + instruction_kind, + SourceInstructionKind::Inline + | SourceInstructionKind::ADDIW + | SourceInstructionKind::ADDW + | SourceInstructionKind::SUBW + | SourceInstructionKind::MULH + | SourceInstructionKind::MULHSU + | SourceInstructionKind::MULW + | SourceInstructionKind::LB + | SourceInstructionKind::LBU + | SourceInstructionKind::LH + | SourceInstructionKind::LHU + | SourceInstructionKind::LW + | SourceInstructionKind::LWU + | SourceInstructionKind::AdviceLB + | SourceInstructionKind::AdviceLH + | SourceInstructionKind::AdviceLW + | SourceInstructionKind::AdviceLD + | SourceInstructionKind::AMOADDD + | SourceInstructionKind::AMOANDD + | SourceInstructionKind::AMOORD + | SourceInstructionKind::AMOXORD + | SourceInstructionKind::AMOSWAPD + | SourceInstructionKind::AMOMAXD + | SourceInstructionKind::AMOMAXUD + | SourceInstructionKind::AMOMIND + | SourceInstructionKind::AMOMINUD + | SourceInstructionKind::AMOADDW + | SourceInstructionKind::AMOANDW + | SourceInstructionKind::AMOORW + | SourceInstructionKind::AMOXORW + | SourceInstructionKind::AMOSWAPW + | SourceInstructionKind::AMOMAXW + | SourceInstructionKind::AMOMAXUW + | SourceInstructionKind::AMOMINW + | SourceInstructionKind::AMOMINUW + | SourceInstructionKind::LRD + | SourceInstructionKind::LRW + | SourceInstructionKind::DIV + | SourceInstructionKind::DIVU + | SourceInstructionKind::DIVW + | SourceInstructionKind::DIVUW + | SourceInstructionKind::REM + | SourceInstructionKind::REMU + | SourceInstructionKind::REMW + | SourceInstructionKind::REMUW + | SourceInstructionKind::SB + | SourceInstructionKind::SCD + | SourceInstructionKind::SCW + | SourceInstructionKind::SH + | SourceInstructionKind::SW + | SourceInstructionKind::CSRRW + | SourceInstructionKind::CSRRS + | SourceInstructionKind::EBREAK + | SourceInstructionKind::ECALL + | SourceInstructionKind::MRET + | SourceInstructionKind::SLL + | SourceInstructionKind::SLLI + | SourceInstructionKind::SLLW + | SourceInstructionKind::SLLIW + | SourceInstructionKind::SRL + | SourceInstructionKind::SRLI + | SourceInstructionKind::SRA + | SourceInstructionKind::SRAI + | SourceInstructionKind::SRLIW + | SourceInstructionKind::SRAIW + | SourceInstructionKind::SRLW + | SourceInstructionKind::SRAW + ) +} + +#[cfg(test)] +mod tests { + use jolt_riscv::{NormalizedOperands, SourceInstructionRow}; + + use super::*; + + fn source() -> SourceInstructionRow { + SourceInstructionRow { + address: 0x8000_0000, + operands: NormalizedOperands::default(), + inline: None, + is_compressed: false, + } + } + + #[test] + fn symbolic_temps_are_limited_to_instruction_register_pool() -> Result<(), ExpansionError> { + let mut builder = ExpansionBuilder::new(source()); + for _ in 0..NUM_VIRTUAL_INSTRUCTION_REGISTERS { + let _ = builder.allocate()?; + } + + assert!(matches!( + builder.allocate(), + Err(ExpansionError::TooManyTemporaryRegisters { actual }) + if actual == NUM_VIRTUAL_INSTRUCTION_REGISTERS + 1 + )); + Ok(()) + } +} diff --git a/crates/jolt-program/src/expand/materialize.rs b/crates/jolt-program/src/expand/materialize.rs new file mode 100644 index 0000000000..d523d55d77 --- /dev/null +++ b/crates/jolt-program/src/expand/materialize.rs @@ -0,0 +1,389 @@ +use jolt_riscv::{ + JoltInstructionProfile, JoltInstructionRow, NormalizedOperands, SourceInstruction, + SourceInstructionRow, +}; + +use crate::expand::{ + allocator::{ExpansionAllocator, NUM_VIRTUAL_INSTRUCTION_REGISTERS}, + expand_source_only_instruction, + grammar::{ + is_source_only, ExpandedInstructionSequence, ExpansionOp, RegisterOperand, RowTemplate, + SourceInstructionRowTemplate, TempId, TemplateOperands, + }, + metadata::stamp_instruction_sequence, + operands::{handles_rd_zero_internally, noop_for}, + ExpansionError, +}; + +pub(super) const MAX_FINAL_ROWS_PER_SOURCE: usize = 64; + +/// Materializes symbolic recipes into concrete instructions (phase 2). +pub(super) struct ExpansionState { + allocator: ExpansionAllocator, + profile: JoltInstructionProfile, +} + +impl ExpansionState { + pub(super) fn new(allocator: ExpansionAllocator, profile: JoltInstructionProfile) -> Self { + Self { allocator, profile } + } + + pub(super) fn into_allocator(self) -> ExpansionAllocator { + self.allocator + } + + pub(super) fn expand_source_recursive( + &mut self, + instruction: &SourceInstruction, + ) -> Result, ExpansionError> { + self.allocator.enter_expansion()?; + let result = self.dispatch_source(instruction); + self.allocator.exit_expansion(); + result + } + + /// Routes: rd=x0 rewrite → recurse, native → pass-through, source-only → build recipe + materialize. + fn dispatch_source( + &mut self, + instruction: &SourceInstruction, + ) -> Result, ExpansionError> { + let kind = instruction.kind(); + if instruction.row().operands.rd == Some(0) && !handles_rd_zero_internally(kind) { + if kind.has_side_effects() { + let virtual_register = self.allocate_register()?; + let rewritten = (*instruction).map_row(|mut row| { + row.operands.rd = Some(virtual_register); + row + }); + let expanded = self.expand_source_recursive(&rewritten); + self.release_register(virtual_register)?; + return expanded; + } + return Ok(vec![noop_for(*instruction.row())]); + } + + if kind == jolt_riscv::SourceInstructionKind::Inline { + return Err(ExpansionError::InlineProviderRequired); + } + if !is_source_only(kind) { + return JoltInstructionRow::try_from(instruction) + .map(|row| vec![row]) + .map_err(ExpansionError::IllegalSourceInstruction); + } + let sequence = expand_source_only_instruction(instruction)?; + self.materialize(sequence) + } + + pub(super) fn allocate_register(&mut self) -> Result { + self.allocator.allocate() + } + + pub(super) fn release_register(&mut self, register: u8) -> Result<(), ExpansionError> { + self.allocator.release(register) + } + + pub(super) fn materialize( + &mut self, + sequence: ExpandedInstructionSequence, + ) -> Result, ExpansionError> { + let mut materializer = SequenceMaterializer::new(sequence.source, self.profile); + for op in sequence.ops { + match op { + ExpansionOp::Emit(row) => materializer.emit(row)?, + ExpansionOp::Expand(row) => { + let instruction = materializer.source_instruction(row)?; + materializer.extend(self.expand_source_recursive(&instruction)?)?; + } + ExpansionOp::Allocate(register) => { + let allocated = self.allocator.allocate()?; + materializer.bind_temp(register, allocated)?; + } + ExpansionOp::Release(register) => { + let register = materializer.resolve_register_for_release(register)?; + self.allocator.release(register)?; + } + } + } + materializer.finish() + } +} + +/// Bounded output collector — rejects sequences exceeding `MAX_FINAL_ROWS_PER_SOURCE`. +#[derive(Debug)] +struct ExpansionBuffer { + rows: Vec, +} + +impl ExpansionBuffer { + fn new() -> Self { + Self { + rows: Vec::with_capacity(MAX_FINAL_ROWS_PER_SOURCE), + } + } + + fn push(&mut self, row: JoltInstructionRow) -> Result<(), ExpansionError> { + if self.rows.len() == MAX_FINAL_ROWS_PER_SOURCE { + return Err(ExpansionError::CapacityExceeded { + actual: self.rows.len() + 1, + capacity: MAX_FINAL_ROWS_PER_SOURCE, + }); + } + self.rows.push(row); + Ok(()) + } + + fn extend_vec(&mut self, rows: Vec) -> Result<(), ExpansionError> { + for row in rows { + self.push(row)?; + } + Ok(()) + } + + fn check_capacity(&self) -> Result<(), ExpansionError> { + if self.rows.len() > MAX_FINAL_ROWS_PER_SOURCE { + return Err(ExpansionError::CapacityExceeded { + actual: self.rows.len(), + capacity: MAX_FINAL_ROWS_PER_SOURCE, + }); + } + Ok(()) + } + + fn into_vec(self) -> Vec { + self.rows + } +} + +/// Maps symbolic `TempId`s to physical virtual registers for one recipe materialization. +struct TempBindings { + slots: [Option; NUM_VIRTUAL_INSTRUCTION_REGISTERS], +} + +impl TempBindings { + fn new() -> Self { + Self { + slots: [None; NUM_VIRTUAL_INSTRUCTION_REGISTERS], + } + } + + fn bind(&mut self, temp: TempId, allocated: u8) -> Result<(), ExpansionError> { + let index = temp.index(); + if self.slots[index].is_some() { + return Err(ExpansionError::DuplicateTemporaryRegister { index }); + } + self.slots[index] = Some(allocated); + Ok(()) + } + + fn get(&self, temp: TempId) -> Result { + let index = temp.index(); + self.slots[index].ok_or(ExpansionError::UnallocatedTemporaryRegister { index }) + } + + fn take(&mut self, temp: TempId) -> Result { + let index = temp.index(); + match self.slots[index].take() { + Some(register) => Ok(register), + None => Err(ExpansionError::UnallocatedTemporaryRegister { index }), + } + } + + fn first_leaked(&self) -> Option { + self.slots.iter().position(Option::is_some) + } +} + +/// Executes a single recipe: resolves temps, collects output rows, checks capacity. +struct SequenceMaterializer { + address: usize, + is_compressed: bool, + profile: JoltInstructionProfile, + rows: ExpansionBuffer, + temps: TempBindings, +} + +impl SequenceMaterializer { + fn new(source: SourceInstructionRow, profile: JoltInstructionProfile) -> Self { + Self { + address: source.address, + is_compressed: source.is_compressed, + profile, + rows: ExpansionBuffer::new(), + temps: TempBindings::new(), + } + } + + fn emit(&mut self, row: RowTemplate) -> Result<(), ExpansionError> { + let row = self.instruction(row)?; + self.rows.push(row) + } + + fn extend(&mut self, rows: Vec) -> Result<(), ExpansionError> { + self.rows.extend_vec(rows) + } + + fn instruction(&self, row: RowTemplate) -> Result { + Ok(JoltInstructionRow { + instruction_kind: row.instruction_kind, + address: self.address, + operands: self.resolve_operands(row.operands)?, + virtual_sequence_remaining: Some(0), + is_first_in_sequence: false, + is_compressed: false, + }) + } + + fn source_instruction( + &self, + row: SourceInstructionRowTemplate, + ) -> Result { + Ok(SourceInstruction::new( + row.instruction_kind, + SourceInstructionRow { + address: self.address, + operands: self.resolve_operands(row.operands)?, + inline: None, + is_compressed: false, + }, + )) + } + + fn bind_temp(&mut self, temp: TempId, allocated: u8) -> Result<(), ExpansionError> { + self.temps.bind(temp, allocated) + } + + fn resolve_register_for_release(&mut self, temp: TempId) -> Result { + self.temps.take(temp) + } + + fn resolve_operands( + &self, + operands: TemplateOperands, + ) -> Result { + Ok(NormalizedOperands { + rd: self.resolve_optional_register(operands.rd)?, + rs1: self.resolve_optional_register(operands.rs1)?, + rs2: self.resolve_optional_register(operands.rs2)?, + imm: operands.imm, + }) + } + + fn resolve_optional_register( + &self, + register: Option, + ) -> Result, ExpansionError> { + match register { + Some(register) => Ok(Some(self.resolve_register(register)?)), + None => Ok(None), + } + } + + fn resolve_register(&self, register: RegisterOperand) -> Result { + match register { + RegisterOperand::Register(register) => Ok(register), + RegisterOperand::Temp(temp) => self.temps.get(temp), + } + } + + fn finish(self) -> Result, ExpansionError> { + if let Some(index) = self.temps.first_leaked() { + return Err(ExpansionError::LeakedTemporaryRegister { index }); + } + self.rows.check_capacity()?; + stamp_instruction_sequence(self.rows.into_vec(), self.is_compressed, self.profile) + } +} + +#[cfg(test)] +mod tests { + use jolt_riscv::{ + JoltInstructionKind, NormalizedOperands, SourceInstructionRow, RV64IMAC_JOLT, + }; + + use crate::expand::grammar::reg; + + use super::*; + + fn source() -> SourceInstructionRow { + SourceInstructionRow { + address: 0x8000_0000, + operands: NormalizedOperands { + rd: Some(3), + rs1: Some(4), + rs2: None, + imm: 1, + }, + inline: None, + is_compressed: false, + } + } + + #[test] + fn materializer_rejects_duplicate_temp_allocation() -> Result<(), ExpansionError> { + let temp = TempId(0); + let sequence = ExpandedInstructionSequence { + source: source(), + ops: vec![ExpansionOp::Allocate(temp), ExpansionOp::Allocate(temp)], + }; + let mut state = ExpansionState::new(ExpansionAllocator::new(), RV64IMAC_JOLT); + + assert!(matches!( + state.materialize(sequence), + Err(ExpansionError::DuplicateTemporaryRegister { index }) if index == temp.index() + )); + Ok(()) + } + + #[test] + fn materializer_rejects_unallocated_temp_use() -> Result<(), ExpansionError> { + let temp = TempId(0); + let sequence = ExpandedInstructionSequence { + source: source(), + ops: vec![ExpansionOp::Emit(RowTemplate::i( + JoltInstructionKind::ADDI, + temp.operand(), + reg(1), + 0, + ))], + }; + let mut state = ExpansionState::new(ExpansionAllocator::new(), RV64IMAC_JOLT); + + assert!(matches!( + state.materialize(sequence), + Err(ExpansionError::UnallocatedTemporaryRegister { index }) if index == temp.index() + )); + Ok(()) + } + + #[test] + fn materializer_rejects_unallocated_temp_release() -> Result<(), ExpansionError> { + let temp = TempId(0); + let sequence = ExpandedInstructionSequence { + source: source(), + ops: vec![ExpansionOp::Release(temp)], + }; + let mut state = ExpansionState::new(ExpansionAllocator::new(), RV64IMAC_JOLT); + + assert!(matches!( + state.materialize(sequence), + Err(ExpansionError::UnallocatedTemporaryRegister { index }) if index == temp.index() + )); + Ok(()) + } + + #[test] + fn materializer_rejects_leaked_temp() -> Result<(), ExpansionError> { + let temp = TempId(0); + let sequence = ExpandedInstructionSequence { + source: source(), + ops: vec![ExpansionOp::Allocate(temp)], + }; + let mut state = ExpansionState::new(ExpansionAllocator::new(), RV64IMAC_JOLT); + + assert!(matches!( + state.materialize(sequence), + Err(ExpansionError::LeakedTemporaryRegister { index }) if index == temp.index() + )); + Ok(()) + } +} diff --git a/crates/jolt-program/src/expand/memory/advice_lb.rs b/crates/jolt-program/src/expand/memory/advice_lb.rs index 580f6fdee4..0d7bb03eb5 100644 --- a/crates/jolt-program/src/expand/memory/advice_lb.rs +++ b/crates/jolt-program/src/expand/memory/advice_lb.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AdviceLB` to an advice-tape byte read and sign-extension sequence. +/// +/// The shared helper reads exactly one byte from the advice tape and then +/// sign-extends that byte to the architectural register width. pub(in crate::expand) fn expand_advice_lb( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_advice_load(instruction, 1, Some(56), allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_advice_load(instruction, 1) } diff --git a/crates/jolt-program/src/expand/memory/advice_ld.rs b/crates/jolt-program/src/expand/memory/advice_ld.rs index 079e900e2c..42902769e0 100644 --- a/crates/jolt-program/src/expand/memory/advice_ld.rs +++ b/crates/jolt-program/src/expand/memory/advice_ld.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AdviceLD` to an eight-byte advice-tape read. +/// +/// Full-width advice loads need no post-read sign extension because the advice +/// row already produces the complete XLEN value. pub(in crate::expand) fn expand_advice_ld( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_advice_load(instruction, 8, None, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_advice_load(instruction, 8) } diff --git a/crates/jolt-program/src/expand/memory/advice_lh.rs b/crates/jolt-program/src/expand/memory/advice_lh.rs index 75cc1ce452..6089dd357b 100644 --- a/crates/jolt-program/src/expand/memory/advice_lh.rs +++ b/crates/jolt-program/src/expand/memory/advice_lh.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AdviceLH` to a two-byte advice-tape read and sign-extension. +/// +/// Advice loads are not RAM reads; the byte length is encoded in the virtual +/// row and the shared helper performs the architectural narrow-load extension. pub(in crate::expand) fn expand_advice_lh( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_advice_load(instruction, 2, Some(48), allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_advice_load(instruction, 2) } diff --git a/crates/jolt-program/src/expand/memory/advice_lw.rs b/crates/jolt-program/src/expand/memory/advice_lw.rs index 3ce2aab84b..b848428d56 100644 --- a/crates/jolt-program/src/expand/memory/advice_lw.rs +++ b/crates/jolt-program/src/expand/memory/advice_lw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AdviceLW` to a four-byte advice-tape read and word sign-extension. +/// +/// The shared helper keeps advice traffic separate from RAM while preserving +/// the same sign-extension behavior as a 32-bit signed load. pub(in crate::expand) fn expand_advice_lw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_advice_load(instruction, 4, Some(32), allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_advice_load(instruction, 4) } diff --git a/crates/jolt-program/src/expand/memory/amoaddd.rs b/crates/jolt-program/src/expand/memory/amoaddd.rs index 5d5b47ba42..e708a32e64 100644 --- a/crates/jolt-program/src/expand/memory/amoaddd.rs +++ b/crates/jolt-program/src/expand/memory/amoaddd.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOADD.D` through the shared doubleword AMO template. +/// +/// The shared helper loads the old doubleword, stores `old + rs2`, and returns +/// the old value in `rd`. pub(in crate::expand) fn expand_amoaddd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_d(instruction, InstructionKind::ADD, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_d(instruction, SourceInstructionKind::ADD) } diff --git a/crates/jolt-program/src/expand/memory/amoaddw.rs b/crates/jolt-program/src/expand/memory/amoaddw.rs index ea5446785b..21d44d63a0 100644 --- a/crates/jolt-program/src/expand/memory/amoaddw.rs +++ b/crates/jolt-program/src/expand/memory/amoaddw.rs @@ -1,8 +1,12 @@ use super::*; +/// Lowers `AMOADD.W` through the shared word AMO template. +/// +/// The helper extracts the old word, stores the low-word result of +/// `old + rs2` back into the containing doubleword, and returns old word +/// sign-extended in `rd`. pub(in crate::expand) fn expand_amoaddw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_w(instruction, InstructionKind::ADD, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_w(instruction, SourceInstructionKind::ADD) } diff --git a/crates/jolt-program/src/expand/memory/amoandd.rs b/crates/jolt-program/src/expand/memory/amoandd.rs index e279fdb468..e36c136ca1 100644 --- a/crates/jolt-program/src/expand/memory/amoandd.rs +++ b/crates/jolt-program/src/expand/memory/amoandd.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOAND.D` through the shared doubleword AMO template. +/// +/// The shared helper loads the old doubleword, stores `old & rs2`, and returns +/// the old value in `rd`. pub(in crate::expand) fn expand_amoandd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_d(instruction, InstructionKind::AND, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_d(instruction, SourceInstructionKind::AND) } diff --git a/crates/jolt-program/src/expand/memory/amoandw.rs b/crates/jolt-program/src/expand/memory/amoandw.rs index 6ee92b94da..0fada892e0 100644 --- a/crates/jolt-program/src/expand/memory/amoandw.rs +++ b/crates/jolt-program/src/expand/memory/amoandw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOAND.W` through the shared word AMO template. +/// +/// The helper extracts the old word, stores `old & rs2` into the selected word +/// lane, and returns old word sign-extended in `rd`. pub(in crate::expand) fn expand_amoandw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_w(instruction, InstructionKind::AND, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_w(instruction, SourceInstructionKind::AND) } diff --git a/crates/jolt-program/src/expand/memory/amomaxd.rs b/crates/jolt-program/src/expand/memory/amomaxd.rs index c9b09363e3..c2a14a046c 100644 --- a/crates/jolt-program/src/expand/memory/amomaxd.rs +++ b/crates/jolt-program/src/expand/memory/amomaxd.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed `AMOMAX.D` through the shared doubleword min/max template. +/// +/// The helper compares old memory and `rs2` with signed `SLT`, stores the +/// maximum, and returns the old memory value in `rd`. pub(in crate::expand) fn expand_amomaxd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_d(instruction, InstructionKind::SLT, false, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_d(instruction, SourceInstructionKind::SLT, false) } diff --git a/crates/jolt-program/src/expand/memory/amomaxud.rs b/crates/jolt-program/src/expand/memory/amomaxud.rs index d73b6249ae..a88442bf07 100644 --- a/crates/jolt-program/src/expand/memory/amomaxud.rs +++ b/crates/jolt-program/src/expand/memory/amomaxud.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned `AMOMAXU.D` through the shared doubleword min/max template. +/// +/// The helper compares old memory and `rs2` with unsigned `SLTU`, stores the +/// maximum, and returns the old memory value in `rd`. pub(in crate::expand) fn expand_amomaxud( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_d(instruction, InstructionKind::SLTU, false, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_d(instruction, SourceInstructionKind::SLTU, false) } diff --git a/crates/jolt-program/src/expand/memory/amomaxuw.rs b/crates/jolt-program/src/expand/memory/amomaxuw.rs index 453772638f..440aa6dc72 100644 --- a/crates/jolt-program/src/expand/memory/amomaxuw.rs +++ b/crates/jolt-program/src/expand/memory/amomaxuw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned `AMOMAXU.W` through the shared word min/max template. +/// +/// The helper compares zero-extended old word and `rs2`, stores the unsigned +/// maximum into the selected word lane, and returns old word sign-extended. pub(in crate::expand) fn expand_amomaxuw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_w(instruction, InstructionKind::SLTU, false, false, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_w(instruction, SourceInstructionKind::SLTU, false, false) } diff --git a/crates/jolt-program/src/expand/memory/amomaxw.rs b/crates/jolt-program/src/expand/memory/amomaxw.rs index aabe16a07b..528ac039c6 100644 --- a/crates/jolt-program/src/expand/memory/amomaxw.rs +++ b/crates/jolt-program/src/expand/memory/amomaxw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed `AMOMAX.W` through the shared word min/max template. +/// +/// The helper compares sign-extended old word and `rs2`, stores the signed +/// maximum into the selected word lane, and returns old word sign-extended. pub(in crate::expand) fn expand_amomaxw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_w(instruction, InstructionKind::SLT, false, true, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_w(instruction, SourceInstructionKind::SLT, false, true) } diff --git a/crates/jolt-program/src/expand/memory/amomind.rs b/crates/jolt-program/src/expand/memory/amomind.rs index a710fdc7c5..5938f897d9 100644 --- a/crates/jolt-program/src/expand/memory/amomind.rs +++ b/crates/jolt-program/src/expand/memory/amomind.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed `AMOMIN.D` through the shared doubleword min/max template. +/// +/// The helper compares old memory and `rs2` with signed `SLT`, stores the +/// minimum, and returns the old memory value in `rd`. pub(in crate::expand) fn expand_amomind( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_d(instruction, InstructionKind::SLT, true, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_d(instruction, SourceInstructionKind::SLT, true) } diff --git a/crates/jolt-program/src/expand/memory/amominud.rs b/crates/jolt-program/src/expand/memory/amominud.rs index b9b3ea304d..1562c8eaa4 100644 --- a/crates/jolt-program/src/expand/memory/amominud.rs +++ b/crates/jolt-program/src/expand/memory/amominud.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned `AMOMINU.D` through the shared doubleword min/max template. +/// +/// The helper compares old memory and `rs2` with unsigned `SLTU`, stores the +/// minimum, and returns the old memory value in `rd`. pub(in crate::expand) fn expand_amominud( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_d(instruction, InstructionKind::SLTU, true, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_d(instruction, SourceInstructionKind::SLTU, true) } diff --git a/crates/jolt-program/src/expand/memory/amominuw.rs b/crates/jolt-program/src/expand/memory/amominuw.rs index 9e2a60fcc1..0858fb8cae 100644 --- a/crates/jolt-program/src/expand/memory/amominuw.rs +++ b/crates/jolt-program/src/expand/memory/amominuw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned `AMOMINU.W` through the shared word min/max template. +/// +/// The helper compares zero-extended old word and `rs2`, stores the unsigned +/// minimum into the selected word lane, and returns old word sign-extended. pub(in crate::expand) fn expand_amominuw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_w(instruction, InstructionKind::SLTU, true, false, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_w(instruction, SourceInstructionKind::SLTU, true, false) } diff --git a/crates/jolt-program/src/expand/memory/amominw.rs b/crates/jolt-program/src/expand/memory/amominw.rs index bafeb15e14..10e528bac6 100644 --- a/crates/jolt-program/src/expand/memory/amominw.rs +++ b/crates/jolt-program/src/expand/memory/amominw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed `AMOMIN.W` through the shared word min/max template. +/// +/// The helper compares sign-extended old word and `rs2`, stores the signed +/// minimum into the selected word lane, and returns old word sign-extended. pub(in crate::expand) fn expand_amominw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_minmax_w(instruction, InstructionKind::SLT, true, true, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_minmax_w(instruction, SourceInstructionKind::SLT, true, true) } diff --git a/crates/jolt-program/src/expand/memory/amoord.rs b/crates/jolt-program/src/expand/memory/amoord.rs index 1a9ba8344d..d7f12b9371 100644 --- a/crates/jolt-program/src/expand/memory/amoord.rs +++ b/crates/jolt-program/src/expand/memory/amoord.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOOR.D` through the shared doubleword AMO template. +/// +/// The shared helper loads the old doubleword, stores `old | rs2`, and returns +/// the old value in `rd`. pub(in crate::expand) fn expand_amoord( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_d(instruction, InstructionKind::OR, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_d(instruction, SourceInstructionKind::OR) } diff --git a/crates/jolt-program/src/expand/memory/amoorw.rs b/crates/jolt-program/src/expand/memory/amoorw.rs index a9db7d0c24..a69cdf69d6 100644 --- a/crates/jolt-program/src/expand/memory/amoorw.rs +++ b/crates/jolt-program/src/expand/memory/amoorw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOOR.W` through the shared word AMO template. +/// +/// The helper extracts the old word, stores `old | rs2` into the selected word +/// lane, and returns old word sign-extended in `rd`. pub(in crate::expand) fn expand_amoorw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_w(instruction, InstructionKind::OR, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_w(instruction, SourceInstructionKind::OR) } diff --git a/crates/jolt-program/src/expand/memory/amoswapd.rs b/crates/jolt-program/src/expand/memory/amoswapd.rs index f45db379c0..942b35c264 100644 --- a/crates/jolt-program/src/expand/memory/amoswapd.rs +++ b/crates/jolt-program/src/expand/memory/amoswapd.rs @@ -1,16 +1,35 @@ use super::*; +/// Lowers `AMOSWAP.D` to load-old, store-new, return-old. +/// +/// The old doubleword is copied to `rd` after `rs2` has been written to memory, +/// matching the atomic swap's read-modify-write result in Jolt's sequential +/// trace model. pub(in crate::expand) fn expand_amoswapd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rd = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::LD, v_rd, rs1(instruction)?, 0)?; - asm.emit_s(InstructionKind::SD, rs1(instruction)?, rs2(instruction)?, 0)?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, v_rd, 0)?; - let sequence = asm.finalize()?; - allocator.release(v_rd)?; - Ok(sequence) + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rd = asm.allocate()?; + + asm.expand_i( + SourceInstructionKind::LD, + v_rd.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.expand_s( + SourceInstructionKind::SD, + reg(rs1(instruction)?), + reg(rs2(instruction)?), + 0, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + v_rd.operand(), + 0, + ); + asm.release(v_rd); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/amoswapw.rs b/crates/jolt-program/src/expand/memory/amoswapw.rs index b820fae318..c68866328d 100644 --- a/crates/jolt-program/src/expand/memory/amoswapw.rs +++ b/crates/jolt-program/src/expand/memory/amoswapw.rs @@ -1,30 +1,39 @@ use super::*; +/// Lowers `AMOSWAP.W` through the shared word-AMO lane update helpers. +/// +/// The pre-helper extracts and sign-extends the old word; the post-helper +/// merges the low word of `rs2` into the containing doubleword and returns the +/// old word in `rd`. pub(in crate::expand) fn expand_amoswapw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_mask = allocator.allocate()?; - let v_dword = allocator.allocate()?; - let v_shift = allocator.allocate()?; - let v_rd = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - super::shared::amo_pre64(&mut asm, rs1(instruction)?, v_rd, v_dword, v_shift)?; - super::shared::amo_post64( + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_mask = asm.allocate()?; + let v_dword = asm.allocate()?; + let v_shift = asm.allocate()?; + let v_rd = asm.allocate()?; + + super::shared::expand_amo_pre64( + &mut asm, + reg(rs1(instruction)?), + v_rd.operand(), + v_dword.operand(), + v_shift.operand(), + )?; + super::shared::expand_amo_post64( &mut asm, - rs1(instruction)?, - rs2(instruction)?, - v_dword, - v_shift, - v_mask, - rd(instruction)?, - v_rd, + super::shared::AmoPost64 { + rs1: reg(rs1(instruction)?), + v_rs2: reg(rs2(instruction)?), + v_dword: v_dword.operand(), + v_shift: v_shift.operand(), + v_mask: v_mask.operand(), + rd: reg(rd(instruction)?), + v_rd: v_rd.operand(), + }, )?; - let sequence = asm.finalize()?; - allocator.release(v_mask)?; - allocator.release(v_dword)?; - allocator.release(v_shift)?; - allocator.release(v_rd)?; - Ok(sequence) + asm.release_many([v_mask, v_dword, v_shift, v_rd]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/amoxord.rs b/crates/jolt-program/src/expand/memory/amoxord.rs index 76a333e91d..576e8cdc3a 100644 --- a/crates/jolt-program/src/expand/memory/amoxord.rs +++ b/crates/jolt-program/src/expand/memory/amoxord.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOXOR.D` through the shared doubleword AMO template. +/// +/// The shared helper loads the old doubleword, stores `old ^ rs2`, and returns +/// the old value in `rd`. pub(in crate::expand) fn expand_amoxord( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_d(instruction, InstructionKind::XOR, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_d(instruction, SourceInstructionKind::XOR) } diff --git a/crates/jolt-program/src/expand/memory/amoxorw.rs b/crates/jolt-program/src/expand/memory/amoxorw.rs index 783fb182d9..56534455ac 100644 --- a/crates/jolt-program/src/expand/memory/amoxorw.rs +++ b/crates/jolt-program/src/expand/memory/amoxorw.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers `AMOXOR.W` through the shared word AMO template. +/// +/// The helper extracts the old word, stores `old ^ rs2` into the selected word +/// lane, and returns old word sign-extended in `rd`. pub(in crate::expand) fn expand_amoxorw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_amo_w(instruction, InstructionKind::XOR, allocator) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_amo_w(instruction, SourceInstructionKind::XOR) } diff --git a/crates/jolt-program/src/expand/memory/lb.rs b/crates/jolt-program/src/expand/memory/lb.rs index d961100ae0..45c9fff21a 100644 --- a/crates/jolt-program/src/expand/memory/lb.rs +++ b/crates/jolt-program/src/expand/memory/lb.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed byte load `LB` through the shared containing-doubleword load. +/// +/// The helper extracts the selected byte and uses arithmetic shift-back to +/// sign-extend it to XLEN. pub(in crate::expand) fn expand_lb( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_byte_load(instruction, allocator, true) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_byte_load(instruction, true) } diff --git a/crates/jolt-program/src/expand/memory/lbu.rs b/crates/jolt-program/src/expand/memory/lbu.rs index 425defda4b..7c6819c8e5 100644 --- a/crates/jolt-program/src/expand/memory/lbu.rs +++ b/crates/jolt-program/src/expand/memory/lbu.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned byte load `LBU` through the shared containing-doubleword load. +/// +/// The helper extracts the selected byte and uses logical shift-back so the +/// upper bits are zero-filled. pub(in crate::expand) fn expand_lbu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_byte_load(instruction, allocator, false) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_byte_load(instruction, false) } diff --git a/crates/jolt-program/src/expand/memory/lh.rs b/crates/jolt-program/src/expand/memory/lh.rs index dde7b69bd7..c47922bbda 100644 --- a/crates/jolt-program/src/expand/memory/lh.rs +++ b/crates/jolt-program/src/expand/memory/lh.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers signed halfword load `LH` through the shared aligned extraction path. +/// +/// The shared helper proves halfword alignment, extracts the selected +/// halfword from the containing doubleword, and sign-extends it. pub(in crate::expand) fn expand_lh( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_halfword_load(instruction, allocator, true) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_halfword_load(instruction, true) } diff --git a/crates/jolt-program/src/expand/memory/lhu.rs b/crates/jolt-program/src/expand/memory/lhu.rs index 1b170c3385..3f8a05adfc 100644 --- a/crates/jolt-program/src/expand/memory/lhu.rs +++ b/crates/jolt-program/src/expand/memory/lhu.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers unsigned halfword load `LHU` through the shared aligned extraction path. +/// +/// The shared helper proves halfword alignment, extracts the selected +/// halfword, and zero-extends it to XLEN. pub(in crate::expand) fn expand_lhu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_halfword_load(instruction, allocator, false) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_halfword_load(instruction, false) } diff --git a/crates/jolt-program/src/expand/memory/lrd.rs b/crates/jolt-program/src/expand/memory/lrd.rs index e11e064869..e0926a4360 100644 --- a/crates/jolt-program/src/expand/memory/lrd.rs +++ b/crates/jolt-program/src/expand/memory/lrd.rs @@ -1,16 +1,51 @@ +use common::constants::RAM_START_ADDRESS; + use super::*; +/// Lowers `LR.D` by recording a doubleword reservation and then performing `LD`. +/// +/// A doubleword reservation also covers word store-conditionals at the same +/// address, so both reservation virtual registers are initialized. As with +/// `LR.W`, reservations are restricted to ordinary RAM addresses. pub(in crate::expand) fn expand_lrd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_reservation_d = allocator.reservation_d_register(); - let v_reservation_w = allocator.reservation_w_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - super::shared::emit_ram_region_assertion(&mut asm, rs1(instruction)?)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_d, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_w, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::LD, rd(instruction)?, rs1(instruction)?, 0)?; + instruction: &SourceInstructionRow, +) -> Result { + let v_reservation_d = reservation_d_register(); + let v_reservation_w = reservation_w_register(); + let mut asm = ExpansionBuilder::new(*instruction); + let ram_start = asm.allocate()?; + + // LR/SC reservations are only modeled for ordinary RAM. + asm.expand_u( + SourceInstructionKind::LUI, + ram_start.operand(), + RAM_START_ADDRESS as i128, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + ram_start.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.release(ram_start); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(v_reservation_d), + reg(rs1(instruction)?), + 0, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(v_reservation_w), + reg(rs1(instruction)?), + 0, + ); + asm.expand_i( + SourceInstructionKind::LD, + reg(rd(instruction)?), + reg(rs1(instruction)?), + 0, + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/lrw.rs b/crates/jolt-program/src/expand/memory/lrw.rs index 05a3875759..52ef64b476 100644 --- a/crates/jolt-program/src/expand/memory/lrw.rs +++ b/crates/jolt-program/src/expand/memory/lrw.rs @@ -1,16 +1,47 @@ +use common::constants::RAM_START_ADDRESS; + use super::*; +/// Lowers `LR.W` by recording a word reservation and then performing `LW`. +/// +/// Reservations live in dedicated virtual registers. The RAM-region assertion +/// rejects I/O addresses before the reservation is recorded, because a failed +/// store-conditional must not accidentally mutate device state through the +/// synthesized store path. pub(in crate::expand) fn expand_lrw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_reservation_w = allocator.reservation_w_register(); - let v_reservation_d = allocator.reservation_d_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - super::shared::emit_ram_region_assertion(&mut asm, rs1(instruction)?)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_w, rs1(instruction)?, 0)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_d, 0, 0)?; - asm.emit_i(InstructionKind::LW, rd(instruction)?, rs1(instruction)?, 0)?; + instruction: &SourceInstructionRow, +) -> Result { + let v_reservation_w = reservation_w_register(); + let v_reservation_d = reservation_d_register(); + let mut asm = ExpansionBuilder::new(*instruction); + let ram_start = asm.allocate()?; + + // LR/SC reservations are only modeled for ordinary RAM. + asm.expand_u( + SourceInstructionKind::LUI, + ram_start.operand(), + RAM_START_ADDRESS as i128, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + ram_start.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.release(ram_start); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(v_reservation_w), + reg(rs1(instruction)?), + 0, + ); + asm.expand_i(SourceInstructionKind::ADDI, reg(v_reservation_d), reg(0), 0); + asm.expand_i( + SourceInstructionKind::LW, + reg(rd(instruction)?), + reg(rs1(instruction)?), + 0, + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/lw.rs b/crates/jolt-program/src/expand/memory/lw.rs index 0228163ae2..99d20191df 100644 --- a/crates/jolt-program/src/expand/memory/lw.rs +++ b/crates/jolt-program/src/expand/memory/lw.rs @@ -1,36 +1,54 @@ use super::*; +/// Lowers signed word load `LW` by reading the containing aligned doubleword. +/// +/// The sequence proves word alignment, loads the aligned 8-byte word, shifts +/// the requested 32-bit lane down, and sign-extends that low word into `rd`. pub(in crate::expand) fn expand_lw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_align( - InstructionKind::VirtualAssertWordAlignment, - rs1(instruction)?, + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + + // RAM is accessed at doubleword granularity here. The word alignment + // assertion is still required by the source `LW` semantics. + asm.expand_address( + SourceInstructionKind::VirtualAssertWordAlignment, + reg(rs1(instruction)?), instruction.operands.imm, - )?; - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v1, v1, 0)?; - asm.emit_i(InstructionKind::SLLI, v0, v0, 3)?; - asm.emit_r(InstructionKind::SRL, v1, v1, v0)?; - asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - v1, + ); + // v1 = containing doubleword address, v0 = byte offset within it. + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v1.operand(), v1.operand(), 0); + asm.expand_i(SourceInstructionKind::SLLI, v0.operand(), v0.operand(), 3); + asm.expand_r( + SourceInstructionKind::SRL, + v1.operand(), + v1.operand(), + v0.operand(), + ); + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + v1.operand(), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - Ok(sequence) + ); + asm.release(v0); + asm.release(v1); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/lwu.rs b/crates/jolt-program/src/expand/memory/lwu.rs index 1b41021da7..9749954219 100644 --- a/crates/jolt-program/src/expand/memory/lwu.rs +++ b/crates/jolt-program/src/expand/memory/lwu.rs @@ -1,32 +1,55 @@ use super::*; +/// Lowers unsigned word load `LWU` by extracting from the containing doubleword. +/// +/// The loaded 32-bit lane is moved into the high half and logically shifted +/// back down, producing the zero-extended RV64 result. pub(in crate::expand) fn expand_lwu( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_align( - InstructionKind::VirtualAssertWordAlignment, - rs1(instruction)?, + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + + // The source op still requires word alignment even though the physical + // proof row reads the aligned containing doubleword. + asm.expand_address( + SourceInstructionKind::VirtualAssertWordAlignment, + reg(rs1(instruction)?), instruction.operands.imm, - )?; - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v1, v1, 0)?; - asm.emit_i(InstructionKind::XORI, v0, v0, 4)?; - asm.emit_i(InstructionKind::SLLI, v0, v0, 3)?; - asm.emit_r(InstructionKind::SLL, v1, v1, v0)?; - asm.emit_i(InstructionKind::SRLI, rd(instruction)?, v1, 32)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - Ok(sequence) + ); + // v1 = containing doubleword address, v0 = byte offset within it. + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v1.operand(), v1.operand(), 0); + // XOR with 4 selects the opposite 32-bit lane after the doubleword is + // shifted left, so the requested word lands in bits 63:32. + asm.expand_i(SourceInstructionKind::XORI, v0.operand(), v0.operand(), 4); + asm.expand_i(SourceInstructionKind::SLLI, v0.operand(), v0.operand(), 3); + asm.expand_r( + SourceInstructionKind::SLL, + v1.operand(), + v1.operand(), + v0.operand(), + ); + asm.expand_i( + SourceInstructionKind::SRLI, + reg(rd(instruction)?), + v1.operand(), + 32, + ); + asm.release(v0); + asm.release(v1); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/sb.rs b/crates/jolt-program/src/expand/memory/sb.rs index 691e8907c1..398336b0bd 100644 --- a/crates/jolt-program/src/expand/memory/sb.rs +++ b/crates/jolt-program/src/expand/memory/sb.rs @@ -1,8 +1,11 @@ use super::*; +/// Lowers byte store `SB` through the shared masked doubleword update. +/// +/// Byte stores have no alignment requirement; the helper updates only the +/// selected 8-bit lane of the containing doubleword. pub(in crate::expand) fn expand_sb( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - super::shared::expand_narrow_store(instruction, allocator, 0xff, None) + instruction: &SourceInstructionRow, +) -> Result { + super::shared::expand_narrow_store(instruction, 0xff, None) } diff --git a/crates/jolt-program/src/expand/memory/scd.rs b/crates/jolt-program/src/expand/memory/scd.rs index 57db81d1ef..02937e9c21 100644 --- a/crates/jolt-program/src/expand/memory/scd.rs +++ b/crates/jolt-program/src/expand/memory/scd.rs @@ -1,50 +1,111 @@ use super::*; +/// Lowers `SC.D` to a conditional doubleword update driven by a success witness. +/// +/// The tracer patches the first `VirtualAdvice` to `1` only when a doubleword +/// reservation covers `rs1`. The sequence proves that successful stores use the +/// reserved address, conditionally selects either `rs2` or the old memory +/// value, stores the selected doubleword, clears reservations, and returns +/// architectural status `0` on success or `1` on failure. pub(in crate::expand) fn expand_scd( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_reservation = allocator.reservation_d_register(); - let v_reservation_w = allocator.reservation_w_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - super::shared::emit_ram_region_assertion(&mut asm, rs1(instruction)?)?; - - let v_success = asm.allocator().allocate()?; - asm.emit_j(InstructionKind::VirtualAdvice, v_success, 0)?; - - let v_one = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::ADDI, v_one, 0, 1)?; - asm.emit_b(InstructionKind::VirtualAssertLTE, v_success, v_one, 0)?; - asm.allocator().release(v_one)?; - - let v_addr_diff = asm.allocator().allocate()?; - asm.emit_r( - InstructionKind::SUB, - v_addr_diff, - v_reservation, - rs1(instruction)?, - )?; - asm.emit_r(InstructionKind::MUL, v_addr_diff, v_success, v_addr_diff)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, v_addr_diff, 0, 0)?; - asm.allocator().release(v_addr_diff)?; - - let v_mem = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::LD, v_mem, rs1(instruction)?, 0)?; - - let v_diff = asm.allocator().allocate()?; - asm.emit_r(InstructionKind::SUB, v_diff, rs2(instruction)?, v_mem)?; - asm.emit_r(InstructionKind::MUL, v_diff, v_diff, v_success)?; - asm.emit_r(InstructionKind::ADD, v_diff, v_mem, v_diff)?; - asm.allocator().release(v_mem)?; - - asm.emit_s(InstructionKind::SD, rs1(instruction)?, v_diff, 0)?; - asm.allocator().release(v_diff)?; - - asm.emit_i(InstructionKind::ADDI, v_reservation, 0, 0)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_w, 0, 0)?; - asm.emit_i(InstructionKind::XORI, rd(instruction)?, v_success, 1)?; - asm.allocator().release(v_success)?; + instruction: &SourceInstructionRow, +) -> Result { + let v_reservation = reservation_d_register(); + let v_reservation_w = reservation_w_register(); + let mut asm = ExpansionBuilder::new(*instruction); + + let ram_start = asm.allocate()?; + super::shared::expand_ram_region_assertion(&mut asm, reg(rs1(instruction)?), ram_start)?; + + let v_success = asm.allocate()?; + // v_success is Boolean advice supplied by the tracer's LR/SC reservation + // check: 1 means the SC succeeds, 0 means it fails. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + v_success.operand(), + 0, + ); + + let v_one = asm.allocate()?; + asm.expand_i(SourceInstructionKind::ADDI, v_one.operand(), reg(0), 1); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + v_success.operand(), + v_one.operand(), + 0, + ); + asm.release(v_one); + + let v_addr_diff = asm.allocate()?; + // If v_success is 1, the doubleword reservation address must equal rs1. + // Failure leaves the address unconstrained because no write should occur. + asm.expand_r( + SourceInstructionKind::SUB, + v_addr_diff.operand(), + reg(v_reservation), + reg(rs1(instruction)?), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v_addr_diff.operand(), + v_success.operand(), + v_addr_diff.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + v_addr_diff.operand(), + reg(0), + 0, + ); + asm.release(v_addr_diff); + + let v_mem = asm.allocate()?; + asm.expand_i( + SourceInstructionKind::LD, + v_mem.operand(), + reg(rs1(instruction)?), + 0, + ); + + let v_diff = asm.allocate()?; + // v_diff = old_mem + success * (rs2 - old_mem), so failure stores the + // previous memory value and success stores rs2. + asm.expand_r( + SourceInstructionKind::SUB, + v_diff.operand(), + reg(rs2(instruction)?), + v_mem.operand(), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v_diff.operand(), + v_diff.operand(), + v_success.operand(), + ); + asm.expand_r( + SourceInstructionKind::ADD, + v_diff.operand(), + v_mem.operand(), + v_diff.operand(), + ); + asm.release(v_mem); + asm.expand_s( + SourceInstructionKind::SD, + reg(rs1(instruction)?), + v_diff.operand(), + 0, + ); + asm.release(v_diff); + // RISC-V invalidates the reservation after every SC, regardless of success. + asm.expand_i(SourceInstructionKind::ADDI, reg(v_reservation), reg(0), 0); + asm.expand_i(SourceInstructionKind::ADDI, reg(v_reservation_w), reg(0), 0); + asm.expand_i( + SourceInstructionKind::XORI, + reg(rd(instruction)?), + v_success.operand(), + 1, + ); + asm.release(v_success); asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/scw.rs b/crates/jolt-program/src/expand/memory/scw.rs index 30b09b832d..5fbb8e61e8 100644 --- a/crates/jolt-program/src/expand/memory/scw.rs +++ b/crates/jolt-program/src/expand/memory/scw.rs @@ -1,54 +1,127 @@ use super::*; +/// Lowers `SC.W` to a conditional word update driven by a success witness. +/// +/// The tracer patches the first `VirtualAdvice` to `1` when the reservation +/// covers the target word and `0` otherwise. The sequence proves that success +/// implies the recorded reservation address matches `rs1`, conditionally +/// selects either `rs2` or the old memory value, stores the selected word, and +/// returns the architectural status `0` on success or `1` on failure. pub(in crate::expand) fn expand_scw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_reservation = allocator.reservation_w_register(); - let v_reservation_d = allocator.reservation_d_register(); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - super::shared::emit_ram_region_assertion(&mut asm, rs1(instruction)?)?; - - let v_success = asm.allocator().allocate()?; - asm.emit_j(InstructionKind::VirtualAdvice, v_success, 0)?; - - let v_one = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::ADDI, v_one, 0, 1)?; - asm.emit_b(InstructionKind::VirtualAssertLTE, v_success, v_one, 0)?; - asm.allocator().release(v_one)?; - - let v_addr_diff = asm.allocator().allocate()?; - asm.emit_r( - InstructionKind::SUB, - v_addr_diff, - v_reservation, - rs1(instruction)?, - )?; - asm.emit_r(InstructionKind::MUL, v_addr_diff, v_success, v_addr_diff)?; - asm.emit_b(InstructionKind::VirtualAssertEQ, v_addr_diff, 0, 0)?; - asm.allocator().release(v_addr_diff)?; - - asm.emit_i(InstructionKind::ADDI, v_reservation, v_success, 0)?; - asm.allocator().release(v_success)?; - - let v_mem = asm.allocator().allocate()?; - asm.emit_i(InstructionKind::LW, v_mem, rs1(instruction)?, 0)?; - - let v_diff = asm.allocator().allocate()?; - asm.emit_r(InstructionKind::SUB, v_diff, rs2(instruction)?, v_mem)?; - asm.emit_r(InstructionKind::MUL, v_diff, v_diff, v_reservation)?; - asm.emit_r(InstructionKind::ADD, v_diff, v_mem, v_diff)?; - asm.allocator().release(v_mem)?; - - asm.emit_i(InstructionKind::ADDI, v_reservation_d, v_diff, 0)?; - asm.allocator().release(v_diff)?; - - asm.emit_s(InstructionKind::SW, rs1(instruction)?, v_reservation_d, 0)?; - - asm.emit_i(InstructionKind::XORI, rd(instruction)?, v_reservation, 1)?; - asm.emit_i(InstructionKind::ADDI, v_reservation, 0, 0)?; - asm.emit_i(InstructionKind::ADDI, v_reservation_d, 0, 0)?; + instruction: &SourceInstructionRow, +) -> Result { + let v_reservation = reservation_w_register(); + let v_reservation_d = reservation_d_register(); + let mut asm = ExpansionBuilder::new(*instruction); + + let ram_start = asm.allocate()?; + super::shared::expand_ram_region_assertion(&mut asm, reg(rs1(instruction)?), ram_start)?; + + let v_success = asm.allocate()?; + // v_success is Boolean advice supplied by the tracer's LR/SC reservation + // check: 1 means the SC succeeds, 0 means it fails. + asm.expand_j( + SourceInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())), + v_success.operand(), + 0, + ); + + let v_one = asm.allocate()?; + asm.expand_i(SourceInstructionKind::ADDI, v_one.operand(), reg(0), 1); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + v_success.operand(), + v_one.operand(), + 0, + ); + asm.release(v_one); + + let v_addr_diff = asm.allocate()?; + // If v_success is 1, the reservation address must equal rs1. If + // v_success is 0, this product is zero regardless of the address. + asm.expand_r( + SourceInstructionKind::SUB, + v_addr_diff.operand(), + reg(v_reservation), + reg(rs1(instruction)?), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v_addr_diff.operand(), + v_success.operand(), + v_addr_diff.operand(), + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertEQ, + v_addr_diff.operand(), + reg(0), + 0, + ); + asm.release(v_addr_diff); + + // Keep the success bit in the reservation register so it can gate the + // conditional memory value below. + asm.expand_i( + SourceInstructionKind::ADDI, + reg(v_reservation), + v_success.operand(), + 0, + ); + asm.release(v_success); + + let v_mem = asm.allocate()?; + asm.expand_i( + SourceInstructionKind::LW, + v_mem.operand(), + reg(rs1(instruction)?), + 0, + ); + + let v_diff = asm.allocate()?; + // v_diff = old_mem + success * (rs2 - old_mem), so failure stores the + // previous memory value and success stores rs2. + asm.expand_r( + SourceInstructionKind::SUB, + v_diff.operand(), + reg(rs2(instruction)?), + v_mem.operand(), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v_diff.operand(), + v_diff.operand(), + reg(v_reservation), + ); + asm.expand_r( + SourceInstructionKind::ADD, + v_diff.operand(), + v_mem.operand(), + v_diff.operand(), + ); + asm.release(v_mem); + + asm.expand_i( + SourceInstructionKind::ADDI, + reg(v_reservation_d), + v_diff.operand(), + 0, + ); + asm.release(v_diff); + asm.expand_s( + SourceInstructionKind::SW, + reg(rs1(instruction)?), + reg(v_reservation_d), + 0, + ); + asm.expand_i( + SourceInstructionKind::XORI, + reg(rd(instruction)?), + reg(v_reservation), + 1, + ); + // RISC-V invalidates the reservation after every SC, regardless of success. + asm.expand_i(SourceInstructionKind::ADDI, reg(v_reservation), reg(0), 0); + asm.expand_i(SourceInstructionKind::ADDI, reg(v_reservation_d), reg(0), 0); asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/sh.rs b/crates/jolt-program/src/expand/memory/sh.rs index 90407e3690..00ece18c4d 100644 --- a/crates/jolt-program/src/expand/memory/sh.rs +++ b/crates/jolt-program/src/expand/memory/sh.rs @@ -1,13 +1,15 @@ use super::*; +/// Lowers halfword store `SH` through the shared masked doubleword update. +/// +/// The shared helper proves halfword alignment, updates only the selected +/// 16-bit lane, and writes the containing doubleword back. pub(in crate::expand) fn expand_sh( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { super::shared::expand_narrow_store( instruction, - allocator, 0xffff, - Some(InstructionKind::VirtualAssertHalfwordAlignment), + Some(SourceInstructionKind::VirtualAssertHalfwordAlignment), ) } diff --git a/crates/jolt-program/src/expand/memory/shared.rs b/crates/jolt-program/src/expand/memory/shared.rs index 0135c63812..87c2b859f7 100644 --- a/crates/jolt-program/src/expand/memory/shared.rs +++ b/crates/jolt-program/src/expand/memory/shared.rs @@ -2,332 +2,549 @@ use common::constants::RAM_START_ADDRESS; use super::*; -pub(in crate::expand) fn emit_ram_region_assertion( - asm: &mut assembler::InstrAssembler<'_>, - address_register: u8, +/// Emits the common LR/SC proof guard that rejects non-RAM reservation targets. +/// +/// Jolt models LR/SC reservations only for ordinary RAM. This assertion keeps +/// synthesized failure-path stores from touching memory-mapped I/O addresses. +pub(in crate::expand) fn expand_ram_region_assertion( + asm: &mut ExpansionBuilder, + address_register: RegisterOperand, + ram_start: TempId, ) -> Result<(), ExpansionError> { - let ram_start = asm.allocator().allocate()?; - asm.emit_u(InstructionKind::LUI, ram_start, RAM_START_ADDRESS as i128)?; - asm.emit_b( - InstructionKind::VirtualAssertLTE, - ram_start, + asm.expand_u( + SourceInstructionKind::LUI, + ram_start.operand(), + RAM_START_ADDRESS as i128, + ); + asm.expand_b( + SourceInstructionKind::VirtualAssertLTE, + ram_start.operand(), address_register, 0, - )?; - asm.allocator().release(ram_start)?; + ); + asm.release(ram_start); Ok(()) } +/// Lowers `LB`/`LBU` by loading the containing doubleword and extracting a byte. +/// +/// The effective address is rounded down to the aligned 8-byte address for the +/// `LD`. The byte offset then determines how far to left-shift the containing +/// doubleword so the requested byte lands in bits 63:56; the final arithmetic +/// or logical right shift performs signed or unsigned extension. pub(in crate::expand) fn expand_byte_load( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, + instruction: &SourceInstructionRow, signed: bool, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + + // v0 = effective address. v1 = aligned address of the containing + // doubleword. + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v1, v1, 0)?; - asm.emit_i(InstructionKind::XORI, v0, v0, 7)?; - asm.emit_i(InstructionKind::SLLI, v0, v0, 3)?; - asm.emit_r(InstructionKind::SLL, v1, v1, v0)?; - asm.emit_i( + ); + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v1.operand(), v1.operand(), 0); + // Under the RV64 shift mask, ((address ^ 7) << 3) is + // (7 - byte_offset) * 8, moving the selected byte to the high end. + asm.expand_i(SourceInstructionKind::XORI, v0.operand(), v0.operand(), 7); + asm.expand_i(SourceInstructionKind::SLLI, v0.operand(), v0.operand(), 3); + asm.expand_r( + SourceInstructionKind::SLL, + v1.operand(), + v1.operand(), + v0.operand(), + ); + asm.expand_i( if signed { - InstructionKind::SRAI + SourceInstructionKind::SRAI } else { - InstructionKind::SRLI + SourceInstructionKind::SRLI }, - rd(instruction)?, - v1, + reg(rd(instruction)?), + v1.operand(), 56, - )?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - Ok(sequence) + ); + asm.release_many([v0, v1]); + + asm.finalize() } +/// Lowers `LH`/`LHU` by loading the containing doubleword and extracting a halfword. +/// +/// Halfword alignment is asserted first. The extraction mirrors byte loads: +/// shift the selected halfword into bits 63:48, then use arithmetic or logical +/// right shift to get signed or unsigned extension. pub(in crate::expand) fn expand_halfword_load( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, + instruction: &SourceInstructionRow, signed: bool, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_align( - InstructionKind::VirtualAssertHalfwordAlignment, - rs1(instruction)?, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + + // Halfword loads may start at byte offsets 0, 2, 4, or 6 within the + // containing doubleword. + asm.expand_address( + SourceInstructionKind::VirtualAssertHalfwordAlignment, + reg(rs1(instruction)?), instruction.operands.imm, - )?; - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v1, v1, 0)?; - asm.emit_i(InstructionKind::XORI, v0, v0, 6)?; - asm.emit_i(InstructionKind::SLLI, v0, v0, 3)?; - asm.emit_r(InstructionKind::SLL, v1, v1, v0)?; - asm.emit_i( + ); + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v1.operand(), v1.operand(), 0); + // Under the RV64 shift mask, ((address ^ 6) << 3) selects the aligned + // halfword lane and moves it to the high end. + asm.expand_i(SourceInstructionKind::XORI, v0.operand(), v0.operand(), 6); + asm.expand_i(SourceInstructionKind::SLLI, v0.operand(), v0.operand(), 3); + asm.expand_r( + SourceInstructionKind::SLL, + v1.operand(), + v1.operand(), + v0.operand(), + ); + asm.expand_i( if signed { - InstructionKind::SRAI + SourceInstructionKind::SRAI } else { - InstructionKind::SRLI + SourceInstructionKind::SRLI }, - rd(instruction)?, - v1, + reg(rd(instruction)?), + v1.operand(), 48, - )?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - Ok(sequence) + ); + asm.release_many([v0, v1]); + + asm.finalize() } +/// Lowers an advice load with byte length 1, 2, 4, or 8. +/// +/// `VirtualAdviceLoad` reads from the advice tape rather than RAM. Narrow +/// advice loads are signed loads, so the helper left-shifts the value into the +/// high bits and arithmetic-shifts it back to XLEN. pub(in crate::expand) fn expand_advice_load( - instruction: &NormalizedInstruction, + instruction: &SourceInstructionRow, byte_len: i128, - sign_extension_shift: Option, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_j( - InstructionKind::VirtualAdviceLoad, - rd(instruction)?, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + + asm.expand_j( + SourceInstructionKind::VirtualAdviceLoad(jolt_riscv::instructions::VirtualAdviceLoad(())), + reg(rd(instruction)?), byte_len, - )?; - if let Some(shift) = sign_extension_shift { - asm.emit_i( - InstructionKind::SLLI, - rd(instruction)?, - rd(instruction)?, + ); + if byte_len < 8 { + let shift = 64 - byte_len * 8; + asm.expand_i( + SourceInstructionKind::SLLI, + reg(rd(instruction)?), + reg(rd(instruction)?), shift, - )?; - asm.emit_i( - InstructionKind::SRAI, - rd(instruction)?, - rd(instruction)?, + ); + asm.expand_i( + SourceInstructionKind::SRAI, + reg(rd(instruction)?), + reg(rd(instruction)?), shift, - )?; + ); } + asm.finalize() } +/// Lowers arithmetic/bitwise doubleword AMOs using the shared read-modify-write shape. +/// +/// The old memory value is loaded, `op(old, rs2)` is stored back, and the old +/// value is copied to `rd`. Jolt traces are sequential, so the atomicity +/// contract reduces to preserving this single-instruction read/modify/write +/// order in the expanded bytecode. pub(in crate::expand) fn expand_amo_d( - instruction: &NormalizedInstruction, - op: InstructionKind, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rs2 = allocator.allocate()?; - let v_rd = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::LD, v_rd, rs1(instruction)?, 0)?; - asm.emit_r(op, v_rs2, v_rd, rs2(instruction)?)?; - asm.emit_s(InstructionKind::SD, rs1(instruction)?, v_rs2, 0)?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, v_rd, 0)?; - let sequence = asm.finalize()?; - allocator.release(v_rs2)?; - allocator.release(v_rd)?; - Ok(sequence) + instruction: &SourceInstructionRow, + op: SourceInstructionKind, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rs2 = asm.allocate()?; + let v_rd = asm.allocate()?; + + asm.expand_i( + SourceInstructionKind::LD, + v_rd.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.expand_r(op, v_rs2.operand(), v_rd.operand(), reg(rs2(instruction)?)); + asm.expand_s( + SourceInstructionKind::SD, + reg(rs1(instruction)?), + v_rs2.operand(), + 0, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + v_rd.operand(), + 0, + ); + asm.release_many([v_rs2, v_rd]); + + asm.finalize() } +/// Lowers signed or unsigned doubleword AMO min/max. +/// +/// `compare_op` decides whether `rs2` should replace the old memory value. +/// The update is computed as `old + take_rs2 * (rs2 - old)`, so the same +/// arithmetic shape handles min and max once the comparison operands are +/// ordered appropriately. pub(in crate::expand) fn expand_amo_minmax_d( - instruction: &NormalizedInstruction, - compare_op: InstructionKind, + instruction: &SourceInstructionRow, + compare_op: SourceInstructionKind, min: bool, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let v2 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::LD, v0, rs1(instruction)?, 0)?; - let (cmp_rs1, cmp_rs2) = if min { - (rs2(instruction)?, v0) +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + let v2 = asm.allocate()?; + let (cmp_rs1, cmp_rs2): (RegisterOperand, RegisterOperand) = if min { + (reg(rs2(instruction)?), v0.operand()) } else { - (v0, rs2(instruction)?) + (v0.operand(), reg(rs2(instruction)?)) }; - asm.emit_r(compare_op, v1, cmp_rs1, cmp_rs2)?; - asm.emit_r(InstructionKind::SUB, v2, rs2(instruction)?, v0)?; - asm.emit_r(InstructionKind::MUL, v2, v2, v1)?; - asm.emit_r(InstructionKind::ADD, v1, v0, v2)?; - asm.emit_s(InstructionKind::SD, rs1(instruction)?, v1, 0)?; - asm.emit_i(InstructionKind::ADDI, rd(instruction)?, v0, 0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - allocator.release(v2)?; - Ok(sequence) + + // v0 = old memory. v1 = whether rs2 should be stored. v2 = conditional + // delta from old memory to rs2. + asm.expand_i( + SourceInstructionKind::LD, + v0.operand(), + reg(rs1(instruction)?), + 0, + ); + asm.expand_r(compare_op, v1.operand(), cmp_rs1, cmp_rs2); + asm.expand_r( + SourceInstructionKind::SUB, + v2.operand(), + reg(rs2(instruction)?), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v2.operand(), + v2.operand(), + v1.operand(), + ); + asm.expand_r( + SourceInstructionKind::ADD, + v1.operand(), + v0.operand(), + v2.operand(), + ); + asm.expand_s( + SourceInstructionKind::SD, + reg(rs1(instruction)?), + v1.operand(), + 0, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + reg(rd(instruction)?), + v0.operand(), + 0, + ); + asm.release_many([v0, v1, v2]); + + asm.finalize() } +/// Lowers arithmetic/bitwise word AMOs by updating one word lane in a doubleword. +/// +/// The pre-helper extracts the old word from the containing doubleword. The +/// final operation is performed on that word, and the post-helper merges the +/// low word of the result back into the containing doubleword while returning +/// the old word sign-extended in `rd`. pub(in crate::expand) fn expand_amo_w( - instruction: &NormalizedInstruction, - op: InstructionKind, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rd = allocator.allocate()?; - let v_rs2 = allocator.allocate()?; - let v_mask = allocator.allocate()?; - let v_dword = allocator.allocate()?; - let v_shift = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - amo_pre64(&mut asm, rs1(instruction)?, v_rd, v_dword, v_shift)?; - asm.emit_r(op, v_rs2, v_rd, rs2(instruction)?)?; - amo_post64( + instruction: &SourceInstructionRow, + op: SourceInstructionKind, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rd = asm.allocate()?; + let v_rs2 = asm.allocate()?; + let v_mask = asm.allocate()?; + let v_dword = asm.allocate()?; + let v_shift = asm.allocate()?; + + expand_amo_pre64( &mut asm, - rs1(instruction)?, - v_rs2, - v_dword, - v_shift, - v_mask, - rd(instruction)?, - v_rd, + reg(rs1(instruction)?), + v_rd.operand(), + v_dword.operand(), + v_shift.operand(), )?; - let sequence = asm.finalize()?; - allocator.release(v_rd)?; - allocator.release(v_rs2)?; - allocator.release(v_mask)?; - allocator.release(v_dword)?; - allocator.release(v_shift)?; - Ok(sequence) + asm.expand_r(op, v_rs2.operand(), v_rd.operand(), reg(rs2(instruction)?)); + expand_amo_post64( + &mut asm, + AmoPost64 { + rs1: reg(rs1(instruction)?), + v_rs2: v_rs2.operand(), + v_dword: v_dword.operand(), + v_shift: v_shift.operand(), + v_mask: v_mask.operand(), + rd: reg(rd(instruction)?), + v_rd: v_rd.operand(), + }, + )?; + asm.release_many([v_rd, v_rs2, v_mask, v_dword, v_shift]); + + asm.finalize() } +/// Lowers signed or unsigned word AMO min/max. +/// +/// The old word and `rs2` are extended according to the comparison mode before +/// `compare_op` runs. The stored value is still the selected low word, merged +/// back into the containing doubleword by `expand_amo_post64`. pub(in crate::expand) fn expand_amo_minmax_w( - instruction: &NormalizedInstruction, - compare_op: InstructionKind, + instruction: &SourceInstructionRow, + compare_op: SourceInstructionKind, min: bool, signed: bool, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rd = allocator.allocate()?; - let v_dword = allocator.allocate()?; - let v_shift = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - amo_pre64(&mut asm, rs1(instruction)?, v_rd, v_dword, v_shift)?; +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rd = asm.allocate()?; + let v_dword = asm.allocate()?; + let v_shift = asm.allocate()?; + + expand_amo_pre64( + &mut asm, + reg(rs1(instruction)?), + v_rd.operand(), + v_dword.operand(), + v_shift.operand(), + )?; - let v_rs2 = asm.allocator().allocate()?; - let v0 = asm.allocator().allocate()?; + let v_rs2 = asm.allocate()?; + let v0 = asm.allocate()?; let extend_op = if signed { - InstructionKind::VirtualSignExtendWord + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ) } else { - InstructionKind::VirtualZeroExtendWord + SourceInstructionKind::VirtualZeroExtendWord( + jolt_riscv::instructions::VirtualZeroExtendWord(()), + ) }; - asm.emit_i(extend_op, v_rs2, rs2(instruction)?, 0)?; - asm.emit_i(extend_op, v0, v_rd, 0)?; - let (cmp_rs1, cmp_rs2) = if min { (v_rs2, v0) } else { (v0, v_rs2) }; - asm.emit_r(compare_op, v0, cmp_rs1, cmp_rs2)?; - asm.emit_r(InstructionKind::SUB, v_rs2, rs2(instruction)?, v_rd)?; - asm.emit_r(InstructionKind::MUL, v_rs2, v_rs2, v0)?; - asm.emit_r(InstructionKind::ADD, v_rs2, v_rs2, v_rd)?; - amo_post64( + // Compare normalized word values, but keep the original low-word payload + // for the value that will be merged back into memory. + asm.expand_i(extend_op, v_rs2.operand(), reg(rs2(instruction)?), 0); + asm.expand_i(extend_op, v0.operand(), v_rd.operand(), 0); + let (cmp_rs1, cmp_rs2) = if min { + (v_rs2.operand(), v0.operand()) + } else { + (v0.operand(), v_rs2.operand()) + }; + asm.expand_r(compare_op, v0.operand(), cmp_rs1, cmp_rs2); + asm.expand_r( + SourceInstructionKind::SUB, + v_rs2.operand(), + reg(rs2(instruction)?), + v_rd.operand(), + ); + asm.expand_r( + SourceInstructionKind::MUL, + v_rs2.operand(), + v_rs2.operand(), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::ADD, + v_rs2.operand(), + v_rs2.operand(), + v_rd.operand(), + ); + expand_amo_post64( &mut asm, - rs1(instruction)?, - v_rs2, - v_dword, - v_shift, - v0, - rd(instruction)?, - v_rd, + AmoPost64 { + rs1: reg(rs1(instruction)?), + v_rs2: v_rs2.operand(), + v_dword: v_dword.operand(), + v_shift: v_shift.operand(), + v_mask: v0.operand(), + rd: reg(rd(instruction)?), + v_rd: v_rd.operand(), + }, )?; - let sequence = asm.finalize()?; - allocator.release(v_rd)?; - allocator.release(v_dword)?; - allocator.release(v_shift)?; - allocator.release(v_rs2)?; - allocator.release(v0)?; - Ok(sequence) + asm.release_many([v_rd, v_dword, v_shift, v_rs2, v0]); + + asm.finalize() } -pub(in crate::expand) fn amo_pre64( - asm: &mut assembler::InstrAssembler<'_>, - rs1: u8, - v_rd: u8, - v_dword: u8, - v_shift: u8, +/// Reads the containing doubleword and extracts the selected word for word AMOs. +/// +/// `rs1` must be word-aligned. `v_dword` receives the containing aligned +/// doubleword, `v_shift` receives the byte offset times eight, and `v_rd` +/// receives the selected old word in its low 32 bits. +pub(in crate::expand) fn expand_amo_pre64( + asm: &mut ExpansionBuilder, + rs1: RegisterOperand, + v_rd: RegisterOperand, + v_dword: RegisterOperand, + v_shift: RegisterOperand, ) -> Result<(), ExpansionError> { - asm.emit_align(InstructionKind::VirtualAssertWordAlignment, rs1, 0)?; - asm.emit_i(InstructionKind::ANDI, v_shift, rs1, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v_dword, v_shift, 0)?; - asm.emit_i(InstructionKind::SLLI, v_shift, rs1, 3)?; - asm.emit_r(InstructionKind::SRL, v_rd, v_dword, v_shift)?; + asm.expand_address(SourceInstructionKind::VirtualAssertWordAlignment, rs1, 0); + asm.expand_i(SourceInstructionKind::ANDI, v_shift, rs1, format_i_imm(-8)); + asm.expand_i(SourceInstructionKind::LD, v_dword, v_shift, 0); + asm.expand_i(SourceInstructionKind::SLLI, v_shift, rs1, 3); + asm.expand_r(SourceInstructionKind::SRL, v_rd, v_dword, v_shift); Ok(()) } -#[expect(clippy::too_many_arguments)] -pub(in crate::expand) fn amo_post64( - asm: &mut assembler::InstrAssembler<'_>, - rs1: u8, - v_rs2: u8, - v_dword: u8, - v_shift: u8, - v_mask: u8, - rd: u8, - v_rd: u8, +/// Register bundle consumed by `expand_amo_post64`. +/// +/// The post-helper needs both the containing doubleword state and the selected +/// lane metadata from `expand_amo_pre64`, plus the new word value and the +/// architectural destination for the old word. +pub(in crate::expand) struct AmoPost64 { + pub(in crate::expand) rs1: RegisterOperand, + pub(in crate::expand) v_rs2: RegisterOperand, + pub(in crate::expand) v_dword: RegisterOperand, + pub(in crate::expand) v_shift: RegisterOperand, + pub(in crate::expand) v_mask: RegisterOperand, + pub(in crate::expand) rd: RegisterOperand, + pub(in crate::expand) v_rd: RegisterOperand, +} + +/// Merges a word-AMO result into its containing doubleword and returns old word. +/// +/// `v_rs2` is shifted into the selected lane, XORed with the old doubleword, +/// masked to that lane, and XORed back. This updates only the selected 32 bits +/// before storing the containing doubleword and sign-extending the old word to +/// `rd`. +pub(in crate::expand) fn expand_amo_post64( + asm: &mut ExpansionBuilder, + registers: AmoPost64, ) -> Result<(), ExpansionError> { - asm.emit_i(InstructionKind::ORI, v_mask, 0, format_i_imm(-1))?; - asm.emit_i(InstructionKind::SRLI, v_mask, v_mask, 32)?; - asm.emit_r(InstructionKind::SLL, v_mask, v_mask, v_shift)?; - asm.emit_r(InstructionKind::SLL, v_shift, v_rs2, v_shift)?; - asm.emit_r(InstructionKind::XOR, v_shift, v_dword, v_shift)?; - asm.emit_r(InstructionKind::AND, v_shift, v_shift, v_mask)?; - asm.emit_r(InstructionKind::XOR, v_dword, v_dword, v_shift)?; - asm.emit_i(InstructionKind::ANDI, v_mask, rs1, format_i_imm(-8))?; - asm.emit_s(InstructionKind::SD, v_mask, v_dword, 0)?; - asm.emit_i(InstructionKind::VirtualSignExtendWord, rd, v_rd, 0)?; + let AmoPost64 { + rs1, + v_rs2, + v_dword, + v_shift, + v_mask, + rd, + v_rd, + } = registers; + + // Build a 32-bit lane mask, shift the new word into place, and use + // masked-XOR replacement: new_dword = old ^ ((old ^ new) & mask). + asm.expand_i(SourceInstructionKind::ORI, v_mask, reg(0), format_i_imm(-1)); + asm.expand_i(SourceInstructionKind::SRLI, v_mask, v_mask, 32); + asm.expand_r(SourceInstructionKind::SLL, v_mask, v_mask, v_shift); + asm.expand_r(SourceInstructionKind::SLL, v_shift, v_rs2, v_shift); + asm.expand_r(SourceInstructionKind::XOR, v_shift, v_dword, v_shift); + asm.expand_r(SourceInstructionKind::AND, v_shift, v_shift, v_mask); + asm.expand_r(SourceInstructionKind::XOR, v_dword, v_dword, v_shift); + asm.expand_i(SourceInstructionKind::ANDI, v_mask, rs1, format_i_imm(-8)); + asm.expand_s(SourceInstructionKind::SD, v_mask, v_dword, 0); + asm.expand_i( + SourceInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + rd, + v_rd, + 0, + ); Ok(()) } +/// Lowers `SB`/`SH` by replacing a narrow lane inside an aligned doubleword. +/// +/// The helper optionally emits the source instruction's alignment assertion, +/// loads the containing doubleword, builds a byte/halfword mask shifted to the +/// selected lane, merges the low bits of `rs2`, and writes the whole +/// doubleword back. pub(in crate::expand) fn expand_narrow_store( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, + instruction: &SourceInstructionRow, mask: i128, - alignment: Option, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let v2 = allocator.allocate()?; - let v3 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + alignment: Option, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + let v2 = asm.allocate()?; + let v3 = asm.allocate()?; + if let Some(alignment) = alignment { - asm.emit_align(alignment, rs1(instruction)?, instruction.operands.imm)?; + // `SH` requires halfword alignment; `SB` passes `None`. + asm.expand_address(alignment, reg(rs1(instruction)?), instruction.operands.imm); } - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v2, v1, 0)?; - asm.emit_i(InstructionKind::SLLI, v3, v0, 3)?; - asm.emit_u(InstructionKind::LUI, v0, mask)?; - asm.emit_r(InstructionKind::SLL, v0, v0, v3)?; - asm.emit_r(InstructionKind::SLL, v3, rs2(instruction)?, v3)?; - asm.emit_r(InstructionKind::XOR, v3, v2, v3)?; - asm.emit_r(InstructionKind::AND, v3, v3, v0)?; - asm.emit_r(InstructionKind::XOR, v2, v2, v3)?; - asm.emit_s(InstructionKind::SD, v1, v2, 0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - allocator.release(v2)?; - allocator.release(v3)?; - Ok(sequence) + ); + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v2.operand(), v1.operand(), 0); + asm.expand_i(SourceInstructionKind::SLLI, v3.operand(), v0.operand(), 3); + asm.expand_u(SourceInstructionKind::LUI, v0.operand(), mask); + // As in the word-store and AMO paths, masked-XOR replacement updates only + // the selected narrow lane. + asm.expand_r( + SourceInstructionKind::SLL, + v0.operand(), + v0.operand(), + v3.operand(), + ); + asm.expand_r( + SourceInstructionKind::SLL, + v3.operand(), + reg(rs2(instruction)?), + v3.operand(), + ); + asm.expand_r( + SourceInstructionKind::XOR, + v3.operand(), + v2.operand(), + v3.operand(), + ); + asm.expand_r( + SourceInstructionKind::AND, + v3.operand(), + v3.operand(), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::XOR, + v2.operand(), + v2.operand(), + v3.operand(), + ); + asm.expand_s(SourceInstructionKind::SD, v1.operand(), v2.operand(), 0); + asm.release_many([v0, v1, v2, v3]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/memory/sw.rs b/crates/jolt-program/src/expand/memory/sw.rs index 3dffceec01..7b26dcf44d 100644 --- a/crates/jolt-program/src/expand/memory/sw.rs +++ b/crates/jolt-program/src/expand/memory/sw.rs @@ -1,41 +1,81 @@ use super::*; +/// Lowers word store `SW` by updating the selected lane of an aligned doubleword. +/// +/// The sequence proves word alignment, reads the containing doubleword, builds +/// a 32-bit lane mask, merges the low word of `rs2` into that lane, and writes +/// the whole doubleword back with `SD`. pub(in crate::expand) fn expand_sw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v0 = allocator.allocate()?; - let v1 = allocator.allocate()?; - let v2 = allocator.allocate()?; - let v3 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_align( - InstructionKind::VirtualAssertWordAlignment, - rs1(instruction)?, + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v0 = asm.allocate()?; + let v1 = asm.allocate()?; + let v2 = asm.allocate()?; + let v3 = asm.allocate()?; + + // Source `SW` requires word alignment even though the synthesized write is + // a doubleword write to the containing aligned address. + asm.expand_address( + SourceInstructionKind::VirtualAssertWordAlignment, + reg(rs1(instruction)?), instruction.operands.imm, - )?; - asm.emit_i( - InstructionKind::ADDI, - v0, - rs1(instruction)?, + ); + asm.expand_i( + SourceInstructionKind::ADDI, + v0.operand(), + reg(rs1(instruction)?), format_i_imm(instruction.operands.imm), - )?; - asm.emit_i(InstructionKind::ANDI, v1, v0, format_i_imm(-8))?; - asm.emit_i(InstructionKind::LD, v2, v1, 0)?; - asm.emit_i(InstructionKind::SLLI, v0, v0, 3)?; - asm.emit_i(InstructionKind::ORI, v3, 0, format_i_imm(-1))?; - asm.emit_i(InstructionKind::SRLI, v3, v3, 32)?; - asm.emit_r(InstructionKind::SLL, v3, v3, v0)?; - asm.emit_r(InstructionKind::SLL, v0, rs2(instruction)?, v0)?; - asm.emit_r(InstructionKind::XOR, v0, v2, v0)?; - asm.emit_r(InstructionKind::AND, v0, v0, v3)?; - asm.emit_r(InstructionKind::XOR, v2, v2, v0)?; - asm.emit_s(InstructionKind::SD, v1, v2, 0)?; - let sequence = asm.finalize()?; - allocator.release(v0)?; - allocator.release(v1)?; - allocator.release(v2)?; - allocator.release(v3)?; - Ok(sequence) + ); + asm.expand_i( + SourceInstructionKind::ANDI, + v1.operand(), + v0.operand(), + format_i_imm(-8), + ); + asm.expand_i(SourceInstructionKind::LD, v2.operand(), v1.operand(), 0); + asm.expand_i(SourceInstructionKind::SLLI, v0.operand(), v0.operand(), 3); + // v3 becomes a 32-bit lane mask shifted into place; v0 then carries the + // shifted source word and finally the masked XOR delta. + asm.expand_i( + SourceInstructionKind::ORI, + v3.operand(), + reg(0), + format_i_imm(-1), + ); + asm.expand_i(SourceInstructionKind::SRLI, v3.operand(), v3.operand(), 32); + asm.expand_r( + SourceInstructionKind::SLL, + v3.operand(), + v3.operand(), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::SLL, + v0.operand(), + reg(rs2(instruction)?), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::XOR, + v0.operand(), + v2.operand(), + v0.operand(), + ); + asm.expand_r( + SourceInstructionKind::AND, + v0.operand(), + v0.operand(), + v3.operand(), + ); + asm.expand_r( + SourceInstructionKind::XOR, + v2.operand(), + v2.operand(), + v0.operand(), + ); + asm.expand_s(SourceInstructionKind::SD, v1.operand(), v2.operand(), 0); + asm.release_many([v0, v1, v2, v3]); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/metadata.rs b/crates/jolt-program/src/expand/metadata.rs new file mode 100644 index 0000000000..f7da18414b --- /dev/null +++ b/crates/jolt-program/src/expand/metadata.rs @@ -0,0 +1,94 @@ +use jolt_riscv::{JoltInstructionProfile, JoltInstructionRow}; + +use crate::expand::{materialize::MAX_FINAL_ROWS_PER_SOURCE, ExpansionError}; + +const MAX_METADATA_SEQUENCE_ROWS: usize = u16::MAX as usize + 1; + +/// Stamps position metadata (`is_first_in_sequence`, `virtual_sequence_remaining`) on recipe output. +pub(super) fn stamp_instruction_sequence( + rows: Vec, + is_compressed: bool, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + stamp_sequence_metadata(rows, is_compressed, MAX_FINAL_ROWS_PER_SOURCE, profile) +} + +/// Same as `stamp_instruction_sequence` but for inline provider output (higher capacity limit). +pub(super) fn stamp_inline_sequence( + rows: Vec, + is_compressed: bool, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + stamp_sequence_metadata(rows, is_compressed, MAX_METADATA_SEQUENCE_ROWS, profile) +} + +fn stamp_sequence_metadata( + mut rows: Vec, + is_compressed: bool, + capacity: usize, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + if rows.is_empty() { + return Err(ExpansionError::EmptySequence); + } + if rows.len() > capacity { + return Err(ExpansionError::CapacityExceeded { + actual: rows.len(), + capacity, + }); + } + for row in &rows { + if !profile.supports_jolt(row.instruction_kind) { + return Err(ExpansionError::IllegalTargetInstruction( + row.instruction_kind, + )); + } + } + + let len = rows.len(); + for (index, row) in rows.iter_mut().enumerate() { + row.is_first_in_sequence = index == 0; + row.virtual_sequence_remaining = Some((len - index - 1) as u16); + row.is_compressed = index == len - 1 && is_compressed; + } + Ok(rows) +} + +#[cfg(test)] +mod tests { + use jolt_riscv::{ + JoltInstructionKind, JoltInstructionProfile, JoltInstructionRow, NormalizedOperands, + SourceExtension, + }; + + use super::*; + + #[test] + fn rejects_profile_illegal_rows_before_stamping() { + const RV64I_ONLY: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[SourceExtension::Rv64I], + inline_extensions: &[], + }; + + let rows = vec![JoltInstructionRow { + instruction_kind: JoltInstructionKind::MUL, + address: 0x8000_0000, + operands: NormalizedOperands { + rd: Some(1), + rs1: Some(2), + rs2: None, + imm: 3, + }, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + }]; + + assert!(matches!( + stamp_instruction_sequence(rows, false, RV64I_ONLY), + Err(ExpansionError::IllegalTargetInstruction( + JoltInstructionKind::MUL + )) + )); + } +} diff --git a/crates/jolt-program/src/expand/mod.rs b/crates/jolt-program/src/expand/mod.rs index f6cfeca118..8fc4e5d262 100644 --- a/crates/jolt-program/src/expand/mod.rs +++ b/crates/jolt-program/src/expand/mod.rs @@ -1,16 +1,25 @@ //! RV64 bytecode expansion from decoded source rows into final Jolt bytecode. //! +//! The pipeline has two phases: +//! 1. **Recipe building** (`grammar.rs`): each source-only instruction maps to a +//! symbolic recipe — a sequence of `ExpansionOp`s referencing `TempId` placeholders. +//! 2. **Materialization** (`materialize.rs`): recipes are executed by binding temps to +//! physical virtual registers, emitting concrete rows, and recursing for nested +//! expansions. +//! //! Expansion intentionally has no `Xlen` parameter: the `jolt-program` pipeline -//! only supports RV64. RV32/ELF32 inputs should be rejected before this module is +//! only supports RV64. RV32/ELF32 inputs are rejected before this module is //! called. pub mod allocator; mod arithmetic; -pub mod assembler; mod control_flow; mod division; pub mod error; +mod grammar; +mod materialize; mod memory; +mod metadata; mod operands; mod shifts; #[cfg(test)] @@ -19,20 +28,36 @@ mod tests; pub use allocator::ExpansionAllocator; pub use error::ExpansionError; +use allocator::{ + mcause_register, mepc_register, mstatus_register, mtval_register, reservation_d_register, + reservation_w_register, trap_handler_register, virtual_register_for_csr, +}; use arithmetic::*; use control_flow::*; use division::*; -use jolt_riscv::{InstructionKind, NormalizedInstruction, NormalizedOperands}; +use grammar::{reg, ExpandedInstructionSequence, ExpansionBuilder, RegisterOperand, TempId}; +use jolt_riscv::{ + JoltInstruction, JoltInstructionKind, JoltInstructionProfile, JoltInstructionRow, + NormalizedOperands, SourceInstruction, SourceInstructionKind, SourceInstructionRow, +}; +use materialize::ExpansionState; use memory::*; +use metadata::stamp_inline_sequence; use operands::*; use shifts::*; pub trait InlineExpansionProvider { + /// Expands a registered inline row into final Jolt instructions. + /// + /// Provider output intentionally stays outside the provider-free builder + /// core. The top-level entry point remaps `rd = x0` before calling this + /// hook, then validates target legality and stamps sequence metadata. fn expand_inline( &mut self, - instruction: &NormalizedInstruction, + instruction: &SourceInstruction, allocator: &mut ExpansionAllocator, - ) -> Result, ExpansionError>; + profile: JoltInstructionProfile, + ) -> Result, ExpansionError>; } #[derive(Debug, Default)] @@ -41,128 +66,234 @@ pub struct NoInlineExpansionProvider; impl InlineExpansionProvider for NoInlineExpansionProvider { fn expand_inline( &mut self, - _instruction: &NormalizedInstruction, + _instruction: &SourceInstruction, _allocator: &mut ExpansionAllocator, - ) -> Result, ExpansionError> { + _profile: JoltInstructionProfile, + ) -> Result, ExpansionError> { Err(ExpansionError::InlineProviderRequired) } } pub fn expand_instruction( - instruction: &NormalizedInstruction, + instruction: &SourceInstruction, allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - expand_instruction_with_provider(instruction, allocator, &mut NoInlineExpansionProvider) + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + expand_instruction_with_provider( + instruction, + allocator, + &mut NoInlineExpansionProvider, + profile, + ) } pub fn expand_instruction_with_provider( - instruction: &NormalizedInstruction, + instruction: &SourceInstruction, allocator: &mut ExpansionAllocator, inline_provider: &mut P, -) -> Result, ExpansionError> { - if instruction.operands.rd == Some(0) - && !handles_rd_zero_internally(instruction.instruction_kind) + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + expand_source_instruction_with_provider(instruction, allocator, inline_provider, profile) +} + +fn expand_source_instruction_with_provider( + instruction: &SourceInstruction, + allocator: &mut ExpansionAllocator, + inline_provider: &mut P, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + let rewritten_source; + let mut allocated_rd_zero_register = None; + let instruction = if instruction.row().operands.rd == Some(0) + && !handles_rd_zero_internally(instruction.kind()) { - if instruction.instruction_kind.has_side_effects() { + if instruction.kind().has_side_effects() { let virtual_register = allocator.allocate()?; - let mut rewritten = *instruction; - rewritten.operands.rd = Some(virtual_register); - let expanded = expand_instruction_with_provider(&rewritten, allocator, inline_provider); - allocator.release(virtual_register)?; - return expanded; + allocated_rd_zero_register = Some(virtual_register); + rewritten_source = (*instruction).map_row(|mut row| { + row.operands.rd = Some(virtual_register); + row + }); + &rewritten_source + } else { + return final_rows_to_instructions(vec![noop_for(*instruction.row())], profile); } - return Ok(vec![noop_for(*instruction)]); + } else { + instruction + }; + let source = *instruction.row(); + + let result = if instruction.kind() == SourceInstructionKind::Inline { + inline_provider + .expand_inline(instruction, allocator, profile) + .and_then(|instructions| { + finalize_inline_provider_instructions(source, allocator, instructions, profile) + }) + } else { + let owned_allocator = std::mem::take(allocator); + let mut state = ExpansionState::new(owned_allocator, profile); + let result = state + .expand_source_recursive(instruction) + .and_then(|rows| final_rows_to_instructions(rows, profile)); + *allocator = state.into_allocator(); + result + }; + if let Some(register) = allocated_rd_zero_register { + allocator.release(register)?; } + result +} + +fn finalize_inline_provider_instructions( + source: SourceInstructionRow, + allocator: &mut ExpansionAllocator, + instructions: Vec, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + let mut rows = instructions + .into_iter() + .map(JoltInstructionRow::from) + .collect::>(); + for register in allocator.take_registers_for_reset()? { + rows.push(JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, + address: source.address, + operands: NormalizedOperands { + rd: Some(register), + rs1: Some(0), + rs2: None, + imm: 0, + }, + virtual_sequence_remaining: Some(0), + is_first_in_sequence: false, + is_compressed: false, + }); + } + final_rows_to_instructions( + stamp_inline_sequence(rows, source.is_compressed, profile)?, + profile, + ) +} + +fn final_rows_to_instructions( + rows: Vec, + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + rows.into_iter() + .map(|row| { + if !profile.supports_jolt(row.instruction_kind) { + return Err(ExpansionError::IllegalTargetInstruction( + row.instruction_kind, + )); + } + JoltInstruction::try_from(row).map_err(ExpansionError::IllegalTargetInstruction) + }) + .collect() +} - match instruction.instruction_kind { - InstructionKind::Inline => inline_provider.expand_inline(instruction, allocator), - InstructionKind::ADDIW => expand_addiw(instruction, allocator), - InstructionKind::ADDW => expand_addw(instruction, allocator), - InstructionKind::SUBW => expand_subw(instruction, allocator), - InstructionKind::MULH => expand_mulh(instruction, allocator), - InstructionKind::MULHSU => expand_mulhsu(instruction, allocator), - InstructionKind::MULW => expand_mulw(instruction, allocator), - InstructionKind::LB => expand_lb(instruction, allocator), - InstructionKind::LBU => expand_lbu(instruction, allocator), - InstructionKind::LH => expand_lh(instruction, allocator), - InstructionKind::LHU => expand_lhu(instruction, allocator), - InstructionKind::LW => expand_lw(instruction, allocator), - InstructionKind::LWU => expand_lwu(instruction, allocator), - InstructionKind::AdviceLB => expand_advice_lb(instruction, allocator), - InstructionKind::AdviceLH => expand_advice_lh(instruction, allocator), - InstructionKind::AdviceLW => expand_advice_lw(instruction, allocator), - InstructionKind::AdviceLD => expand_advice_ld(instruction, allocator), - InstructionKind::AMOADDD => expand_amoaddd(instruction, allocator), - InstructionKind::AMOANDD => expand_amoandd(instruction, allocator), - InstructionKind::AMOORD => expand_amoord(instruction, allocator), - InstructionKind::AMOXORD => expand_amoxord(instruction, allocator), - InstructionKind::AMOSWAPD => expand_amoswapd(instruction, allocator), - InstructionKind::AMOMAXD => expand_amomaxd(instruction, allocator), - InstructionKind::AMOMAXUD => expand_amomaxud(instruction, allocator), - InstructionKind::AMOMIND => expand_amomind(instruction, allocator), - InstructionKind::AMOMINUD => expand_amominud(instruction, allocator), - InstructionKind::AMOADDW => expand_amoaddw(instruction, allocator), - InstructionKind::AMOANDW => expand_amoandw(instruction, allocator), - InstructionKind::AMOORW => expand_amoorw(instruction, allocator), - InstructionKind::AMOXORW => expand_amoxorw(instruction, allocator), - InstructionKind::AMOSWAPW => expand_amoswapw(instruction, allocator), - InstructionKind::AMOMAXW => expand_amomaxw(instruction, allocator), - InstructionKind::AMOMAXUW => expand_amomaxuw(instruction, allocator), - InstructionKind::AMOMINW => expand_amominw(instruction, allocator), - InstructionKind::AMOMINUW => expand_amominuw(instruction, allocator), - InstructionKind::LRD => expand_lrd(instruction, allocator), - InstructionKind::LRW => expand_lrw(instruction, allocator), - InstructionKind::DIV => expand_div(instruction, allocator), - InstructionKind::DIVU => expand_divu(instruction, allocator), - InstructionKind::DIVW => expand_divw(instruction, allocator), - InstructionKind::DIVUW => expand_divuw(instruction, allocator), - InstructionKind::REM => expand_rem(instruction, allocator), - InstructionKind::REMU => expand_remu(instruction, allocator), - InstructionKind::REMW => expand_remw(instruction, allocator), - InstructionKind::REMUW => expand_remuw(instruction, allocator), - InstructionKind::SB => expand_sb(instruction, allocator), - InstructionKind::SCD => expand_scd(instruction, allocator), - InstructionKind::SCW => expand_scw(instruction, allocator), - InstructionKind::SH => expand_sh(instruction, allocator), - InstructionKind::SW => expand_sw(instruction, allocator), - InstructionKind::CSRRW => expand_csrrw(instruction, allocator), - InstructionKind::CSRRS => expand_csrrs(instruction, allocator), - InstructionKind::EBREAK => expand_ebreak(instruction, allocator), - InstructionKind::ECALL => expand_ecall(instruction, allocator), - InstructionKind::MRET => expand_mret(instruction, allocator), - InstructionKind::SLL => expand_sll(instruction, allocator), - InstructionKind::SLLI => expand_slli(instruction, allocator), - InstructionKind::SLLW => expand_sllw(instruction, allocator), - InstructionKind::SLLIW => expand_slliw(instruction, allocator), - InstructionKind::SRL => expand_srl(instruction, allocator), - InstructionKind::SRLI => expand_srli(instruction, allocator), - InstructionKind::SRA => expand_sra(instruction, allocator), - InstructionKind::SRAI => expand_srai(instruction, allocator), - InstructionKind::SRLIW => expand_srliw(instruction, allocator), - InstructionKind::SRAIW => expand_sraiw(instruction, allocator), - InstructionKind::SRLW => expand_srlw(instruction, allocator), - InstructionKind::SRAW => expand_sraw(instruction, allocator), - _ => Ok(vec![*instruction]), +/// Dispatches one source-only instruction to the recipe that explains its +/// final bytecode semantics. +/// +/// Each callee returns a symbolic sequence, not concrete rows. During +/// materialization, `emit_*` rows become final bytecode directly while +/// `expand_*` rows are routed back through this dispatcher. That recursive +/// route is intentional: common substeps such as narrow loads, word shifts, and +/// virtual assertions keep one definition of their own lowering contract. +fn expand_source_only_instruction( + instruction: &SourceInstruction, +) -> Result { + let row = instruction.row(); + match instruction.kind() { + SourceInstructionKind::ADDIW => expand_addiw(row), + SourceInstructionKind::ADDW => expand_addw(row), + SourceInstructionKind::SUBW => expand_subw(row), + SourceInstructionKind::MULH => expand_mulh(row), + SourceInstructionKind::MULHSU => expand_mulhsu(row), + SourceInstructionKind::MULW => expand_mulw(row), + SourceInstructionKind::LB => expand_lb(row), + SourceInstructionKind::LBU => expand_lbu(row), + SourceInstructionKind::LH => expand_lh(row), + SourceInstructionKind::LHU => expand_lhu(row), + SourceInstructionKind::LW => expand_lw(row), + SourceInstructionKind::LWU => expand_lwu(row), + SourceInstructionKind::AdviceLB => expand_advice_lb(row), + SourceInstructionKind::AdviceLH => expand_advice_lh(row), + SourceInstructionKind::AdviceLW => expand_advice_lw(row), + SourceInstructionKind::AdviceLD => expand_advice_ld(row), + SourceInstructionKind::AMOADDD => expand_amoaddd(row), + SourceInstructionKind::AMOANDD => expand_amoandd(row), + SourceInstructionKind::AMOORD => expand_amoord(row), + SourceInstructionKind::AMOXORD => expand_amoxord(row), + SourceInstructionKind::AMOSWAPD => expand_amoswapd(row), + SourceInstructionKind::AMOMAXD => expand_amomaxd(row), + SourceInstructionKind::AMOMAXUD => expand_amomaxud(row), + SourceInstructionKind::AMOMIND => expand_amomind(row), + SourceInstructionKind::AMOMINUD => expand_amominud(row), + SourceInstructionKind::AMOADDW => expand_amoaddw(row), + SourceInstructionKind::AMOANDW => expand_amoandw(row), + SourceInstructionKind::AMOORW => expand_amoorw(row), + SourceInstructionKind::AMOXORW => expand_amoxorw(row), + SourceInstructionKind::AMOSWAPW => expand_amoswapw(row), + SourceInstructionKind::AMOMAXW => expand_amomaxw(row), + SourceInstructionKind::AMOMAXUW => expand_amomaxuw(row), + SourceInstructionKind::AMOMINW => expand_amominw(row), + SourceInstructionKind::AMOMINUW => expand_amominuw(row), + SourceInstructionKind::LRD => expand_lrd(row), + SourceInstructionKind::LRW => expand_lrw(row), + SourceInstructionKind::DIV => expand_div(row), + SourceInstructionKind::DIVU => expand_divu(row), + SourceInstructionKind::DIVW => expand_divw(row), + SourceInstructionKind::DIVUW => expand_divuw(row), + SourceInstructionKind::REM => expand_rem(row), + SourceInstructionKind::REMU => expand_remu(row), + SourceInstructionKind::REMW => expand_remw(row), + SourceInstructionKind::REMUW => expand_remuw(row), + SourceInstructionKind::SB => expand_sb(row), + SourceInstructionKind::SCD => expand_scd(row), + SourceInstructionKind::SCW => expand_scw(row), + SourceInstructionKind::SH => expand_sh(row), + SourceInstructionKind::SW => expand_sw(row), + SourceInstructionKind::CSRRW => expand_csrrw(row), + SourceInstructionKind::CSRRS => expand_csrrs(row), + SourceInstructionKind::EBREAK => expand_ebreak(row), + SourceInstructionKind::ECALL => expand_ecall(row), + SourceInstructionKind::MRET => expand_mret(row), + SourceInstructionKind::SLL => expand_sll(row), + SourceInstructionKind::SLLI => expand_slli(row), + SourceInstructionKind::SLLW => expand_sllw(row), + SourceInstructionKind::SLLIW => expand_slliw(row), + SourceInstructionKind::SRL => expand_srl(row), + SourceInstructionKind::SRLI => expand_srli(row), + SourceInstructionKind::SRA => expand_sra(row), + SourceInstructionKind::SRAI => expand_srai(row), + SourceInstructionKind::SRLIW => expand_srliw(row), + SourceInstructionKind::SRAIW => expand_sraiw(row), + SourceInstructionKind::SRLW => expand_srlw(row), + SourceInstructionKind::SRAW => expand_sraw(row), + _ => Err(ExpansionError::UnsupportedInstruction), } } pub fn expand_program( - instructions: impl IntoIterator, -) -> Result, ExpansionError> { - expand_program_with_provider(instructions, &mut NoInlineExpansionProvider) + instructions: &[SourceInstruction], + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { + expand_program_with_provider(instructions, &mut NoInlineExpansionProvider, profile) } pub fn expand_program_with_provider( - instructions: impl IntoIterator, + instructions: &[SourceInstruction], inline_provider: &mut P, -) -> Result, ExpansionError> { + profile: JoltInstructionProfile, +) -> Result, ExpansionError> { let mut allocator = ExpansionAllocator::new(); let mut expanded = Vec::new(); for instruction in instructions { - expanded.extend(expand_instruction_with_provider( - &instruction, + expanded.extend(expand_source_instruction_with_provider( + instruction, &mut allocator, inline_provider, + profile, )?); } Ok(expanded) diff --git a/crates/jolt-program/src/expand/operands.rs b/crates/jolt-program/src/expand/operands.rs index 0b9f6ab5d4..efe1fadc88 100644 --- a/crates/jolt-program/src/expand/operands.rs +++ b/crates/jolt-program/src/expand/operands.rs @@ -1,9 +1,10 @@ use super::*; -pub(super) fn noop_for(instruction: NormalizedInstruction) -> NormalizedInstruction { +/// Replaces a side-effect-free rd=x0 instruction with `ADDI x0, x0, 0`. +pub(super) fn noop_for(instruction: SourceInstructionRow) -> JoltInstructionRow { debug_assert_eq!(instruction.operands.rd, Some(0)); - NormalizedInstruction { - instruction_kind: InstructionKind::ADDI, + JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, address: instruction.address, operands: NormalizedOperands { rd: Some(0), @@ -17,21 +18,21 @@ pub(super) fn noop_for(instruction: NormalizedInstruction) -> NormalizedInstruct } } -pub(super) fn rd(instruction: &NormalizedInstruction) -> Result { +pub(super) fn rd(instruction: &SourceInstructionRow) -> Result { instruction .operands .rd .ok_or(ExpansionError::MalformedInstruction("missing rd")) } -pub(super) fn rs1(instruction: &NormalizedInstruction) -> Result { +pub(super) fn rs1(instruction: &SourceInstructionRow) -> Result { instruction .operands .rs1 .ok_or(ExpansionError::MalformedInstruction("missing rs1")) } -pub(super) fn rs2(instruction: &NormalizedInstruction) -> Result { +pub(super) fn rs2(instruction: &SourceInstructionRow) -> Result { instruction .operands .rs2 @@ -42,17 +43,18 @@ pub(super) fn format_i_imm(imm: i128) -> i128 { (imm as i64 as u64) as i128 } -pub(super) fn csr_address(instruction: &NormalizedInstruction) -> u16 { +pub(super) fn csr_address(instruction: &SourceInstructionRow) -> u16 { (instruction.operands.imm & 0xfff) as u16 } -pub(super) const fn handles_rd_zero_internally(instruction_kind: InstructionKind) -> bool { +/// Instructions whose expansion recipes handle rd=x0 themselves (trap, CSR). +pub(super) const fn handles_rd_zero_internally(instruction_kind: SourceInstructionKind) -> bool { matches!( instruction_kind, - InstructionKind::ECALL - | InstructionKind::MRET - | InstructionKind::EBREAK - | InstructionKind::CSRRW - | InstructionKind::CSRRS + SourceInstructionKind::ECALL + | SourceInstructionKind::MRET + | SourceInstructionKind::EBREAK + | SourceInstructionKind::CSRRW + | SourceInstructionKind::CSRRS ) } diff --git a/crates/jolt-program/src/expand/shifts/shared.rs b/crates/jolt-program/src/expand/shifts/shared.rs index 0a6a57a78f..a924953238 100644 --- a/crates/jolt-program/src/expand/shifts/shared.rs +++ b/crates/jolt-program/src/expand/shifts/shared.rs @@ -1,3 +1,9 @@ +/// Returns the mask-shaped immediate expected by the virtual right-shift rows. +/// +/// The virtual shift instructions recover the actual shift amount from the +/// immediate's trailing-zero count. Keeping the upper `len - shift` bits set +/// gives the lookup table the same mask value used for range/correctness +/// checks while still encoding the immediate shift compactly. pub(in crate::expand) fn right_shift_bitmask(shift: u32, len: u32) -> u64 { let ones = (1u128 << (len - shift)) - 1; (ones << shift) as u64 diff --git a/crates/jolt-program/src/expand/shifts/sll.rs b/crates/jolt-program/src/expand/shifts/sll.rs index 79af3bd30a..a3190f1939 100644 --- a/crates/jolt-program/src/expand/shifts/sll.rs +++ b/crates/jolt-program/src/expand/shifts/sll.rs @@ -1,20 +1,29 @@ use super::*; +/// Lowers variable `SLL` to a proved power-of-two lookup followed by `MUL`. +/// +/// `VirtualPow2` applies the RV64 shift-mask rule to `rs2` and returns +/// `2^(rs2 & 0x3f)`. Multiplying by that value is equivalent to a left shift +/// modulo 2^64, which is the architectural result. pub(in crate::expand) fn expand_sll( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_pow2 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::VirtualPow2, v_pow2, rs2(instruction)?, 0)?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_pow2 = asm.allocate()?; + + asm.expand_i( + SourceInstructionKind::VirtualPow2, + v_pow2.operand(), + reg(rs2(instruction)?), + 0, + ); asm.emit_r( - InstructionKind::MUL, - rd(instruction)?, - rs1(instruction)?, - v_pow2, - )?; - let sequence = asm.finalize()?; - allocator.release(v_pow2)?; - Ok(sequence) + JoltInstructionKind::MUL, + reg(rd(instruction)?), + reg(rs1(instruction)?), + v_pow2.operand(), + ); + asm.release(v_pow2); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/slli.rs b/crates/jolt-program/src/expand/shifts/slli.rs index dd739aec26..66c77b7c68 100644 --- a/crates/jolt-program/src/expand/shifts/slli.rs +++ b/crates/jolt-program/src/expand/shifts/slli.rs @@ -1,17 +1,22 @@ use super::*; +/// Lowers immediate `SLLI` to multiplication by the encoded power of two. +/// +/// The source decoder has already normalized this as an RV64 instruction; the +/// immediate is masked to six bits and then emitted as a final `VirtualMULI` +/// row so the shift is represented as arithmetic in the proving circuit. pub(in crate::expand) fn expand_slli( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let shift = instruction.operands.imm & 0x3f; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_i( - InstructionKind::VirtualMULI, - rd(instruction)?, - rs1(instruction)?, + JoltInstructionKind::VirtualMULI, + reg(rd(instruction)?), + reg(rs1(instruction)?), 1i128 << shift, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/slliw.rs b/crates/jolt-program/src/expand/shifts/slliw.rs index 1d7c7ab3c1..7f418255b3 100644 --- a/crates/jolt-program/src/expand/shifts/slliw.rs +++ b/crates/jolt-program/src/expand/shifts/slliw.rs @@ -1,23 +1,30 @@ use super::*; +/// Lowers `SLLIW` to immediate multiplication followed by RV64 word cleanup. +/// +/// The shift amount is restricted to five bits. After the low word has been +/// shifted, `VirtualSignExtendWord` restores the required sign extension from +/// bit 31 into the destination register. pub(in crate::expand) fn expand_slliw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let shift = instruction.operands.imm & 0x1f; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_i( - InstructionKind::VirtualMULI, - rd(instruction)?, - rs1(instruction)?, + JoltInstructionKind::VirtualMULI, + reg(rd(instruction)?), + reg(rs1(instruction)?), 1i128 << shift, - )?; + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/sllw.rs b/crates/jolt-program/src/expand/shifts/sllw.rs index 2c5da1f408..052e66b37e 100644 --- a/crates/jolt-program/src/expand/shifts/sllw.rs +++ b/crates/jolt-program/src/expand/shifts/sllw.rs @@ -1,26 +1,37 @@ use super::*; +/// Lowers variable `SLLW` through the word-sized power-of-two helper. +/// +/// `VirtualPow2W` uses `rs2 & 0x1f`, matching the RV64 word shift rule. The +/// product is then sign-extended from 32 bits so the final row sequence has the +/// same result as the source `SLLW`. pub(in crate::expand) fn expand_sllw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_pow2 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i(InstructionKind::VirtualPow2W, v_pow2, rs2(instruction)?, 0)?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_pow2 = asm.allocate()?; + + asm.emit_i( + JoltInstructionKind::VirtualPow2W, + v_pow2.operand(), + reg(rs2(instruction)?), + 0, + ); asm.emit_r( - InstructionKind::MUL, - rd(instruction)?, - rs1(instruction)?, - v_pow2, - )?; + JoltInstructionKind::MUL, + reg(rd(instruction)?), + reg(rs1(instruction)?), + v_pow2.operand(), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v_pow2)?; - Ok(sequence) + ); + asm.release(v_pow2); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/sra.rs b/crates/jolt-program/src/expand/shifts/sra.rs index d98a24fb58..a98bc6f2d7 100644 --- a/crates/jolt-program/src/expand/shifts/sra.rs +++ b/crates/jolt-program/src/expand/shifts/sra.rs @@ -1,25 +1,31 @@ use super::*; +/// Lowers variable arithmetic right shift by first materializing the shift mask. +/// +/// `VirtualShiftRightBitmask` encodes `rs2 & 0x3f` as the mask consumed by +/// `VirtualSRA`. Splitting the sequence this way keeps dynamic shift amount +/// checking in the lookup table while preserving the signed right-shift result. pub(in crate::expand) fn expand_sra( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_bitmask = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_bitmask = asm.allocate()?; + asm.emit_i( - InstructionKind::VirtualShiftRightBitmask, - v_bitmask, - rs2(instruction)?, + JoltInstructionKind::VirtualShiftRightBitmask( + jolt_riscv::instructions::VirtualShiftRightBitmask(()), + ), + v_bitmask.operand(), + reg(rs2(instruction)?), 0, - )?; + ); asm.emit_r( - InstructionKind::VirtualSRA, - rd(instruction)?, - rs1(instruction)?, - v_bitmask, - )?; - let sequence = asm.finalize()?; - allocator.release(v_bitmask)?; - Ok(sequence) + JoltInstructionKind::VirtualSRA, + reg(rd(instruction)?), + reg(rs1(instruction)?), + v_bitmask.operand(), + ); + asm.release(v_bitmask); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/srai.rs b/crates/jolt-program/src/expand/shifts/srai.rs index 86f1d276ec..3b8c542d86 100644 --- a/crates/jolt-program/src/expand/shifts/srai.rs +++ b/crates/jolt-program/src/expand/shifts/srai.rs @@ -1,18 +1,23 @@ use super::*; +/// Lowers immediate arithmetic right shift to a single virtual final row. +/// +/// The immediate is converted into the same bitmask shape used by dynamic +/// shifts. `VirtualSRAI` recovers the shift amount from that mask and performs +/// the signed RV64 shift. pub(in crate::expand) fn expand_srai( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let shift = instruction.operands.imm & 0x3f; let bitmask = super::shared::right_shift_bitmask(shift as u32, 64); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_i( - InstructionKind::VirtualSRAI, - rd(instruction)?, - rs1(instruction)?, + JoltInstructionKind::VirtualSRAI, + reg(rd(instruction)?), + reg(rs1(instruction)?), bitmask as i128, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/sraiw.rs b/crates/jolt-program/src/expand/shifts/sraiw.rs index d94032a819..ce5f88b0ac 100644 --- a/crates/jolt-program/src/expand/shifts/sraiw.rs +++ b/crates/jolt-program/src/expand/shifts/sraiw.rs @@ -1,33 +1,41 @@ use super::*; +/// Lowers `SRAIW` by first restoring the signed 32-bit source value. +/// +/// Arithmetic word shifts operate on the sign-extended low word of `rs1`, not +/// on arbitrary high bits already present in the register. The final +/// `VirtualSignExtendWord` preserves the RV64 word-result contract. pub(in crate::expand) fn expand_sraiw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rs1 = allocator.allocate()?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rs1 = asm.allocate()?; let shift = instruction.operands.imm & 0x1f; let bitmask = super::shared::right_shift_bitmask(shift as u32, 64); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + asm.emit_i( - InstructionKind::VirtualSignExtendWord, - v_rs1, - rs1(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + v_rs1.operand(), + reg(rs1(instruction)?), 0, - )?; + ); asm.emit_i( - InstructionKind::VirtualSRAI, - rd(instruction)?, - v_rs1, + JoltInstructionKind::VirtualSRAI, + reg(rd(instruction)?), + v_rs1.operand(), bitmask as i128, - )?; + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v_rs1)?; - Ok(sequence) + ); + asm.release(v_rs1); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/sraw.rs b/crates/jolt-program/src/expand/shifts/sraw.rs index c7d3eb6e18..291c48789f 100644 --- a/crates/jolt-program/src/expand/shifts/sraw.rs +++ b/crates/jolt-program/src/expand/shifts/sraw.rs @@ -1,40 +1,55 @@ use super::*; +/// Lowers variable `SRAW` as a signed 32-bit shift embedded in RV64 rows. +/// +/// The low word of `rs1` is sign-extended before shifting, `rs2` is masked to +/// five bits, and the output is sign-extended again. This prevents unrelated +/// high bits of `rs1` or `rs2` from influencing the architectural word result. pub(in crate::expand) fn expand_sraw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rs1 = allocator.allocate()?; - let v_bitmask = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rs1 = asm.allocate()?; + let v_bitmask = asm.allocate()?; + asm.emit_i( - InstructionKind::VirtualSignExtendWord, - v_rs1, - rs1(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + v_rs1.operand(), + reg(rs1(instruction)?), 0, - )?; - asm.emit_i(InstructionKind::ANDI, v_bitmask, rs2(instruction)?, 0x1f)?; + ); + asm.emit_i( + JoltInstructionKind::ANDI, + v_bitmask.operand(), + reg(rs2(instruction)?), + 0x1f, + ); asm.emit_i( - InstructionKind::VirtualShiftRightBitmask, - v_bitmask, - v_bitmask, + JoltInstructionKind::VirtualShiftRightBitmask( + jolt_riscv::instructions::VirtualShiftRightBitmask(()), + ), + v_bitmask.operand(), + v_bitmask.operand(), 0, - )?; + ); asm.emit_r( - InstructionKind::VirtualSRA, - rd(instruction)?, - v_rs1, - v_bitmask, - )?; + JoltInstructionKind::VirtualSRA, + reg(rd(instruction)?), + v_rs1.operand(), + v_bitmask.operand(), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v_rs1)?; - allocator.release(v_bitmask)?; - Ok(sequence) + ); + asm.release(v_rs1); + asm.release(v_bitmask); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/srl.rs b/crates/jolt-program/src/expand/shifts/srl.rs index dc3ebd00f9..14595020e4 100644 --- a/crates/jolt-program/src/expand/shifts/srl.rs +++ b/crates/jolt-program/src/expand/shifts/srl.rs @@ -1,25 +1,30 @@ use super::*; +/// Lowers variable logical right shift through a dynamic bitmask helper. +/// +/// `VirtualShiftRightBitmask` applies the RV64 `rs2 & 0x3f` rule and produces +/// the mask consumed by `VirtualSRL`, which then performs the unsigned shift. pub(in crate::expand) fn expand_srl( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_bitmask = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_bitmask = asm.allocate()?; + asm.emit_i( - InstructionKind::VirtualShiftRightBitmask, - v_bitmask, - rs2(instruction)?, + JoltInstructionKind::VirtualShiftRightBitmask( + jolt_riscv::instructions::VirtualShiftRightBitmask(()), + ), + v_bitmask.operand(), + reg(rs2(instruction)?), 0, - )?; + ); asm.emit_r( - InstructionKind::VirtualSRL, - rd(instruction)?, - rs1(instruction)?, - v_bitmask, - )?; - let sequence = asm.finalize()?; - allocator.release(v_bitmask)?; - Ok(sequence) + JoltInstructionKind::VirtualSRL, + reg(rd(instruction)?), + reg(rs1(instruction)?), + v_bitmask.operand(), + ); + asm.release(v_bitmask); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/srli.rs b/crates/jolt-program/src/expand/shifts/srli.rs index a5fa4e677a..4badc239c1 100644 --- a/crates/jolt-program/src/expand/shifts/srli.rs +++ b/crates/jolt-program/src/expand/shifts/srli.rs @@ -1,18 +1,22 @@ use super::*; +/// Lowers immediate logical right shift to one virtual final row. +/// +/// The mask-shaped immediate records the same shift amount as `imm & 0x3f` and +/// lets the final `VirtualSRLI` row perform an unsigned RV64 shift. pub(in crate::expand) fn expand_srli( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { + instruction: &SourceInstructionRow, +) -> Result { let shift = instruction.operands.imm & 0x3f; let bitmask = super::shared::right_shift_bitmask(shift as u32, 64); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + let mut asm = ExpansionBuilder::new(*instruction); + asm.emit_i( - InstructionKind::VirtualSRLI, - rd(instruction)?, - rs1(instruction)?, + JoltInstructionKind::VirtualSRLI, + reg(rd(instruction)?), + reg(rs1(instruction)?), bitmask as i128, - )?; + ); + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/srliw.rs b/crates/jolt-program/src/expand/shifts/srliw.rs index 51790b9237..9f2335fb98 100644 --- a/crates/jolt-program/src/expand/shifts/srliw.rs +++ b/crates/jolt-program/src/expand/shifts/srliw.rs @@ -1,33 +1,39 @@ use super::*; +/// Lowers `SRLIW` by shifting the source word through the high half first. +/// +/// Multiplying by `2^32` moves the low 32 bits into bits 63:32. A logical +/// right shift by `32 + shamt` then yields the zero-filled word result, after +/// which `VirtualSignExtendWord` applies RV64's required word sign extension. pub(in crate::expand) fn expand_srliw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_rs1 = allocator.allocate()?; + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_rs1 = asm.allocate()?; let shift = (instruction.operands.imm & 0x1f) + 32; let bitmask = super::shared::right_shift_bitmask(shift as u32, 64); - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + asm.emit_i( - InstructionKind::VirtualMULI, - v_rs1, - rs1(instruction)?, + JoltInstructionKind::VirtualMULI, + v_rs1.operand(), + reg(rs1(instruction)?), 1i128 << 32, - )?; + ); asm.emit_i( - InstructionKind::VirtualSRLI, - rd(instruction)?, - v_rs1, + JoltInstructionKind::VirtualSRLI, + reg(rd(instruction)?), + v_rs1.operand(), bitmask as i128, - )?; + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v_rs1)?; - Ok(sequence) + ); + asm.release(v_rs1); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/shifts/srlw.rs b/crates/jolt-program/src/expand/shifts/srlw.rs index d66a79f403..d71cf61686 100644 --- a/crates/jolt-program/src/expand/shifts/srlw.rs +++ b/crates/jolt-program/src/expand/shifts/srlw.rs @@ -1,40 +1,54 @@ use super::*; +/// Lowers variable `SRLW` by embedding the 32-bit logical shift in RV64 space. +/// +/// The low word is first moved into the high half. Setting bit 5 of the shift +/// operand makes `VirtualShiftRightBitmask` encode `32 + (rs2 & 0x1f)`, so the +/// logical shift extracts exactly the zero-filled 32-bit result before final +/// word sign extension. pub(in crate::expand) fn expand_srlw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let v_bitmask = allocator.allocate()?; - let v_rs1 = allocator.allocate()?; - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); + instruction: &SourceInstructionRow, +) -> Result { + let mut asm = ExpansionBuilder::new(*instruction); + let v_bitmask = asm.allocate()?; + let v_rs1 = asm.allocate()?; + asm.emit_i( - InstructionKind::VirtualMULI, - v_rs1, - rs1(instruction)?, + JoltInstructionKind::VirtualMULI, + v_rs1.operand(), + reg(rs1(instruction)?), 1i128 << 32, - )?; - asm.emit_i(InstructionKind::ORI, v_bitmask, rs2(instruction)?, 32)?; + ); + asm.emit_i( + JoltInstructionKind::ORI, + v_bitmask.operand(), + reg(rs2(instruction)?), + 32, + ); asm.emit_i( - InstructionKind::VirtualShiftRightBitmask, - v_bitmask, - v_bitmask, + JoltInstructionKind::VirtualShiftRightBitmask( + jolt_riscv::instructions::VirtualShiftRightBitmask(()), + ), + v_bitmask.operand(), + v_bitmask.operand(), 0, - )?; + ); asm.emit_r( - InstructionKind::VirtualSRL, - rd(instruction)?, - v_rs1, - v_bitmask, - )?; + JoltInstructionKind::VirtualSRL, + reg(rd(instruction)?), + v_rs1.operand(), + v_bitmask.operand(), + ); asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, + JoltInstructionKind::VirtualSignExtendWord( + jolt_riscv::instructions::VirtualSignExtendWord(()), + ), + reg(rd(instruction)?), + reg(rd(instruction)?), 0, - )?; - let sequence = asm.finalize()?; - allocator.release(v_bitmask)?; - allocator.release(v_rs1)?; - Ok(sequence) + ); + asm.release(v_bitmask); + asm.release(v_rs1); + + asm.finalize() } diff --git a/crates/jolt-program/src/expand/tests.rs b/crates/jolt-program/src/expand/tests.rs index 5dd06ad253..8a216b651e 100644 --- a/crates/jolt-program/src/expand/tests.rs +++ b/crates/jolt-program/src/expand/tests.rs @@ -1,14 +1,34 @@ use super::*; use common::constants::RAM_START_ADDRESS; +use jolt_riscv::{ + JoltInstruction, JoltInstructionProfile, SourceExtension, SourceInlineKey, + SourceInstructionRow, RV64IMAC_JOLT, +}; +#[cfg(feature = "serialization")] +use serde::Deserialize; +#[cfg(feature = "serialization")] +use sha2::{Digest, Sha256}; -fn instruction( - instruction_kind: InstructionKind, +#[cfg(feature = "serialization")] +#[derive(Debug, Deserialize)] +struct ExpansionParityCase { + name: String, + input: SourceInstruction, + output_sha256: String, +} + +fn source_row( + instruction_kind: SourceInstructionKind, rd: Option, is_compressed: bool, -) -> NormalizedInstruction { - NormalizedInstruction { - instruction_kind, +) -> SourceInstructionRow { + let inline = (instruction_kind == SourceInstructionKind::Inline).then_some(SourceInlineKey { + opcode: 0x2b, + funct3: 0, + funct7: 0, + }); + SourceInstructionRow { address: 0x8000_0000, operands: NormalizedOperands { rd, @@ -16,22 +36,44 @@ fn instruction( rs2: Some(2), imm: 7, }, - virtual_sequence_remaining: None, - is_first_in_sequence: false, + inline, is_compressed, } } +fn instruction( + instruction_kind: SourceInstructionKind, + rd: Option, + is_compressed: bool, +) -> SourceInstruction { + SourceInstruction::new( + instruction_kind, + source_row(instruction_kind, rd, is_compressed), + ) +} + +fn final_instruction(row: JoltInstructionRow) -> Result { + JoltInstruction::try_from(row).map_err(ExpansionError::IllegalTargetInstruction) +} + +fn rows(instructions: Vec) -> Vec { + instructions + .into_iter() + .map(JoltInstructionRow::from) + .collect() +} + #[test] fn side_effect_free_rd_zero_becomes_noop_addi() -> Result<(), ExpansionError> { let mut allocator = ExpansionAllocator::new(); - let expanded = expand_instruction( - &instruction(InstructionKind::ADD, Some(0), true), + let expanded = rows(expand_instruction( + &instruction(SourceInstructionKind::ADD, Some(0), true), &mut allocator, - )?; + RV64IMAC_JOLT, + )?); assert_eq!(expanded.len(), 1); - assert_eq!(expanded[0].instruction_kind, InstructionKind::ADDI); + assert_eq!(expanded[0].instruction_kind, JoltInstructionKind::ADDI); assert_eq!(expanded[0].operands.rd, Some(0)); assert_eq!(expanded[0].operands.rs1, Some(0)); assert_eq!(expanded[0].operands.rs2, None); @@ -43,13 +85,14 @@ fn side_effect_free_rd_zero_becomes_noop_addi() -> Result<(), ExpansionError> { #[test] fn side_effecting_rd_zero_rewrites_to_temporary_register() -> Result<(), ExpansionError> { let mut allocator = ExpansionAllocator::new(); - let expanded = expand_instruction( - &instruction(InstructionKind::JAL, Some(0), false), + let expanded = rows(expand_instruction( + &instruction(SourceInstructionKind::JAL, Some(0), false), &mut allocator, - )?; + RV64IMAC_JOLT, + )?); assert_eq!(expanded.len(), 1); - assert_eq!(expanded[0].instruction_kind, InstructionKind::JAL); + assert_eq!(expanded[0].instruction_kind, JoltInstructionKind::JAL); assert_eq!(expanded[0].operands.rd, Some(40)); Ok(()) } @@ -57,35 +100,36 @@ fn side_effecting_rd_zero_rewrites_to_temporary_register() -> Result<(), Expansi #[test] fn trap_related_rd_zero_uses_instruction_expansion() -> Result<(), ExpansionError> { let mut allocator = ExpansionAllocator::new(); - let input = instruction(InstructionKind::ECALL, Some(0), false); - let expanded = expand_instruction(&input, &mut allocator)?; + let input = instruction(SourceInstructionKind::ECALL, Some(0), false); + let expanded = rows(expand_instruction(&input, &mut allocator, RV64IMAC_JOLT)?); assert_eq!(expanded.len(), 7); - assert_eq!(expanded[0].instruction_kind, InstructionKind::AUIPC); - assert_eq!(expanded[6].instruction_kind, InstructionKind::JALR); + assert_eq!(expanded[0].instruction_kind, JoltInstructionKind::AUIPC); + assert_eq!(expanded[6].instruction_kind, JoltInstructionKind::JALR); Ok(()) } #[test] fn inline_requires_provider() { let mut allocator = ExpansionAllocator::new(); - let input = instruction(InstructionKind::Inline, Some(3), false); + let input = instruction(SourceInstructionKind::Inline, Some(3), false); assert!(matches!( - expand_instruction(&input, &mut allocator), + expand_instruction(&input, &mut allocator, RV64IMAC_JOLT), Err(ExpansionError::InlineProviderRequired) )); } #[test] fn csr_zero_is_rejected() { - for instruction_kind in [InstructionKind::CSRRW, InstructionKind::CSRRS] { + for instruction_kind in [SourceInstructionKind::CSRRW, SourceInstructionKind::CSRRS] { let mut allocator = ExpansionAllocator::new(); - let mut input = instruction(instruction_kind, Some(3), false); + let mut input = source_row(instruction_kind, Some(3), false); input.operands.imm = 0; + let input = SourceInstruction::new(instruction_kind, input); assert!(matches!( - expand_instruction(&input, &mut allocator), + expand_instruction(&input, &mut allocator, RV64IMAC_JOLT), Err(ExpansionError::UnsupportedCsr(0)) )); } @@ -94,23 +138,24 @@ fn csr_zero_is_rejected() { #[test] fn lr_sc_expansions_restrict_address_to_ram() -> Result<(), ExpansionError> { for instruction_kind in [ - InstructionKind::LRW, - InstructionKind::LRD, - InstructionKind::SCW, - InstructionKind::SCD, + SourceInstructionKind::LRW, + SourceInstructionKind::LRD, + SourceInstructionKind::SCW, + SourceInstructionKind::SCD, ] { let mut allocator = ExpansionAllocator::new(); - let expanded = expand_instruction( + let expanded = rows(expand_instruction( &instruction(instruction_kind, Some(3), false), &mut allocator, - )?; + RV64IMAC_JOLT, + )?); - assert_eq!(expanded[0].instruction_kind, InstructionKind::LUI); + assert_eq!(expanded[0].instruction_kind, JoltInstructionKind::LUI); assert_eq!(expanded[0].operands.rd, Some(40)); assert_eq!(expanded[0].operands.imm, RAM_START_ADDRESS as i128); assert_eq!( expanded[1].instruction_kind, - InstructionKind::VirtualAssertLTE + JoltInstructionKind::VirtualAssertLTE ); assert_eq!(expanded[1].operands.rs1, Some(40)); assert_eq!(expanded[1].operands.rs2, Some(1)); @@ -120,15 +165,17 @@ fn lr_sc_expansions_restrict_address_to_ram() -> Result<(), ExpansionError> { #[test] fn sc_success_advice_is_not_position_dependent() -> Result<(), ExpansionError> { - for instruction_kind in [InstructionKind::SCW, InstructionKind::SCD] { + for instruction_kind in [SourceInstructionKind::SCW, SourceInstructionKind::SCD] { let mut allocator = ExpansionAllocator::new(); - let expanded = expand_instruction( + let expanded = rows(expand_instruction( &instruction(instruction_kind, Some(3), false), &mut allocator, - )?; - let advice_position = expanded - .iter() - .position(|instruction| instruction.instruction_kind == InstructionKind::VirtualAdvice); + RV64IMAC_JOLT, + )?); + let advice_position = expanded.iter().position(|instruction| { + instruction.instruction_kind + == JoltInstructionKind::VirtualAdvice(jolt_riscv::instructions::VirtualAdvice(())) + }); assert!( matches!(advice_position, Some(position) if position > 1), @@ -142,22 +189,35 @@ fn sc_success_advice_is_not_position_dependent() -> Result<(), ExpansionError> { fn inline_rd_zero_is_remapped_before_provider() -> Result<(), ExpansionError> { #[derive(Default)] struct CapturingProvider { - captured: Option, + captured: Option, } impl InlineExpansionProvider for CapturingProvider { fn expand_inline( &mut self, - instruction: &NormalizedInstruction, + instruction: &SourceInstruction, _allocator: &mut ExpansionAllocator, - ) -> Result, ExpansionError> { + _profile: jolt_riscv::JoltInstructionProfile, + ) -> Result, ExpansionError> { self.captured = Some(*instruction); - Ok(vec![*instruction]) + let row = instruction.row(); + Ok(vec![final_instruction(JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, + address: row.address, + operands: NormalizedOperands { + rd: row.operands.rd, + rs1: Some(0), + rs2: None, + imm: 0, + }, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + })?]) } } - let input = NormalizedInstruction { - instruction_kind: InstructionKind::Inline, + let input = SourceInstructionRow { address: 0x8000_0000, operands: NormalizedOperands { rd: Some(0), @@ -165,19 +225,288 @@ fn inline_rd_zero_is_remapped_before_provider() -> Result<(), ExpansionError> { rs2: Some(20), imm: 0x0b, }, - virtual_sequence_remaining: None, - is_first_in_sequence: false, + inline: Some(SourceInlineKey { + opcode: 0x2b, + funct3: 0, + funct7: 0, + }), is_compressed: false, }; let mut allocator = ExpansionAllocator::new(); let mut provider = CapturingProvider::default(); + let input = SourceInstruction::new(SourceInstructionKind::Inline, input); - let expanded = expand_instruction_with_provider(&input, &mut allocator, &mut provider)?; + let expanded = rows(expand_instruction_with_provider( + &input, + &mut allocator, + &mut provider, + RV64IMAC_JOLT, + )?); - let mut expected = input; - expected.operands.rd = Some(40); + let expected = input.map_row(|mut row| { + row.operands.rd = Some(40); + row + }); assert_eq!(provider.captured, Some(expected)); - assert_eq!(expanded, vec![expected]); + assert_eq!(expanded.len(), 1); + assert_eq!(expanded[0].instruction_kind, JoltInstructionKind::ADDI); + assert_eq!(expanded[0].operands.rd, Some(40)); + assert_eq!(expanded[0].virtual_sequence_remaining, Some(0)); + assert!(expanded[0].is_first_in_sequence); + Ok(()) +} + +#[test] +fn inline_provider_error_releases_rd_zero_temporary() -> Result<(), ExpansionError> { + struct FailingProvider; + + impl InlineExpansionProvider for FailingProvider { + fn expand_inline( + &mut self, + _instruction: &SourceInstruction, + _allocator: &mut ExpansionAllocator, + _profile: jolt_riscv::JoltInstructionProfile, + ) -> Result, ExpansionError> { + Err(ExpansionError::UnsupportedInstruction) + } + } + + let input = instruction(SourceInstructionKind::Inline, Some(0), false); + let mut allocator = ExpansionAllocator::new(); + assert!(matches!( + expand_instruction_with_provider( + &input, + &mut allocator, + &mut FailingProvider, + RV64IMAC_JOLT + ), + Err(ExpansionError::UnsupportedInstruction) + )); + + let register = allocator.allocate()?; + assert_eq!(register, 40); + allocator.release(register)?; + Ok(()) +} + +#[test] +fn inline_provider_output_is_validated_and_stamped() { + const RV64I_ONLY: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[SourceExtension::Rv64I], + inline_extensions: &[], + }; + + struct BadProvider; + + impl InlineExpansionProvider for BadProvider { + fn expand_inline( + &mut self, + _instruction: &SourceInstruction, + _allocator: &mut ExpansionAllocator, + _profile: jolt_riscv::JoltInstructionProfile, + ) -> Result, ExpansionError> { + final_instruction(JoltInstructionRow { + instruction_kind: JoltInstructionKind::MUL, + address: 0x8000_0000, + operands: Default::default(), + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + }) + .map(|instruction| vec![instruction]) + } + } + + let input = instruction(SourceInstructionKind::Inline, Some(3), true); + let mut allocator = ExpansionAllocator::new(); + + assert!(matches!( + expand_instruction_with_provider(&input, &mut allocator, &mut BadProvider, RV64I_ONLY), + Err(ExpansionError::IllegalTargetInstruction( + JoltInstructionKind::MUL + )) + )); +} + +#[test] +fn inline_provider_allocator_resets_are_appended() -> Result<(), ExpansionError> { + struct AllocatingProvider; + + impl InlineExpansionProvider for AllocatingProvider { + fn expand_inline( + &mut self, + instruction: &SourceInstruction, + allocator: &mut ExpansionAllocator, + _profile: jolt_riscv::JoltInstructionProfile, + ) -> Result, ExpansionError> { + let row = instruction.row(); + let register = allocator.allocate_for_inline()?; + allocator.release(register)?; + Ok(vec![final_instruction(JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, + address: row.address, + operands: NormalizedOperands { + rd: Some(register), + rs1: Some(0), + rs2: None, + imm: 1, + }, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + })?]) + } + } + + let input = instruction(SourceInstructionKind::Inline, Some(3), true); + let mut allocator = ExpansionAllocator::new(); + let expanded = rows(expand_instruction_with_provider( + &input, + &mut allocator, + &mut AllocatingProvider, + RV64IMAC_JOLT, + )?); + + assert_eq!(expanded.len(), 2); + assert_eq!(expanded[0].virtual_sequence_remaining, Some(1)); + assert!(expanded[0].is_first_in_sequence); + assert!(!expanded[0].is_compressed); + assert_eq!(expanded[1].instruction_kind, JoltInstructionKind::ADDI); + assert_eq!(expanded[1].operands.rs1, Some(0)); + assert_eq!(expanded[1].operands.imm, 0); + assert_eq!(expanded[1].virtual_sequence_remaining, Some(0)); + assert!(expanded[1].is_compressed); + Ok(()) +} + +#[test] +fn inline_provider_allows_sequences_larger_than_instruction_recipes() -> Result<(), ExpansionError> +{ + struct LargeProvider; + + impl InlineExpansionProvider for LargeProvider { + fn expand_inline( + &mut self, + instruction: &SourceInstruction, + _allocator: &mut ExpansionAllocator, + _profile: jolt_riscv::JoltInstructionProfile, + ) -> Result, ExpansionError> { + let row = instruction.row(); + (0..=materialize::MAX_FINAL_ROWS_PER_SOURCE) + .map(|_| { + final_instruction(JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, + address: row.address, + operands: NormalizedOperands { + rd: Some(0), + rs1: Some(0), + rs2: None, + imm: 0, + }, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: false, + }) + }) + .collect::, _>>() + } + } + + let input = instruction(SourceInstructionKind::Inline, Some(3), true); + let mut allocator = ExpansionAllocator::new(); + let expanded = rows(expand_instruction_with_provider( + &input, + &mut allocator, + &mut LargeProvider, + RV64IMAC_JOLT, + )?); + + assert_eq!(expanded.len(), materialize::MAX_FINAL_ROWS_PER_SOURCE + 1); + assert_eq!( + expanded[0].virtual_sequence_remaining, + Some(materialize::MAX_FINAL_ROWS_PER_SOURCE as u16) + ); + assert!(expanded[0].is_first_in_sequence); + assert!(expanded[materialize::MAX_FINAL_ROWS_PER_SOURCE].is_compressed); + Ok(()) +} + +#[test] +fn source_only_expanders_are_not_target_legal() { + macro_rules! assert_source_only { + ($($kind:ident),* $(,)?) => { + $( + assert!( + SourceInstructionKind::$kind.jolt_kind().is_none(), + concat!(stringify!($kind), " has an expander but maps directly to a final row") + ); + )* + }; + } + + assert_source_only! { + ADDIW, ADDW, SUBW, MULH, MULHSU, MULW, + LB, LBU, LH, LHU, LW, LWU, + AdviceLB, AdviceLH, AdviceLW, AdviceLD, + AMOADDD, AMOANDD, AMOORD, AMOXORD, AMOSWAPD, + AMOMAXD, AMOMAXUD, AMOMIND, AMOMINUD, + AMOADDW, AMOANDW, AMOORW, AMOXORW, AMOSWAPW, + AMOMAXW, AMOMAXUW, AMOMINW, AMOMINUW, + LRD, LRW, + DIV, DIVU, DIVW, DIVUW, REM, REMU, REMW, REMUW, + SB, SCD, SCW, SH, SW, + CSRRW, CSRRS, EBREAK, ECALL, MRET, + SLL, SLLI, SLLW, SLLIW, SRL, SRLI, SRA, SRAI, + SRLIW, SRAIW, SRLW, SRAW, + } + assert_eq!(SourceInstructionKind::Inline.jolt_kind(), None); +} + +#[test] +fn recursive_helper_expansion_is_stamped_as_one_sequence() -> Result<(), ExpansionError> { + let mut allocator = ExpansionAllocator::new(); + let input = instruction(SourceInstructionKind::SLL, Some(3), true); + let expanded = rows(expand_instruction(&input, &mut allocator, RV64IMAC_JOLT)?); + + assert!(expanded.len() > 1); + for (i, row) in expanded.iter().enumerate() { + assert_eq!(row.address, input.row().address); + assert_eq!( + row.virtual_sequence_remaining, + Some((expanded.len() - i - 1) as u16) + ); + assert_eq!(row.is_first_in_sequence, i == 0); + assert_eq!(row.is_compressed, i + 1 == expanded.len()); + } + assert!(expanded + .iter() + .all(|row| RV64IMAC_JOLT.supports_jolt(row.instruction_kind))); + + Ok(()) +} + +#[test] +#[cfg(feature = "serialization")] +fn expansion_matches_main_golden_fixture() -> Result<(), Box> { + // Expected hashes generated from baseline main commit 51d81a36e. This catches + // recursive expansion order and virtual-register reuse regressions without + // checking a giant expanded-row fixture into the repository. + let cases: Vec = + serde_json::from_str(include_str!("fixtures/main_expand_parity_hashes.json"))?; + + for case in cases { + let mut allocator = ExpansionAllocator::new(); + let expanded = rows(expand_instruction( + &case.input, + &mut allocator, + RV64IMAC_JOLT, + )?); + let encoded = serde_json::to_vec(&expanded)?; + let output_sha256 = hex::encode(Sha256::digest(encoded)); + + assert_eq!(output_sha256, case.output_sha256, "{}", case.name); + } + Ok(()) } diff --git a/crates/jolt-program/src/image/decode.rs b/crates/jolt-program/src/image/decode.rs index ff8f750e6a..82b9967080 100644 --- a/crates/jolt-program/src/image/decode.rs +++ b/crates/jolt-program/src/image/decode.rs @@ -3,7 +3,10 @@ reason = "RISC-V decode tables are easiest to audit in ISA bit-field widths" )] -use jolt_riscv::{InstructionKind, NormalizedInstruction, NormalizedOperands}; +use jolt_riscv::{ + JoltInstructionProfile, NormalizedOperands, SourceInlineKey, SourceInstruction, + SourceInstructionKind, SourceInstructionRow, +}; use crate::ProgramError; @@ -11,252 +14,269 @@ pub fn decode_instruction( word: u32, address: u64, is_compressed: bool, -) -> Result { + profile: JoltInstructionProfile, +) -> Result { let opcode = word & 0x7f; let kind = match opcode { - 0b0110111 => InstructionKind::LUI, - 0b0010111 => InstructionKind::AUIPC, - 0b1101111 => InstructionKind::JAL, + 0b0110111 => SourceInstructionKind::LUI, + 0b0010111 => SourceInstructionKind::AUIPC, + 0b1101111 => SourceInstructionKind::JAL, 0b1100111 => match funct3(word) { - 0b000 => InstructionKind::JALR, + 0b000 => SourceInstructionKind::JALR, _ => return invalid("invalid JALR funct3"), }, 0b1100011 => match funct3(word) { - 0b000 => InstructionKind::BEQ, - 0b001 => InstructionKind::BNE, - 0b100 => InstructionKind::BLT, - 0b101 => InstructionKind::BGE, - 0b110 => InstructionKind::BLTU, - 0b111 => InstructionKind::BGEU, + 0b000 => SourceInstructionKind::BEQ, + 0b001 => SourceInstructionKind::BNE, + 0b100 => SourceInstructionKind::BLT, + 0b101 => SourceInstructionKind::BGE, + 0b110 => SourceInstructionKind::BLTU, + 0b111 => SourceInstructionKind::BGEU, _ => return invalid("invalid branch funct3"), }, 0b0000011 => match funct3(word) { - 0b000 => InstructionKind::LB, - 0b001 => InstructionKind::LH, - 0b010 => InstructionKind::LW, - 0b011 => InstructionKind::LD, - 0b100 => InstructionKind::LBU, - 0b101 => InstructionKind::LHU, - 0b110 => InstructionKind::LWU, + 0b000 => SourceInstructionKind::LB, + 0b001 => SourceInstructionKind::LH, + 0b010 => SourceInstructionKind::LW, + 0b011 => SourceInstructionKind::LD, + 0b100 => SourceInstructionKind::LBU, + 0b101 => SourceInstructionKind::LHU, + 0b110 => SourceInstructionKind::LWU, _ => return invalid("invalid load funct3"), }, 0b0100011 => match funct3(word) { - 0b000 => InstructionKind::SB, - 0b001 => InstructionKind::SH, - 0b010 => InstructionKind::SW, - 0b011 => InstructionKind::SD, + 0b000 => SourceInstructionKind::SB, + 0b001 => SourceInstructionKind::SH, + 0b010 => SourceInstructionKind::SW, + 0b011 => SourceInstructionKind::SD, _ => return invalid("invalid store funct3"), }, 0b0010011 => decode_op_imm(word)?, 0b0011011 => decode_op_imm_32(word)?, 0b0110011 => decode_op(word)?, 0b0111011 => decode_op_32(word)?, - 0b0001111 => InstructionKind::FENCE, + 0b0001111 => SourceInstructionKind::FENCE, 0b0101111 => decode_amo(word)?, 0b1110011 => decode_system(word)?, - 0b0001011 | 0b0101011 => InstructionKind::Inline, + 0b0001011 | 0b0101011 => SourceInstructionKind::Inline, 0b1011011 => decode_custom(word)?, _ => return invalid("unknown RV64 opcode"), }; - Ok(normalized(kind, word, address, is_compressed)) + if !profile.supports_source(kind) { + return Err(ProgramError::IllegalSourceInstruction(kind)); + } + Ok(source_instruction(kind, word, address, is_compressed)) } -fn decode_op_imm(word: u32) -> Result { +fn decode_op_imm(word: u32) -> Result { match funct3(word) { - 0b001 if funct6(word) == 0 => Ok(InstructionKind::SLLI), + 0b001 if funct6(word) == 0 => Ok(SourceInstructionKind::SLLI), 0b001 => invalid("invalid SLLI funct6"), - 0b101 if funct6(word) == 0b000000 => Ok(InstructionKind::SRLI), - 0b101 if funct6(word) == 0b010000 => Ok(InstructionKind::SRAI), + 0b101 if funct6(word) == 0b000000 => Ok(SourceInstructionKind::SRLI), + 0b101 if funct6(word) == 0b010000 => Ok(SourceInstructionKind::SRAI), 0b101 => invalid("invalid shift-immediate funct6"), - 0b000 => Ok(InstructionKind::ADDI), - 0b010 => Ok(InstructionKind::SLTI), - 0b011 => Ok(InstructionKind::SLTIU), - 0b100 => Ok(InstructionKind::XORI), - 0b110 => Ok(InstructionKind::ORI), - 0b111 => Ok(InstructionKind::ANDI), + 0b000 => Ok(SourceInstructionKind::ADDI), + 0b010 => Ok(SourceInstructionKind::SLTI), + 0b011 => Ok(SourceInstructionKind::SLTIU), + 0b100 => Ok(SourceInstructionKind::XORI), + 0b110 => Ok(SourceInstructionKind::ORI), + 0b111 => Ok(SourceInstructionKind::ANDI), _ => invalid("invalid op-imm funct3"), } } -fn decode_op_imm_32(word: u32) -> Result { +fn decode_op_imm_32(word: u32) -> Result { match (funct3(word), funct7(word)) { - (0b000, _) => Ok(InstructionKind::ADDIW), - (0b001, 0b0000000) => Ok(InstructionKind::SLLIW), - (0b101, 0b0000000) => Ok(InstructionKind::SRLIW), - (0b101, 0b0100000) => Ok(InstructionKind::SRAIW), + (0b000, _) => Ok(SourceInstructionKind::ADDIW), + (0b001, 0b0000000) => Ok(SourceInstructionKind::SLLIW), + (0b101, 0b0000000) => Ok(SourceInstructionKind::SRLIW), + (0b101, 0b0100000) => Ok(SourceInstructionKind::SRAIW), _ => invalid("invalid RV64 op-imm-32 instruction"), } } -fn decode_op(word: u32) -> Result { +fn decode_op(word: u32) -> Result { match (funct3(word), funct7(word)) { - (0b000, 0b0000000) => Ok(InstructionKind::ADD), - (0b000, 0b0100000) => Ok(InstructionKind::SUB), - (0b001, 0b0000000) => Ok(InstructionKind::SLL), - (0b010, 0b0000000) => Ok(InstructionKind::SLT), - (0b011, 0b0000000) => Ok(InstructionKind::SLTU), - (0b100, 0b0000000) => Ok(InstructionKind::XOR), - (0b101, 0b0000000) => Ok(InstructionKind::SRL), - (0b101, 0b0100000) => Ok(InstructionKind::SRA), - (0b110, 0b0000000) => Ok(InstructionKind::OR), - (0b111, 0b0000000) => Ok(InstructionKind::AND), - (0b000, 0b0000001) => Ok(InstructionKind::MUL), - (0b001, 0b0000001) => Ok(InstructionKind::MULH), - (0b010, 0b0000001) => Ok(InstructionKind::MULHSU), - (0b011, 0b0000001) => Ok(InstructionKind::MULHU), - (0b100, 0b0000001) => Ok(InstructionKind::DIV), - (0b101, 0b0000001) => Ok(InstructionKind::DIVU), - (0b110, 0b0000001) => Ok(InstructionKind::REM), - (0b111, 0b0000001) => Ok(InstructionKind::REMU), + (0b000, 0b0000000) => Ok(SourceInstructionKind::ADD), + (0b000, 0b0100000) => Ok(SourceInstructionKind::SUB), + (0b001, 0b0000000) => Ok(SourceInstructionKind::SLL), + (0b010, 0b0000000) => Ok(SourceInstructionKind::SLT), + (0b011, 0b0000000) => Ok(SourceInstructionKind::SLTU), + (0b100, 0b0000000) => Ok(SourceInstructionKind::XOR), + (0b101, 0b0000000) => Ok(SourceInstructionKind::SRL), + (0b101, 0b0100000) => Ok(SourceInstructionKind::SRA), + (0b110, 0b0000000) => Ok(SourceInstructionKind::OR), + (0b111, 0b0000000) => Ok(SourceInstructionKind::AND), + (0b000, 0b0000001) => Ok(SourceInstructionKind::MUL), + (0b001, 0b0000001) => Ok(SourceInstructionKind::MULH), + (0b010, 0b0000001) => Ok(SourceInstructionKind::MULHSU), + (0b011, 0b0000001) => Ok(SourceInstructionKind::MULHU), + (0b100, 0b0000001) => Ok(SourceInstructionKind::DIV), + (0b101, 0b0000001) => Ok(SourceInstructionKind::DIVU), + (0b110, 0b0000001) => Ok(SourceInstructionKind::REM), + (0b111, 0b0000001) => Ok(SourceInstructionKind::REMU), _ => invalid("invalid op instruction"), } } -fn decode_op_32(word: u32) -> Result { +fn decode_op_32(word: u32) -> Result { match (funct3(word), funct7(word)) { - (0b000, 0b0000000) => Ok(InstructionKind::ADDW), - (0b000, 0b0100000) => Ok(InstructionKind::SUBW), - (0b001, 0b0000000) => Ok(InstructionKind::SLLW), - (0b100, 0b0000001) => Ok(InstructionKind::DIVW), - (0b101, 0b0000000) => Ok(InstructionKind::SRLW), - (0b101, 0b0100000) => Ok(InstructionKind::SRAW), - (0b000, 0b0000001) => Ok(InstructionKind::MULW), - (0b101, 0b0000001) => Ok(InstructionKind::DIVUW), - (0b110, 0b0000001) => Ok(InstructionKind::REMW), - (0b111, 0b0000001) => Ok(InstructionKind::REMUW), + (0b000, 0b0000000) => Ok(SourceInstructionKind::ADDW), + (0b000, 0b0100000) => Ok(SourceInstructionKind::SUBW), + (0b001, 0b0000000) => Ok(SourceInstructionKind::SLLW), + (0b100, 0b0000001) => Ok(SourceInstructionKind::DIVW), + (0b101, 0b0000000) => Ok(SourceInstructionKind::SRLW), + (0b101, 0b0100000) => Ok(SourceInstructionKind::SRAW), + (0b000, 0b0000001) => Ok(SourceInstructionKind::MULW), + (0b101, 0b0000001) => Ok(SourceInstructionKind::DIVUW), + (0b110, 0b0000001) => Ok(SourceInstructionKind::REMW), + (0b111, 0b0000001) => Ok(SourceInstructionKind::REMUW), _ => invalid("invalid RV64 op-32 instruction"), } } -fn decode_amo(word: u32) -> Result { +fn decode_amo(word: u32) -> Result { match (funct3(word), (word >> 27) & 0x1f) { - (0b010, 0b00010) => Ok(InstructionKind::LRW), - (0b011, 0b00010) => Ok(InstructionKind::LRD), - (0b010, 0b00011) => Ok(InstructionKind::SCW), - (0b011, 0b00011) => Ok(InstructionKind::SCD), - (0b010, 0b00001) => Ok(InstructionKind::AMOSWAPW), - (0b011, 0b00001) => Ok(InstructionKind::AMOSWAPD), - (0b010, 0b00000) => Ok(InstructionKind::AMOADDW), - (0b011, 0b00000) => Ok(InstructionKind::AMOADDD), - (0b010, 0b01100) => Ok(InstructionKind::AMOANDW), - (0b011, 0b01100) => Ok(InstructionKind::AMOANDD), - (0b010, 0b01000) => Ok(InstructionKind::AMOORW), - (0b011, 0b01000) => Ok(InstructionKind::AMOORD), - (0b010, 0b00100) => Ok(InstructionKind::AMOXORW), - (0b011, 0b00100) => Ok(InstructionKind::AMOXORD), - (0b010, 0b10000) => Ok(InstructionKind::AMOMINW), - (0b011, 0b10000) => Ok(InstructionKind::AMOMIND), - (0b010, 0b10100) => Ok(InstructionKind::AMOMAXW), - (0b011, 0b10100) => Ok(InstructionKind::AMOMAXD), - (0b010, 0b11000) => Ok(InstructionKind::AMOMINUW), - (0b011, 0b11000) => Ok(InstructionKind::AMOMINUD), - (0b010, 0b11100) => Ok(InstructionKind::AMOMAXUW), - (0b011, 0b11100) => Ok(InstructionKind::AMOMAXUD), + (0b010, 0b00010) => Ok(SourceInstructionKind::LRW), + (0b011, 0b00010) => Ok(SourceInstructionKind::LRD), + (0b010, 0b00011) => Ok(SourceInstructionKind::SCW), + (0b011, 0b00011) => Ok(SourceInstructionKind::SCD), + (0b010, 0b00001) => Ok(SourceInstructionKind::AMOSWAPW), + (0b011, 0b00001) => Ok(SourceInstructionKind::AMOSWAPD), + (0b010, 0b00000) => Ok(SourceInstructionKind::AMOADDW), + (0b011, 0b00000) => Ok(SourceInstructionKind::AMOADDD), + (0b010, 0b01100) => Ok(SourceInstructionKind::AMOANDW), + (0b011, 0b01100) => Ok(SourceInstructionKind::AMOANDD), + (0b010, 0b01000) => Ok(SourceInstructionKind::AMOORW), + (0b011, 0b01000) => Ok(SourceInstructionKind::AMOORD), + (0b010, 0b00100) => Ok(SourceInstructionKind::AMOXORW), + (0b011, 0b00100) => Ok(SourceInstructionKind::AMOXORD), + (0b010, 0b10000) => Ok(SourceInstructionKind::AMOMINW), + (0b011, 0b10000) => Ok(SourceInstructionKind::AMOMIND), + (0b010, 0b10100) => Ok(SourceInstructionKind::AMOMAXW), + (0b011, 0b10100) => Ok(SourceInstructionKind::AMOMAXD), + (0b010, 0b11000) => Ok(SourceInstructionKind::AMOMINUW), + (0b011, 0b11000) => Ok(SourceInstructionKind::AMOMINUD), + (0b010, 0b11100) => Ok(SourceInstructionKind::AMOMAXUW), + (0b011, 0b11100) => Ok(SourceInstructionKind::AMOMAXUD), _ => invalid("invalid atomic memory operation"), } } -fn decode_system(word: u32) -> Result { +fn decode_system(word: u32) -> Result { match (funct3(word), funct7(word), (word >> 20) & 0x1f) { - (0, 0, 0) if word == 0x00000073 => Ok(InstructionKind::ECALL), - (0, 0, 1) if word == 0x00100073 => Ok(InstructionKind::EBREAK), - (0, 0x18, 2) if word == 0x30200073 => Ok(InstructionKind::MRET), - (1, _, _) => Ok(InstructionKind::CSRRW), - (2, _, _) => Ok(InstructionKind::CSRRS), + (0, 0, 0) if word == 0x00000073 => Ok(SourceInstructionKind::ECALL), + (0, 0, 1) if word == 0x00100073 => Ok(SourceInstructionKind::EBREAK), + (0, 0x18, 2) if word == 0x30200073 => Ok(SourceInstructionKind::MRET), + (1, _, _) => Ok(SourceInstructionKind::CSRRW), + (2, _, _) => Ok(SourceInstructionKind::CSRRS), _ => invalid("unsupported system instruction"), } } -fn decode_custom(word: u32) -> Result { +fn decode_custom(word: u32) -> Result { match funct3(word) { - 0b000 => Ok(InstructionKind::VirtualRev8W), - 0b001 => Ok(InstructionKind::VirtualAssertEQ), - 0b010 => Ok(InstructionKind::VirtualHostIO), - 0b011 => Ok(InstructionKind::AdviceLB), - 0b100 => Ok(InstructionKind::AdviceLH), - 0b101 => Ok(InstructionKind::AdviceLW), - 0b110 => Ok(InstructionKind::AdviceLD), - 0b111 => Ok(InstructionKind::VirtualAdviceLen), + 0b000 => Ok(SourceInstructionKind::VirtualRev8W( + jolt_riscv::instructions::VirtualRev8W(()), + )), + 0b001 => Ok(SourceInstructionKind::VirtualAssertEQ), + 0b010 => Ok(SourceInstructionKind::VirtualHostIO( + jolt_riscv::instructions::VirtualHostIO(()), + )), + 0b011 => Ok(SourceInstructionKind::AdviceLB), + 0b100 => Ok(SourceInstructionKind::AdviceLH), + 0b101 => Ok(SourceInstructionKind::AdviceLW), + 0b110 => Ok(SourceInstructionKind::AdviceLD), + 0b111 => Ok(SourceInstructionKind::VirtualAdviceLen( + jolt_riscv::instructions::VirtualAdviceLen(()), + )), _ => invalid("invalid custom instruction"), } } -fn normalized( - instruction_kind: InstructionKind, +fn source_instruction( + instruction_kind: SourceInstructionKind, word: u32, address: u64, is_compressed: bool, -) -> NormalizedInstruction { - NormalizedInstruction { +) -> SourceInstruction { + let inline = + (instruction_kind == SourceInstructionKind::Inline).then(|| source_inline_key(word)); + SourceInstruction::new( instruction_kind, - address: address as usize, - operands: operands(instruction_kind, word), - virtual_sequence_remaining: None, - is_first_in_sequence: false, - is_compressed, - } + SourceInstructionRow { + address: address as usize, + operands: operands(instruction_kind, word), + inline, + is_compressed, + }, + ) } -fn operands(instruction_kind: InstructionKind, word: u32) -> NormalizedOperands { +fn operands(instruction_kind: SourceInstructionKind, word: u32) -> NormalizedOperands { match instruction_kind { - InstructionKind::LUI | InstructionKind::AUIPC => format_u_operands(word), - InstructionKind::JAL => format_j_operands(word), - InstructionKind::BEQ - | InstructionKind::BNE - | InstructionKind::BLT - | InstructionKind::BGE - | InstructionKind::BLTU - | InstructionKind::BGEU - | InstructionKind::VirtualAssertEQ => format_b_operands(word), - InstructionKind::SB | InstructionKind::SH | InstructionKind::SW | InstructionKind::SD => { - format_s_operands(word) - } - InstructionKind::LB - | InstructionKind::LH - | InstructionKind::LW - | InstructionKind::LD - | InstructionKind::LBU - | InstructionKind::LHU - | InstructionKind::LWU => format_load_operands(word), - InstructionKind::LRW - | InstructionKind::LRD - | InstructionKind::SCW - | InstructionKind::SCD - | InstructionKind::AMOSWAPW - | InstructionKind::AMOSWAPD - | InstructionKind::AMOADDW - | InstructionKind::AMOADDD - | InstructionKind::AMOANDW - | InstructionKind::AMOANDD - | InstructionKind::AMOORW - | InstructionKind::AMOORD - | InstructionKind::AMOXORW - | InstructionKind::AMOXORD - | InstructionKind::AMOMINW - | InstructionKind::AMOMIND - | InstructionKind::AMOMAXW - | InstructionKind::AMOMAXD - | InstructionKind::AMOMINUW - | InstructionKind::AMOMINUD - | InstructionKind::AMOMAXUW - | InstructionKind::AMOMAXUD => format_r_operands(word), - InstructionKind::AdviceLB - | InstructionKind::AdviceLH - | InstructionKind::AdviceLW - | InstructionKind::AdviceLD => format_advice_load_operands(word), - InstructionKind::Inline => format_inline_operands(word), - InstructionKind::ECALL | InstructionKind::EBREAK | InstructionKind::MRET => { - format_i_operands(word) - } - InstructionKind::FENCE | InstructionKind::NoOp | InstructionKind::Unimpl => { - NormalizedOperands::default() - } + SourceInstructionKind::LUI | SourceInstructionKind::AUIPC => format_u_operands(word), + SourceInstructionKind::JAL => format_j_operands(word), + SourceInstructionKind::BEQ + | SourceInstructionKind::BNE + | SourceInstructionKind::BLT + | SourceInstructionKind::BGE + | SourceInstructionKind::BLTU + | SourceInstructionKind::BGEU + | SourceInstructionKind::VirtualAssertEQ => format_b_operands(word), + SourceInstructionKind::SB + | SourceInstructionKind::SH + | SourceInstructionKind::SW + | SourceInstructionKind::SD => format_s_operands(word), + SourceInstructionKind::LB + | SourceInstructionKind::LH + | SourceInstructionKind::LW + | SourceInstructionKind::LD + | SourceInstructionKind::LBU + | SourceInstructionKind::LHU + | SourceInstructionKind::LWU => format_load_operands(word), + SourceInstructionKind::LRW + | SourceInstructionKind::LRD + | SourceInstructionKind::SCW + | SourceInstructionKind::SCD + | SourceInstructionKind::AMOSWAPW + | SourceInstructionKind::AMOSWAPD + | SourceInstructionKind::AMOADDW + | SourceInstructionKind::AMOADDD + | SourceInstructionKind::AMOANDW + | SourceInstructionKind::AMOANDD + | SourceInstructionKind::AMOORW + | SourceInstructionKind::AMOORD + | SourceInstructionKind::AMOXORW + | SourceInstructionKind::AMOXORD + | SourceInstructionKind::AMOMINW + | SourceInstructionKind::AMOMIND + | SourceInstructionKind::AMOMAXW + | SourceInstructionKind::AMOMAXD + | SourceInstructionKind::AMOMINUW + | SourceInstructionKind::AMOMINUD + | SourceInstructionKind::AMOMAXUW + | SourceInstructionKind::AMOMAXUD => format_r_operands(word), + SourceInstructionKind::AdviceLB + | SourceInstructionKind::AdviceLH + | SourceInstructionKind::AdviceLW + | SourceInstructionKind::AdviceLD => format_advice_load_operands(word), + SourceInstructionKind::Inline => format_inline_operands(word), + SourceInstructionKind::ECALL + | SourceInstructionKind::EBREAK + | SourceInstructionKind::MRET => format_i_operands(word), + SourceInstructionKind::FENCE + | SourceInstructionKind::NoOp + | SourceInstructionKind::Unimpl => NormalizedOperands::default(), _ => format_i_or_r_operands(instruction_kind, word), } } -fn format_i_or_r_operands(instruction_kind: InstructionKind, word: u32) -> NormalizedOperands { +fn format_i_or_r_operands( + instruction_kind: SourceInstructionKind, + word: u32, +) -> NormalizedOperands { if uses_r_format(instruction_kind) { format_r_operands(word) } else { @@ -264,37 +284,37 @@ fn format_i_or_r_operands(instruction_kind: InstructionKind, word: u32) -> Norma } } -fn uses_r_format(instruction_kind: InstructionKind) -> bool { +fn uses_r_format(instruction_kind: SourceInstructionKind) -> bool { matches!( instruction_kind, - InstructionKind::ADD - | InstructionKind::SUB - | InstructionKind::SLL - | InstructionKind::SLT - | InstructionKind::SLTU - | InstructionKind::XOR - | InstructionKind::SRL - | InstructionKind::SRA - | InstructionKind::OR - | InstructionKind::AND - | InstructionKind::MUL - | InstructionKind::MULH - | InstructionKind::MULHSU - | InstructionKind::MULHU - | InstructionKind::DIV - | InstructionKind::DIVU - | InstructionKind::REM - | InstructionKind::REMU - | InstructionKind::ADDW - | InstructionKind::SUBW - | InstructionKind::SLLW - | InstructionKind::DIVW - | InstructionKind::SRLW - | InstructionKind::SRAW - | InstructionKind::MULW - | InstructionKind::DIVUW - | InstructionKind::REMW - | InstructionKind::REMUW + SourceInstructionKind::ADD + | SourceInstructionKind::SUB + | SourceInstructionKind::SLL + | SourceInstructionKind::SLT + | SourceInstructionKind::SLTU + | SourceInstructionKind::XOR + | SourceInstructionKind::SRL + | SourceInstructionKind::SRA + | SourceInstructionKind::OR + | SourceInstructionKind::AND + | SourceInstructionKind::MUL + | SourceInstructionKind::MULH + | SourceInstructionKind::MULHSU + | SourceInstructionKind::MULHU + | SourceInstructionKind::DIV + | SourceInstructionKind::DIVU + | SourceInstructionKind::REM + | SourceInstructionKind::REMU + | SourceInstructionKind::ADDW + | SourceInstructionKind::SUBW + | SourceInstructionKind::SLLW + | SourceInstructionKind::DIVW + | SourceInstructionKind::SRLW + | SourceInstructionKind::SRAW + | SourceInstructionKind::MULW + | SourceInstructionKind::DIVUW + | SourceInstructionKind::REMW + | SourceInstructionKind::REMUW ) } @@ -384,15 +404,16 @@ fn format_inline_operands(word: u32) -> NormalizedOperands { rd: Some(rd(word)), rs1: Some(rs1(word)), rs2: Some(rs2(word)), - imm: inline_metadata(word) as i128, + imm: 0, } } -fn inline_metadata(word: u32) -> u32 { - let opcode = word & 0x7f; - let funct3 = funct3(word); - let funct7 = funct7(word); - opcode | (funct3 << 7) | (funct7 << 10) +fn source_inline_key(word: u32) -> SourceInlineKey { + SourceInlineKey { + opcode: (word & 0x7f) as u8, + funct3: funct3(word) as u8, + funct7: funct7(word) as u8, + } } fn rd(word: u32) -> u8 { diff --git a/crates/jolt-program/src/image/elf.rs b/crates/jolt-program/src/image/elf.rs index a5632b33ad..ce82b04cb4 100644 --- a/crates/jolt-program/src/image/elf.rs +++ b/crates/jolt-program/src/image/elf.rs @@ -1,5 +1,5 @@ use common::constants::RAM_START_ADDRESS; -use jolt_riscv::{uncompress_rv64_instruction, NormalizedInstruction}; +use jolt_riscv::{uncompress_rv64_instruction, JoltInstructionProfile, SourceInstruction}; use object::{Object, ObjectSection, SectionKind}; use std::collections::BTreeMap; @@ -10,10 +10,14 @@ use crate::ProgramError; /// The instruction rows here match the executable text after RV64 decoding and /// compressed-instruction normalization. They have not been expanded into Jolt /// bytecode yet. -#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct Rv64ProgramImage { - /// Source instruction rows decoded from executable text sections. - pub instructions: Vec, + /// Source instructions decoded from executable text sections. + pub instructions: Vec, /// Initial byte values for memory-backed ELF sections. pub memory_init: Vec<(u64, u8)>, /// End address of the loaded program image. @@ -22,7 +26,10 @@ pub struct Rv64ProgramImage { pub entry_address: u64, } -pub fn decode_elf(elf: &[u8]) -> Result { +pub fn decode_elf( + elf: &[u8], + profile: JoltInstructionProfile, +) -> Result { let obj = object::File::parse(elf).map_err(|_| ProgramError::MalformedImage("invalid ELF object"))?; if let object::File::Elf32(_) = &obj { @@ -71,7 +78,7 @@ pub fn decode_elf(elf: &[u8]) -> Result { let raw_data: Vec<_> = (start..end) .map(|address| memory_image.get(&address).copied().unwrap_or(0)) .collect(); - decode_text_section(start, &raw_data, &mut instructions)?; + decode_text_section(start, &raw_data, &mut instructions, profile)?; } Ok(Rv64ProgramImage { @@ -100,7 +107,8 @@ fn merge_ranges(mut ranges: Vec<(u64, u64)>) -> Vec<(u64, u64)> { fn decode_text_section( section_address: u64, raw_data: &[u8], - instructions: &mut Vec, + instructions: &mut Vec, + profile: JoltInstructionProfile, ) -> Result<(), ProgramError> { let mut offset = 0; while offset < raw_data.len() { @@ -115,7 +123,7 @@ fn decode_text_section( if (first_halfword & 0b11) != 0b11 { if first_halfword != 0 { let word = uncompress_rv64_instruction(first_halfword); - let instruction = super::decode::decode_instruction(word, address, true)?; + let instruction = super::decode::decode_instruction(word, address, true, profile)?; instructions.push(instruction); } offset += 2; @@ -134,7 +142,7 @@ fn decode_text_section( raw_data[offset + 2], raw_data[offset + 3], ]); - let instruction = super::decode::decode_instruction(word, address, false)?; + let instruction = super::decode::decode_instruction(word, address, false, profile)?; instructions.push(instruction); offset += 4; } diff --git a/crates/jolt-program/src/image/mod.rs b/crates/jolt-program/src/image/mod.rs index 1d623cff5b..e86c1c8850 100644 --- a/crates/jolt-program/src/image/mod.rs +++ b/crates/jolt-program/src/image/mod.rs @@ -1,8 +1,7 @@ //! RV64 program-image decoding. //! -//! This module owns the architecture gate for the new program pipeline. ELF32 -//! and RV32 inputs are unsupported here even if tracer keeps historical RV32 -//! execution branches internally. +//! This module owns the architecture gate for the program pipeline. ELF32 and +//! RV32 inputs are unsupported. pub mod decode; pub mod elf; diff --git a/crates/jolt-program/src/lib.rs b/crates/jolt-program/src/lib.rs index 5d864a7cf4..e669c035e7 100644 --- a/crates/jolt-program/src/lib.rs +++ b/crates/jolt-program/src/lib.rs @@ -1,8 +1,7 @@ //! Program image, bytecode expansion, and preprocessing pipeline for Jolt. //! //! This crate's program-construction pipeline is RV64-only. ELF32/RV32 inputs -//! are rejected at the image boundary; historical RV32 execution code may remain -//! in `tracer`, but it is not part of the verifier-facing `jolt-program` path. +//! are rejected at the image boundary. pub mod error; pub mod execution; diff --git a/crates/jolt-program/src/preprocess/bytecode.rs b/crates/jolt-program/src/preprocess/bytecode.rs index a619f181b1..36baaf5d3a 100644 --- a/crates/jolt-program/src/preprocess/bytecode.rs +++ b/crates/jolt-program/src/preprocess/bytecode.rs @@ -1,23 +1,23 @@ +#[cfg(feature = "serialization")] use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use common::constants::{ALIGNMENT_FACTOR_BYTECODE, RAM_START_ADDRESS}; -use jolt_riscv::{InstructionKind, NormalizedInstruction}; +use jolt_riscv::{JoltInstructionKind, JoltInstructionProfile, JoltInstructionRow}; use crate::preprocess::PreprocessingError; -#[derive( - Default, - Debug, - Clone, - PartialEq, - Eq, - CanonicalSerialize, - CanonicalDeserialize, - serde::Serialize, - serde::Deserialize, +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive( + CanonicalSerialize, + CanonicalDeserialize, + serde::Serialize, + serde::Deserialize + ) )] pub struct BytecodePreprocessing { pub code_size: usize, - pub bytecode: Vec, + pub bytecode: Vec, /// Maps each unexpanded instruction address to its virtual bytecode index. pub pc_map: BytecodePCMapper, pub entry_address: u64, @@ -25,9 +25,17 @@ pub struct BytecodePreprocessing { impl BytecodePreprocessing { pub fn preprocess( - mut bytecode: Vec, + mut bytecode: Vec, entry_address: u64, + profile: JoltInstructionProfile, ) -> Result { + for instruction in &bytecode { + if !profile.supports_jolt(instruction.instruction_kind) { + return Err(PreprocessingError::IllegalTargetInstruction( + instruction.instruction_kind, + )); + } + } bytecode.insert(0, noop_instruction()); let pc_map = BytecodePCMapper::try_new(&bytecode)?; @@ -43,11 +51,11 @@ impl BytecodePreprocessing { } pub fn entry_bytecode_index(&self) -> Option { - self.pc_map.get_pc(self.entry_address as usize, 0) + self.pc_map.get_first_pc(self.entry_address as usize) } - pub fn get_pc(&self, instruction: &NormalizedInstruction) -> Option { - if instruction.instruction_kind == InstructionKind::NoOp { + pub fn get_pc(&self, instruction: &JoltInstructionRow) -> Option { + if instruction.instruction_kind == JoltInstructionKind::NoOp { return Some(0); } self.pc_map.get_pc( @@ -57,24 +65,23 @@ impl BytecodePreprocessing { } } -#[derive( - Default, - Debug, - Clone, - PartialEq, - Eq, - CanonicalSerialize, - CanonicalDeserialize, - serde::Serialize, - serde::Deserialize, +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive( + CanonicalSerialize, + CanonicalDeserialize, + serde::Serialize, + serde::Deserialize + ) )] pub struct BytecodePCMapper { indices: Vec>, } impl BytecodePCMapper { - pub fn try_new(bytecode: &[NormalizedInstruction]) -> Result { - let mut indices = vec![Vec::new(); Self::index_count(bytecode)]; + pub fn try_new(bytecode: &[JoltInstructionRow]) -> Result { + let mut indices = vec![Vec::new(); Self::index_count(bytecode)?]; let mut last_pc = 0; indices[0].push((0, last_pc)); @@ -84,7 +91,7 @@ impl BytecodePCMapper { } last_pc += 1; - let bytecode_index = Self::get_index(instruction.address); + let bytecode_index = Self::try_get_index(instruction.address)?; indices[bytecode_index] .push((instruction.virtual_sequence_remaining.unwrap_or(0), last_pc)); } @@ -97,13 +104,31 @@ impl BytecodePCMapper { } pub fn get_pc(&self, address: usize, virtual_sequence_remaining: u16) -> Option { - let index = Self::get_index(address); + let index = Self::try_get_index(address).ok()?; self.indices .get(index)? .iter() .find_map(|(sequence, pc)| (*sequence == virtual_sequence_remaining).then_some(*pc)) } + pub fn get_first_pc(&self, address: usize) -> Option { + let index = if address == 0 { + 0 + } else { + Self::try_get_index(address).ok()? + }; + self.indices.get(index)?.first().map(|(_sequence, pc)| *pc) + } + + fn try_get_index(address: usize) -> Result { + if address < RAM_START_ADDRESS as usize + || !address.is_multiple_of(ALIGNMENT_FACTOR_BYTECODE) + { + return Err(PreprocessingError::InvalidBytecodeAddress(address)); + } + Ok(Self::get_index(address)) + } + pub const fn get_index(address: usize) -> usize { assert!(address >= RAM_START_ADDRESS as usize); assert!(address.is_multiple_of(ALIGNMENT_FACTOR_BYTECODE)); @@ -148,23 +173,23 @@ impl BytecodePCMapper { Ok(()) } - fn index_count(bytecode: &[NormalizedInstruction]) -> usize { + fn index_count(bytecode: &[JoltInstructionRow]) -> Result { let max_address = bytecode .iter() .map(|instruction| instruction.address) .max() .unwrap_or(0); if max_address == 0 { - 1 + Ok(1) } else { - Self::get_index(max_address) + 1 + Ok(Self::try_get_index(max_address)? + 1) } } } -const fn noop_instruction() -> NormalizedInstruction { - NormalizedInstruction { - instruction_kind: InstructionKind::NoOp, +const fn noop_instruction() -> JoltInstructionRow { + JoltInstructionRow { + instruction_kind: JoltInstructionKind::NoOp, address: 0, operands: jolt_riscv::NormalizedOperands { rs1: None, @@ -181,7 +206,10 @@ const fn noop_instruction() -> NormalizedInstruction { #[cfg(test)] #[expect(clippy::unwrap_used)] mod tests { - use jolt_riscv::{InstructionKind, NormalizedInstruction, NormalizedOperands}; + use jolt_riscv::{ + JoltInstructionKind, JoltInstructionProfile, JoltInstructionRow, NormalizedOperands, + SourceExtension, RV64IMAC_JOLT, + }; use super::{BytecodePCMapper, BytecodePreprocessing, PreprocessingError}; @@ -189,12 +217,13 @@ mod tests { fn preprocess_prepends_and_pads_bytecode() { let bytecode = vec![instruction(0x8000_0000, None)]; - let preprocessing = BytecodePreprocessing::preprocess(bytecode, 0x8000_0000).unwrap(); + let preprocessing = + BytecodePreprocessing::preprocess(bytecode, 0x8000_0000, RV64IMAC_JOLT).unwrap(); assert_eq!(preprocessing.code_size, 2); assert_eq!( preprocessing.bytecode[0].instruction_kind, - InstructionKind::NoOp + JoltInstructionKind::NoOp ); assert_eq!(preprocessing.entry_bytecode_index(), Some(1)); } @@ -207,9 +236,10 @@ mod tests { instruction(0x8000_0004, Some(0)), ]; - let preprocessing = BytecodePreprocessing::preprocess(bytecode, 0x8000_0004).unwrap(); + let preprocessing = + BytecodePreprocessing::preprocess(bytecode, 0x8000_0004, RV64IMAC_JOLT).unwrap(); - assert_eq!(preprocessing.entry_bytecode_index(), Some(3)); + assert_eq!(preprocessing.entry_bytecode_index(), Some(1)); assert_eq!( preprocessing.get_pc(&instruction(0x8000_0004, Some(2))), Some(1) @@ -264,12 +294,17 @@ mod tests { ); } - fn instruction( - address: usize, - virtual_sequence_remaining: Option, - ) -> NormalizedInstruction { - NormalizedInstruction { - instruction_kind: InstructionKind::ADDI, + #[test] + fn rejects_invalid_bytecode_addresses() { + let bytecode = vec![instruction(0x7fff_fffc, None)]; + + let err = BytecodePCMapper::try_new(&bytecode).unwrap_err(); + assert_eq!(err, PreprocessingError::InvalidBytecodeAddress(0x7fff_fffc)); + } + + fn instruction(address: usize, virtual_sequence_remaining: Option) -> JoltInstructionRow { + JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, address, operands: NormalizedOperands { rd: Some(1), @@ -282,4 +317,22 @@ mod tests { is_compressed: false, } } + + #[test] + fn rejects_profile_illegal_target_rows() { + const RV64I_ONLY: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[SourceExtension::Rv64I], + inline_extensions: &[], + }; + + let mut row = instruction(0x8000_0000, None); + row.instruction_kind = JoltInstructionKind::MUL; + + let err = + BytecodePreprocessing::preprocess(vec![row], 0x8000_0000, RV64I_ONLY).unwrap_err(); + assert_eq!( + err, + PreprocessingError::IllegalTargetInstruction(JoltInstructionKind::MUL) + ); + } } diff --git a/crates/jolt-program/src/preprocess/error.rs b/crates/jolt-program/src/preprocess/error.rs index a25340e000..7951c70f29 100644 --- a/crates/jolt-program/src/preprocess/error.rs +++ b/crates/jolt-program/src/preprocess/error.rs @@ -1,5 +1,9 @@ #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum PreprocessingError { + #[error("bytecode instruction is not legal in the selected target profile: {0:?}")] + IllegalTargetInstruction(jolt_riscv::JoltInstructionKind), + #[error("bytecode instruction address is invalid for bytecode indexing: {0:#x}")] + InvalidBytecodeAddress(usize), #[error( "bytecode has invalid inline sequence at index {bytecode_index} (address {address:#x}): previous sequence {previous_sequence}, expected next sequence {expected_sequence}, new sequence {new_sequence}" )] diff --git a/crates/jolt-program/src/preprocess/mod.rs b/crates/jolt-program/src/preprocess/mod.rs index a4fd5136f9..0f1c1ba131 100644 --- a/crates/jolt-program/src/preprocess/mod.rs +++ b/crates/jolt-program/src/preprocess/mod.rs @@ -1,9 +1,11 @@ pub mod bytecode; pub mod error; pub mod program; +pub mod public_io; pub mod ram; pub use bytecode::{BytecodePCMapper, BytecodePreprocessing}; pub use error::PreprocessingError; pub use program::JoltProgramPreprocessing; -pub use ram::RAMPreprocessing; +pub use public_io::{PublicIoMemory, PublicMemorySegment}; +pub use ram::{PublicInitialRam, RAMPreprocessing}; diff --git a/crates/jolt-program/src/preprocess/program.rs b/crates/jolt-program/src/preprocess/program.rs index 2a31e5dda9..895f9e4f88 100644 --- a/crates/jolt-program/src/preprocess/program.rs +++ b/crates/jolt-program/src/preprocess/program.rs @@ -1,11 +1,15 @@ use common::jolt_device::MemoryLayout; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::{JoltInstructionProfile, JoltInstructionRow}; use crate::preprocess::{ bytecode::BytecodePreprocessing, ram::RAMPreprocessing, PreprocessingError, }; -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] pub struct JoltProgramPreprocessing { pub bytecode: BytecodePreprocessing, pub ram: RAMPreprocessing, @@ -15,14 +19,15 @@ pub struct JoltProgramPreprocessing { impl JoltProgramPreprocessing { pub fn new( - bytecode: Vec, + bytecode: Vec, memory_init: Vec<(u64, u8)>, memory_layout: MemoryLayout, entry_address: u64, max_padded_trace_length: usize, + profile: JoltInstructionProfile, ) -> Result { Ok(Self { - bytecode: BytecodePreprocessing::preprocess(bytecode, entry_address)?, + bytecode: BytecodePreprocessing::preprocess(bytecode, entry_address, profile)?, ram: RAMPreprocessing::preprocess(memory_init), memory_layout, max_padded_trace_length, diff --git a/crates/jolt-program/src/preprocess/public_io.rs b/crates/jolt-program/src/preprocess/public_io.rs new file mode 100644 index 0000000000..4c02130fcc --- /dev/null +++ b/crates/jolt-program/src/preprocess/public_io.rs @@ -0,0 +1,112 @@ +use common::{ + constants::RAM_START_ADDRESS, + jolt_device::{JoltDevice, MemoryLayoutError}, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicMemorySegment { + pub start_index: usize, + pub words: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicIoMemory { + pub segments: Vec, + pub io_mask_start: u128, + pub io_mask_end: u128, + pub io_len_words: usize, +} + +impl PublicIoMemory { + pub fn new(public_io: &JoltDevice) -> Result { + let layout = &public_io.memory_layout; + let io_mask_start = layout.remapped_word_address(layout.input_start)? as u128; + let io_mask_end = layout.remapped_word_address(RAM_START_ADDRESS)? as u128; + let io_len_words = (io_mask_end as usize).next_power_of_two().max(1); + let mut segments = Vec::new(); + + if !public_io.inputs.is_empty() { + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(layout.input_start)? as usize, + words: public_io.input_words_le(), + }); + } + + if !public_io.outputs.is_empty() { + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(layout.output_start)? as usize, + words: public_io.output_words_le(), + }); + } + + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(layout.panic)? as usize, + words: vec![public_io.panic as u64], + }); + + if !public_io.panic { + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(layout.termination)? as usize, + words: vec![1], + }); + } + + Ok(Self { + segments, + io_mask_start, + io_mask_end, + io_len_words, + }) + } + + pub fn io_num_vars(&self) -> usize { + self.io_len_words.ilog2() as usize + } +} + +#[cfg(test)] +mod tests { + #![expect(clippy::expect_used, reason = "tests should fail loudly")] + + use super::*; + use common::jolt_device::{JoltDevice, MemoryConfig}; + + #[test] + fn materializes_public_io_segments() { + let mut device = JoltDevice::new(&MemoryConfig { + program_size: Some(1024), + max_trusted_advice_size: 0, + max_untrusted_advice_size: 0, + max_input_size: 16, + max_output_size: 16, + ..Default::default() + }); + device.inputs = vec![42]; + device.outputs = vec![7]; + + let memory = PublicIoMemory::new(&device).expect("public IO memory should materialize"); + + assert_eq!(memory.segments.len(), 4); + assert_eq!(memory.segments[0].words, vec![42]); + assert_eq!(memory.segments[1].words, vec![7]); + assert_eq!(memory.segments[2].words, vec![0]); + assert_eq!(memory.segments[3].words, vec![1]); + assert!(memory.io_mask_start < memory.io_mask_end); + } + + #[test] + fn omits_termination_word_on_panic() { + let mut device = JoltDevice::new(&MemoryConfig { + program_size: Some(1024), + max_trusted_advice_size: 0, + max_untrusted_advice_size: 0, + ..Default::default() + }); + device.panic = true; + + let memory = PublicIoMemory::new(&device).expect("public IO memory should materialize"); + + assert_eq!(memory.segments.len(), 1); + assert_eq!(memory.segments[0].words, vec![1]); + } +} diff --git a/crates/jolt-program/src/preprocess/ram.rs b/crates/jolt-program/src/preprocess/ram.rs index 7d01e6467a..66efed6b4d 100644 --- a/crates/jolt-program/src/preprocess/ram.rs +++ b/crates/jolt-program/src/preprocess/ram.rs @@ -1,16 +1,19 @@ +#[cfg(feature = "serialization")] use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use common::constants::BYTES_PER_INSTRUCTION; +use common::jolt_device::{JoltDevice, MemoryLayoutError}; -#[derive( - Default, - Debug, - Clone, - PartialEq, - Eq, - CanonicalSerialize, - CanonicalDeserialize, - serde::Serialize, - serde::Deserialize, +use super::public_io::PublicMemorySegment; + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive( + CanonicalSerialize, + CanonicalDeserialize, + serde::Serialize, + serde::Deserialize + ) )] pub struct RAMPreprocessing { pub min_bytecode_address: u64, @@ -54,9 +57,38 @@ impl RAMPreprocessing { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicInitialRam { + pub segments: Vec, +} + +impl PublicInitialRam { + pub fn new(ram: &RAMPreprocessing, public_io: &JoltDevice) -> Result { + let layout = &public_io.memory_layout; + let mut segments = Vec::new(); + + if !ram.bytecode_words.is_empty() { + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(ram.min_bytecode_address)? as usize, + words: ram.bytecode_words.clone(), + }); + } + + if !public_io.inputs.is_empty() { + segments.push(PublicMemorySegment { + start_index: layout.remapped_word_address(layout.input_start)? as usize, + words: public_io.input_words_le(), + }); + } + + Ok(Self { segments }) + } +} + #[cfg(test)] mod tests { - use super::RAMPreprocessing; + use super::{PublicInitialRam, RAMPreprocessing}; + use common::jolt_device::{JoltDevice, MemoryConfig}; #[test] fn preprocesses_memory_bytes_into_words() { @@ -70,4 +102,33 @@ mod tests { assert_eq!(preprocessing.bytecode_words[0], 0x0201); assert_eq!(preprocessing.bytecode_words[1], 0x03); } + + #[test] + fn materializes_public_initial_ram_segments() { + let mut device = JoltDevice::new(&MemoryConfig { + program_size: Some(1024), + max_trusted_advice_size: 0, + max_untrusted_advice_size: 0, + max_input_size: 16, + ..Default::default() + }); + device.inputs = vec![0x2a, 0, 0, 0, 0, 0, 0, 0, 0x07]; + let preprocessing = RAMPreprocessing { + min_bytecode_address: 0x8000_0000, + bytecode_words: vec![0x0201, 0x03], + }; + + let initial = PublicInitialRam::new(&preprocessing, &device); + assert!( + initial.is_ok(), + "initial RAM should materialize: {initial:?}" + ); + let Ok(initial) = initial else { + return; + }; + + assert_eq!(initial.segments.len(), 2); + assert_eq!(initial.segments[0].words, vec![0x0201, 0x03]); + assert_eq!(initial.segments[1].words, vec![0x2a, 0x07]); + } } diff --git a/crates/jolt-r1cs/Cargo.toml b/crates/jolt-r1cs/Cargo.toml index a5fa80995a..4795d5e5d9 100644 --- a/crates/jolt-r1cs/Cargo.toml +++ b/crates/jolt-r1cs/Cargo.toml @@ -9,10 +9,12 @@ description = "R1CS constraint data structures for the Jolt proving system" workspace = true [dependencies] +jolt-claims.workspace = true jolt-field = { path = "../jolt-field" } jolt-poly = { path = "../jolt-poly" } rayon = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } tracing = { workspace = true } [dev-dependencies] diff --git a/crates/jolt-r1cs/src/builder.rs b/crates/jolt-r1cs/src/builder.rs new file mode 100644 index 0000000000..efb1f81608 --- /dev/null +++ b/crates/jolt-r1cs/src/builder.rs @@ -0,0 +1,414 @@ +use std::collections::BTreeMap; +use std::ops::{Add, Neg, Sub}; + +use jolt_field::Field; +use thiserror::Error; + +use crate::constraint::SparseRow; +use crate::ConstraintMatrices; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Variable(usize); + +impl Variable { + pub const ONE: Self = Self(0); + + pub const fn new(index: usize) -> Self { + Self(index) + } + + pub const fn index(self) -> usize { + self.0 + } +} + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum R1csBuilderError { + #[error("missing witness value for variable {variable:?}")] + MissingWitnessValue { variable: Variable }, + #[error("variable {variable:?} is out of bounds for witness with {num_vars} variables")] + VariableOutOfBounds { variable: Variable, num_vars: usize }, + #[error("cannot assign the constant-one variable")] + CannotAssignOne, + #[error("variable {variable:?} already has a witness value")] + WitnessAlreadyAssigned { variable: Variable }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinearCombination { + pub terms: Vec<(Variable, F)>, +} + +impl LinearCombination { + pub fn zero() -> Self { + Self { terms: Vec::new() } + } +} + +impl LinearCombination { + pub fn one() -> Self { + Self::constant(F::one()) + } + + pub fn constant(value: F) -> Self { + if value.is_zero() { + Self::zero() + } else { + Self { + terms: vec![(Variable::ONE, value)], + } + } + } + + pub fn variable(variable: Variable) -> Self { + Self { + terms: vec![(variable, F::one())], + } + } + + pub fn scale(mut self, scale: F) -> Self { + if scale.is_zero() { + return Self::zero(); + } + for (_, coefficient) in &mut self.terms { + *coefficient *= scale; + } + self + } + + pub fn evaluate(&self, witness: &[Option]) -> Result { + let mut result = F::zero(); + for &(variable, coefficient) in &self.terms { + let value = witness + .get(variable.index()) + .copied() + .flatten() + .ok_or(R1csBuilderError::MissingWitnessValue { variable })?; + result += coefficient * value; + } + Ok(result) + } + + pub fn into_sparse_row(self) -> SparseRow { + let mut terms = BTreeMap::::new(); + for (variable, coefficient) in self.terms { + if coefficient.is_zero() { + continue; + } + let _ = terms + .entry(variable.index()) + .and_modify(|existing| *existing += coefficient) + .or_insert(coefficient); + } + terms + .into_iter() + .filter(|(_, coefficient)| !coefficient.is_zero()) + .collect() + } +} + +impl From for LinearCombination { + fn from(variable: Variable) -> Self { + Self::variable(variable) + } +} + +impl Add for LinearCombination { + type Output = Self; + + fn add(mut self, mut rhs: Self) -> Self::Output { + self.terms.append(&mut rhs.terms); + self + } +} + +impl Sub for LinearCombination { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + -rhs + } +} + +impl Neg for LinearCombination { + type Output = Self; + + fn neg(mut self) -> Self::Output { + for (_, coefficient) in &mut self.terms { + *coefficient = -*coefficient; + } + self + } +} + +#[derive(Clone, Debug)] +pub struct R1csBuilder { + witness: Vec>, + a: Vec>, + b: Vec>, + c: Vec>, +} + +impl Default for R1csBuilder { + fn default() -> Self { + Self::new() + } +} + +impl R1csBuilder { + pub fn new() -> Self { + Self { + witness: vec![Some(F::one())], + a: Vec::new(), + b: Vec::new(), + c: Vec::new(), + } + } + + pub fn alloc(&mut self, value: F) -> Variable { + self.alloc_witness(Some(value)) + } + + pub fn alloc_unknown(&mut self) -> Variable { + self.alloc_witness(None) + } + + pub fn alloc_witness(&mut self, value: Option) -> Variable { + let variable = Variable::new(self.witness.len()); + self.witness.push(value); + variable + } + + pub fn num_vars(&self) -> usize { + self.witness.len() + } + + pub fn assign(&mut self, variable: Variable, value: F) -> Result<(), R1csBuilderError> { + if variable == Variable::ONE { + return Err(R1csBuilderError::CannotAssignOne); + } + + let num_vars = self.witness.len(); + let slot = self + .witness + .get_mut(variable.index()) + .ok_or(R1csBuilderError::VariableOutOfBounds { variable, num_vars })?; + if slot.is_some() { + return Err(R1csBuilderError::WitnessAlreadyAssigned { variable }); + } + + *slot = Some(value); + Ok(()) + } + + pub fn witness(&self) -> Result, R1csBuilderError> { + self.witness + .iter() + .enumerate() + .map(|(index, value)| { + value.ok_or(R1csBuilderError::MissingWitnessValue { + variable: Variable::new(index), + }) + }) + .collect() + } + + pub fn assert_product(&mut self, lhs: Lhs, rhs: Rhs, output: Output) + where + Lhs: Into>, + Rhs: Into>, + Output: Into>, + { + let lhs = lhs.into(); + let rhs = rhs.into(); + let output = output.into(); + self.assert_known_variables(&lhs); + self.assert_known_variables(&rhs); + self.assert_known_variables(&output); + self.a.push(lhs.into_sparse_row()); + self.b.push(rhs.into_sparse_row()); + self.c.push(output.into_sparse_row()); + } + + pub fn assert_zero(&mut self, value: Value) + where + Value: Into>, + { + self.assert_product(value, LinearCombination::one(), LinearCombination::zero()); + } + + pub fn assert_equal(&mut self, lhs: Lhs, rhs: Rhs) + where + Lhs: Into>, + Rhs: Into>, + { + let lhs = lhs.into(); + let rhs = rhs.into(); + self.assert_zero(lhs - rhs); + } + + pub fn multiply(&mut self, lhs: Lhs, rhs: Rhs) -> LinearCombination + where + Lhs: Into>, + Rhs: Into>, + { + let lhs = lhs.into(); + let rhs = rhs.into(); + let value = lhs + .evaluate(&self.witness) + .ok() + .zip(rhs.evaluate(&self.witness).ok()) + .map(|(lhs, rhs)| lhs * rhs); + let output = self.alloc_witness(value); + let output = LinearCombination::variable(output); + self.assert_product(lhs, rhs, output.clone()); + output + } + + pub fn into_matrices(self) -> ConstraintMatrices { + ConstraintMatrices::new(self.a.len(), self.witness.len(), self.a, self.b, self.c) + } + + fn assert_known_variables(&self, linear_combination: &LinearCombination) { + for &(variable, _) in &linear_combination.terms { + assert!( + variable.index() < self.witness.len(), + "R1CS linear combination references variable {} but builder has {} variables", + variable.index(), + self.witness.len() + ); + } + } +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + #[test] + fn builder_checks_satisfied_product() { + let mut builder = R1csBuilder::::new(); + let x = builder.alloc(Fr::from_u64(3)); + let y = builder.alloc(Fr::from_u64(9)); + + builder.assert_product(x, x, y); + + let witness = builder.witness().expect("witness is assigned"); + let matrices = builder.into_matrices(); + assert!(matrices.check_witness(&witness).is_ok()); + } + + #[test] + fn linear_combination_dedupes_sparse_row_terms() { + let variable = Variable::new(4); + let row = (LinearCombination::::variable(variable) + + LinearCombination::variable(variable).scale(Fr::from_u64(3)) + - LinearCombination::variable(variable).scale(Fr::from_u64(4))) + .into_sparse_row(); + + assert!(row.is_empty()); + } + + #[test] + #[should_panic(expected = "R1CS linear combination references variable 99")] + fn assert_product_rejects_out_of_bounds_variable_before_matrix_conversion() { + let mut builder = R1csBuilder::::new(); + builder.assert_product( + Variable::new(99), + LinearCombination::one(), + LinearCombination::zero(), + ); + } + + #[test] + fn multiply_allocates_intermediate_witness() { + let mut builder = R1csBuilder::::new(); + let x = builder.alloc(Fr::from_u64(4)); + let y = builder.alloc(Fr::from_u64(5)); + + let product = builder.multiply(x, y); + + let witness = builder.witness().expect("witness is assigned"); + assert_eq!( + product.evaluate(&witness.iter().copied().map(Some).collect::>()), + Ok(Fr::from_u64(20)) + ); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn missing_witness_delays_intermediate_assignment() { + let mut builder = R1csBuilder::::new(); + let x = builder.alloc(Fr::from_u64(4)); + let y = builder.alloc_unknown(); + + let product = builder.multiply(x, y); + + assert_eq!( + builder.witness(), + Err(R1csBuilderError::MissingWitnessValue { variable: y }) + ); + assert_eq!( + product.evaluate(&builder.witness), + Err(R1csBuilderError::MissingWitnessValue { + variable: Variable::new(3) + }) + ); + } + + #[test] + fn assign_fills_unknown_witness_value() { + let mut builder = R1csBuilder::::new(); + let variable = builder.alloc_unknown(); + + builder + .assign(variable, Fr::from_u64(17)) + .expect("assignment succeeds"); + + let witness = builder.witness().expect("witness is assigned"); + assert_eq!(witness[variable.index()], Fr::from_u64(17)); + } + + #[test] + fn assign_rejects_constant_column() { + let mut builder = R1csBuilder::::new(); + + let error = builder + .assign(Variable::ONE, Fr::from_u64(2)) + .expect_err("constant column is not assignable"); + + assert_eq!(error, R1csBuilderError::CannotAssignOne); + } + + #[test] + fn assign_rejects_already_assigned_variable() { + let mut builder = R1csBuilder::::new(); + let variable = builder.alloc(Fr::from_u64(9)); + + let error = builder + .assign(variable, Fr::from_u64(10)) + .expect_err("assigned variable cannot be overwritten"); + + assert_eq!(error, R1csBuilderError::WitnessAlreadyAssigned { variable }); + } + + #[test] + fn assign_rejects_out_of_bounds_variable() { + let mut builder = R1csBuilder::::new(); + let variable = Variable::new(7); + + let error = builder + .assign(variable, Fr::from_u64(10)) + .expect_err("variable is out of bounds"); + + assert_eq!( + error, + R1csBuilderError::VariableOutOfBounds { + variable, + num_vars: 1, + } + ); + } +} diff --git a/crates/jolt-r1cs/src/constraint.rs b/crates/jolt-r1cs/src/constraint.rs index 7222b4c352..5ba0d2b510 100644 --- a/crates/jolt-r1cs/src/constraint.rs +++ b/crates/jolt-r1cs/src/constraint.rs @@ -2,10 +2,23 @@ use jolt_field::Field; use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; /// Sparse row: `[(variable_index, coefficient)]`. pub type SparseRow = Vec<(usize, F)>; +#[derive(Clone, Debug, ThisError, PartialEq, Eq)] +pub enum ConstraintMatrixEvalError { + #[error("row weights length mismatch: expected at least {expected}, got {actual}")] + RowWeightsLengthMismatch { expected: usize, actual: usize }, + #[error("column weights length mismatch: expected {expected}, got {actual}")] + ColumnWeightsLengthMismatch { expected: usize, actual: usize }, + #[error("column {column} out of bounds for {num_vars} variables")] + ColumnOutOfBounds { column: usize, num_vars: usize }, + #[error("matrix column range overflow: start {start}, count {count}")] + ColumnRangeOverflow { start: usize, count: usize }, +} + /// Per-cycle sparse R1CS constraint matrices. /// /// Represents the local A, B, C matrices for a single cycle in a uniform @@ -30,6 +43,20 @@ pub struct ConstraintMatrices { pub c: Vec>, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct WeightedMatrixColumns { + pub a: Vec, + pub b: Vec, + pub c: Vec, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MatrixColumnContributions { + pub a: F, + pub b: F, + pub c: F, +} + /// Deserialization helper; never exposed directly. #[derive(Deserialize)] #[serde(bound = "")] @@ -135,6 +162,91 @@ impl ConstraintMatrices { } Ok(()) } + + pub fn public_column_contributions( + &self, + row_weights: &[F], + column: usize, + scalar: F, + ) -> Result, ConstraintMatrixEvalError> { + Ok(MatrixColumnContributions { + a: matrix_column_eval(&self.a, row_weights, column)? * scalar, + b: matrix_column_eval(&self.b, row_weights, column)? * scalar, + c: matrix_column_eval(&self.c, row_weights, column)? * scalar, + }) + } + + pub fn weighted_columns( + &self, + row_weights: &[F], + columns: &[usize], + ) -> Result, ConstraintMatrixEvalError> { + if row_weights.len() < self.num_constraints { + return Err(ConstraintMatrixEvalError::RowWeightsLengthMismatch { + expected: self.num_constraints, + actual: row_weights.len(), + }); + } + + let mut weighted = WeightedMatrixColumns { + a: Vec::with_capacity(columns.len()), + b: Vec::with_capacity(columns.len()), + c: Vec::with_capacity(columns.len()), + }; + + for &column in columns { + if column >= self.num_vars { + return Err(ConstraintMatrixEvalError::ColumnOutOfBounds { + column, + num_vars: self.num_vars, + }); + } + + weighted + .a + .push(matrix_column_eval(&self.a, row_weights, column)?); + weighted + .b + .push(matrix_column_eval(&self.b, row_weights, column)?); + weighted + .c + .push(matrix_column_eval(&self.c, row_weights, column)?); + } + + Ok(weighted) + } + + pub fn linear_form_bilinear_eval( + &self, + row_weights: &[F], + column_weights: &[F], + start_col: usize, + col_count: usize, + weights: [F; 3], + ) -> Result { + let a = matrix_bilinear_eval_columns( + &self.a, + row_weights, + column_weights, + start_col, + col_count, + )?; + let b = matrix_bilinear_eval_columns( + &self.b, + row_weights, + column_weights, + start_col, + col_count, + )?; + let c = matrix_bilinear_eval_columns( + &self.c, + row_weights, + column_weights, + start_col, + col_count, + )?; + Ok(weights[0] * a + weights[1] * b + weights[2] * c) + } } #[inline] @@ -146,7 +258,69 @@ fn dot(row: &[(usize, F)], witness: &[F]) -> F { acc } +fn matrix_column_eval( + rows: &[SparseRow], + row_weights: &[F], + column: usize, +) -> Result { + if row_weights.len() < rows.len() { + return Err(ConstraintMatrixEvalError::RowWeightsLengthMismatch { + expected: rows.len(), + actual: row_weights.len(), + }); + } + + let mut acc = F::zero(); + for (row, &row_weight) in rows.iter().zip(row_weights) { + for &(col, coeff) in row { + if col == column { + acc += row_weight * coeff; + } + } + } + Ok(acc) +} + +fn matrix_bilinear_eval_columns( + rows: &[SparseRow], + row_weights: &[F], + column_weights: &[F], + start_col: usize, + col_count: usize, +) -> Result { + if row_weights.len() < rows.len() { + return Err(ConstraintMatrixEvalError::RowWeightsLengthMismatch { + expected: rows.len(), + actual: row_weights.len(), + }); + } + if column_weights.len() != col_count { + return Err(ConstraintMatrixEvalError::ColumnWeightsLengthMismatch { + expected: col_count, + actual: column_weights.len(), + }); + } + + let end_col = + start_col + .checked_add(col_count) + .ok_or(ConstraintMatrixEvalError::ColumnRangeOverflow { + start: start_col, + count: col_count, + })?; + let mut acc = F::zero(); + for (row, &row_weight) in rows.iter().zip(row_weights) { + for &(col, coeff) in row { + if (start_col..end_col).contains(&col) { + acc += row_weight * column_weights[col - start_col] * coeff; + } + } + } + Ok(acc) +} + #[cfg(test)] +#[expect(clippy::expect_used, reason = "tests should fail loudly")] mod tests { use super::*; use jolt_field::{Fr, FromPrimitiveInt}; @@ -201,4 +375,169 @@ mod tests { vec![vec![(2, Fr::from_u64(1))]], ); } + + #[test] + fn public_column_contributions_projects_abc_column() { + let m = ConstraintMatrices::new( + 2, + 3, + vec![ + vec![(0, Fr::from_u64(2)), (1, Fr::from_u64(99))], + vec![(0, Fr::from_u64(3))], + ], + vec![vec![(0, Fr::from_u64(5))], vec![(2, Fr::from_u64(13))]], + vec![vec![(1, Fr::from_u64(17))], vec![(0, Fr::from_u64(7))]], + ); + let row_weights = [Fr::from_u64(11), Fr::from_u64(19), Fr::from_u64(23)]; + + let contributions = m + .public_column_contributions(&row_weights, 0, Fr::from_u64(3)) + .expect("row weights cover all constraints"); + + assert_eq!( + contributions, + MatrixColumnContributions { + a: Fr::from_u64(237), + b: Fr::from_u64(165), + c: Fr::from_u64(399), + } + ); + } + + #[test] + fn weighted_columns_projects_multiple_abc_columns() { + let m = ConstraintMatrices::new( + 2, + 3, + vec![ + vec![(0, Fr::from_u64(2)), (1, Fr::from_u64(99))], + vec![(0, Fr::from_u64(3)), (2, Fr::from_u64(4))], + ], + vec![ + vec![(0, Fr::from_u64(5)), (2, Fr::from_u64(7))], + vec![(2, Fr::from_u64(13))], + ], + vec![vec![(1, Fr::from_u64(17))], vec![(0, Fr::from_u64(7))]], + ); + let row_weights = [Fr::from_u64(11), Fr::from_u64(19)]; + + let weighted = m + .weighted_columns(&row_weights, &[0, 2]) + .expect("row weights cover all constraints"); + + assert_eq!( + weighted, + WeightedMatrixColumns { + a: vec![Fr::from_u64(79), Fr::from_u64(76)], + b: vec![Fr::from_u64(55), Fr::from_u64(324)], + c: vec![Fr::from_u64(133), Fr::from_u64(0)], + } + ); + } + + #[test] + fn linear_form_bilinear_eval_combines_weighted_matrices() { + let m = ConstraintMatrices::new( + 2, + 4, + vec![ + vec![(0, Fr::from_u64(100)), (1, Fr::from_u64(2))], + vec![(2, Fr::from_u64(3))], + ], + vec![ + vec![(1, Fr::from_u64(5)), (3, Fr::from_u64(7))], + vec![(2, Fr::from_u64(11))], + ], + vec![vec![(3, Fr::from_u64(13))], vec![(1, Fr::from_u64(17))]], + ); + let row_weights = [Fr::from_u64(2), Fr::from_u64(3)]; + let column_weights = [Fr::from_u64(5), Fr::from_u64(7), Fr::from_u64(11)]; + + let value = m + .linear_form_bilinear_eval( + &row_weights, + &column_weights, + 1, + 3, + [Fr::from_u64(19), Fr::from_u64(23), Fr::from_u64(29)], + ) + .expect("weights match the selected columns"); + + assert_eq!(value, Fr::from_u64(27271)); + } + + #[test] + fn matrix_eval_rejects_short_row_weights() { + let m = ConstraintMatrices::new( + 2, + 2, + vec![vec![(0, Fr::from_u64(1))], vec![(1, Fr::from_u64(2))]], + vec![Vec::new(), Vec::new()], + vec![Vec::new(), Vec::new()], + ); + + let error = m + .public_column_contributions(&[Fr::from_u64(1)], 0, Fr::from_u64(1)) + .expect_err("one row weight cannot cover two constraints"); + + assert_eq!( + error, + ConstraintMatrixEvalError::RowWeightsLengthMismatch { + expected: 2, + actual: 1, + } + ); + } + + #[test] + fn matrix_eval_rejects_column_weight_mismatch() { + let m = ConstraintMatrices::new( + 1, + 3, + vec![vec![(1, Fr::from_u64(1))]], + vec![Vec::new()], + vec![Vec::new()], + ); + + let error = m + .linear_form_bilinear_eval( + &[Fr::from_u64(1)], + &[Fr::from_u64(1)], + 1, + 2, + [Fr::from_u64(1), Fr::from_u64(1), Fr::from_u64(1)], + ) + .expect_err("selected column count must match weights"); + + assert_eq!( + error, + ConstraintMatrixEvalError::ColumnWeightsLengthMismatch { + expected: 2, + actual: 1, + } + ); + } + + #[test] + fn matrix_eval_rejects_column_range_overflow() { + let m = ConstraintMatrices::new(0, 1, Vec::new(), Vec::new(), Vec::new()); + + let error = m + .linear_form_bilinear_eval( + &[], + &[Fr::from_u64(1), Fr::from_u64(1)], + usize::MAX, + 2, + [Fr::from_u64(1), Fr::from_u64(1), Fr::from_u64(1)], + ) + .expect_err("column range must not overflow"); + + assert_eq!( + error, + ConstraintMatrixEvalError::ColumnRangeOverflow { + start: usize::MAX, + count: 2, + } + ); + } } diff --git a/crates/jolt-r1cs/src/constraints/field_constraints.rs b/crates/jolt-r1cs/src/constraints/field_constraints.rs new file mode 100644 index 0000000000..7e2e923f38 --- /dev/null +++ b/crates/jolt-r1cs/src/constraints/field_constraints.rs @@ -0,0 +1,354 @@ +//! Native field-inline R1CS variable layout and constraints. +//! +//! This module defines the per-cycle local constraints for field-inline +//! instruction semantics. The bridge constraints here use a native single-field +//! representation for ordinary values; multi-limb bridge packing should be +//! layered on once those bridge payloads are explicit in the trace. + +use crate::constraint::SparseRow; +use jolt_field::Field; + +type ConstraintRows = (Vec>, Vec>, Vec>); + +pub const V_CONST: usize = 0; + +pub const V_FIELD_RS1_VALUE: usize = 1; +pub const V_FIELD_RS2_VALUE: usize = 2; +pub const V_FIELD_RD_VALUE: usize = 3; +pub const V_FIELD_PRODUCT: usize = 4; +pub const V_FIELD_INV_PRODUCT: usize = 5; + +pub const V_X_RS1_VALUE: usize = 6; +pub const V_X_RD_WRITE_VALUE: usize = 7; +pub const V_IMM: usize = 8; + +pub const V_IS_FIELD_ADD: usize = 9; +pub const V_IS_FIELD_SUB: usize = 10; +pub const V_IS_FIELD_MUL: usize = 11; +pub const V_IS_FIELD_INV: usize = 12; +pub const V_IS_FIELD_ASSERT_EQ: usize = 13; +pub const V_IS_FIELD_LOAD_FROM_X: usize = 14; +pub const V_IS_FIELD_STORE_TO_X: usize = 15; +pub const V_IS_FIELD_LOAD_IMM: usize = 16; + +pub const NUM_R1CS_INPUTS: usize = NUM_VARS_PER_CYCLE - 1; +pub const NUM_VARS_PER_CYCLE: usize = 17; + +pub const ROW_FADD: usize = 0; +pub const ROW_FSUB: usize = 1; +pub const ROW_FMUL: usize = 2; +pub const ROW_FINV: usize = 3; +pub const ROW_ASSERT_EQ: usize = 4; +pub const ROW_LOAD_FROM_X: usize = 5; +pub const ROW_STORE_TO_X: usize = 6; +pub const ROW_LOAD_IMM: usize = 7; +pub const NUM_EQ_CONSTRAINTS: usize = 8; + +pub const ROW_FIELD_PRODUCT: usize = NUM_EQ_CONSTRAINTS; +pub const ROW_FIELD_INV_PRODUCT: usize = NUM_EQ_CONSTRAINTS + 1; +pub const NUM_PRODUCT_CONSTRAINTS: usize = 2; +pub const NUM_CONSTRAINTS_PER_CYCLE: usize = NUM_EQ_CONSTRAINTS + NUM_PRODUCT_CONSTRAINTS; + +pub const fn const_column() -> usize { + V_CONST +} + +pub const fn input_column(input_index: usize) -> Option { + if input_index < NUM_R1CS_INPUTS { + Some(1 + input_index) + } else { + None + } +} + +fn row(entries: &[(usize, i64)]) -> SparseRow { + entries + .iter() + .filter(|(_, coefficient)| *coefficient != 0) + .map(|&(index, coefficient)| (index, F::from_i64(coefficient))) + .collect() +} + +fn field_eq_constraint_rows() -> ConstraintRows { + let mut a_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); + let mut b_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); + let mut c_rows = Vec::with_capacity(NUM_EQ_CONSTRAINTS); + + let empty = || Vec::new(); + + a_rows.push(row::(&[(V_IS_FIELD_ADD, 1)])); + b_rows.push(row::(&[ + (V_FIELD_RS1_VALUE, 1), + (V_FIELD_RS2_VALUE, 1), + (V_FIELD_RD_VALUE, -1), + ])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_SUB, 1)])); + b_rows.push(row::(&[ + (V_FIELD_RS1_VALUE, 1), + (V_FIELD_RS2_VALUE, -1), + (V_FIELD_RD_VALUE, -1), + ])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_MUL, 1)])); + b_rows.push(row::(&[(V_FIELD_PRODUCT, 1), (V_FIELD_RD_VALUE, -1)])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_INV, 1)])); + b_rows.push(row::(&[(V_FIELD_INV_PRODUCT, 1), (V_CONST, -1)])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_ASSERT_EQ, 1)])); + b_rows.push(row::(&[(V_FIELD_RS1_VALUE, 1), (V_FIELD_RS2_VALUE, -1)])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_LOAD_FROM_X, 1)])); + b_rows.push(row::(&[(V_FIELD_RD_VALUE, 1), (V_X_RS1_VALUE, -1)])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_STORE_TO_X, 1)])); + b_rows.push(row::(&[ + (V_X_RD_WRITE_VALUE, 1), + (V_FIELD_RS1_VALUE, -1), + ])); + c_rows.push(empty()); + + a_rows.push(row::(&[(V_IS_FIELD_LOAD_IMM, 1)])); + b_rows.push(row::(&[(V_FIELD_RD_VALUE, 1), (V_IMM, -1)])); + c_rows.push(empty()); + + (a_rows, b_rows, c_rows) +} + +fn append_product_constraints( + a_rows: &mut Vec>, + b_rows: &mut Vec>, + c_rows: &mut Vec>, +) { + a_rows.push(row::(&[(V_FIELD_RS1_VALUE, 1)])); + b_rows.push(row::(&[(V_FIELD_RS2_VALUE, 1)])); + c_rows.push(row::(&[(V_FIELD_PRODUCT, 1)])); + + a_rows.push(row::(&[(V_FIELD_RS1_VALUE, 1)])); + b_rows.push(row::(&[(V_FIELD_RD_VALUE, 1)])); + c_rows.push(row::(&[(V_FIELD_INV_PRODUCT, 1)])); +} + +/// Build only field-inline guarded equality constraints. +/// +/// Product constraints are intentionally excluded for consumers that handle the +/// field multiplication checks in a separate protocol step. +pub fn field_eq_constraints() -> crate::ConstraintMatrices { + let (a_rows, b_rows, c_rows) = field_eq_constraint_rows(); + crate::ConstraintMatrices::new( + NUM_EQ_CONSTRAINTS, + NUM_VARS_PER_CYCLE, + a_rows, + b_rows, + c_rows, + ) +} + +/// Build the full native field-inline R1CS constraint matrices. +/// +/// Returns 10 constraints over 17 variables per cycle: +/// - 8 equality-conditional rows: `guard * (left - right) = 0` +/// - 2 product rows for `FieldProduct` and `FieldInvProduct` +pub fn field_constraints() -> crate::ConstraintMatrices { + let (mut a_rows, mut b_rows, mut c_rows) = field_eq_constraint_rows(); + a_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + b_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + c_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + append_product_constraints(&mut a_rows, &mut b_rows, &mut c_rows); + + crate::ConstraintMatrices::new( + NUM_CONSTRAINTS_PER_CYCLE, + NUM_VARS_PER_CYCLE, + a_rows, + b_rows, + c_rows, + ) +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may unwind via panic")] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt, Invertible}; + use num_traits::Zero; + + fn witness(field_rs1: Fr, field_rs2: Fr, field_rd: Fr, flags: &[(usize, Fr)]) -> Vec { + let mut witness = vec![Fr::zero(); NUM_VARS_PER_CYCLE]; + witness[V_CONST] = Fr::from_u64(1); + witness[V_FIELD_RS1_VALUE] = field_rs1; + witness[V_FIELD_RS2_VALUE] = field_rs2; + witness[V_FIELD_RD_VALUE] = field_rd; + witness[V_FIELD_PRODUCT] = field_rs1 * field_rs2; + witness[V_FIELD_INV_PRODUCT] = field_rs1 * field_rd; + witness[V_X_RS1_VALUE] = field_rd; + witness[V_X_RD_WRITE_VALUE] = field_rs1; + witness[V_IMM] = field_rd; + for &(index, value) in flags { + witness[index] = value; + } + witness + } + + fn one() -> Fr { + Fr::from_u64(1) + } + + #[test] + fn field_add_satisfies_constraints() { + let witness = witness( + Fr::from_u64(5), + Fr::from_u64(7), + Fr::from_u64(12), + &[(V_IS_FIELD_ADD, one())], + ); + + field_constraints::() + .check_witness(&witness) + .expect("FADD witness satisfies constraints"); + } + + #[test] + fn field_sub_satisfies_constraints() { + let witness = witness( + Fr::from_u64(13), + Fr::from_u64(5), + Fr::from_u64(8), + &[(V_IS_FIELD_SUB, one())], + ); + + field_constraints::() + .check_witness(&witness) + .expect("FSUB witness satisfies constraints"); + } + + #[test] + fn field_mul_checks_product_is_destination() { + let witness = witness( + Fr::from_u64(5), + Fr::from_u64(7), + Fr::from_u64(35), + &[(V_IS_FIELD_MUL, one())], + ); + + field_constraints::() + .check_witness(&witness) + .expect("FMUL witness satisfies constraints"); + } + + #[test] + fn field_mul_rejects_bad_destination() { + let witness = witness( + Fr::from_u64(5), + Fr::from_u64(7), + Fr::from_u64(36), + &[(V_IS_FIELD_MUL, one())], + ); + + assert_eq!( + field_constraints::().check_witness(&witness), + Err(ROW_FMUL) + ); + } + + #[test] + fn product_row_rejects_bad_field_product() { + let mut witness = witness(Fr::from_u64(5), Fr::from_u64(7), Fr::from_u64(35), &[]); + witness[V_FIELD_PRODUCT] = Fr::from_u64(34); + + assert_eq!( + field_constraints::().check_witness(&witness), + Err(ROW_FIELD_PRODUCT) + ); + } + + #[test] + fn inactive_field_mul_does_not_pin_destination_to_product() { + let witness = witness(Fr::from_u64(5), Fr::from_u64(7), Fr::from_u64(99), &[]); + + field_constraints::() + .check_witness(&witness) + .expect("inactive FMUL guard leaves destination unconstrained"); + } + + #[test] + fn field_inverse_uses_intermediate_product() { + let field_rs1 = Fr::from_u64(5); + let field_rd = field_rs1 + .inverse() + .expect("nonzero test element has inverse"); + let witness = witness( + field_rs1, + Fr::from_u64(9), + field_rd, + &[(V_IS_FIELD_INV, one())], + ); + + field_constraints::() + .check_witness(&witness) + .expect("FINV witness satisfies constraints"); + } + + #[test] + fn field_inverse_rejects_bad_inverse() { + let witness = witness( + Fr::from_u64(5), + Fr::from_u64(9), + Fr::from_u64(8), + &[(V_IS_FIELD_INV, one())], + ); + + assert_eq!( + field_constraints::().check_witness(&witness), + Err(ROW_FINV) + ); + } + + #[test] + fn field_assert_eq_checks_inputs_match() { + let witness = witness( + Fr::from_u64(11), + Fr::from_u64(11), + Fr::from_u64(4), + &[(V_IS_FIELD_ASSERT_EQ, one())], + ); + + field_constraints::() + .check_witness(&witness) + .expect("ASSERT_EQ witness satisfies constraints"); + } + + #[test] + fn native_identity_bridge_constraints_are_gated() { + let mut witness = witness(Fr::from_u64(5), Fr::from_u64(7), Fr::from_u64(42), &[]); + witness[V_X_RS1_VALUE] = Fr::from_u64(42); + witness[V_X_RD_WRITE_VALUE] = Fr::from_u64(5); + witness[V_IMM] = Fr::from_u64(42); + + for selector in [ + V_IS_FIELD_LOAD_FROM_X, + V_IS_FIELD_STORE_TO_X, + V_IS_FIELD_LOAD_IMM, + ] { + let mut selected = witness.clone(); + selected[selector] = one(); + field_constraints::() + .check_witness(&selected) + .expect("native identity bridge witness satisfies constraints"); + } + } + + #[test] + fn input_columns_follow_const_then_inputs_layout() { + assert_eq!(const_column(), V_CONST); + assert_eq!(input_column(0), Some(V_FIELD_RS1_VALUE)); + assert_eq!(input_column(NUM_R1CS_INPUTS - 1), Some(V_IS_FIELD_LOAD_IMM)); + assert_eq!(input_column(NUM_R1CS_INPUTS), None); + } +} diff --git a/crates/jolt-r1cs/src/constraints/mod.rs b/crates/jolt-r1cs/src/constraints/mod.rs index b8e9fef021..aa9069dff6 100644 --- a/crates/jolt-r1cs/src/constraints/mod.rs +++ b/crates/jolt-r1cs/src/constraints/mod.rs @@ -1,3 +1,4 @@ //! ISA-specific R1CS constraint definitions. +pub mod field_constraints; pub mod rv64; diff --git a/crates/jolt-r1cs/src/constraints/rv64.rs b/crates/jolt-r1cs/src/constraints/rv64.rs index a7d4b63b55..29eed9bce4 100644 --- a/crates/jolt-r1cs/src/constraints/rv64.rs +++ b/crates/jolt-r1cs/src/constraints/rv64.rs @@ -10,8 +10,8 @@ //! | Range | Description | //! |-------|-------------| //! | `[0]` | Constant 1 | -//! | `[1..=34]` | R1CS inputs (registers, flags, PC, lookups) | -//! | `[35..=36]` | Product factor variables (`Branch`, `NextIsNoop`) | +//! | `[1..=35]` | R1CS inputs (registers, flags, PC, lookups) | +//! | `[36..=37]` | Product factor variables (`Branch`, `NextIsNoop`) | //! //! # Constraint forms //! @@ -68,11 +68,169 @@ pub const NUM_EQ_CONSTRAINTS: usize = 19; pub const NUM_PRODUCT_CONSTRAINTS: usize = 3; pub const NUM_CONSTRAINTS_PER_CYCLE: usize = NUM_EQ_CONSTRAINTS + NUM_PRODUCT_CONSTRAINTS; // 22 +pub const fn const_column() -> usize { + V_CONST +} + +pub const fn input_column(input_index: usize) -> Option { + if input_index < NUM_R1CS_INPUTS { + Some(1 + input_index) + } else { + None + } +} + /// Two's complement bias for subtraction: 2^64. const TWOS_COMPLEMENT_BIAS: i128 = 0x1_0000_0000_0000_0000; -use crate::constraint::SparseRow; +use crate::constraint::{ConstraintMatrixEvalError, SparseRow}; +use jolt_claims::protocols::jolt::{ + formulas::spartan::{ + SpartanOuterClaimError, SpartanOuterDimensions, SpartanOuterLinearForms, + SpartanOuterRemainderPlan, + }, + SpartanOuterPublic, +}; use jolt_field::Field; +use thiserror::Error as ThisError; + +type ConstraintRows = (Vec>, Vec>, Vec>); + +/// Errors while deriving the RV64 Spartan outer remainder claim. +#[derive(Clone, Debug, ThisError, PartialEq, Eq)] +pub enum Rv64SpartanOuterRemainderError { + /// The remainder proof did not produce the stream-selector challenge. + #[error("missing Spartan outer remainder stream challenge")] + MissingStreamChallenge, + /// A `jolt-claims` Spartan outer formula parameter was invalid. + #[error("{0}")] + Claim(#[from] SpartanOuterClaimError), + /// The RV64 R1CS input cannot be represented by a matrix column. + #[error("R1CS input index {index} has no matrix column")] + MissingInputColumn { + /// R1CS input index. + index: usize, + }, + /// R1CS matrix evaluation failed. + #[error("{0}")] + Matrix(#[from] ConstraintMatrixEvalError), + /// The provided opening vector did not match the expected R1CS input count. + #[error("opening length mismatch: expected {expected}, got {got}")] + OpeningLengthMismatch { + /// Expected number of input openings. + expected: usize, + /// Actual number of input openings. + got: usize, + }, + /// RV64 equality rows should not contribute to the C linear form. + #[error("RV64 equality rows unexpectedly contribute to the C linear form")] + UnexpectedCContribution, +} + +/// Coefficients needed to evaluate the RV64 Spartan outer remainder claim. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Rv64SpartanOuterRemainder { + tau_kernel: F, + linear_forms: SpartanOuterLinearForms, +} + +/// Fiat-Shamir challenges used to derive the RV64 Spartan outer remainder claim. +#[derive(Clone, Copy, Debug)] +pub struct Rv64SpartanOuterRemainderChallenges<'a, F> { + pub tau: &'a [F], + pub uniskip: F, + pub remainder: &'a [F], +} + +impl Rv64SpartanOuterRemainder { + /// Derives the verifier-side remainder claim coefficients for RV64. + pub fn new( + dimensions: &SpartanOuterDimensions, + challenges: Rv64SpartanOuterRemainderChallenges<'_, F>, + ) -> Result { + let plan = SpartanOuterRemainderPlan::from_dimensions(dimensions); + let Some((&r_stream, _)) = challenges.remainder.split_first() else { + return Err(Rv64SpartanOuterRemainderError::MissingStreamChallenge); + }; + + let row_weights = plan.row_weights(challenges.uniskip, r_stream)?; + let input_indices = plan.r1cs_input_indices()?; + let columns: Vec<_> = input_indices + .into_iter() + .map(|index| { + input_column(index) + .ok_or(Rv64SpartanOuterRemainderError::MissingInputColumn { index }) + }) + .collect::>()?; + + let matrices = rv64_eq_constraints::(); + if matrices.c.iter().any(|row| !row.is_empty()) { + return Err(Rv64SpartanOuterRemainderError::UnexpectedCContribution); + } + let weighted = matrices.weighted_columns(&row_weights, &columns)?; + let constant_contributions = + matrices.public_column_contributions(&row_weights, const_column(), F::one())?; + if !constant_contributions.c.is_zero() { + return Err(Rv64SpartanOuterRemainderError::UnexpectedCContribution); + } + let tau_kernel = + plan.tau_kernel(challenges.tau, challenges.uniskip, challenges.remainder)?; + + Ok(Self { + tau_kernel, + linear_forms: SpartanOuterLinearForms { + az_coefficients: weighted.a, + bz_coefficients: weighted.b, + az_constant: constant_contributions.a, + bz_constant: constant_contributions.b, + }, + }) + } + + /// Evaluates the expected unbatched output claim from ordered R1CS openings. + pub fn expected_output_claim( + &self, + r1cs_input_openings: &[F], + ) -> Result { + let expected = self.linear_forms.az_coefficients.len(); + if r1cs_input_openings.len() != expected { + return Err(Rv64SpartanOuterRemainderError::OpeningLengthMismatch { + expected, + got: r1cs_input_openings.len(), + }); + } + + Ok(self.tau_kernel + * eval_linear_form( + &self.linear_forms.az_coefficients, + self.linear_forms.az_constant, + r1cs_input_openings, + ) + * eval_linear_form( + &self.linear_forms.bz_coefficients, + self.linear_forms.bz_constant, + r1cs_input_openings, + )) + } + + pub fn public_claims( + &self, + dimensions: &SpartanOuterDimensions, + ) -> Result, Rv64SpartanOuterRemainderError> { + SpartanOuterRemainderPlan::from_dimensions(dimensions) + .public_claims(self.tau_kernel, &self.linear_forms) + .map_err(Into::into) + } +} + +fn eval_linear_form(coefficients: &[F], constant: F, inputs: &[F]) -> F { + coefficients + .iter() + .zip(inputs) + .fold(constant, |acc, (&coefficient, &input)| { + acc + coefficient * input + }) +} /// Helper: sparse row from `[(variable_index, coefficient)]` pairs. /// @@ -104,18 +262,10 @@ fn row_wide(entries: &[(usize, i128)]) -> SparseRow { .collect() } -/// Build the Jolt RV64 R1CS constraint matrices. -/// -/// Returns 22 constraints over 38 variables per cycle: -/// - 19 equality-conditional: `guard · (left − right) = 0` → A=guard, B=left−right, C=0 -/// - 3 product: `left · right = output` → A=left, B=right, C=output -/// -/// Variable layout matches the constants in this module (V_CONST=0, inputs at 1–35, -/// product factors at 36–37). -pub fn rv64_constraints() -> crate::ConstraintMatrices { - let mut a_rows: Vec> = Vec::with_capacity(NUM_CONSTRAINTS_PER_CYCLE); - let mut b_rows: Vec> = Vec::with_capacity(NUM_CONSTRAINTS_PER_CYCLE); - let mut c_rows: Vec> = Vec::with_capacity(NUM_CONSTRAINTS_PER_CYCLE); +fn rv64_eq_constraint_rows() -> ConstraintRows { + let mut a_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); + let mut b_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); + let mut c_rows: Vec> = Vec::with_capacity(NUM_EQ_CONSTRAINTS); let empty = || Vec::new(); @@ -357,6 +507,14 @@ pub fn rv64_constraints() -> crate::ConstraintMatrices { ])); c_rows.push(empty()); + (a_rows, b_rows, c_rows) +} + +fn append_product_constraints( + a_rows: &mut Vec>, + b_rows: &mut Vec>, + c_rows: &mut Vec>, +) { // Product constraints (19-21) // Form: left · right = output → A=left, B=right, C=output @@ -374,6 +532,39 @@ pub fn rv64_constraints() -> crate::ConstraintMatrices { a_rows.push(row::(&[(V_FLAG_JUMP, 1)])); b_rows.push(row::(&[(V_CONST, 1), (V_NEXT_IS_NOOP, -1)])); c_rows.push(row::(&[(V_SHOULD_JUMP, 1)])); +} + +/// Build only the Jolt RV64 equality-conditional constraints. +/// +/// Returns the 19 rows with form `guard · (left - right) = 0`, over the +/// standard 38-variable per-cycle witness layout. Product constraints are +/// intentionally excluded for consumers that handle multiplication checks in +/// a separate protocol step. +pub fn rv64_eq_constraints() -> crate::ConstraintMatrices { + let (a_rows, b_rows, c_rows) = rv64_eq_constraint_rows(); + crate::ConstraintMatrices::new( + NUM_EQ_CONSTRAINTS, + NUM_VARS_PER_CYCLE, + a_rows, + b_rows, + c_rows, + ) +} + +/// Build the full Jolt RV64 R1CS constraint matrices. +/// +/// Returns 22 constraints over 38 variables per cycle: +/// - 19 equality-conditional: `guard · (left − right) = 0` → A=guard, B=left−right, C=0 +/// - 3 product: `left · right = output` → A=left, B=right, C=output +/// +/// Variable layout matches the constants in this module (V_CONST=0, inputs at 1–35, +/// product factors at 36–37). +pub fn rv64_constraints() -> crate::ConstraintMatrices { + let (mut a_rows, mut b_rows, mut c_rows) = rv64_eq_constraint_rows(); + a_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + b_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + c_rows.reserve(NUM_PRODUCT_CONSTRAINTS); + append_product_constraints(&mut a_rows, &mut b_rows, &mut c_rows); crate::ConstraintMatrices::new( NUM_CONSTRAINTS_PER_CYCLE, @@ -422,4 +613,82 @@ mod tests { assert_eq!(matrices.b.len(), 22); assert_eq!(matrices.c.len(), 22); } + + #[test] + fn eq_constraints_plus_product_constraints_match_full_constraints() { + let (mut a_rows, mut b_rows, mut c_rows) = rv64_eq_constraint_rows::(); + append_product_constraints(&mut a_rows, &mut b_rows, &mut c_rows); + let matrices = rv64_constraints::(); + + assert_eq!(matrices.a, a_rows); + assert_eq!(matrices.b, b_rows); + assert_eq!(matrices.c, c_rows); + } + + #[test] + fn eq_constraint_public_column_has_no_c_contribution() { + let matrices = rv64_eq_constraints::(); + assert!(matrices.c.iter().all(|row| row.is_empty())); + + let row_weights = vec![Fr::from_u64(1); NUM_EQ_CONSTRAINTS]; + let contributions = matrices + .public_column_contributions(&row_weights, const_column(), Fr::from_u64(1)) + .expect("const column evaluates"); + + assert!(contributions.c.is_zero()); + } + + #[test] + fn outer_remainder_expected_claim_matches_public_coefficients() { + let dimensions = SpartanOuterDimensions::rv64(1); + let tau = [Fr::from_u64(0), Fr::from_u64(0), Fr::from_i64(-4)]; + let remainder_challenges = [Fr::from_u64(0), Fr::from_u64(0)]; + let formula = Rv64SpartanOuterRemainder::new( + &dimensions, + Rv64SpartanOuterRemainderChallenges { + tau: &tau, + uniskip: Fr::from_i64(-4), + remainder: &remainder_challenges, + }, + ) + .expect("remainder formula derives"); + let openings = (1..=NUM_R1CS_INPUTS) + .map(|value| Fr::from_u64(value as u64)) + .collect::>(); + let expected = formula + .expected_output_claim(&openings) + .expect("opening length matches"); + + let hand_computed = + (Fr::from_u64(1) - openings[V_FLAG_LOAD - 1] - openings[V_FLAG_STORE - 1]) + * openings[V_RAM_ADDRESS - 1]; + assert_eq!(expected, hand_computed); + + let mut reconstructed = Fr::zero(); + for (public, value) in formula + .public_claims(&dimensions) + .expect("public coefficients derive") + { + reconstructed += match public { + SpartanOuterPublic::QuadraticCoefficient { left, right } => { + value * openings[left] * openings[right] + } + SpartanOuterPublic::LinearCoefficient(index) => value * openings[index], + SpartanOuterPublic::ConstantCoefficient => value, + }; + } + + assert_eq!(expected, reconstructed); + } + + #[test] + fn input_columns_follow_const_then_inputs_layout() { + assert_eq!(const_column(), V_CONST); + assert_eq!(input_column(0), Some(V_LEFT_INSTRUCTION_INPUT)); + assert_eq!( + input_column(NUM_R1CS_INPUTS - 1), + Some(V_FLAG_IS_LAST_IN_SEQUENCE) + ); + assert_eq!(input_column(NUM_R1CS_INPUTS), None); + } } diff --git a/crates/jolt-r1cs/src/lib.rs b/crates/jolt-r1cs/src/lib.rs index 62f0f4176d..7d2ad193b1 100644 --- a/crates/jolt-r1cs/src/lib.rs +++ b/crates/jolt-r1cs/src/lib.rs @@ -7,14 +7,25 @@ //! - [`R1csSource`] — materializes R1CS-derived polynomials (Az, Bz, Cz, etc.) //! - [`R1csColumn`] — names the derived polynomial columns (Az/Bz/Cz/…) //! - [`constraints::rv64`] — Jolt RV64IMAC variable layout and dimension constants +//! - [`constraints::field_constraints`] — native field-inline constraint layout +pub mod builder; pub mod column; pub mod constraint; pub mod constraints; pub mod key; +pub mod lowering; pub mod provider; +pub use builder::{LinearCombination, R1csBuilder, R1csBuilderError, Variable}; pub use column::R1csColumn; -pub use constraint::ConstraintMatrices; +pub use constraint::{ + ConstraintMatrices, ConstraintMatrixEvalError, MatrixColumnContributions, SparseRow, + WeightedMatrixColumns, +}; pub use key::R1csKey; +pub use lowering::{ + assert_claim_expr_eq, lower_claim_expr, ClaimLoweringError, ClaimSourceTable, ClaimSources, + SourceValue, +}; pub use provider::{R1csSource, SpartanChallenges}; diff --git a/crates/jolt-r1cs/src/lowering.rs b/crates/jolt-r1cs/src/lowering.rs new file mode 100644 index 0000000000..0656f39b6c --- /dev/null +++ b/crates/jolt-r1cs/src/lowering.rs @@ -0,0 +1,488 @@ +use jolt_claims::{Expr, Source}; +use jolt_field::Field; +use thiserror::Error; + +use crate::{LinearCombination, R1csBuilder, Variable}; + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum ClaimLoweringError { + #[error("missing opening source")] + MissingOpening, + #[error("missing challenge source")] + MissingChallenge, + #[error("missing public source")] + MissingPublic, +} + +pub trait ClaimSources { + type Opening; + type Challenge; + type Public; + + fn opening(&mut self, id: &Self::Opening) -> Result, ClaimLoweringError>; + fn challenge(&mut self, id: &Self::Challenge) -> Result, ClaimLoweringError>; + fn public(&mut self, id: &Self::Public) -> Result, ClaimLoweringError>; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SourceValue { + Constant(F), + LinearCombination(LinearCombination), +} + +impl SourceValue { + pub fn variable(variable: Variable) -> Self { + Self::LinearCombination(LinearCombination::variable(variable)) + } + + pub fn linear_combination(linear_combination: LinearCombination) -> Self { + Self::LinearCombination(linear_combination) + } + + pub fn into_linear_combination(self) -> LinearCombination { + match self { + Self::Constant(value) => LinearCombination::constant(value), + Self::LinearCombination(linear_combination) => linear_combination, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct ClaimSourceTable { + openings: Vec<(O, SourceValue)>, + challenges: Vec<(C, SourceValue)>, + publics: Vec<(P, SourceValue)>, +} + +impl ClaimSourceTable { + pub fn new() -> Self { + Self { + openings: Vec::new(), + challenges: Vec::new(), + publics: Vec::new(), + } + } + + pub fn insert_opening(&mut self, id: O, variable: Variable) + where + F: Field, + O: PartialEq, + { + self.insert_opening_source(id, SourceValue::variable(variable)); + } + + pub fn insert_opening_lc(&mut self, id: O, linear_combination: LinearCombination) + where + F: Field, + O: PartialEq, + { + self.insert_opening_source(id, SourceValue::linear_combination(linear_combination)); + } + + pub fn insert_opening_source(&mut self, id: O, source: SourceValue) + where + O: PartialEq, + { + assert!( + !self.openings.iter().any(|(candidate, _)| candidate == &id), + "duplicate opening source" + ); + self.openings.push((id, source)); + } + + pub fn insert_challenge(&mut self, id: C, value: F) + where + C: PartialEq, + { + self.insert_challenge_source(id, SourceValue::Constant(value)); + } + + pub fn insert_challenge_lc(&mut self, id: C, linear_combination: LinearCombination) + where + F: Field, + C: PartialEq, + { + self.insert_challenge_source(id, SourceValue::linear_combination(linear_combination)); + } + + pub fn insert_challenge_source(&mut self, id: C, source: SourceValue) + where + C: PartialEq, + { + assert!( + !self + .challenges + .iter() + .any(|(candidate, _)| candidate == &id), + "duplicate challenge source" + ); + self.challenges.push((id, source)); + } + + pub fn insert_public(&mut self, id: P, value: F) + where + P: PartialEq, + { + self.insert_public_source(id, SourceValue::Constant(value)); + } + + pub fn insert_public_lc(&mut self, id: P, linear_combination: LinearCombination) + where + F: Field, + P: PartialEq, + { + self.insert_public_source(id, SourceValue::linear_combination(linear_combination)); + } + + pub fn insert_public_source(&mut self, id: P, source: SourceValue) + where + P: PartialEq, + { + assert!( + !self.publics.iter().any(|(candidate, _)| candidate == &id), + "duplicate public source" + ); + self.publics.push((id, source)); + } +} + +impl ClaimSources + for ClaimSourceTable +{ + type Opening = O; + type Challenge = C; + type Public = P; + + fn opening(&mut self, id: &Self::Opening) -> Result, ClaimLoweringError> { + self.openings + .iter() + .find_map(|(candidate, source)| (candidate == id).then_some(source.clone())) + .ok_or(ClaimLoweringError::MissingOpening) + } + + fn challenge(&mut self, id: &Self::Challenge) -> Result, ClaimLoweringError> { + self.challenges + .iter() + .find_map(|(candidate, source)| (candidate == id).then_some(source.clone())) + .ok_or(ClaimLoweringError::MissingChallenge) + } + + fn public(&mut self, id: &Self::Public) -> Result, ClaimLoweringError> { + self.publics + .iter() + .find_map(|(candidate, source)| (candidate == id).then_some(source.clone())) + .ok_or(ClaimLoweringError::MissingPublic) + } +} + +pub fn lower_claim_expr( + builder: &mut R1csBuilder, + expression: &Expr, + sources: &mut R, +) -> Result, ClaimLoweringError> +where + F: Field, + R: ClaimSources, +{ + let mut result = LinearCombination::zero(); + + for term in &expression.terms { + let mut coefficient = term.coefficient; + let mut factors = Vec::new(); + + for source in &term.factors { + let source = match source { + Source::Opening(id) => sources.opening(id)?, + Source::Challenge(id) => sources.challenge(id)?, + Source::Public(id) => sources.public(id)?, + }; + match source { + SourceValue::Constant(value) => coefficient *= value, + SourceValue::LinearCombination(linear_combination) => { + factors.push(linear_combination); + } + } + } + + result = result + lower_product(builder, coefficient, factors); + } + + Ok(result) +} + +pub fn assert_claim_expr_eq( + builder: &mut R1csBuilder, + expression: &Expr, + expected: Expected, + sources: &mut R, +) -> Result<(), ClaimLoweringError> +where + F: Field, + R: ClaimSources, + Expected: Into>, +{ + let actual = lower_claim_expr(builder, expression, sources)?; + builder.assert_equal(actual, expected); + Ok(()) +} + +fn lower_product( + builder: &mut R1csBuilder, + coefficient: F, + factors: Vec>, +) -> LinearCombination { + if coefficient.is_zero() { + return LinearCombination::zero(); + } + + let mut factors = factors.into_iter(); + let Some(mut product) = factors.next() else { + return LinearCombination::constant(coefficient); + }; + + for factor in factors { + product = builder.multiply(product, factor); + } + + product.scale(coefficient) +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] +mod tests { + use super::*; + use jolt_claims::{challenge, constant, opening, public, Expr}; + use jolt_field::{Fr, FromPrimitiveInt}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Opening { + A, + B, + C, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Public { + Offset, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Challenge { + Gamma, + } + + #[test] + fn lowers_expression_to_satisfied_r1cs() { + let mut builder = R1csBuilder::::new(); + let a = builder.alloc(Fr::from_u64(3)); + let b = builder.alloc(Fr::from_u64(5)); + let out = builder.alloc(Fr::from_u64(23)); + + let mut sources = ClaimSourceTable::new(); + sources.insert_opening(Opening::A, a); + sources.insert_opening(Opening::B, b); + sources.insert_challenge(0, Fr::from_u64(2)); + sources.insert_public(Public::Offset, Fr::from_u64(4)); + + let expression: Expr = + opening(Opening::A) * opening(Opening::B) + challenge(0) * public(Public::Offset); + + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("expression lowers"); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn lowered_constraint_rejects_bad_witness() { + let mut builder = R1csBuilder::::new(); + let a = builder.alloc(Fr::from_u64(3)); + let b = builder.alloc(Fr::from_u64(5)); + let out = builder.alloc(Fr::from_u64(22)); + + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::A, a); + sources.insert_opening(Opening::B, b); + + let expression: Expr = + opening(Opening::A) * opening(Opening::B) + constant(Fr::from_u64(8)); + + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("expression lowers"); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_err()); + } + + #[test] + fn multi_factor_product_allocates_chain() { + let mut builder = R1csBuilder::::new(); + let a = builder.alloc(Fr::from_u64(2)); + let b = builder.alloc(Fr::from_u64(3)); + let c = builder.alloc(Fr::from_u64(4)); + let out = builder.alloc(Fr::from_u64(24)); + + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::A, a); + sources.insert_opening(Opening::B, b); + sources.insert_opening(Opening::C, c); + + let expression: Expr = + opening(Opening::A) * opening(Opening::B) * opening(Opening::C); + + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("expression lowers"); + + let witness = builder.witness().expect("witness is assigned"); + let matrices = builder.into_matrices(); + assert_eq!(matrices.num_constraints, 3); + assert!(matrices.check_witness(&witness).is_ok()); + } + + #[test] + fn lowers_variable_challenge_and_public_sources() { + let mut builder = R1csBuilder::::new(); + let opening_value = builder.alloc(Fr::from_u64(3)); + let challenge_value = builder.alloc(Fr::from_u64(4)); + let public_value = builder.alloc(Fr::from_u64(7)); + let out = builder.alloc(Fr::from_u64(19)); + + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::A, opening_value); + sources.insert_challenge_lc( + Challenge::Gamma, + LinearCombination::variable(challenge_value), + ); + sources.insert_public_lc(Public::Offset, LinearCombination::variable(public_value)); + + let expression: Expr = + opening(Opening::A) * challenge(Challenge::Gamma) + public(Public::Offset); + + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("variable sources lower"); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn variable_source_products_reject_bad_witness() { + let mut builder = R1csBuilder::::new(); + let opening_value = builder.alloc(Fr::from_u64(3)); + let challenge_value = builder.alloc(Fr::from_u64(4)); + let out = builder.alloc(Fr::from_u64(13)); + + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::A, opening_value); + sources.insert_challenge_lc( + Challenge::Gamma, + LinearCombination::variable(challenge_value), + ); + + let expression: Expr = + opening(Opening::A) * challenge(Challenge::Gamma); + + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("variable sources lower"); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_err()); + } + + #[test] + fn constant_sources_do_not_allocate_product_constraints() { + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + sources.insert_challenge(Challenge::Gamma, Fr::from_u64(4)); + sources.insert_public(Public::Offset, Fr::from_u64(7)); + + let expression: Expr = + challenge(Challenge::Gamma) * public(Public::Offset); + let lowered = lower_claim_expr(&mut builder, &expression, &mut sources) + .expect("constant sources lower"); + + assert_eq!(lowered, LinearCombination::constant(Fr::from_u64(28))); + assert_eq!(builder.into_matrices().num_constraints, 0); + } + + #[test] + fn single_variable_source_stays_linear() { + let mut builder = R1csBuilder::::new(); + let challenge_value = builder.alloc(Fr::from_u64(4)); + let mut sources = ClaimSourceTable::::new(); + sources.insert_challenge_lc( + Challenge::Gamma, + LinearCombination::variable(challenge_value), + ); + + let expression: Expr = challenge(Challenge::Gamma); + let lowered = lower_claim_expr(&mut builder, &expression, &mut sources) + .expect("variable challenge lowers"); + + assert_eq!(lowered, LinearCombination::variable(challenge_value)); + assert_eq!(builder.into_matrices().num_constraints, 0); + } + + #[test] + fn missing_challenge_is_typed_error() { + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + let expression: Expr = challenge(2); + + let error = lower_claim_expr(&mut builder, &expression, &mut sources) + .expect_err("challenge is missing"); + + assert_eq!(error, ClaimLoweringError::MissingChallenge); + } + + #[test] + fn missing_opening_is_typed_error() { + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + let expression: Expr = opening(Opening::A); + + let error = lower_claim_expr(&mut builder, &expression, &mut sources) + .expect_err("opening is missing"); + + assert_eq!(error, ClaimLoweringError::MissingOpening); + } + + #[test] + fn missing_public_is_typed_error() { + let mut builder = R1csBuilder::::new(); + let mut sources = ClaimSourceTable::::new(); + let expression: Expr = public(Public::Offset); + + let error = lower_claim_expr(&mut builder, &expression, &mut sources) + .expect_err("public is missing"); + + assert_eq!(error, ClaimLoweringError::MissingPublic); + } + + #[test] + #[should_panic(expected = "duplicate opening source")] + fn duplicate_opening_source_panics() { + let mut builder = R1csBuilder::::new(); + let a = builder.alloc(Fr::from_u64(3)); + let b = builder.alloc(Fr::from_u64(5)); + let mut sources = ClaimSourceTable::::new(); + sources.insert_opening(Opening::A, a); + sources.insert_opening(Opening::A, b); + } + + #[test] + fn lowers_typed_challenge_sources() { + let mut builder = R1csBuilder::::new(); + let out = builder.alloc(Fr::from_u64(6)); + let mut sources = ClaimSourceTable::::new(); + sources.insert_challenge(Challenge::Gamma, Fr::from_u64(6)); + + let expression: Expr = challenge(Challenge::Gamma); + assert_claim_expr_eq(&mut builder, &expression, out, &mut sources) + .expect("typed challenge lowers"); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } +} diff --git a/crates/jolt-riscv/Cargo.toml b/crates/jolt-riscv/Cargo.toml index 89f402b487..570b62d77a 100644 --- a/crates/jolt-riscv/Cargo.toml +++ b/crates/jolt-riscv/Cargo.toml @@ -12,13 +12,16 @@ categories = ["cryptography"] workspace = true [features] +default = ["serialization"] +serialization = ["dep:ark-serialize", "dep:serde"] test-utils = ["dep:rand"] [dependencies] -ark-serialize.workspace = true +ark-serialize = { workspace = true, optional = true } rand = { workspace = true, optional = true } -serde.workspace = true +serde = { workspace = true, optional = true } strum = { workspace = true, features = ["derive"] } [dev-dependencies] rand.workspace = true +serde_json = { workspace = true, features = ["std"] } diff --git a/crates/jolt-riscv/src/flags.rs b/crates/jolt-riscv/src/flags.rs index 37baa38e01..e83a61fa45 100644 --- a/crates/jolt-riscv/src/flags.rs +++ b/crates/jolt-riscv/src/flags.rs @@ -15,6 +15,10 @@ use strum::EnumCount; /// /// Note: the flags below deviate somewhat from those described in Appendix A.1 /// of the Jolt paper. +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, EnumCount)] #[repr(u8)] pub enum CircuitFlags { @@ -51,9 +55,30 @@ pub enum CircuitFlags { /// Number of circuit flags. pub const NUM_CIRCUIT_FLAGS: usize = CircuitFlags::COUNT; +pub const CIRCUIT_FLAGS: [CircuitFlags; NUM_CIRCUIT_FLAGS] = [ + CircuitFlags::AddOperands, + CircuitFlags::SubtractOperands, + CircuitFlags::MultiplyOperands, + CircuitFlags::Load, + CircuitFlags::Store, + CircuitFlags::Jump, + CircuitFlags::WriteLookupOutputToRD, + CircuitFlags::VirtualInstruction, + CircuitFlags::Assert, + CircuitFlags::DoNotUpdateUnexpandedPC, + CircuitFlags::Advice, + CircuitFlags::IsCompressed, + CircuitFlags::IsFirstInSequence, + CircuitFlags::IsLastInSequence, +]; + /// Boolean flags that are NOT part of Jolt's R1CS constraints. /// /// These control witness generation, operand routing, and auxiliary prover logic. +#[cfg_attr( + feature = "serialization", + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, EnumCount)] #[repr(u8)] pub enum InstructionFlags { diff --git a/crates/jolt-riscv/src/instructions/a/amoaddw.rs b/crates/jolt-riscv/src/instructions/a/amoaddw.rs index 92ed2e3cc0..596d798926 100644 --- a/crates/jolt-riscv/src/instructions/a/amoaddw.rs +++ b/crates/jolt-riscv/src/instructions/a/amoaddw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOADD.W: atomic add word. + /// Atomic AMOADD.W: atomic add word. AmoAddW ); diff --git a/crates/jolt-riscv/src/instructions/a/amoandw.rs b/crates/jolt-riscv/src/instructions/a/amoandw.rs index 91b52d1334..05739c82bd 100644 --- a/crates/jolt-riscv/src/instructions/a/amoandw.rs +++ b/crates/jolt-riscv/src/instructions/a/amoandw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOAND.W: atomic AND word. + /// Atomic AMOAND.W: atomic AND word. AmoAndW ); diff --git a/crates/jolt-riscv/src/instructions/a/amomaxuw.rs b/crates/jolt-riscv/src/instructions/a/amomaxuw.rs index 1ddab7ea80..7abe9132d5 100644 --- a/crates/jolt-riscv/src/instructions/a/amomaxuw.rs +++ b/crates/jolt-riscv/src/instructions/a/amomaxuw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOMAXU.W: atomic unsigned max word. + /// Atomic AMOMAXU.W: atomic unsigned max word. AmoMaxUW ); diff --git a/crates/jolt-riscv/src/instructions/a/amomaxw.rs b/crates/jolt-riscv/src/instructions/a/amomaxw.rs index 9eeeecedb2..7984ba5ada 100644 --- a/crates/jolt-riscv/src/instructions/a/amomaxw.rs +++ b/crates/jolt-riscv/src/instructions/a/amomaxw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOMAX.W: atomic signed max word. + /// Atomic AMOMAX.W: atomic signed max word. AmoMaxW ); diff --git a/crates/jolt-riscv/src/instructions/a/amominuw.rs b/crates/jolt-riscv/src/instructions/a/amominuw.rs index 0d0a6773a1..46fe086d59 100644 --- a/crates/jolt-riscv/src/instructions/a/amominuw.rs +++ b/crates/jolt-riscv/src/instructions/a/amominuw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOMINU.W: atomic unsigned min word. + /// Atomic AMOMINU.W: atomic unsigned min word. AmoMinUW ); diff --git a/crates/jolt-riscv/src/instructions/a/amominw.rs b/crates/jolt-riscv/src/instructions/a/amominw.rs index a2e8591b67..d7e2cba020 100644 --- a/crates/jolt-riscv/src/instructions/a/amominw.rs +++ b/crates/jolt-riscv/src/instructions/a/amominw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOMIN.W: atomic signed min word. + /// Atomic AMOMIN.W: atomic signed min word. AmoMinW ); diff --git a/crates/jolt-riscv/src/instructions/a/amoorw.rs b/crates/jolt-riscv/src/instructions/a/amoorw.rs index 7bce7917c0..a469ec5aa1 100644 --- a/crates/jolt-riscv/src/instructions/a/amoorw.rs +++ b/crates/jolt-riscv/src/instructions/a/amoorw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOOR.W: atomic OR word. + /// Atomic AMOOR.W: atomic OR word. AmoOrW ); diff --git a/crates/jolt-riscv/src/instructions/a/amoswapw.rs b/crates/jolt-riscv/src/instructions/a/amoswapw.rs index f1e509e1bf..ef1cf711de 100644 --- a/crates/jolt-riscv/src/instructions/a/amoswapw.rs +++ b/crates/jolt-riscv/src/instructions/a/amoswapw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOSWAP.W: atomic swap word. + /// Atomic AMOSWAP.W: atomic swap word. AmoSwapW ); diff --git a/crates/jolt-riscv/src/instructions/a/amoxorw.rs b/crates/jolt-riscv/src/instructions/a/amoxorw.rs index dea3b14f4d..42f19ecf05 100644 --- a/crates/jolt-riscv/src/instructions/a/amoxorw.rs +++ b/crates/jolt-riscv/src/instructions/a/amoxorw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A AMOXOR.W: atomic XOR word. + /// Atomic AMOXOR.W: atomic XOR word. AmoXorW ); diff --git a/crates/jolt-riscv/src/instructions/a/lrw.rs b/crates/jolt-riscv/src/instructions/a/lrw.rs index 538b070f6f..501236c50f 100644 --- a/crates/jolt-riscv/src/instructions/a/lrw.rs +++ b/crates/jolt-riscv/src/instructions/a/lrw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A LR.W: load-reserved word. + /// Atomic LR.W: load-reserved word. LrW ); diff --git a/crates/jolt-riscv/src/instructions/a/mod.rs b/crates/jolt-riscv/src/instructions/a/mod.rs index f19bbcd0e7..a1e510c664 100644 --- a/crates/jolt-riscv/src/instructions/a/mod.rs +++ b/crates/jolt-riscv/src/instructions/a/mod.rs @@ -1,4 +1,4 @@ -//! RV32A and RV64A atomic memory operations. +//! Atomic memory operations. pub mod amoaddd; pub mod amoaddw; diff --git a/crates/jolt-riscv/src/instructions/a/scw.rs b/crates/jolt-riscv/src/instructions/a/scw.rs index c64b2897a8..74ed30516c 100644 --- a/crates/jolt-riscv/src/instructions/a/scw.rs +++ b/crates/jolt-riscv/src/instructions/a/scw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32A SC.W: store-conditional word. + /// Atomic SC.W: store-conditional word. ScW ); diff --git a/crates/jolt-riscv/src/instructions/i/csrrs.rs b/crates/jolt-riscv/src/instructions/i/csrrs.rs index f3227f4cdf..0cd14f1c25 100644 --- a/crates/jolt-riscv/src/instructions/i/csrrs.rs +++ b/crates/jolt-riscv/src/instructions/i/csrrs.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32I (Zicsr) CSRRS: atomic CSR read+set bits. + /// Zicsr CSRRS: atomic CSR read+set bits. Csrrs ); diff --git a/crates/jolt-riscv/src/instructions/i/csrrw.rs b/crates/jolt-riscv/src/instructions/i/csrrw.rs index 9eeb1f1f9e..f0c0898b3c 100644 --- a/crates/jolt-riscv/src/instructions/i/csrrw.rs +++ b/crates/jolt-riscv/src/instructions/i/csrrw.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32I (Zicsr) CSRRW: atomic CSR read+write. + /// Zicsr CSRRW: atomic CSR read+write. Csrrw ); diff --git a/crates/jolt-riscv/src/instructions/i/mret.rs b/crates/jolt-riscv/src/instructions/i/mret.rs index b1a77c0b1b..a49fde91b9 100644 --- a/crates/jolt-riscv/src/instructions/i/mret.rs +++ b/crates/jolt-riscv/src/instructions/i/mret.rs @@ -1,6 +1,6 @@ use crate::jolt_instruction; jolt_instruction!( - /// RV32I MRET: machine-mode return from trap. + /// RISC-V MRET: machine-mode return from trap. Mret ); diff --git a/crates/jolt-riscv/src/instructions/mod.rs b/crates/jolt-riscv/src/instructions/mod.rs index def750e6cf..e7e65b6ed2 100644 --- a/crates/jolt-riscv/src/instructions/mod.rs +++ b/crates/jolt-riscv/src/instructions/mod.rs @@ -4,13 +4,16 @@ //! instructions emitted by the tracer) and `assert` (virtual asserts used //! inside virtual sequences). Each `Foo(pub T)` represents an //! instruction kind: with the default `T = ()` it is a zero-sized marker -//! (used by `JoltInstructions` variants and static-flag tests); with `T` set +//! (used by `JoltInstruction` variants and static-flag tests); with `T` set //! to an `Instruction`/`Cycle` payload it becomes the constructed form used //! by `LookupQuery` impls. `#[derive(Flags)]` declares the R1CS circuit and //! witness-generation flags. The `InstructionLookupTable` impls (in //! `jolt-lookup-tables`) map instructions to lookup tables. -use serde::{Deserialize, Serialize}; +#[cfg(feature = "serialization")] +use serde::ser::SerializeStruct; +#[cfg(feature = "serialization")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod a; pub mod assert; @@ -18,7 +21,10 @@ pub mod i; pub mod m; pub mod virt; -use crate::{InstructionKind, NormalizedInstruction}; +use crate::{ + JoltInstructionKind, JoltInstructionRow, NormalizedOperands, SourceInlineKey, + SourceInstructionKind, SourceInstructionRow, +}; pub use assert::AssertEq; pub use assert::AssertHalfwordAlignment; pub use assert::AssertLte; @@ -158,335 +164,419 @@ pub use virt::AdviceLw; pub use virt::VirtualLw; pub use virt::VirtualSw; -/// Enum with one variant per Jolt instruction. -/// -/// Each variant wraps a Jolt newtype parameterized by the canonical -/// [`NormalizedInstruction`](crate::NormalizedInstruction) row. Static-flag -/// dispatch and the flag-exclusivity tests rely on this concretization to -/// satisfy `T: JoltInstruction` on the `Flags` impls. -/// -/// Deliberately omitted instruction kinds (declared and re-exported above -/// but not proven by Jolt): the Zicsr ops (`Csrrs`, `Csrrw`), `Mret`, -/// the entire RV32A/RV64A atomic family (`Amo*`, `Lr*`, `Sc*`), -/// the advice-load helpers (`AdviceLb`/`Ld`/`Lh`/`Lw`), and `VirtualLw` / -/// `VirtualSw`. These are intentionally absent from `JoltInstructions` and -/// from the flag-exclusivity tests below. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, strum::EnumIter)] -pub enum JoltInstructions { - Noop, - Add(Add), - Addi(Addi), - Sub(Sub), - Lui(Lui), - Auipc(Auipc), - Mul(Mul), - MulH(MulH), - MulHSU(MulHSU), - MulHU(MulHU), - Div(Div), - DivU(DivU), - Rem(Rem), - RemU(RemU), - AddW(AddW), - AddiW(AddiW), - SubW(SubW), - MulW(MulW), - DivW(DivW), - DivUW(DivUW), - RemW(RemW), - RemUW(RemUW), - And(And), - AndI(AndI), - Or(Or), - OrI(OrI), - Xor(Xor), - XorI(XorI), - Andn(Andn), - Sll(Sll), - SllI(SllI), - Srl(Srl), - SrlI(SrlI), - Sra(Sra), - SraI(SraI), - SllW(SllW), - SllIW(SllIW), - SrlW(SrlW), - SrlIW(SrlIW), - SraW(SraW), - SraIW(SraIW), - Slt(Slt), - SltI(SltI), - SltU(SltU), - SltIU(SltIU), - Beq(Beq), - Bne(Bne), - Blt(Blt), - Bge(Bge), - BltU(BltU), - BgeU(BgeU), - Lb(Lb), - Lbu(Lbu), - Lh(Lh), - Lhu(Lhu), - Lw(Lw), - Lwu(Lwu), - Ld(Ld), - Sb(Sb), - Sh(Sh), - Sw(Sw), - Sd(Sd), - Ecall(Ecall), - Ebreak(Ebreak), - Fence(Fence), - Jal(Jal), - Jalr(Jalr), - AssertEq(AssertEq), - AssertLte(AssertLte), - AssertValidDiv0(AssertValidDiv0), - AssertValidUnsignedRemainder(AssertValidUnsignedRemainder), - AssertMulUNoOverflow(AssertMulUNoOverflow), - AssertWordAlignment(AssertWordAlignment), - AssertHalfwordAlignment(AssertHalfwordAlignment), - Pow2(Pow2), - Pow2I(Pow2I), - Pow2W(Pow2W), - Pow2IW(Pow2IW), - MulI(MulI), - MovSign(MovSign), - VirtualRev8W(VirtualRev8W), - VirtualChangeDivisor(VirtualChangeDivisor), - VirtualChangeDivisorW(VirtualChangeDivisorW), - VirtualSignExtendWord(VirtualSignExtendWord), - VirtualZeroExtendWord(VirtualZeroExtendWord), - VirtualSrl(VirtualSrl), - VirtualSrli(VirtualSrli), - VirtualSra(VirtualSra), - VirtualSrai(VirtualSrai), - VirtualShiftRightBitmask(VirtualShiftRightBitmask), - VirtualShiftRightBitmaski(VirtualShiftRightBitmaski), - VirtualRotri(VirtualRotri), - VirtualRotriw(VirtualRotriw), - VirtualXorRot32(VirtualXorRot32), - VirtualXorRot24(VirtualXorRot24), - VirtualXorRot16(VirtualXorRot16), - VirtualXorRot63(VirtualXorRot63), - VirtualXorRotW16(VirtualXorRotW16), - VirtualXorRotW12(VirtualXorRotW12), - VirtualXorRotW8(VirtualXorRotW8), - VirtualXorRotW7(VirtualXorRotW7), - VirtualAdvice(VirtualAdvice), - VirtualAdviceLen(VirtualAdviceLen), - VirtualAdviceLoad(VirtualAdviceLoad), - VirtualHostIO(VirtualHostIO), -} +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub struct Unimpl(pub T); -impl TryFrom for JoltInstructions { - type Error = InstructionKind; - - fn try_from(instruction: NormalizedInstruction) -> Result { - Ok(match instruction.instruction_kind { - InstructionKind::NoOp => Self::Noop, - InstructionKind::ADD => Self::Add(Add(instruction)), - InstructionKind::ADDI => Self::Addi(Addi(instruction)), - InstructionKind::SUB => Self::Sub(Sub(instruction)), - InstructionKind::LUI => Self::Lui(Lui(instruction)), - InstructionKind::AUIPC => Self::Auipc(Auipc(instruction)), - InstructionKind::MUL => Self::Mul(Mul(instruction)), - InstructionKind::MULH => Self::MulH(MulH(instruction)), - InstructionKind::MULHSU => Self::MulHSU(MulHSU(instruction)), - InstructionKind::MULHU => Self::MulHU(MulHU(instruction)), - InstructionKind::DIV => Self::Div(Div(instruction)), - InstructionKind::DIVU => Self::DivU(DivU(instruction)), - InstructionKind::REM => Self::Rem(Rem(instruction)), - InstructionKind::REMU => Self::RemU(RemU(instruction)), - InstructionKind::ADDW => Self::AddW(AddW(instruction)), - InstructionKind::ADDIW => Self::AddiW(AddiW(instruction)), - InstructionKind::SUBW => Self::SubW(SubW(instruction)), - InstructionKind::MULW => Self::MulW(MulW(instruction)), - InstructionKind::DIVW => Self::DivW(DivW(instruction)), - InstructionKind::DIVUW => Self::DivUW(DivUW(instruction)), - InstructionKind::REMW => Self::RemW(RemW(instruction)), - InstructionKind::REMUW => Self::RemUW(RemUW(instruction)), - InstructionKind::AND => Self::And(And(instruction)), - InstructionKind::ANDI => Self::AndI(AndI(instruction)), - InstructionKind::OR => Self::Or(Or(instruction)), - InstructionKind::ORI => Self::OrI(OrI(instruction)), - InstructionKind::XOR => Self::Xor(Xor(instruction)), - InstructionKind::XORI => Self::XorI(XorI(instruction)), - InstructionKind::ANDN => Self::Andn(Andn(instruction)), - InstructionKind::SLL => Self::Sll(Sll(instruction)), - InstructionKind::SLLI => Self::SllI(SllI(instruction)), - InstructionKind::SRL => Self::Srl(Srl(instruction)), - InstructionKind::SRLI => Self::SrlI(SrlI(instruction)), - InstructionKind::SRA => Self::Sra(Sra(instruction)), - InstructionKind::SRAI => Self::SraI(SraI(instruction)), - InstructionKind::SLLW => Self::SllW(SllW(instruction)), - InstructionKind::SLLIW => Self::SllIW(SllIW(instruction)), - InstructionKind::SRLW => Self::SrlW(SrlW(instruction)), - InstructionKind::SRLIW => Self::SrlIW(SrlIW(instruction)), - InstructionKind::SRAW => Self::SraW(SraW(instruction)), - InstructionKind::SRAIW => Self::SraIW(SraIW(instruction)), - InstructionKind::SLT => Self::Slt(Slt(instruction)), - InstructionKind::SLTI => Self::SltI(SltI(instruction)), - InstructionKind::SLTU => Self::SltU(SltU(instruction)), - InstructionKind::SLTIU => Self::SltIU(SltIU(instruction)), - InstructionKind::BEQ => Self::Beq(Beq(instruction)), - InstructionKind::BNE => Self::Bne(Bne(instruction)), - InstructionKind::BLT => Self::Blt(Blt(instruction)), - InstructionKind::BGE => Self::Bge(Bge(instruction)), - InstructionKind::BLTU => Self::BltU(BltU(instruction)), - InstructionKind::BGEU => Self::BgeU(BgeU(instruction)), - InstructionKind::LB => Self::Lb(Lb(instruction)), - InstructionKind::LBU => Self::Lbu(Lbu(instruction)), - InstructionKind::LH => Self::Lh(Lh(instruction)), - InstructionKind::LHU => Self::Lhu(Lhu(instruction)), - InstructionKind::LW => Self::Lw(Lw(instruction)), - InstructionKind::LWU => Self::Lwu(Lwu(instruction)), - InstructionKind::LD => Self::Ld(Ld(instruction)), - InstructionKind::SB => Self::Sb(Sb(instruction)), - InstructionKind::SH => Self::Sh(Sh(instruction)), - InstructionKind::SW => Self::Sw(Sw(instruction)), - InstructionKind::SD => Self::Sd(Sd(instruction)), - InstructionKind::ECALL => Self::Ecall(Ecall(instruction)), - InstructionKind::EBREAK => Self::Ebreak(Ebreak(instruction)), - InstructionKind::FENCE => Self::Fence(Fence(instruction)), - InstructionKind::JAL => Self::Jal(Jal(instruction)), - InstructionKind::JALR => Self::Jalr(Jalr(instruction)), - InstructionKind::VirtualAssertEQ => Self::AssertEq(AssertEq(instruction)), - InstructionKind::VirtualAssertLTE => Self::AssertLte(AssertLte(instruction)), - InstructionKind::VirtualAssertValidDiv0 => { - Self::AssertValidDiv0(AssertValidDiv0(instruction)) - } - InstructionKind::VirtualAssertValidUnsignedRemainder => { - Self::AssertValidUnsignedRemainder(AssertValidUnsignedRemainder(instruction)) - } - InstructionKind::VirtualAssertMulUNoOverflow => { - Self::AssertMulUNoOverflow(AssertMulUNoOverflow(instruction)) +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub struct Inline(pub T); + +macro_rules! define_source_instruction { + ( + instructions: [$($instr:ident => $marker:ident => $canonical_name:expr),* $(,)?] + ) => { + /// Typed view over decoded source rows. + /// + /// This is the source-side phase boundary: decode produces this enum, + /// expansion consumes it, and final bytecode is emitted as + /// [`JoltInstructionRow`](crate::JoltInstructionRow). The enum variant is the source + /// instruction identity; the row payload carries only row data, so the + /// two cannot silently disagree. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum SourceInstruction { + Noop(Noop), + Unimplemented(Unimpl), + $( + $marker($marker), + )* + InlineDispatch(Inline), + } + + impl SourceInstruction { + pub const fn kind(&self) -> SourceInstructionKind { + match self { + Self::Noop(_) => SourceInstruction::Noop(Noop(())), + Self::Unimplemented(_) => SourceInstruction::Unimplemented(Unimpl(())), + $( + Self::$marker(_) => SourceInstruction::$marker($marker(())), + )* + Self::InlineDispatch(_) => SourceInstruction::InlineDispatch(Inline(())), + } } - InstructionKind::VirtualAssertWordAlignment => { - Self::AssertWordAlignment(AssertWordAlignment(instruction)) + + pub fn new(kind: SourceInstructionKind, row: SourceInstructionRow) -> Self { + match kind { + SourceInstruction::Noop(_) => Self::Noop(Noop(row)), + SourceInstruction::Unimplemented(_) => Self::Unimplemented(Unimpl(row)), + $( + SourceInstruction::$marker(_) => Self::$marker($marker(row)), + )* + SourceInstruction::InlineDispatch(_) => Self::InlineDispatch(Inline(row)), + } } - InstructionKind::VirtualAssertHalfwordAlignment => { - Self::AssertHalfwordAlignment(AssertHalfwordAlignment(instruction)) + + pub const fn row(&self) -> &SourceInstructionRow { + match self { + Self::Noop(instruction) => &instruction.0, + Self::Unimplemented(instruction) => &instruction.0, + $( + Self::$marker(instruction) => &instruction.0, + )* + Self::InlineDispatch(instruction) => &instruction.0, + } } - InstructionKind::VirtualPow2 => Self::Pow2(Pow2(instruction)), - InstructionKind::VirtualPow2I => Self::Pow2I(Pow2I(instruction)), - InstructionKind::VirtualPow2W => Self::Pow2W(Pow2W(instruction)), - InstructionKind::VirtualPow2IW => Self::Pow2IW(Pow2IW(instruction)), - InstructionKind::VirtualMULI => Self::MulI(MulI(instruction)), - InstructionKind::VirtualMovsign => Self::MovSign(MovSign(instruction)), - InstructionKind::VirtualRev8W => Self::VirtualRev8W(VirtualRev8W(instruction)), - InstructionKind::VirtualChangeDivisor => { - Self::VirtualChangeDivisor(VirtualChangeDivisor(instruction)) + + pub fn into_row(self) -> SourceInstructionRow { + match self { + Self::Noop(instruction) => instruction.0, + Self::Unimplemented(instruction) => instruction.0, + $( + Self::$marker(instruction) => instruction.0, + )* + Self::InlineDispatch(instruction) => instruction.0, + } } - InstructionKind::VirtualChangeDivisorW => { - Self::VirtualChangeDivisorW(VirtualChangeDivisorW(instruction)) + + pub fn map_row(self, f: impl FnOnce(SourceInstructionRow) -> SourceInstructionRow) -> Self { + let kind = self.kind(); + Self::new(kind, f(self.into_row())) } - InstructionKind::VirtualSignExtendWord => { - Self::VirtualSignExtendWord(VirtualSignExtendWord(instruction)) + } + + impl TryFrom<&SourceInstruction> for JoltInstructionRow { + type Error = SourceInstructionKind; + + fn try_from(instruction: &SourceInstruction) -> Result { + let source_kind = instruction.kind(); + let Some(jolt_kind) = source_kind.jolt_kind() else { + return Err(source_kind); + }; + let row = instruction.row().jolt_instruction_row(jolt_kind); + JoltInstruction::try_from(row) + .map(|_| row) + .map_err(|_| source_kind) } - InstructionKind::VirtualZeroExtendWord => { - Self::VirtualZeroExtendWord(VirtualZeroExtendWord(instruction)) + } + + #[cfg(feature = "serialization")] + impl Serialize for SourceInstruction { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let row = self.row(); + let mut state = serializer.serialize_struct("SourceInstruction", 5)?; + state.serialize_field("instruction_kind", &self.kind())?; + state.serialize_field("address", &row.address)?; + state.serialize_field("operands", &row.operands)?; + state.serialize_field("inline", &row.inline)?; + state.serialize_field("is_compressed", &row.is_compressed)?; + state.end() } - InstructionKind::VirtualSRL => Self::VirtualSrl(VirtualSrl(instruction)), - InstructionKind::VirtualSRLI => Self::VirtualSrli(VirtualSrli(instruction)), - InstructionKind::VirtualSRA => Self::VirtualSra(VirtualSra(instruction)), - InstructionKind::VirtualSRAI => Self::VirtualSrai(VirtualSrai(instruction)), - InstructionKind::VirtualShiftRightBitmask => { - Self::VirtualShiftRightBitmask(VirtualShiftRightBitmask(instruction)) + } + + #[cfg(feature = "serialization")] + impl<'de> Deserialize<'de> for SourceInstruction { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct SerializedSourceInstruction { + instruction_kind: SourceInstructionKind, + address: usize, + operands: NormalizedOperands, + #[serde(default)] + inline: Option, + is_compressed: bool, + } + + let instruction = SerializedSourceInstruction::deserialize(deserializer)?; + Ok(Self::new( + instruction.instruction_kind, + SourceInstructionRow { + address: instruction.address, + operands: instruction.operands, + inline: instruction.inline, + is_compressed: instruction.is_compressed, + }, + )) } - InstructionKind::VirtualShiftRightBitmaskI => { - Self::VirtualShiftRightBitmaski(VirtualShiftRightBitmaski(instruction)) + } + }; +} + +crate::for_each_instruction_kind!(define_source_instruction); + +/// Typed view over expanded rows that have static lookup/circuit metadata. +/// +/// Each variant wraps an instruction newtype parameterized by the canonical +/// [`JoltInstructionRow`](crate::JoltInstructionRow) row. Static-flag +/// dispatch and the flag-exclusivity tests rely on this concretization to +/// satisfy `T: JoltInstructionRowData` on the `Flags` impls. +/// +/// Deliberately omitted instruction kinds (declared and re-exported above +/// but not proven by Jolt): the Zicsr ops (`Csrrs`, `Csrrw`), `Mret`, +/// the atomic family (`Amo*`, `Lr*`, `Sc*`), +/// the advice-load helpers (`AdviceLb`/`Ld`/`Lh`/`Lw`), and `VirtualLw` / +/// `VirtualSw`. These are intentionally absent from `JoltInstruction` and +/// from the flag-exclusivity tests below. + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum JoltInstruction { + Noop(Noop), + Add(Add), + Addi(Addi), + Sub(Sub), + Lui(Lui), + Auipc(Auipc), + Mul(Mul), + MulHU(MulHU), + And(And), + AndI(AndI), + Or(Or), + OrI(OrI), + Xor(Xor), + XorI(XorI), + Andn(Andn), + Slt(Slt), + SltI(SltI), + SltU(SltU), + SltIU(SltIU), + Beq(Beq), + Bne(Bne), + Blt(Blt), + Bge(Bge), + BltU(BltU), + BgeU(BgeU), + Ld(Ld), + Sd(Sd), + Fence(Fence), + Jal(Jal), + Jalr(Jalr), + AssertEq(AssertEq), + AssertLte(AssertLte), + AssertValidDiv0(AssertValidDiv0), + AssertValidUnsignedRemainder(AssertValidUnsignedRemainder), + AssertMulUNoOverflow(AssertMulUNoOverflow), + AssertWordAlignment(AssertWordAlignment), + AssertHalfwordAlignment(AssertHalfwordAlignment), + Pow2(Pow2), + Pow2I(Pow2I), + Pow2W(Pow2W), + Pow2IW(Pow2IW), + MulI(MulI), + MovSign(MovSign), + VirtualRev8W(VirtualRev8W), + VirtualChangeDivisor(VirtualChangeDivisor), + VirtualChangeDivisorW(VirtualChangeDivisorW), + VirtualSignExtendWord(VirtualSignExtendWord), + VirtualZeroExtendWord(VirtualZeroExtendWord), + VirtualSrl(VirtualSrl), + VirtualSrli(VirtualSrli), + VirtualSra(VirtualSra), + VirtualSrai(VirtualSrai), + VirtualShiftRightBitmask(VirtualShiftRightBitmask), + VirtualShiftRightBitmaski(VirtualShiftRightBitmaski), + VirtualRotri(VirtualRotri), + VirtualRotriw(VirtualRotriw), + VirtualXorRot32(VirtualXorRot32), + VirtualXorRot24(VirtualXorRot24), + VirtualXorRot16(VirtualXorRot16), + VirtualXorRot63(VirtualXorRot63), + VirtualXorRotW16(VirtualXorRotW16), + VirtualXorRotW12(VirtualXorRotW12), + VirtualXorRotW8(VirtualXorRotW8), + VirtualXorRotW7(VirtualXorRotW7), + VirtualAdvice(VirtualAdvice), + VirtualAdviceLen(VirtualAdviceLen), + VirtualAdviceLoad(VirtualAdviceLoad), + VirtualHostIO(VirtualHostIO), +} + +macro_rules! impl_jolt_instruction_try_from_row { + ( + instructions: [$($instr:ident => $marker:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + impl TryFrom for JoltInstruction { + type Error = JoltInstructionKind; + + fn try_from(instruction: JoltInstructionRow) -> Result { + Ok(match instruction.instruction_kind { + JoltInstruction::Noop(_) => Self::Noop(Noop(instruction)), + $( + JoltInstruction::$marker(_) => Self::$marker($marker(instruction)), + )* + }) } - InstructionKind::VirtualROTRI => Self::VirtualRotri(VirtualRotri(instruction)), - InstructionKind::VirtualROTRIW => Self::VirtualRotriw(VirtualRotriw(instruction)), - InstructionKind::VirtualXORROT32 => Self::VirtualXorRot32(VirtualXorRot32(instruction)), - InstructionKind::VirtualXORROT24 => Self::VirtualXorRot24(VirtualXorRot24(instruction)), - InstructionKind::VirtualXORROT16 => Self::VirtualXorRot16(VirtualXorRot16(instruction)), - InstructionKind::VirtualXORROT63 => Self::VirtualXorRot63(VirtualXorRot63(instruction)), - InstructionKind::VirtualXORROTW16 => { - Self::VirtualXorRotW16(VirtualXorRotW16(instruction)) + } + }; +} + +crate::for_each_jolt_instruction_kind!(impl_jolt_instruction_try_from_row); + +macro_rules! impl_jolt_instructions_flags { + ($($variant:ident => $kind:ident),* $(,)?) => { + impl JoltInstruction { + pub const fn kind(&self) -> JoltInstructionKind { + match self { + Self::Noop(_) => JoltInstruction::Noop(Noop(())), + $( + Self::$variant(_) => JoltInstruction::$variant($variant(())), + )* + } } - InstructionKind::VirtualXORROTW12 => { - Self::VirtualXorRotW12(VirtualXorRotW12(instruction)) + + pub const fn row(&self) -> &T { + match self { + Self::Noop(instruction) => &instruction.0, + $( + Self::$variant(instruction) => &instruction.0, + )* + } } - InstructionKind::VirtualXORROTW8 => Self::VirtualXorRotW8(VirtualXorRotW8(instruction)), - InstructionKind::VirtualXORROTW7 => Self::VirtualXorRotW7(VirtualXorRotW7(instruction)), - InstructionKind::VirtualAdvice => Self::VirtualAdvice(VirtualAdvice(instruction)), - InstructionKind::VirtualAdviceLen => { - Self::VirtualAdviceLen(VirtualAdviceLen(instruction)) + + } + + impl JoltInstruction { + pub fn into_row(self) -> JoltInstructionRow { + match self { + Self::Noop(instruction) => { + let mut row = instruction.0.into(); + row.instruction_kind = JoltInstruction::Noop(Noop(())); + row + } + $( + Self::$variant(instruction) => { + let mut row = instruction.0.into(); + row.instruction_kind = JoltInstruction::$variant($variant(())); + row + } + )* + } } - InstructionKind::VirtualAdviceLoad => { - Self::VirtualAdviceLoad(VirtualAdviceLoad(instruction)) + } + + impl From> for JoltInstructionRow { + fn from(instruction: JoltInstruction) -> Self { + instruction.into_row() } - InstructionKind::VirtualHostIO => Self::VirtualHostIO(VirtualHostIO(instruction)), - unsupported => return Err(unsupported), - }) - } -} + } -macro_rules! impl_jolt_instructions_flags { - ($($variant:ident),* $(,)?) => { - impl crate::flags::Flags for JoltInstructions { + impl crate::flags::Flags for JoltInstruction { fn circuit_flags(&self) -> crate::flags::CircuitFlagSet { match self { - JoltInstructions::Noop =>{ + JoltInstruction::Noop(_) =>{ crate::flags::CircuitFlagSet::default() .set(crate::flags::CircuitFlags::DoNotUpdateUnexpandedPC) }, - $(JoltInstructions::$variant(t) => t.circuit_flags(),)* + $(JoltInstruction::$variant(t) => t.circuit_flags(),)* } } fn instruction_flags(&self) -> crate::flags::InstructionFlagSet { match self { - JoltInstructions::Noop =>{ + JoltInstruction::Noop(_) =>{ crate::flags::InstructionFlagSet::default() .set(crate::flags::InstructionFlags::IsNoop) }, - $(JoltInstructions::$variant(t) => t.instruction_flags(),)* + $(JoltInstruction::$variant(t) => t.instruction_flags(),)* } } } + + impl JoltInstruction { + pub fn iter() -> impl Iterator { + [ + Self::Noop(Noop(JoltInstructionRow::default())), + $( + Self::$variant($variant(JoltInstructionRow::default())), + )* + ] + .into_iter() + } + } }; } impl_jolt_instructions_flags! { - Add, Addi, Sub, Lui, Auipc, - Mul, MulH, MulHSU, MulHU, Div, DivU, Rem, RemU, - AddW, AddiW, SubW, MulW, DivW, DivUW, RemW, RemUW, - And, AndI, Or, OrI, Xor, XorI, Andn, - Sll, SllI, Srl, SrlI, Sra, SraI, - SllW, SllIW, SrlW, SrlIW, SraW, SraIW, - Slt, SltI, SltU, SltIU, - Beq, Bne, Blt, Bge, BltU, BgeU, - Lb, Lbu, Lh, Lhu, Lw, Lwu, Ld, - Sb, Sh, Sw, Sd, - Ecall, Ebreak, Fence, - Jal, Jalr, - AssertEq, AssertLte, AssertValidDiv0, AssertValidUnsignedRemainder, - AssertMulUNoOverflow, AssertWordAlignment, AssertHalfwordAlignment, - Pow2, Pow2I, Pow2W, Pow2IW, MulI, - MovSign, VirtualRev8W, - VirtualChangeDivisor, VirtualChangeDivisorW, - VirtualSignExtendWord, VirtualZeroExtendWord, - VirtualSrl, VirtualSrli, VirtualSra, VirtualSrai, - VirtualShiftRightBitmask, VirtualShiftRightBitmaski, - VirtualRotri, VirtualRotriw, - VirtualXorRot32, VirtualXorRot24, VirtualXorRot16, VirtualXorRot63, - VirtualXorRotW16, VirtualXorRotW12, VirtualXorRotW8, VirtualXorRotW7, - VirtualAdvice, VirtualAdviceLen, VirtualAdviceLoad, VirtualHostIO, + Add => ADD, + Addi => ADDI, + Sub => SUB, + Lui => LUI, + Auipc => AUIPC, + Mul => MUL, + MulHU => MULHU, + And => AND, + AndI => ANDI, + Or => OR, + OrI => ORI, + Xor => XOR, + XorI => XORI, + Andn => ANDN, + Slt => SLT, + SltI => SLTI, + SltU => SLTU, + SltIU => SLTIU, + Beq => BEQ, + Bne => BNE, + Blt => BLT, + Bge => BGE, + BltU => BLTU, + BgeU => BGEU, + Ld => LD, + Sd => SD, + Fence => FENCE, + Jal => JAL, + Jalr => JALR, + AssertEq => VirtualAssertEQ, + AssertLte => VirtualAssertLTE, + AssertValidDiv0 => VirtualAssertValidDiv0, + AssertValidUnsignedRemainder => VirtualAssertValidUnsignedRemainder, + AssertMulUNoOverflow => VirtualAssertMulUNoOverflow, + AssertWordAlignment => VirtualAssertWordAlignment, + AssertHalfwordAlignment => VirtualAssertHalfwordAlignment, + Pow2 => VirtualPow2, + Pow2I => VirtualPow2I, + Pow2W => VirtualPow2W, + Pow2IW => VirtualPow2IW, + MulI => VirtualMULI, + MovSign => VirtualMovsign, + VirtualRev8W => VirtualRev8W, + VirtualChangeDivisor => VirtualChangeDivisor, + VirtualChangeDivisorW => VirtualChangeDivisorW, + VirtualSignExtendWord => VirtualSignExtendWord, + VirtualZeroExtendWord => VirtualZeroExtendWord, + VirtualSrl => VirtualSRL, + VirtualSrli => VirtualSRLI, + VirtualSra => VirtualSRA, + VirtualSrai => VirtualSRAI, + VirtualShiftRightBitmask => VirtualShiftRightBitmask, + VirtualShiftRightBitmaski => VirtualShiftRightBitmaskI, + VirtualRotri => VirtualROTRI, + VirtualRotriw => VirtualROTRIW, + VirtualXorRot32 => VirtualXORROT32, + VirtualXorRot24 => VirtualXORROT24, + VirtualXorRot16 => VirtualXORROT16, + VirtualXorRot63 => VirtualXORROT63, + VirtualXorRotW16 => VirtualXORROTW16, + VirtualXorRotW12 => VirtualXORROTW12, + VirtualXorRotW8 => VirtualXORROTW8, + VirtualXorRotW7 => VirtualXORROTW7, + VirtualAdvice => VirtualAdvice, + VirtualAdviceLen => VirtualAdviceLen, + VirtualAdviceLoad => VirtualAdviceLoad, + VirtualHostIO => VirtualHostIO, } - #[cfg(test)] mod tests { use super::*; use crate::flags::{CircuitFlags, Flags, InstructionFlags}; - use strum::IntoEnumIterator; #[test] fn left_operand_exclusive() { - for instr in JoltInstructions::iter() { + for instr in JoltInstruction::iter() { let flags = instr.instruction_flags(); assert!( !(flags[InstructionFlags::LeftOperandIsPC] @@ -498,7 +588,7 @@ mod tests { #[test] fn right_operand_exclusive() { - for instr in JoltInstructions::iter() { + for instr in JoltInstruction::iter() { let flags = instr.instruction_flags(); assert!( !(flags[InstructionFlags::RightOperandIsRs2Value] @@ -510,7 +600,7 @@ mod tests { #[test] fn lookup_shape_exclusive() { - for instr in JoltInstructions::iter() { + for instr in JoltInstruction::iter() { let flags = instr.circuit_flags(); let num_true = [ flags[CircuitFlags::AddOperands], @@ -530,7 +620,7 @@ mod tests { #[test] fn load_store_exclusive() { - for instr in JoltInstructions::iter() { + for instr in JoltInstruction::iter() { let flags = instr.circuit_flags(); assert!( !(flags[CircuitFlags::Load] && flags[CircuitFlags::Store]), @@ -540,22 +630,133 @@ mod tests { } #[test] - fn terminal_virtual_instruction_marks_last_in_sequence() -> Result<(), InstructionKind> { - fn flags_for(row: NormalizedInstruction) -> Result { - JoltInstructions::try_from(row).map(|instruction| instruction.circuit_flags()) + fn phase_specific_instruction_kinds_are_distinct() { + let source_kind = crate::SourceInstructionKind::AMOADDW; + + assert_eq!(source_kind.jolt_kind(), None); + assert!(source_kind.expands_to_jolt()); + } + + #[test] + fn source_instruction_variant_is_the_source_identity() { + let row = SourceInstructionRow { + address: 0x8000_0000, + operands: crate::NormalizedOperands { + rd: Some(1), + rs1: Some(2), + rs2: Some(3), + imm: 4, + }, + inline: None, + is_compressed: false, + }; + + let add = SourceInstruction::new(SourceInstructionKind::ADD, row); + let beq = SourceInstruction::new(SourceInstructionKind::BEQ, row); + + assert_eq!(add.kind(), SourceInstructionKind::ADD); + assert_eq!(beq.kind(), SourceInstructionKind::BEQ); + assert_eq!( + JoltInstructionRow::try_from(&add).map(|row| row.instruction_kind), + Ok(JoltInstructionKind::ADD) + ); + assert_eq!( + JoltInstructionRow::try_from(&beq).map(|row| row.instruction_kind), + Ok(JoltInstructionKind::BEQ) + ); + assert!(matches!(add, SourceInstruction::Add(Add(..)))); + assert!(matches!(beq, SourceInstruction::Beq(Beq(..)))); + } + + #[test] + fn source_instruction_uses_catalog_marker_types() { + let row = SourceInstructionRow::default(); + + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::VirtualAssertEQ, row), + SourceInstruction::AssertEq(AssertEq(..)) + )); + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::VirtualMULI, row), + SourceInstruction::MulI(MulI(..)) + )); + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::VirtualROTRI, row), + SourceInstruction::VirtualRotri(VirtualRotri(..)) + )); + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::AMOMAXUD, row), + SourceInstruction::AmoMaxUD(AmoMaxUD(..)) + )); + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::Inline, row), + SourceInstruction::InlineDispatch(Inline(..)) + )); + assert!(matches!( + SourceInstruction::new(SourceInstructionKind::Unimpl, row), + SourceInstruction::Unimplemented(Unimpl(..)) + )); + } + + #[test] + fn jolt_instruction_identifies_explicit_final_subset() { + assert!(matches!( + JoltInstruction::try_from(JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADD, + ..Default::default() + }), + Ok(JoltInstruction::Add(..)) + )); + for kind in [ + SourceInstructionKind::ADDW, + SourceInstructionKind::DIV, + SourceInstructionKind::LW, + SourceInstructionKind::SW, + SourceInstructionKind::AMOADDW, + SourceInstructionKind::CSRRS, + SourceInstructionKind::VirtualSW, + ] { + assert_eq!( + JoltInstructionRow::try_from(&SourceInstruction::new( + kind, + SourceInstructionRow::default() + )), + Err(kind) + ); + } + } + + #[test] + fn jolt_instruction_variant_normalizes_row_kind() { + let row = JoltInstructionRow { + instruction_kind: JoltInstructionKind::SUB, + ..Default::default() + }; + + let normalized = JoltInstructionRow::from(JoltInstruction::Add(Add(row))); + + assert_eq!(normalized.instruction_kind, JoltInstructionKind::ADD); + } + + #[test] + fn terminal_virtual_instruction_marks_last_in_sequence() -> Result<(), JoltInstructionKind> { + fn flags_for( + row: JoltInstructionRow, + ) -> Result { + JoltInstruction::try_from(row).map(|instruction| instruction.circuit_flags()) } - let mut row = NormalizedInstruction { + let mut row = JoltInstructionRow { virtual_sequence_remaining: Some(0), ..Default::default() }; - row.instruction_kind = InstructionKind::ADDI; + row.instruction_kind = JoltInstructionKind::ADDI; let addi_flags = flags_for(row)?; assert!(addi_flags[CircuitFlags::VirtualInstruction]); assert!(addi_flags[CircuitFlags::IsLastInSequence]); - row.instruction_kind = InstructionKind::JALR; + row.instruction_kind = JoltInstructionKind::JALR; let jalr_flags = flags_for(row)?; assert!(jalr_flags[CircuitFlags::VirtualInstruction]); assert!(jalr_flags[CircuitFlags::IsLastInSequence]); diff --git a/crates/jolt-riscv/src/instructions/virt/pow2w.rs b/crates/jolt-riscv/src/instructions/virt/pow2w.rs index 5ad7ef016d..05e06485a4 100644 --- a/crates/jolt-riscv/src/instructions/virt/pow2w.rs +++ b/crates/jolt-riscv/src/instructions/virt/pow2w.rs @@ -1,7 +1,7 @@ use crate::jolt_instruction; jolt_instruction!( - /// Virtual POW2W: computes `2^(rs1 mod 32)` for 32-bit mode. + /// Virtual POW2W: computes `2^(rs1 mod 32)` for word operations. Pow2W, circuit flags: [AddOperands, WriteLookupOutputToRD], instruction flags: [LeftOperandIsRs1Value] diff --git a/crates/jolt-riscv/src/jolt_instruction.rs b/crates/jolt-riscv/src/jolt_instruction.rs deleted file mode 100644 index 6c38a087c2..0000000000 --- a/crates/jolt-riscv/src/jolt_instruction.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Marker trait for concrete instruction types that can convert through -//! Jolt's canonical normalized bytecode row. - -use crate::NormalizedInstruction; - -pub trait JoltInstruction: - Copy + Into + TryFrom -{ - fn normalize(&self) -> NormalizedInstruction { - (*self).into() - } - - fn is_virtual(&self) -> bool { - self.normalize().virtual_sequence_remaining.is_some() - } -} - -impl JoltInstruction for NormalizedInstruction {} diff --git a/crates/jolt-riscv/src/kind.rs b/crates/jolt-riscv/src/kind.rs index 60607d3313..208dc4861f 100644 --- a/crates/jolt-riscv/src/kind.rs +++ b/crates/jolt-riscv/src/kind.rs @@ -1,147 +1,1854 @@ +#[cfg(feature = "serialization")] use ark_serialize::{ CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, Write, }; -use serde::{Deserialize, Serialize}; +#[cfg(feature = "serialization")] +use serde::{de::Visitor, Deserializer, Serialize, Serializer}; -macro_rules! define_instruction_kind { +use crate::instructions::*; +use crate::profile::{JoltTargetExtension, SourceExtension}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct JoltInstructionTag(pub u16); + +/// Source instruction identity, represented as the zero-payload source enum. +/// +/// `SourceInstruction` is the only source-instruction enum. Instantiating it +/// with `T = ()` gives the kind-level view; instantiating it with +/// `T = SourceInstructionRow` gives a concrete decoded row. +pub type SourceInstructionKind = SourceInstruction<()>; + +/// Final Jolt instruction identity, represented as the zero-payload final enum. +/// +/// `JoltInstruction` is the only final-instruction enum. Instantiating it +/// with `T = ()` gives the kind-level view; instantiating it with +/// `T = JoltInstructionRow` gives a concrete final row. +pub type JoltInstructionKind = JoltInstruction<()>; + +pub trait SourceInstructionMeta { + const CANONICAL_NAME: &'static str; + const SOURCE_EXTENSION: Option; + const HAS_SIDE_EFFECTS: bool; +} + +pub trait JoltInstructionMeta { + const CANONICAL_NAME: &'static str; + const JOLT_TAG: JoltInstructionTag; + const TARGET_EXTENSION: Option; + const HAS_SIDE_EFFECTS: bool; +} + +macro_rules! source_extension_for_marker { + (Add) => { + Some(SourceExtension::Rv64I) + }; + (Addi) => { + Some(SourceExtension::Rv64I) + }; + (AddiW) => { + Some(SourceExtension::Rv64I) + }; + (AddW) => { + Some(SourceExtension::Rv64I) + }; + (And) => { + Some(SourceExtension::Rv64I) + }; + (AndI) => { + Some(SourceExtension::Rv64I) + }; + (Auipc) => { + Some(SourceExtension::Rv64I) + }; + (Beq) => { + Some(SourceExtension::Rv64I) + }; + (Bge) => { + Some(SourceExtension::Rv64I) + }; + (BgeU) => { + Some(SourceExtension::Rv64I) + }; + (Blt) => { + Some(SourceExtension::Rv64I) + }; + (BltU) => { + Some(SourceExtension::Rv64I) + }; + (Bne) => { + Some(SourceExtension::Rv64I) + }; + (Ebreak) => { + Some(SourceExtension::Rv64I) + }; + (Ecall) => { + Some(SourceExtension::Rv64I) + }; + (Fence) => { + Some(SourceExtension::Rv64I) + }; + (Jal) => { + Some(SourceExtension::Rv64I) + }; + (Jalr) => { + Some(SourceExtension::Rv64I) + }; + (Lb) => { + Some(SourceExtension::Rv64I) + }; + (Lbu) => { + Some(SourceExtension::Rv64I) + }; + (Ld) => { + Some(SourceExtension::Rv64I) + }; + (Lh) => { + Some(SourceExtension::Rv64I) + }; + (Lhu) => { + Some(SourceExtension::Rv64I) + }; + (Lui) => { + Some(SourceExtension::Rv64I) + }; + (Lw) => { + Some(SourceExtension::Rv64I) + }; + (Lwu) => { + Some(SourceExtension::Rv64I) + }; + (Or) => { + Some(SourceExtension::Rv64I) + }; + (OrI) => { + Some(SourceExtension::Rv64I) + }; + (Sb) => { + Some(SourceExtension::Rv64I) + }; + (Sd) => { + Some(SourceExtension::Rv64I) + }; + (Sh) => { + Some(SourceExtension::Rv64I) + }; + (Sll) => { + Some(SourceExtension::Rv64I) + }; + (SllI) => { + Some(SourceExtension::Rv64I) + }; + (SllIW) => { + Some(SourceExtension::Rv64I) + }; + (SllW) => { + Some(SourceExtension::Rv64I) + }; + (Slt) => { + Some(SourceExtension::Rv64I) + }; + (SltI) => { + Some(SourceExtension::Rv64I) + }; + (SltIU) => { + Some(SourceExtension::Rv64I) + }; + (SltU) => { + Some(SourceExtension::Rv64I) + }; + (Sra) => { + Some(SourceExtension::Rv64I) + }; + (SraI) => { + Some(SourceExtension::Rv64I) + }; + (SraIW) => { + Some(SourceExtension::Rv64I) + }; + (SraW) => { + Some(SourceExtension::Rv64I) + }; + (Srl) => { + Some(SourceExtension::Rv64I) + }; + (SrlI) => { + Some(SourceExtension::Rv64I) + }; + (SrlIW) => { + Some(SourceExtension::Rv64I) + }; + (SrlW) => { + Some(SourceExtension::Rv64I) + }; + (Sub) => { + Some(SourceExtension::Rv64I) + }; + (SubW) => { + Some(SourceExtension::Rv64I) + }; + (Sw) => { + Some(SourceExtension::Rv64I) + }; + (Xor) => { + Some(SourceExtension::Rv64I) + }; + (XorI) => { + Some(SourceExtension::Rv64I) + }; + (Mul) => { + Some(SourceExtension::Rv64M) + }; + (MulH) => { + Some(SourceExtension::Rv64M) + }; + (MulHSU) => { + Some(SourceExtension::Rv64M) + }; + (MulHU) => { + Some(SourceExtension::Rv64M) + }; + (MulW) => { + Some(SourceExtension::Rv64M) + }; + (Div) => { + Some(SourceExtension::Rv64M) + }; + (DivU) => { + Some(SourceExtension::Rv64M) + }; + (DivUW) => { + Some(SourceExtension::Rv64M) + }; + (DivW) => { + Some(SourceExtension::Rv64M) + }; + (Rem) => { + Some(SourceExtension::Rv64M) + }; + (RemU) => { + Some(SourceExtension::Rv64M) + }; + (RemUW) => { + Some(SourceExtension::Rv64M) + }; + (RemW) => { + Some(SourceExtension::Rv64M) + }; + (LrW) => { + Some(SourceExtension::Rv64A) + }; + (ScW) => { + Some(SourceExtension::Rv64A) + }; + (AmoSwapW) => { + Some(SourceExtension::Rv64A) + }; + (AmoAddW) => { + Some(SourceExtension::Rv64A) + }; + (AmoAndW) => { + Some(SourceExtension::Rv64A) + }; + (AmoOrW) => { + Some(SourceExtension::Rv64A) + }; + (AmoXorW) => { + Some(SourceExtension::Rv64A) + }; + (AmoMinW) => { + Some(SourceExtension::Rv64A) + }; + (AmoMaxW) => { + Some(SourceExtension::Rv64A) + }; + (AmoMinUW) => { + Some(SourceExtension::Rv64A) + }; + (AmoMaxUW) => { + Some(SourceExtension::Rv64A) + }; + (LrD) => { + Some(SourceExtension::Rv64A) + }; + (ScD) => { + Some(SourceExtension::Rv64A) + }; + (AmoSwapD) => { + Some(SourceExtension::Rv64A) + }; + (AmoAddD) => { + Some(SourceExtension::Rv64A) + }; + (AmoAndD) => { + Some(SourceExtension::Rv64A) + }; + (AmoOrD) => { + Some(SourceExtension::Rv64A) + }; + (AmoXorD) => { + Some(SourceExtension::Rv64A) + }; + (AmoMinD) => { + Some(SourceExtension::Rv64A) + }; + (AmoMaxD) => { + Some(SourceExtension::Rv64A) + }; + (AmoMinUD) => { + Some(SourceExtension::Rv64A) + }; + (AmoMaxUD) => { + Some(SourceExtension::Rv64A) + }; + (Csrrs) => { + Some(SourceExtension::Zicsr) + }; + (Csrrw) => { + Some(SourceExtension::Zicsr) + }; + (Mret) => { + Some(SourceExtension::RvPrivileged) + }; + (Andn) => { + Some(SourceExtension::JoltCustom) + }; + (AdviceLb) => { + Some(SourceExtension::JoltCustom) + }; + (AdviceLd) => { + Some(SourceExtension::JoltCustom) + }; + (AdviceLh) => { + Some(SourceExtension::JoltCustom) + }; + (AdviceLw) => { + Some(SourceExtension::JoltCustom) + }; + (AssertEq) => { + Some(SourceExtension::JoltCustom) + }; + (AssertHalfwordAlignment) => { + Some(SourceExtension::JoltCustom) + }; + (AssertWordAlignment) => { + Some(SourceExtension::JoltCustom) + }; + (AssertLte) => { + Some(SourceExtension::JoltCustom) + }; + (AssertValidDiv0) => { + Some(SourceExtension::JoltCustom) + }; + (AssertValidUnsignedRemainder) => { + Some(SourceExtension::JoltCustom) + }; + (AssertMulUNoOverflow) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualAdvice) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualAdviceLen) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualAdviceLoad) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualHostIO) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualChangeDivisor) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualChangeDivisorW) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualLw) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSw) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualZeroExtendWord) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSignExtendWord) => { + Some(SourceExtension::JoltCustom) + }; + (Pow2W) => { + Some(SourceExtension::JoltCustom) + }; + (Pow2IW) => { + Some(SourceExtension::JoltCustom) + }; + (MovSign) => { + Some(SourceExtension::JoltCustom) + }; + (MulI) => { + Some(SourceExtension::JoltCustom) + }; + (Pow2) => { + Some(SourceExtension::JoltCustom) + }; + (Pow2I) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualRev8W) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualRotri) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualRotriw) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualShiftRightBitmask) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualShiftRightBitmaski) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSra) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSrai) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSrl) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualSrli) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRot32) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRot24) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRot16) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRot63) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRotW16) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRotW12) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRotW8) => { + Some(SourceExtension::JoltCustom) + }; + (VirtualXorRotW7) => { + Some(SourceExtension::JoltCustom) + }; +} + +macro_rules! source_side_effects_for_marker { + (AdviceLb) => { + true + }; + (AdviceLd) => { + true + }; + (AdviceLh) => { + true + }; + (AdviceLw) => { + true + }; + (AmoAddD) => { + true + }; + (AmoAddW) => { + true + }; + (AmoAndD) => { + true + }; + (AmoAndW) => { + true + }; + (AmoMaxD) => { + true + }; + (AmoMaxUD) => { + true + }; + (AmoMaxUW) => { + true + }; + (AmoMaxW) => { + true + }; + (AmoMinD) => { + true + }; + (AmoMinUD) => { + true + }; + (AmoMinUW) => { + true + }; + (AmoMinW) => { + true + }; + (AmoOrD) => { + true + }; + (AmoOrW) => { + true + }; + (AmoSwapD) => { + true + }; + (AmoSwapW) => { + true + }; + (AmoXorD) => { + true + }; + (AmoXorW) => { + true + }; + (Beq) => { + true + }; + (Bge) => { + true + }; + (BgeU) => { + true + }; + (Blt) => { + true + }; + (BltU) => { + true + }; + (Bne) => { + true + }; + (Csrrs) => { + true + }; + (Csrrw) => { + true + }; + (Ebreak) => { + true + }; + (Ecall) => { + true + }; + (Inline) => { + true + }; + (Jal) => { + true + }; + (Jalr) => { + true + }; + (Lb) => { + true + }; + (Lbu) => { + true + }; + (Ld) => { + true + }; + (Lh) => { + true + }; + (Lhu) => { + true + }; + (LrD) => { + true + }; + (LrW) => { + true + }; + (Lw) => { + true + }; + (Lwu) => { + true + }; + (Mret) => { + true + }; + (Sb) => { + true + }; + (ScD) => { + true + }; + (ScW) => { + true + }; + (Sd) => { + true + }; + (Sh) => { + true + }; + (Sw) => { + true + }; + (VirtualAdviceLoad) => { + true + }; + (VirtualHostIO) => { + true + }; + (VirtualSw) => { + true + }; + (Add) => { + false + }; + (Addi) => { + false + }; + (AddiW) => { + false + }; + (AddW) => { + false + }; + (And) => { + false + }; + (AndI) => { + false + }; + (Andn) => { + false + }; + (Auipc) => { + false + }; + (Div) => { + false + }; + (DivU) => { + false + }; + (DivUW) => { + false + }; + (DivW) => { + false + }; + (Fence) => { + false + }; + (Lui) => { + false + }; + (Mul) => { + false + }; + (MulH) => { + false + }; + (MulHSU) => { + false + }; + (MulHU) => { + false + }; + (MulW) => { + false + }; + (Or) => { + false + }; + (OrI) => { + false + }; + (Rem) => { + false + }; + (RemU) => { + false + }; + (RemUW) => { + false + }; + (RemW) => { + false + }; + (Sll) => { + false + }; + (SllI) => { + false + }; + (SllIW) => { + false + }; + (SllW) => { + false + }; + (Slt) => { + false + }; + (SltI) => { + false + }; + (SltIU) => { + false + }; + (SltU) => { + false + }; + (Sra) => { + false + }; + (SraI) => { + false + }; + (SraIW) => { + false + }; + (SraW) => { + false + }; + (Srl) => { + false + }; + (SrlI) => { + false + }; + (SrlIW) => { + false + }; + (SrlW) => { + false + }; + (Sub) => { + false + }; + (SubW) => { + false + }; + (Xor) => { + false + }; + (XorI) => { + false + }; + (AssertEq) => { + false + }; + (AssertHalfwordAlignment) => { + false + }; + (AssertWordAlignment) => { + false + }; + (AssertLte) => { + false + }; + (AssertValidDiv0) => { + false + }; + (AssertValidUnsignedRemainder) => { + false + }; + (AssertMulUNoOverflow) => { + false + }; + (VirtualAdvice) => { + false + }; + (VirtualAdviceLen) => { + false + }; + (VirtualChangeDivisor) => { + false + }; + (VirtualChangeDivisorW) => { + false + }; + (VirtualLw) => { + false + }; + (VirtualZeroExtendWord) => { + false + }; + (VirtualSignExtendWord) => { + false + }; + (Pow2W) => { + false + }; + (Pow2IW) => { + false + }; + (MovSign) => { + false + }; + (MulI) => { + false + }; + (Pow2) => { + false + }; + (Pow2I) => { + false + }; + (VirtualRev8W) => { + false + }; + (VirtualRotri) => { + false + }; + (VirtualRotriw) => { + false + }; + (VirtualShiftRightBitmask) => { + false + }; + (VirtualShiftRightBitmaski) => { + false + }; + (VirtualSra) => { + false + }; + (VirtualSrai) => { + false + }; + (VirtualSrl) => { + false + }; + (VirtualSrli) => { + false + }; + (VirtualXorRot32) => { + false + }; + (VirtualXorRot24) => { + false + }; + (VirtualXorRot16) => { + false + }; + (VirtualXorRot63) => { + false + }; + (VirtualXorRotW16) => { + false + }; + (VirtualXorRotW12) => { + false + }; + (VirtualXorRotW8) => { + false + }; + (VirtualXorRotW7) => { + false + }; +} + +macro_rules! jolt_target_extension_for_marker { + (Add) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Addi) => { + Some(JoltTargetExtension::IntegerCore) + }; + (And) => { + Some(JoltTargetExtension::IntegerCore) + }; + (AndI) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Auipc) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Lui) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Or) => { + Some(JoltTargetExtension::IntegerCore) + }; + (OrI) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Slt) => { + Some(JoltTargetExtension::IntegerCore) + }; + (SltI) => { + Some(JoltTargetExtension::IntegerCore) + }; + (SltIU) => { + Some(JoltTargetExtension::IntegerCore) + }; + (SltU) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Sub) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Xor) => { + Some(JoltTargetExtension::IntegerCore) + }; + (XorI) => { + Some(JoltTargetExtension::IntegerCore) + }; + (Mul) => { + Some(JoltTargetExtension::IntegerMultiply) + }; + (MulHU) => { + Some(JoltTargetExtension::IntegerMultiply) + }; + (Beq) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Bge) => { + Some(JoltTargetExtension::ControlFlow) + }; + (BgeU) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Blt) => { + Some(JoltTargetExtension::ControlFlow) + }; + (BltU) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Bne) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Fence) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Jal) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Jalr) => { + Some(JoltTargetExtension::ControlFlow) + }; + (Ld) => { + Some(JoltTargetExtension::LoadStore64) + }; + (Sd) => { + Some(JoltTargetExtension::LoadStore64) + }; + (VirtualAdvice) => { + Some(JoltTargetExtension::Advice) + }; + (VirtualAdviceLen) => { + Some(JoltTargetExtension::Advice) + }; + (VirtualAdviceLoad) => { + Some(JoltTargetExtension::Advice) + }; + (VirtualHostIO) => { + Some(JoltTargetExtension::HostIO) + }; + (AssertEq) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertHalfwordAlignment) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertWordAlignment) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertLte) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertValidDiv0) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertValidUnsignedRemainder) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (AssertMulUNoOverflow) => { + Some(JoltTargetExtension::VirtualAssertions) + }; + (VirtualChangeDivisor) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (VirtualChangeDivisorW) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (VirtualZeroExtendWord) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (VirtualSignExtendWord) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (Pow2W) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (Pow2IW) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (MovSign) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (MulI) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (Pow2) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (Pow2I) => { + Some(JoltTargetExtension::VirtualArithmetic) + }; + (VirtualRotri) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualRotriw) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualShiftRightBitmask) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualShiftRightBitmaski) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualSra) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualSrai) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualSrl) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (VirtualSrli) => { + Some(JoltTargetExtension::VirtualShifts) + }; + (Andn) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualRev8W) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRot32) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRot24) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRot16) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRot63) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRotW16) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRotW12) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRotW8) => { + Some(JoltTargetExtension::BitManipulation) + }; + (VirtualXorRotW7) => { + Some(JoltTargetExtension::BitManipulation) + }; +} + +macro_rules! jolt_side_effects_for_marker { + (Beq) => { + true + }; + (Bge) => { + true + }; + (BgeU) => { + true + }; + (Blt) => { + true + }; + (BltU) => { + true + }; + (Bne) => { + true + }; + (Fence) => { + true + }; + (Jal) => { + true + }; + (Jalr) => { + true + }; + (Ld) => { + true + }; + (Sd) => { + true + }; + (VirtualAdviceLoad) => { + true + }; + (VirtualHostIO) => { + true + }; + (Add) => { + false + }; + (Addi) => { + false + }; + (And) => { + false + }; + (AndI) => { + false + }; + (Andn) => { + false + }; + (Auipc) => { + false + }; + (Lui) => { + false + }; + (Mul) => { + false + }; + (MulHU) => { + false + }; + (Or) => { + false + }; + (OrI) => { + false + }; + (Slt) => { + false + }; + (SltI) => { + false + }; + (SltIU) => { + false + }; + (SltU) => { + false + }; + (Sub) => { + false + }; + (Xor) => { + false + }; + (XorI) => { + false + }; + (AssertEq) => { + false + }; + (AssertHalfwordAlignment) => { + false + }; + (AssertWordAlignment) => { + false + }; + (AssertLte) => { + false + }; + (AssertValidDiv0) => { + false + }; + (AssertValidUnsignedRemainder) => { + false + }; + (AssertMulUNoOverflow) => { + false + }; + (VirtualAdvice) => { + false + }; + (VirtualAdviceLen) => { + false + }; + (VirtualChangeDivisor) => { + false + }; + (VirtualChangeDivisorW) => { + false + }; + (VirtualZeroExtendWord) => { + false + }; + (VirtualSignExtendWord) => { + false + }; + (Pow2W) => { + false + }; + (Pow2IW) => { + false + }; + (MovSign) => { + false + }; + (MulI) => { + false + }; + (Pow2) => { + false + }; + (Pow2I) => { + false + }; + (VirtualRev8W) => { + false + }; + (VirtualRotri) => { + false + }; + (VirtualRotriw) => { + false + }; + (VirtualShiftRightBitmask) => { + false + }; + (VirtualShiftRightBitmaski) => { + false + }; + (VirtualSra) => { + false + }; + (VirtualSrai) => { + false + }; + (VirtualSrl) => { + false + }; + (VirtualSrli) => { + false + }; + (VirtualXorRot32) => { + false + }; + (VirtualXorRot24) => { + false + }; + (VirtualXorRot16) => { + false + }; + (VirtualXorRot63) => { + false + }; + (VirtualXorRotW16) => { + false + }; + (VirtualXorRotW12) => { + false + }; + (VirtualXorRotW8) => { + false + }; + (VirtualXorRotW7) => { + false + }; +} + +impl SourceInstructionMeta for Noop { + const CANONICAL_NAME: &'static str = "jolt.pseudo.noop"; + const SOURCE_EXTENSION: Option = None; + const HAS_SIDE_EFFECTS: bool = false; +} + +impl SourceInstructionMeta for Unimpl { + const CANONICAL_NAME: &'static str = "jolt.pseudo.unimpl"; + const SOURCE_EXTENSION: Option = None; + const HAS_SIDE_EFFECTS: bool = false; +} + +impl SourceInstructionMeta for Inline { + const CANONICAL_NAME: &'static str = "jolt.inline.dispatch"; + const SOURCE_EXTENSION: Option = Some(SourceExtension::JoltInline); + const HAS_SIDE_EFFECTS: bool = true; +} + +impl JoltInstructionMeta for Noop { + const CANONICAL_NAME: &'static str = "jolt.pseudo.noop"; + const JOLT_TAG: JoltInstructionTag = JoltInstructionTag(0x0000); + const TARGET_EXTENSION: Option = None; + const HAS_SIDE_EFFECTS: bool = false; +} + +macro_rules! define_source_instruction_meta { + ( + instructions: [$($instr:ident => $marker:ident => $canonical_name:expr),* $(,)?] + ) => { + $( + impl SourceInstructionMeta for $marker { + const CANONICAL_NAME: &'static str = $canonical_name; + const SOURCE_EXTENSION: Option = + source_extension_for_marker!($marker); + const HAS_SIDE_EFFECTS: bool = source_side_effects_for_marker!($marker); + } + )* + }; +} + +macro_rules! define_jolt_instruction_meta { + ( + instructions: [$($instr:ident => $marker:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + $( + impl JoltInstructionMeta for $marker { + const CANONICAL_NAME: &'static str = $canonical_name; + const JOLT_TAG: JoltInstructionTag = JoltInstructionTag($tag); + const TARGET_EXTENSION: Option = + jolt_target_extension_for_marker!($marker); + const HAS_SIDE_EFFECTS: bool = jolt_side_effects_for_marker!($marker); + } + )* + }; +} + +crate::for_each_instruction_kind!(define_source_instruction_meta); +crate::for_each_jolt_instruction_kind!(define_jolt_instruction_meta); + +macro_rules! define_source_instruction_kind { ( - instructions: [$($instr:ident),* $(,)?] + instructions: [$($instr:ident => $marker:ident => $canonical_name:expr),* $(,)?] ) => { - #[derive( - Default, - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, + #[expect( + non_upper_case_globals, + reason = "Kind constants preserve existing instruction spelling" )] - #[repr(u16)] - pub enum InstructionKind { - #[default] - NoOp, - Unimpl, + impl SourceInstructionKind { + pub const NoOp: Self = SourceInstruction::Noop(Noop(())); + pub const Unimpl: Self = SourceInstruction::Unimplemented(Unimpl(())); $( - $instr, + pub const $instr: Self = SourceInstruction::$marker($marker(())); )* - Inline, - } + pub const Inline: Self = SourceInstruction::InlineDispatch(Inline(())); + + pub const ALL: &'static [Self] = &[ + SourceInstruction::Noop(Noop(())), + SourceInstruction::Unimplemented(Unimpl(())), + $( + SourceInstruction::$marker($marker(())), + )* + SourceInstruction::InlineDispatch(Inline(())), + ]; - impl CanonicalSerialize for InstructionKind { - fn serialize_with_mode( - &self, - writer: W, - compress: Compress, - ) -> Result<(), SerializationError> { - (*self as u16).serialize_with_mode(writer, compress) + pub const fn name(self) -> &'static str { + match self { + SourceInstruction::Noop(_) => "NoOp", + SourceInstruction::Unimplemented(_) => "Unimpl", + $( + SourceInstruction::$marker(_) => stringify!($instr), + )* + SourceInstruction::InlineDispatch(_) => "Inline", + } + } + + pub const fn canonical_name(self) -> &'static str { + match self { + SourceInstruction::Noop(_) => as SourceInstructionMeta>::CANONICAL_NAME, + SourceInstruction::Unimplemented(_) => { + as SourceInstructionMeta>::CANONICAL_NAME + } + $( + SourceInstruction::$marker(_) => { + <$marker<()> as SourceInstructionMeta>::CANONICAL_NAME + } + )* + SourceInstruction::InlineDispatch(_) => { + as SourceInstructionMeta>::CANONICAL_NAME + } + } } - fn serialized_size(&self, compress: Compress) -> usize { - (*self as u16).serialized_size(compress) + pub const fn source_extension(self) -> Option { + match self { + SourceInstruction::Noop(_) => as SourceInstructionMeta>::SOURCE_EXTENSION, + SourceInstruction::Unimplemented(_) => { + as SourceInstructionMeta>::SOURCE_EXTENSION + } + $( + SourceInstruction::$marker(_) => { + <$marker<()> as SourceInstructionMeta>::SOURCE_EXTENSION + } + )* + SourceInstruction::InlineDispatch(_) => { + as SourceInstructionMeta>::SOURCE_EXTENSION + } + } } - } - impl CanonicalDeserialize for InstructionKind { - fn deserialize_with_mode( - reader: R, - compress: Compress, - validate: Validate, - ) -> Result { - let value = u16::deserialize_with_mode(reader, compress, validate)?; - match value { - x if x == Self::NoOp as u16 => Ok(Self::NoOp), - x if x == Self::Unimpl as u16 => Ok(Self::Unimpl), + pub fn from_canonical_name(name: &str) -> Option { + match name { + "jolt.pseudo.noop" => Some(SourceInstruction::Noop(Noop(()))), + "jolt.pseudo.unimpl" => Some(SourceInstruction::Unimplemented(Unimpl(()))), $( - x if x == Self::$instr as u16 => Ok(Self::$instr), + $canonical_name => Some(SourceInstruction::$marker($marker(()))), )* - x if x == Self::Inline as u16 => Ok(Self::Inline), - _ => Err(SerializationError::InvalidData), + "jolt.inline.dispatch" => Some(SourceInstruction::InlineDispatch(Inline(()))), + _ => None, + } + } + + pub fn from_name(name: &str) -> Option { + match name { + "NoOp" => Some(SourceInstruction::Noop(Noop(()))), + "Unimpl" => Some(SourceInstruction::Unimplemented(Unimpl(()))), + $( + stringify!($instr) => Some(SourceInstruction::$marker($marker(()))), + )* + "Inline" => Some(SourceInstruction::InlineDispatch(Inline(()))), + _ => None, + } + } + + pub const fn expands_to_jolt(self) -> bool { + !matches!( + self, + SourceInstruction::Noop(_) | SourceInstruction::Unimplemented(_) + ) + } + + pub const fn has_side_effects(self) -> bool { + match self { + SourceInstruction::Noop(_) => as SourceInstructionMeta>::HAS_SIDE_EFFECTS, + SourceInstruction::Unimplemented(_) => { + as SourceInstructionMeta>::HAS_SIDE_EFFECTS + } + $( + SourceInstruction::$marker(_) => { + <$marker<()> as SourceInstructionMeta>::HAS_SIDE_EFFECTS + } + )* + SourceInstruction::InlineDispatch(_) => { + as SourceInstructionMeta>::HAS_SIDE_EFFECTS + } } } } - impl Valid for InstructionKind { - fn check(&self) -> Result<(), SerializationError> { - Ok(()) + impl Default for SourceInstructionKind { + fn default() -> Self { + SourceInstruction::Noop(Noop(())) } } + }; +} + +macro_rules! define_jolt_instruction_kind { + ( + instructions: [$($instr:ident => $marker:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + #[expect( + non_upper_case_globals, + reason = "Kind constants preserve existing instruction spelling" + )] + impl JoltInstructionKind { + pub const NoOp: Self = JoltInstruction::Noop(Noop(())); + $( + pub const $instr: Self = JoltInstruction::$marker($marker(())); + )* + + pub const ALL: &'static [Self] = &[ + JoltInstruction::Noop(Noop(())), + $( + JoltInstruction::$marker($marker(())), + )* + ]; - impl InstructionKind { pub const fn name(self) -> &'static str { match self { - Self::NoOp => "NoOp", - Self::Unimpl => "Unimpl", + JoltInstruction::Noop(_) => "NoOp", + $( + JoltInstruction::$marker(_) => stringify!($instr), + )* + } + } + + pub const fn canonical_name(self) -> &'static str { + match self { + JoltInstruction::Noop(_) => as JoltInstructionMeta>::CANONICAL_NAME, + $( + JoltInstruction::$marker(_) => { + <$marker<()> as JoltInstructionMeta>::CANONICAL_NAME + } + )* + } + } + + pub const fn tag(self) -> JoltInstructionTag { + match self { + JoltInstruction::Noop(_) => as JoltInstructionMeta>::JOLT_TAG, + $( + JoltInstruction::$marker(_) => <$marker<()> as JoltInstructionMeta>::JOLT_TAG, + )* + } + } + + pub const fn target_extension(self) -> Option { + match self { + JoltInstruction::Noop(_) => as JoltInstructionMeta>::TARGET_EXTENSION, + $( + JoltInstruction::$marker(_) => { + <$marker<()> as JoltInstructionMeta>::TARGET_EXTENSION + } + )* + } + } + + pub const fn from_tag(tag: JoltInstructionTag) -> Option { + match tag.0 { + 0x0000 => Some(JoltInstruction::Noop(Noop(()))), $( - Self::$instr => stringify!($instr), + $tag => Some(JoltInstruction::$marker($marker(()))), )* - Self::Inline => "Inline", + _ => None, + } + } + + pub fn from_name(name: &str) -> Option { + match name { + "NoOp" => Some(JoltInstruction::Noop(Noop(()))), + $( + stringify!($instr) => Some(JoltInstruction::$marker($marker(()))), + )* + _ => None, } } pub const fn has_side_effects(self) -> bool { - matches!( - self, - Self::AdviceLB - | Self::AdviceLD - | Self::AdviceLH - | Self::AdviceLW - | Self::AMOADDD - | Self::AMOADDW - | Self::AMOANDD - | Self::AMOANDW - | Self::AMOMAXD - | Self::AMOMAXUD - | Self::AMOMAXUW - | Self::AMOMAXW - | Self::AMOMIND - | Self::AMOMINUD - | Self::AMOMINUW - | Self::AMOMINW - | Self::AMOORD - | Self::AMOORW - | Self::AMOSWAPD - | Self::AMOSWAPW - | Self::AMOXORD - | Self::AMOXORW - | Self::BEQ - | Self::BGE - | Self::BGEU - | Self::BLT - | Self::BLTU - | Self::BNE - | Self::CSRRS - | Self::CSRRW - | Self::EBREAK - | Self::ECALL - | Self::Inline - | Self::JAL - | Self::JALR - | Self::LB - | Self::LBU - | Self::LD - | Self::LH - | Self::LHU - | Self::LRD - | Self::LRW - | Self::LW - | Self::LWU - | Self::MRET - | Self::SB - | Self::SCD - | Self::SCW - | Self::SD - | Self::SH - | Self::SW - | Self::VirtualAdviceLoad - | Self::VirtualHostIO - | Self::VirtualSW - ) + match self { + JoltInstruction::Noop(_) => as JoltInstructionMeta>::HAS_SIDE_EFFECTS, + $( + JoltInstruction::$marker(_) => { + <$marker<()> as JoltInstructionMeta>::HAS_SIDE_EFFECTS + } + )* + } + } + } + + impl SourceInstructionKind { + pub const fn from_jolt_kind(kind: JoltInstructionKind) -> Option { + match kind { + JoltInstruction::Noop(_) => Some(SourceInstruction::Noop(Noop(()))), + $( + JoltInstruction::$marker(_) => Some(SourceInstruction::$marker($marker(()))), + )* + } + } + + pub const fn jolt_kind(self) -> Option { + match self { + SourceInstruction::Noop(_) => Some(JoltInstruction::Noop(Noop(()))), + $( + SourceInstruction::$marker(_) => Some(JoltInstruction::$marker($marker(()))), + )* + _ => None, + } + } + } + + impl Default for JoltInstructionKind { + fn default() -> Self { + JoltInstruction::Noop(Noop(())) } } }; } -crate::for_each_instruction_kind!(define_instruction_kind); +crate::for_each_instruction_kind!(define_source_instruction_kind); +crate::for_each_jolt_instruction_kind!(define_jolt_instruction_kind); + +#[cfg(feature = "serialization")] +impl Serialize for SourceInstructionKind { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.canonical_name()) + } +} + +#[cfg(feature = "serialization")] +impl<'de> serde::Deserialize<'de> for SourceInstructionKind { + fn deserialize>(deserializer: D) -> Result { + struct SourceInstructionKindVisitor; + + impl Visitor<'_> for SourceInstructionKindVisitor { + type Value = SourceInstructionKind; + + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("a source instruction canonical name") + } + + fn visit_str(self, name: &str) -> Result + where + E: serde::de::Error, + { + SourceInstructionKind::from_canonical_name(name) + .ok_or_else(|| E::custom("unknown source instruction canonical name")) + } + } + + deserializer.deserialize_str(SourceInstructionKindVisitor) + } +} + +#[cfg(feature = "serialization")] +impl Serialize for JoltInstructionKind { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_u16(self.tag().0) + } +} + +#[cfg(feature = "serialization")] +impl<'de> serde::Deserialize<'de> for JoltInstructionKind { + fn deserialize>(deserializer: D) -> Result { + struct JoltInstructionKindVisitor; + + impl Visitor<'_> for JoltInstructionKindVisitor { + type Value = JoltInstructionKind; + + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("a final Jolt instruction u16 tag") + } + + fn visit_u16(self, value: u16) -> Result + where + E: serde::de::Error, + { + JoltInstructionKind::from_tag(JoltInstructionTag(value)) + .ok_or_else(|| E::custom("unknown final Jolt instruction tag")) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + let tag = u16::try_from(value) + .map_err(|_| E::custom("final Jolt instruction tag out of range"))?; + self.visit_u16(tag) + } + } + + deserializer.deserialize_u16(JoltInstructionKindVisitor) + } +} + +#[cfg(feature = "serialization")] +impl CanonicalSerialize for SourceInstructionKind { + fn serialize_with_mode( + &self, + writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.canonical_name() + .as_bytes() + .to_vec() + .serialize_with_mode(writer, compress) + } + + fn serialized_size(&self, compress: Compress) -> usize { + self.canonical_name() + .as_bytes() + .to_vec() + .serialized_size(compress) + } +} + +#[cfg(feature = "serialization")] +impl CanonicalDeserialize for SourceInstructionKind { + fn deserialize_with_mode( + reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let bytes = Vec::::deserialize_with_mode(reader, compress, validate)?; + let name = std::str::from_utf8(&bytes).map_err(|_| SerializationError::InvalidData)?; + Self::from_canonical_name(name).ok_or(SerializationError::InvalidData) + } +} + +#[cfg(feature = "serialization")] +impl Valid for SourceInstructionKind { + fn check(&self) -> Result<(), SerializationError> { + Ok(()) + } +} + +#[cfg(feature = "serialization")] +impl CanonicalSerialize for JoltInstructionKind { + fn serialize_with_mode( + &self, + writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.tag().0.serialize_with_mode(writer, compress) + } + + fn serialized_size(&self, compress: Compress) -> usize { + self.tag().0.serialized_size(compress) + } +} + +#[cfg(feature = "serialization")] +impl CanonicalDeserialize for JoltInstructionKind { + fn deserialize_with_mode( + reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let value = u16::deserialize_with_mode(reader, compress, validate)?; + Self::from_tag(JoltInstructionTag(value)).ok_or(SerializationError::InvalidData) + } +} + +#[cfg(feature = "serialization")] +impl Valid for JoltInstructionKind { + fn check(&self) -> Result<(), SerializationError> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{JoltInstructionKind, JoltInstructionTag, SourceInstructionKind}; + use crate::instructions::{JoltInstruction, SourceInstruction, VirtualHostIO}; + use std::collections::HashSet; + + #[test] + fn tags_are_stable_for_representative_final_rows() { + assert_eq!(JoltInstructionKind::NoOp.tag(), JoltInstructionTag(0x0000)); + assert_eq!(JoltInstructionKind::ADD.tag(), JoltInstructionTag(0x0002)); + assert_eq!( + JoltInstruction::VirtualHostIO(VirtualHostIO(())).tag(), + JoltInstructionTag(0x0068) + ); + assert_eq!( + JoltInstructionKind::VirtualXORROTW7.tag(), + JoltInstructionTag(0x0088) + ); + } + + #[test] + fn final_tags_round_trip_and_are_unique() { + let mut seen = HashSet::new(); + for kind in JoltInstructionKind::ALL { + let tag = kind.tag(); + assert!(seen.insert(tag), "duplicate tag {tag:?} for {kind:?}"); + assert_eq!(JoltInstructionKind::from_tag(tag), Some(*kind)); + } + } + + #[test] + fn source_kinds_use_canonical_names_instead_of_tags() { + assert_eq!(SourceInstructionKind::ADD.canonical_name(), "rv64.add"); + assert_eq!( + SourceInstructionKind::Inline.canonical_name(), + "jolt.inline.dispatch" + ); + assert_eq!( + SourceInstructionKind::from_canonical_name("rv64.addw"), + Some(SourceInstructionKind::ADDW) + ); + } + + #[test] + fn source_canonical_names_are_unique_and_non_empty() { + let mut seen = HashSet::new(); + for kind in SourceInstructionKind::ALL { + let name = kind.canonical_name(); + assert!(!name.is_empty(), "empty canonical name for {kind:?}"); + assert!(seen.insert(name), "duplicate canonical name {name:?}"); + } + } + + #[test] + fn source_to_final_mapping_is_partial() { + assert_eq!( + SourceInstructionKind::ADD.jolt_kind(), + Some(JoltInstructionKind::ADD) + ); + assert_eq!( + SourceInstruction::VirtualHostIO(VirtualHostIO(())).jolt_kind(), + Some(JoltInstruction::VirtualHostIO(VirtualHostIO(()))) + ); + assert_eq!(SourceInstructionKind::ADDW.jolt_kind(), None); + assert_eq!(SourceInstructionKind::Inline.jolt_kind(), None); + assert_eq!(SourceInstructionKind::Unimpl.jolt_kind(), None); + } + + #[test] + fn final_canonical_names_are_unique_and_non_empty() { + let mut seen = HashSet::new(); + for kind in JoltInstructionKind::ALL { + let name = kind.canonical_name(); + assert!(!name.is_empty(), "empty canonical name for {kind:?}"); + assert!(seen.insert(name), "duplicate canonical name {name:?}"); + } + } + + #[cfg(feature = "serialization")] + #[test] + fn serde_uses_source_names_and_final_tags() -> Result<(), Box> { + assert_eq!( + serde_json::to_string(&SourceInstructionKind::ADDW)?, + "\"rv64.addw\"" + ); + assert_eq!( + serde_json::from_str::("\"rv64.addw\"")?, + SourceInstructionKind::ADDW + ); + + assert_eq!( + serde_json::to_string(&JoltInstructionKind::ADD)?, + JoltInstructionKind::ADD.tag().0.to_string() + ); + assert_eq!( + serde_json::from_str::("2")?, + JoltInstructionKind::ADD + ); + assert!(serde_json::from_str::("1").is_err()); + Ok(()) + } + + #[cfg(feature = "serialization")] + #[test] + fn canonical_serialization_round_trips_stable_identity() { + use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; + + let mut source_bytes = Vec::new(); + assert!(SourceInstructionKind::ADDW + .serialize_compressed(&mut source_bytes) + .is_ok()); + assert!(matches!( + SourceInstructionKind::deserialize_compressed(&source_bytes[..]), + Ok(SourceInstructionKind::ADDW) + )); + + let mut final_bytes = Vec::new(); + assert!(JoltInstructionKind::ADD + .serialize_compressed(&mut final_bytes) + .is_ok()); + assert!(matches!( + JoltInstructionKind::deserialize_compressed(&final_bytes[..]), + Ok(JoltInstructionKind::ADD) + )); + assert_eq!(final_bytes, JoltInstructionKind::ADD.tag().0.to_le_bytes()); + } +} diff --git a/crates/jolt-riscv/src/lib.rs b/crates/jolt-riscv/src/lib.rs index 379666bf1a..959b49e345 100644 --- a/crates/jolt-riscv/src/lib.rs +++ b/crates/jolt-riscv/src/lib.rs @@ -8,9 +8,10 @@ mod flags; pub mod instructions; -mod jolt_instruction; mod kind; -mod normalized; +mod profile; +mod row; +mod row_data; pub mod trace; mod uncompress; @@ -19,31 +20,218 @@ macro_rules! for_each_instruction_kind { ($callback:ident) => { $callback! { instructions: [ - ADD, ADDI, AND, ANDI, ANDN, AUIPC, BEQ, BGE, BGEU, BLT, BLTU, BNE, - CSRRS, CSRRW, DIV, DIVU, - EBREAK, ECALL, FENCE, JAL, JALR, LB, LBU, LD, LH, LHU, LUI, LW, MRET, MUL, - MULH, MULHSU, MULHU, OR, ORI, REM, REMU, SB, SD, SH, SLL, SLLI, SLT, - SLTI, SLTIU, SLTU, SRA, SRAI, SRL, SRLI, SUB, SW, XOR, XORI, - ADDIW, SLLIW, SRLIW, SRAIW, ADDW, SUBW, SLLW, SRLW, SRAW, LWU, - DIVUW, DIVW, MULW, REMUW, REMW, - LRW, SCW, AMOSWAPW, AMOADDW, AMOANDW, AMOORW, AMOXORW, AMOMINW, - AMOMAXW, AMOMINUW, AMOMAXUW, - LRD, SCD, AMOSWAPD, AMOADDD, AMOANDD, AMOORD, AMOXORD, AMOMIND, - AMOMAXD, AMOMINUD, AMOMAXUD, - AdviceLB, AdviceLD, AdviceLH, AdviceLW, - VirtualAdvice, VirtualAdviceLen, VirtualAdviceLoad, - VirtualAssertEQ, VirtualAssertHalfwordAlignment, VirtualAssertWordAlignment, - VirtualAssertLTE, VirtualHostIO, - VirtualAssertValidDiv0, VirtualAssertValidUnsignedRemainder, - VirtualAssertMulUNoOverflow, - VirtualChangeDivisor, VirtualChangeDivisorW, VirtualLW, VirtualSW, - VirtualZeroExtendWord, VirtualSignExtendWord, VirtualPow2W, VirtualPow2IW, - VirtualMovsign, VirtualMULI, VirtualPow2, VirtualPow2I, VirtualRev8W, - VirtualROTRI, VirtualROTRIW, - VirtualShiftRightBitmask, VirtualShiftRightBitmaskI, - VirtualSRA, VirtualSRAI, VirtualSRL, VirtualSRLI, - VirtualXORROT32, VirtualXORROT24, VirtualXORROT16, VirtualXORROT63, - VirtualXORROTW16, VirtualXORROTW12, VirtualXORROTW8, VirtualXORROTW7, + ADD => Add => "rv64.add", + ADDI => Addi => "rv64.addi", + AND => And => "rv64.and", + ANDI => AndI => "rv64.andi", + ANDN => Andn => "rv64.andn", + AUIPC => Auipc => "rv64.auipc", + BEQ => Beq => "rv64.beq", + BGE => Bge => "rv64.bge", + BGEU => BgeU => "rv64.bgeu", + BLT => Blt => "rv64.blt", + BLTU => BltU => "rv64.bltu", + BNE => Bne => "rv64.bne", + CSRRS => Csrrs => "rv64.csrrs", + CSRRW => Csrrw => "rv64.csrrw", + DIV => Div => "rv64.div", + DIVU => DivU => "rv64.divu", + EBREAK => Ebreak => "rv64.ebreak", + ECALL => Ecall => "rv64.ecall", + FENCE => Fence => "rv64.fence", + JAL => Jal => "rv64.jal", + JALR => Jalr => "rv64.jalr", + LB => Lb => "rv64.lb", + LBU => Lbu => "rv64.lbu", + LD => Ld => "rv64.ld", + LH => Lh => "rv64.lh", + LHU => Lhu => "rv64.lhu", + LUI => Lui => "rv64.lui", + LW => Lw => "rv64.lw", + MRET => Mret => "rv64.mret", + MUL => Mul => "rv64.mul", + MULH => MulH => "rv64.mulh", + MULHSU => MulHSU => "rv64.mulhsu", + MULHU => MulHU => "rv64.mulhu", + OR => Or => "rv64.or", + ORI => OrI => "rv64.ori", + REM => Rem => "rv64.rem", + REMU => RemU => "rv64.remu", + SB => Sb => "rv64.sb", + SD => Sd => "rv64.sd", + SH => Sh => "rv64.sh", + SLL => Sll => "rv64.sll", + SLLI => SllI => "rv64.slli", + SLT => Slt => "rv64.slt", + SLTI => SltI => "rv64.slti", + SLTIU => SltIU => "rv64.sltiu", + SLTU => SltU => "rv64.sltu", + SRA => Sra => "rv64.sra", + SRAI => SraI => "rv64.srai", + SRL => Srl => "rv64.srl", + SRLI => SrlI => "rv64.srli", + SUB => Sub => "rv64.sub", + SW => Sw => "rv64.sw", + XOR => Xor => "rv64.xor", + XORI => XorI => "rv64.xori", + ADDIW => AddiW => "rv64.addiw", + SLLIW => SllIW => "rv64.slliw", + SRLIW => SrlIW => "rv64.srliw", + SRAIW => SraIW => "rv64.sraiw", + ADDW => AddW => "rv64.addw", + SUBW => SubW => "rv64.subw", + SLLW => SllW => "rv64.sllw", + SRLW => SrlW => "rv64.srlw", + SRAW => SraW => "rv64.sraw", + LWU => Lwu => "rv64.lwu", + DIVUW => DivUW => "rv64.divuw", + DIVW => DivW => "rv64.divw", + MULW => MulW => "rv64.mulw", + REMUW => RemUW => "rv64.remuw", + REMW => RemW => "rv64.remw", + LRW => LrW => "rv64.lrw", + SCW => ScW => "rv64.scw", + AMOSWAPW => AmoSwapW => "rv64.amoswapw", + AMOADDW => AmoAddW => "rv64.amoaddw", + AMOANDW => AmoAndW => "rv64.amoandw", + AMOORW => AmoOrW => "rv64.amoorw", + AMOXORW => AmoXorW => "rv64.amoxorw", + AMOMINW => AmoMinW => "rv64.amominw", + AMOMAXW => AmoMaxW => "rv64.amomaxw", + AMOMINUW => AmoMinUW => "rv64.amominuw", + AMOMAXUW => AmoMaxUW => "rv64.amomaxuw", + LRD => LrD => "rv64.lrd", + SCD => ScD => "rv64.scd", + AMOSWAPD => AmoSwapD => "rv64.amoswapd", + AMOADDD => AmoAddD => "rv64.amoaddd", + AMOANDD => AmoAndD => "rv64.amoandd", + AMOORD => AmoOrD => "rv64.amoord", + AMOXORD => AmoXorD => "rv64.amoxord", + AMOMIND => AmoMinD => "rv64.amomind", + AMOMAXD => AmoMaxD => "rv64.amomaxd", + AMOMINUD => AmoMinUD => "rv64.amominud", + AMOMAXUD => AmoMaxUD => "rv64.amomaxud", + AdviceLB => AdviceLb => "jolt.advice.lb", + AdviceLD => AdviceLd => "jolt.advice.ld", + AdviceLH => AdviceLh => "jolt.advice.lh", + AdviceLW => AdviceLw => "jolt.advice.lw", + VirtualAdvice => VirtualAdvice => "jolt.virtual.advice", + VirtualAdviceLen => VirtualAdviceLen => "jolt.virtual.advice_len", + VirtualAdviceLoad => VirtualAdviceLoad => "jolt.virtual.advice_load", + VirtualAssertEQ => AssertEq => "jolt.virtual.assert_eq", + VirtualAssertHalfwordAlignment => AssertHalfwordAlignment => "jolt.virtual.assert_halfword_alignment", + VirtualAssertWordAlignment => AssertWordAlignment => "jolt.virtual.assert_word_alignment", + VirtualAssertLTE => AssertLte => "jolt.virtual.assert_lte", + VirtualHostIO => VirtualHostIO => "jolt.virtual.host_io", + VirtualAssertValidDiv0 => AssertValidDiv0 => "jolt.virtual.assert_valid_div0", + VirtualAssertValidUnsignedRemainder => AssertValidUnsignedRemainder => "jolt.virtual.assert_valid_unsigned_remainder", + VirtualAssertMulUNoOverflow => AssertMulUNoOverflow => "jolt.virtual.assert_mul_u_no_overflow", + VirtualChangeDivisor => VirtualChangeDivisor => "jolt.virtual.change_divisor", + VirtualChangeDivisorW => VirtualChangeDivisorW => "jolt.virtual.change_divisor_w", + VirtualLW => VirtualLw => "jolt.virtual.lw", + VirtualSW => VirtualSw => "jolt.virtual.sw", + VirtualZeroExtendWord => VirtualZeroExtendWord => "jolt.virtual.zero_extend_word", + VirtualSignExtendWord => VirtualSignExtendWord => "jolt.virtual.sign_extend_word", + VirtualPow2W => Pow2W => "jolt.virtual.pow2_w", + VirtualPow2IW => Pow2IW => "jolt.virtual.pow2_iw", + VirtualMovsign => MovSign => "jolt.virtual.movsign", + VirtualMULI => MulI => "jolt.virtual.muli", + VirtualPow2 => Pow2 => "jolt.virtual.pow2", + VirtualPow2I => Pow2I => "jolt.virtual.pow2_i", + VirtualRev8W => VirtualRev8W => "jolt.virtual.rev8_w", + VirtualROTRI => VirtualRotri => "jolt.virtual.rotri", + VirtualROTRIW => VirtualRotriw => "jolt.virtual.rotriw", + VirtualShiftRightBitmask => VirtualShiftRightBitmask => "jolt.virtual.shift_right_bitmask", + VirtualShiftRightBitmaskI => VirtualShiftRightBitmaski => "jolt.virtual.shift_right_bitmask_i", + VirtualSRA => VirtualSra => "jolt.virtual.sra", + VirtualSRAI => VirtualSrai => "jolt.virtual.srai", + VirtualSRL => VirtualSrl => "jolt.virtual.srl", + VirtualSRLI => VirtualSrli => "jolt.virtual.srli", + VirtualXORROT32 => VirtualXorRot32 => "jolt.virtual.xorrot32", + VirtualXORROT24 => VirtualXorRot24 => "jolt.virtual.xorrot24", + VirtualXORROT16 => VirtualXorRot16 => "jolt.virtual.xorrot16", + VirtualXORROT63 => VirtualXorRot63 => "jolt.virtual.xorrot63", + VirtualXORROTW16 => VirtualXorRotW16 => "jolt.virtual.xorrotw16", + VirtualXORROTW12 => VirtualXorRotW12 => "jolt.virtual.xorrotw12", + VirtualXORROTW8 => VirtualXorRotW8 => "jolt.virtual.xorrotw8", + VirtualXORROTW7 => VirtualXorRotW7 => "jolt.virtual.xorrotw7", + ] + } + }; +} + +#[macro_export] +macro_rules! for_each_jolt_instruction_kind { + ($callback:ident) => { + $callback! { + instructions: [ + ADD => Add => (0x0002, "rv64.add"), + ADDI => Addi => (0x0003, "rv64.addi"), + AND => And => (0x0004, "rv64.and"), + ANDI => AndI => (0x0005, "rv64.andi"), + ANDN => Andn => (0x0006, "rv64.andn"), + AUIPC => Auipc => (0x0007, "rv64.auipc"), + BEQ => Beq => (0x0008, "rv64.beq"), + BGE => Bge => (0x0009, "rv64.bge"), + BGEU => BgeU => (0x000a, "rv64.bgeu"), + BLT => Blt => (0x000b, "rv64.blt"), + BLTU => BltU => (0x000c, "rv64.bltu"), + BNE => Bne => (0x000d, "rv64.bne"), + FENCE => Fence => (0x0014, "rv64.fence"), + JAL => Jal => (0x0015, "rv64.jal"), + JALR => Jalr => (0x0016, "rv64.jalr"), + LD => Ld => (0x0019, "rv64.ld"), + LUI => Lui => (0x001c, "rv64.lui"), + MUL => Mul => (0x001f, "rv64.mul"), + MULHU => MulHU => (0x0022, "rv64.mulhu"), + OR => Or => (0x0023, "rv64.or"), + ORI => OrI => (0x0024, "rv64.ori"), + SD => Sd => (0x0028, "rv64.sd"), + SLT => Slt => (0x002c, "rv64.slt"), + SLTI => SltI => (0x002d, "rv64.slti"), + SLTIU => SltIU => (0x002e, "rv64.sltiu"), + SLTU => SltU => (0x002f, "rv64.sltu"), + SUB => Sub => (0x0034, "rv64.sub"), + XOR => Xor => (0x0036, "rv64.xor"), + XORI => XorI => (0x0037, "rv64.xori"), + VirtualAdvice => VirtualAdvice => (0x0061, "jolt.virtual.advice"), + VirtualAdviceLen => VirtualAdviceLen => (0x0062, "jolt.virtual.advice_len"), + VirtualAdviceLoad => VirtualAdviceLoad => (0x0063, "jolt.virtual.advice_load"), + VirtualAssertEQ => AssertEq => (0x0064, "jolt.virtual.assert_eq"), + VirtualAssertHalfwordAlignment => AssertHalfwordAlignment => (0x0065, "jolt.virtual.assert_halfword_alignment"), + VirtualAssertWordAlignment => AssertWordAlignment => (0x0066, "jolt.virtual.assert_word_alignment"), + VirtualAssertLTE => AssertLte => (0x0067, "jolt.virtual.assert_lte"), + VirtualHostIO => VirtualHostIO => (0x0068, "jolt.virtual.host_io"), + VirtualAssertValidDiv0 => AssertValidDiv0 => (0x0069, "jolt.virtual.assert_valid_div0"), + VirtualAssertValidUnsignedRemainder => AssertValidUnsignedRemainder => (0x006a, "jolt.virtual.assert_valid_unsigned_remainder"), + VirtualAssertMulUNoOverflow => AssertMulUNoOverflow => (0x006b, "jolt.virtual.assert_mul_u_no_overflow"), + VirtualChangeDivisor => VirtualChangeDivisor => (0x006c, "jolt.virtual.change_divisor"), + VirtualChangeDivisorW => VirtualChangeDivisorW => (0x006d, "jolt.virtual.change_divisor_w"), + VirtualZeroExtendWord => VirtualZeroExtendWord => (0x0070, "jolt.virtual.zero_extend_word"), + VirtualSignExtendWord => VirtualSignExtendWord => (0x0071, "jolt.virtual.sign_extend_word"), + VirtualPow2W => Pow2W => (0x0072, "jolt.virtual.pow2_w"), + VirtualPow2IW => Pow2IW => (0x0073, "jolt.virtual.pow2_iw"), + VirtualMovsign => MovSign => (0x0074, "jolt.virtual.movsign"), + VirtualMULI => MulI => (0x0075, "jolt.virtual.muli"), + VirtualPow2 => Pow2 => (0x0076, "jolt.virtual.pow2"), + VirtualPow2I => Pow2I => (0x0077, "jolt.virtual.pow2_i"), + VirtualRev8W => VirtualRev8W => (0x0078, "jolt.virtual.rev8_w"), + VirtualROTRI => VirtualRotri => (0x0079, "jolt.virtual.rotri"), + VirtualROTRIW => VirtualRotriw => (0x007a, "jolt.virtual.rotriw"), + VirtualShiftRightBitmask => VirtualShiftRightBitmask => (0x007b, "jolt.virtual.shift_right_bitmask"), + VirtualShiftRightBitmaskI => VirtualShiftRightBitmaski => (0x007c, "jolt.virtual.shift_right_bitmask_i"), + VirtualSRA => VirtualSra => (0x007d, "jolt.virtual.sra"), + VirtualSRAI => VirtualSrai => (0x007e, "jolt.virtual.srai"), + VirtualSRL => VirtualSrl => (0x007f, "jolt.virtual.srl"), + VirtualSRLI => VirtualSrli => (0x0080, "jolt.virtual.srli"), + VirtualXORROT32 => VirtualXorRot32 => (0x0081, "jolt.virtual.xorrot32"), + VirtualXORROT24 => VirtualXorRot24 => (0x0082, "jolt.virtual.xorrot24"), + VirtualXORROT16 => VirtualXorRot16 => (0x0083, "jolt.virtual.xorrot16"), + VirtualXORROT63 => VirtualXorRot63 => (0x0084, "jolt.virtual.xorrot63"), + VirtualXORROTW16 => VirtualXorRotW16 => (0x0085, "jolt.virtual.xorrotw16"), + VirtualXORROTW12 => VirtualXorRotW12 => (0x0086, "jolt.virtual.xorrotw12"), + VirtualXORROTW8 => VirtualXorRotW8 => (0x0087, "jolt.virtual.xorrotw8"), + VirtualXORROTW7 => VirtualXorRotW7 => (0x0088, "jolt.virtual.xorrotw7"), ] } }; @@ -51,12 +239,20 @@ macro_rules! for_each_instruction_kind { pub use flags::{ CircuitFlagSet, CircuitFlags, Flags, InstructionFlagSet, InstructionFlags, - InterleavedBitsMarker, NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS, + InterleavedBitsMarker, CIRCUIT_FLAGS, NUM_CIRCUIT_FLAGS, NUM_INSTRUCTION_FLAGS, }; -pub use instructions::JoltInstructions; -pub use jolt_instruction::JoltInstruction; -pub use kind::InstructionKind; -pub use normalized::{NormalizedInstruction, NormalizedOperands}; +pub use instructions::{JoltInstruction, SourceInstruction}; +pub use kind::{ + JoltInstructionKind, JoltInstructionMeta, JoltInstructionTag, SourceInstructionKind, + SourceInstructionMeta, +}; +pub use profile::{ + jolt_target_extension, source_extension, InlineExtension, JoltInstructionProfile, + JoltTargetExtension, ProfileInstructionIndex, SourceExtension, RV64IMAC_JOLT, + RV64IMAC_JOLT_ALL_INLINES, RV64IM_JOLT, +}; +pub use row::{JoltInstructionRow, NormalizedOperands, SourceInlineKey, SourceInstructionRow}; +pub use row_data::JoltInstructionRowData; pub use trace::JoltCycle; pub use uncompress::uncompress_rv64_instruction; @@ -75,7 +271,7 @@ pub use uncompress::uncompress_rv64_instruction; /// /// // No flag config — struct only, no `Flags` impl: /// jolt_instruction!( -/// /// RV32I (Zicsr) CSRRS: atomic CSR read+set bits. +/// /// Zicsr CSRRS: atomic CSR read+set bits. /// Csrrs /// ); /// ``` @@ -89,10 +285,10 @@ macro_rules! jolt_instruction { ) => { $crate::jolt_instruction!(@struct $(#[$attr])* $name); - impl $crate::Flags for $name { + impl $crate::Flags for $name { #[inline] fn circuit_flags(&self) -> $crate::CircuitFlagSet { - let instruction: $crate::NormalizedInstruction = self.0.into(); + let instruction: $crate::JoltInstructionRow = self.0.into(); let mut flags = $crate::CircuitFlagSet::default() $(.set($crate::CircuitFlags::$circuit))*; if let Some(virtual_sequence_remaining) = instruction.virtual_sequence_remaining { @@ -129,10 +325,10 @@ macro_rules! jolt_instruction { ) => { $crate::jolt_instruction!(@struct $(#[$attr])* $name); - impl $crate::Flags for $name { + impl $crate::Flags for $name { #[inline] fn circuit_flags(&self) -> $crate::CircuitFlagSet { - let instruction: $crate::NormalizedInstruction = self.0.into(); + let instruction: $crate::JoltInstructionRow = self.0.into(); let mut flags = $crate::CircuitFlagSet::default(); if let Some(virtual_sequence_remaining) = instruction.virtual_sequence_remaining { flags = flags.set($crate::CircuitFlags::VirtualInstruction); @@ -172,32 +368,34 @@ macro_rules! jolt_instruction { PartialEq, Eq, Hash, - ::serde::Serialize, - ::serde::Deserialize, + )] + #[cfg_attr( + feature = "serialization", + derive(::serde::Serialize, ::serde::Deserialize) )] pub struct $name(pub T); }; - // Internal: make the wrapper newtype participate in the normalized-row + // Internal: make the wrapper newtype participate in Jolt instruction-row // conversion marker by delegating through its payload. (@jolt_instruction_impl $name:ident) => { - impl From<$name> for $crate::NormalizedInstruction { + impl From<$name> for $crate::JoltInstructionRow { #[inline] fn from(instruction: $name) -> Self { instruction.0.into() } } - impl TryFrom<$crate::NormalizedInstruction> for $name { - type Error = >::Error; + impl TryFrom<$crate::JoltInstructionRow> for $name { + type Error = >::Error; #[inline] - fn try_from(instruction: $crate::NormalizedInstruction) -> Result { + fn try_from(instruction: $crate::JoltInstructionRow) -> Result { T::try_from(instruction).map($name) } } - impl $crate::JoltInstruction for $name { + impl $crate::JoltInstructionRowData for $name { } }; } diff --git a/crates/jolt-riscv/src/normalized.rs b/crates/jolt-riscv/src/normalized.rs deleted file mode 100644 index e910c88687..0000000000 --- a/crates/jolt-riscv/src/normalized.rs +++ /dev/null @@ -1,44 +0,0 @@ -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use serde::{Deserialize, Serialize}; - -use crate::InstructionKind; - -#[derive( - Default, - Debug, - Clone, - Copy, - PartialEq, - Eq, - CanonicalSerialize, - CanonicalDeserialize, - Serialize, - Deserialize, -)] -pub struct NormalizedOperands { - pub rs1: Option, - pub rs2: Option, - pub rd: Option, - pub imm: i128, -} - -#[derive( - Default, - Debug, - Clone, - Copy, - PartialEq, - Eq, - CanonicalSerialize, - CanonicalDeserialize, - Serialize, - Deserialize, -)] -pub struct NormalizedInstruction { - pub instruction_kind: InstructionKind, - pub address: usize, - pub operands: NormalizedOperands, - pub virtual_sequence_remaining: Option, - pub is_first_in_sequence: bool, - pub is_compressed: bool, -} diff --git a/crates/jolt-riscv/src/profile.rs b/crates/jolt-riscv/src/profile.rs new file mode 100644 index 0000000000..26839dc187 --- /dev/null +++ b/crates/jolt-riscv/src/profile.rs @@ -0,0 +1,323 @@ +use crate::{JoltInstructionKind, JoltInstructionTag, SourceInstructionKind}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SourceExtension { + Rv64I, + Rv64M, + Rv64A, + Rv64C, + Zicsr, + RvPrivileged, + JoltCustom, + JoltInline, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum JoltTargetExtension { + IntegerCore, + IntegerMultiply, + ControlFlow, + LoadStore64, + Advice, + HostIO, + VirtualAssertions, + VirtualArithmetic, + VirtualShifts, + BitManipulation, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum InlineExtension { + Sha2, + Keccak256, + Blake2, + Blake3, + BigInt256, + Secp256k1, + Grumpkin, + P256, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ProfileInstructionIndex(pub u16); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct JoltInstructionProfile { + pub source_extensions: &'static [SourceExtension], + pub inline_extensions: &'static [InlineExtension], +} + +pub const RV64IM_JOLT: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[ + SourceExtension::Rv64I, + SourceExtension::Rv64M, + SourceExtension::Zicsr, + SourceExtension::RvPrivileged, + SourceExtension::JoltCustom, + SourceExtension::JoltInline, + ], + inline_extensions: &[], +}; + +pub const RV64IMAC_JOLT: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[ + SourceExtension::Rv64I, + SourceExtension::Rv64M, + SourceExtension::Rv64A, + SourceExtension::Rv64C, + SourceExtension::Zicsr, + SourceExtension::RvPrivileged, + SourceExtension::JoltCustom, + SourceExtension::JoltInline, + ], + inline_extensions: &[], +}; + +pub const RV64IMAC_JOLT_ALL_INLINES: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: RV64IMAC_JOLT.source_extensions, + inline_extensions: &[ + InlineExtension::Sha2, + InlineExtension::Keccak256, + InlineExtension::Blake2, + InlineExtension::Blake3, + InlineExtension::BigInt256, + InlineExtension::Secp256k1, + InlineExtension::Grumpkin, + InlineExtension::P256, + ], +}; + +impl JoltInstructionProfile { + pub fn supports_source(self, kind: SourceInstructionKind) -> bool { + match source_extension(kind) { + None => true, + Some(extension) => self.source_extensions.contains(&extension), + } + } + + pub fn supports_jolt(self, kind: JoltInstructionKind) -> bool { + match jolt_target_extension(kind) { + None => matches!(kind, JoltInstructionKind::NoOp), + Some(extension) => self.supports_target_extension(extension), + } + } + + pub fn supports_inline(self, extension: InlineExtension) -> bool { + self.inline_extensions.contains(&extension) + } + + pub fn source_dense_index( + self, + kind: SourceInstructionKind, + ) -> Option { + dense_index( + SourceInstructionKind::ALL.iter().copied(), + |candidate| self.supports_source(candidate), + kind, + ) + } + + pub fn jolt_dense_index(self, kind: JoltInstructionKind) -> Option { + dense_index( + JoltInstructionKind::ALL.iter().copied(), + |candidate| self.supports_jolt(candidate), + kind, + ) + } + + pub fn fingerprint(self) -> u64 { + let mut hash = hash_byte(FNV_OFFSET_BASIS, FINGERPRINT_SCHEMA_VERSION); + for extension in self.source_extensions { + hash = hash_byte(hash, 0x01); + hash = hash_byte(hash, source_extension_code(*extension)); + } + for extension in self.inline_extensions { + hash = hash_byte(hash, 0x02); + hash = hash_byte(hash, inline_extension_code(*extension)); + } + for kind in SourceInstructionKind::ALL { + if self.supports_source(*kind) { + hash = hash_str(hash_byte(hash, 0x03), kind.canonical_name()); + } + } + for kind in JoltInstructionKind::ALL { + if self.supports_jolt(*kind) { + hash = hash_tag(hash_byte(hash, 0x04), kind.tag()); + hash = hash_str(hash_byte(hash, 0x05), kind.canonical_name()); + } + } + hash + } + + fn supports_target_extension(self, extension: JoltTargetExtension) -> bool { + match extension { + JoltTargetExtension::IntegerCore + | JoltTargetExtension::ControlFlow + | JoltTargetExtension::LoadStore64 + | JoltTargetExtension::VirtualAssertions + | JoltTargetExtension::VirtualArithmetic + | JoltTargetExtension::VirtualShifts => { + self.source_extensions.contains(&SourceExtension::Rv64I) + } + JoltTargetExtension::IntegerMultiply => { + self.source_extensions.contains(&SourceExtension::Rv64M) + } + JoltTargetExtension::Advice => { + self.source_extensions.contains(&SourceExtension::Rv64M) + || self.source_extensions.contains(&SourceExtension::Rv64A) + || self + .source_extensions + .contains(&SourceExtension::JoltCustom) + } + JoltTargetExtension::HostIO => self + .source_extensions + .contains(&SourceExtension::JoltCustom), + JoltTargetExtension::BitManipulation => { + self.source_extensions + .contains(&SourceExtension::JoltCustom) + || !self.inline_extensions.is_empty() + } + } + } +} + +pub const fn source_extension(kind: SourceInstructionKind) -> Option { + kind.source_extension() +} + +pub const fn jolt_target_extension(kind: JoltInstructionKind) -> Option { + kind.target_extension() +} + +fn dense_index( + kinds: I, + mut is_supported: impl FnMut(K) -> bool, + needle: K, +) -> Option +where + I: IntoIterator, + K: Copy + Eq, +{ + let mut index = 0u16; + for candidate in kinds { + if !is_supported(candidate) { + continue; + } + if candidate == needle { + return Some(ProfileInstructionIndex(index)); + } + index = index.checked_add(1)?; + } + None +} + +const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325; +const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; +const FINGERPRINT_SCHEMA_VERSION: u8 = 1; + +const fn hash_byte(hash: u64, byte: u8) -> u64 { + (hash ^ byte as u64).wrapping_mul(FNV_PRIME) +} + +const fn hash_tag(hash: u64, tag: JoltInstructionTag) -> u64 { + let bytes = tag.0.to_le_bytes(); + hash_byte(hash_byte(hash, bytes[0]), bytes[1]) +} + +fn hash_str(mut hash: u64, value: &str) -> u64 { + for byte in value.as_bytes() { + hash = hash_byte(hash, *byte); + } + hash +} + +const fn source_extension_code(extension: SourceExtension) -> u8 { + match extension { + SourceExtension::Rv64I => 0, + SourceExtension::Rv64M => 1, + SourceExtension::Rv64A => 2, + SourceExtension::Rv64C => 3, + SourceExtension::Zicsr => 4, + SourceExtension::RvPrivileged => 5, + SourceExtension::JoltCustom => 6, + SourceExtension::JoltInline => 7, + } +} + +const fn inline_extension_code(extension: InlineExtension) -> u8 { + match extension { + InlineExtension::Sha2 => 0, + InlineExtension::Keccak256 => 1, + InlineExtension::Blake2 => 2, + InlineExtension::Blake3 => 3, + InlineExtension::BigInt256 => 4, + InlineExtension::Secp256k1 => 5, + InlineExtension::Grumpkin => 6, + InlineExtension::P256 => 7, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_profile_matches_current_source_shape() { + assert!(RV64IMAC_JOLT.supports_source(SourceInstructionKind::ADD)); + assert!(RV64IMAC_JOLT.supports_source(SourceInstructionKind::ADDW)); + assert!(RV64IMAC_JOLT.supports_source(SourceInstructionKind::AMOADDW)); + assert!(RV64IMAC_JOLT.supports_source(SourceInstructionKind::Inline)); + assert!(!RV64IM_JOLT.supports_source(SourceInstructionKind::AMOADDW)); + } + + #[test] + fn target_legality_rejects_source_only_rows() { + assert!(RV64IMAC_JOLT.supports_jolt(JoltInstructionKind::ADD)); + assert!(RV64IMAC_JOLT.supports_jolt(JoltInstructionKind::LD)); + assert!(RV64IMAC_JOLT.supports_jolt(JoltInstructionKind::VirtualAssertEQ)); + + assert_eq!(SourceInstructionKind::Unimpl.jolt_kind(), None); + assert_eq!(SourceInstructionKind::ADDW.jolt_kind(), None); + assert_eq!(SourceInstructionKind::LW.jolt_kind(), None); + assert_eq!(SourceInstructionKind::SW.jolt_kind(), None); + assert_eq!(SourceInstructionKind::Inline.jolt_kind(), None); + } + + #[test] + fn dense_indexes_are_profile_local_and_contiguous() { + assert_eq!( + RV64IMAC_JOLT.source_dense_index(SourceInstructionKind::NoOp), + Some(ProfileInstructionIndex(0)) + ); + assert_eq!( + RV64IMAC_JOLT.jolt_dense_index(JoltInstructionKind::NoOp), + Some(ProfileInstructionIndex(0)) + ); + assert_eq!( + RV64IM_JOLT.source_dense_index(SourceInstructionKind::AMOADDW), + None + ); + assert_eq!(SourceInstructionKind::ADDW.jolt_kind(), None); + + let supported = JoltInstructionKind::ALL + .iter() + .copied() + .filter(|kind| RV64IMAC_JOLT.supports_jolt(*kind)) + .collect::>(); + for (expected, kind) in supported.into_iter().enumerate() { + assert_eq!( + RV64IMAC_JOLT.jolt_dense_index(kind), + Some(ProfileInstructionIndex(expected as u16)) + ); + } + } + + #[test] + fn profile_fingerprint_changes_with_legality_sets() { + assert_ne!(RV64IM_JOLT.fingerprint(), RV64IMAC_JOLT.fingerprint()); + assert_ne!( + RV64IMAC_JOLT.fingerprint(), + RV64IMAC_JOLT_ALL_INLINES.fingerprint() + ); + } +} diff --git a/crates/jolt-riscv/src/row.rs b/crates/jolt-riscv/src/row.rs new file mode 100644 index 0000000000..556f417a39 --- /dev/null +++ b/crates/jolt-riscv/src/row.rs @@ -0,0 +1,81 @@ +#[cfg(feature = "serialization")] +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +#[cfg(feature = "serialization")] +use serde::{Deserialize, Serialize}; + +use crate::JoltInstructionKind; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(CanonicalSerialize, CanonicalDeserialize, Serialize, Deserialize) +)] +pub struct NormalizedOperands { + pub rs1: Option, + pub rs2: Option, + pub rd: Option, + pub imm: i128, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(CanonicalSerialize, CanonicalDeserialize, Serialize, Deserialize) +)] +pub struct SourceInlineKey { + pub opcode: u8, + pub funct3: u8, + pub funct7: u8, +} + +impl SourceInlineKey { + #[inline] + pub const fn packed(self) -> u32 { + self.opcode as u32 | ((self.funct3 as u32) << 7) | ((self.funct7 as u32) << 10) + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(CanonicalSerialize, CanonicalDeserialize, Serialize, Deserialize) +)] +pub struct SourceInstructionRow { + pub address: usize, + pub operands: NormalizedOperands, + #[cfg_attr(feature = "serialization", serde(default))] + pub inline: Option, + pub is_compressed: bool, +} + +impl SourceInstructionRow { + #[inline] + pub fn jolt_instruction_row(self, instruction_kind: JoltInstructionKind) -> JoltInstructionRow { + let mut operands = self.operands; + if let Some(inline) = self.inline { + operands.imm = inline.packed() as i128; + } + JoltInstructionRow { + instruction_kind, + address: self.address, + operands, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: self.is_compressed, + } + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "serialization", + derive(CanonicalSerialize, CanonicalDeserialize, Serialize, Deserialize) +)] +pub struct JoltInstructionRow { + pub instruction_kind: JoltInstructionKind, + pub address: usize, + pub operands: NormalizedOperands, + pub virtual_sequence_remaining: Option, + pub is_first_in_sequence: bool, + pub is_compressed: bool, +} diff --git a/crates/jolt-riscv/src/row_data.rs b/crates/jolt-riscv/src/row_data.rs new file mode 100644 index 0000000000..7679513ba3 --- /dev/null +++ b/crates/jolt-riscv/src/row_data.rs @@ -0,0 +1,20 @@ +//! Marker trait for concrete instruction payloads that can convert through +//! Jolt's canonical final bytecode row. + +use crate::JoltInstructionRow; + +pub trait JoltInstructionRowData: + Copy + Into + TryFrom +{ + fn jolt_instruction_row(&self) -> JoltInstructionRow { + (*self).into() + } + + fn is_virtual(&self) -> bool { + self.jolt_instruction_row() + .virtual_sequence_remaining + .is_some() + } +} + +impl JoltInstructionRowData for JoltInstructionRow {} diff --git a/crates/jolt-riscv/src/trace.rs b/crates/jolt-riscv/src/trace.rs index 9fa5262895..376c58fe45 100644 --- a/crates/jolt-riscv/src/trace.rs +++ b/crates/jolt-riscv/src/trace.rs @@ -1,15 +1,15 @@ //! Tracer-free runtime views of executed Jolt instructions. //! -//! [`JoltCycle`] pairs the static [`JoltInstruction`](crate::JoltInstruction) +//! [`JoltCycle`] pairs the static [`JoltInstructionRowData`](crate::JoltInstructionRowData) //! vocabulary with register and RAM values captured during execution, so lookup //! table code can operate on cycle data without depending on tracer's concrete //! cycle types. -use crate::JoltInstruction; +use crate::JoltInstructionRowData; /// Dynamic cycle view: a populated instruction plus runtime register state. pub trait JoltCycle { - type Instruction: JoltInstruction; + type Instruction: JoltInstructionRowData; /// The instruction executed during this cycle. fn instruction(&self) -> Self::Instruction; diff --git a/crates/jolt-riscv/src/uncompress.rs b/crates/jolt-riscv/src/uncompress.rs index 84c29e10f4..ab7837e9e8 100644 --- a/crates/jolt-riscv/src/uncompress.rs +++ b/crates/jolt-riscv/src/uncompress.rs @@ -1,9 +1,8 @@ /// Expand a 16-bit RVC instruction into the RV64 32-bit instruction word that /// Jolt's decoder expects. /// -/// This helper deliberately has no RV32 mode. The new `jolt-program` pipeline -/// rejects ELF32/RV32 at the image boundary; tracer may keep legacy RV32 paths -/// until its own cleanup lands. +/// This helper deliberately has no RV32 mode. Program image decoding rejects +/// ELF32/RV32 before compressed-instruction normalization. #[inline] pub fn uncompress_rv64_instruction(halfword: u16) -> u32 { let halfword = u32::from(halfword); diff --git a/crates/jolt-sumcheck/Cargo.toml b/crates/jolt-sumcheck/Cargo.toml index ff40a99d36..8b7e70f17c 100644 --- a/crates/jolt-sumcheck/Cargo.toml +++ b/crates/jolt-sumcheck/Cargo.toml @@ -5,12 +5,18 @@ edition = "2021" license = "MIT" description = "Sumcheck protocol types and verification for the Jolt zkVM" +[features] +r1cs = ["dep:jolt-r1cs"] + [lints] workspace = true [dependencies] +jolt-crypto.workspace = true jolt-field.workspace = true +jolt-openings.workspace = true jolt-poly.workspace = true +jolt-r1cs = { workspace = true, optional = true } jolt-transcript.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/batched_sumcheck_verifier.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/batched_sumcheck_verifier.rs index f998fc11da..0f1d280c46 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/batched_sumcheck_verifier.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/batched_sumcheck_verifier.rs @@ -10,7 +10,7 @@ use jolt_field::{Fr, ReducingBytes}; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{BatchedSumcheckVerifier, SumcheckClaim}; +use jolt_sumcheck::{BatchedSumcheckVerifier, BooleanHypercube, SumcheckClaim}; use jolt_transcript::{Blake2bTranscript, Transcript}; use libfuzzer_sys::fuzz_target; @@ -76,9 +76,10 @@ fuzz_target!(|data: &[u8]| { // The verifier must terminate without panicking on any input — including // `n_claims = 0`, which must surface as `SumcheckError::EmptyClaims`. let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-batched-fuzz"); - let _ = BatchedSumcheckVerifier::verify::>( + let _ = BatchedSumcheckVerifier::verify::, _>( &claims, &round_proofs, + BooleanHypercube, &mut transcript, ); }); diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs index 15e1dc1103..8e843d862a 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs @@ -10,7 +10,7 @@ use jolt_field::{Fr, ReducingBytes}; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{SumcheckClaim, SumcheckVerifier}; +use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{Blake2bTranscript, Transcript}; use libfuzzer_sys::fuzz_target; @@ -61,9 +61,10 @@ fuzz_target!(|data: &[u8]| { // The verifier must terminate without panicking on any input. let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-fuzz"); - let _ = SumcheckVerifier::verify::>( + let _ = SumcheckVerifier::verify::, _>( &claim, &round_proofs, + BooleanHypercube, &mut transcript, ); }); diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs index 4dbd1293eb..9532b07085 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs @@ -24,7 +24,7 @@ use jolt_field::{Fr, ReducingBytes}; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{SumcheckClaim, SumcheckVerifier}; +use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; use libfuzzer_sys::fuzz_target; @@ -114,9 +114,10 @@ fuzz_target!(|data: &[u8]| { } let mut verifier_transcript = Blake2bTranscript::new(b"jolt-sumcheck-valid-fuzz"); - let result = SumcheckVerifier::verify::>( + let result = SumcheckVerifier::verify::, _>( &claim, &round_proofs, + BooleanHypercube, &mut verifier_transcript, ); diff --git a/crates/jolt-sumcheck/src/batched_verifier.rs b/crates/jolt-sumcheck/src/batched_verifier.rs index 984c9afdb2..70be11600f 100644 --- a/crates/jolt-sumcheck/src/batched_verifier.rs +++ b/crates/jolt-sumcheck/src/batched_verifier.rs @@ -7,12 +7,156 @@ //! earlier rounds. Each claim is scaled by $2^{N - n_i}$ where $N$ is the //! maximum `num_vars` across all claims. +use jolt_field::Field; use jolt_transcript::{AppendToTranscript, Transcript}; -use crate::claim::{EvaluationClaim, SumcheckClaim}; +use crate::claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; +use crate::committed::{CommittedSumcheckConsistency, CommittedSumcheckProof}; +use crate::domain::{BooleanHypercube, SumcheckDomain}; use crate::error::SumcheckError; -use crate::round_proof::RoundProof; +use crate::proof::{ClearProof, CompressedSumcheckProof, SumcheckProof}; +use crate::round_proof::ClearRound; use crate::scalar::SumcheckScalar; +use crate::{append_sumcheck_claim, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BatchedEvaluationClaim { + pub reduction: EvaluationClaim, + pub batching_coefficients: Vec, + pub max_num_vars: usize, + pub max_degree: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BatchedCommittedSumcheckConsistency { + pub consistency: CommittedSumcheckConsistency, + pub batching_coefficients: Vec, + pub max_num_vars: usize, + pub max_degree: usize, +} + +impl BatchedEvaluationClaim { + #[must_use] + pub const fn round_offset(&self, num_vars: usize) -> usize { + self.max_num_vars - num_vars + } + + /// Returns the front-padding offset for an instance with `num_vars`. + /// + /// Batched verification front-loads dummy rounds for smaller instances, so + /// an instance with `num_vars` is evaluated on the suffix beginning at + /// `max_num_vars - num_vars`. + pub fn try_round_offset(&self, num_vars: usize) -> Result> { + self.max_num_vars + .checked_sub(num_vars) + .ok_or(SumcheckError::BatchedPointOutOfRange { + offset: 0, + num_vars, + total: self.reduction.point.len(), + }) + } + + #[must_use] + pub fn instance_point(&self, num_vars: usize) -> &[F] { + let offset = self.round_offset(num_vars); + &self.reduction.point.as_slice()[offset..offset + num_vars] + } + + /// Returns the suffix challenge slice for an instance with `num_vars`. + pub fn try_instance_point(&self, num_vars: usize) -> Result<&[F], SumcheckError> { + self.try_instance_point_at(self.try_round_offset(num_vars)?, num_vars) + } + + /// Returns a challenge slice starting at `offset`. + /// + /// This is useful for protocols whose instance point is embedded inside the + /// batched challenge vector but not necessarily at the canonical suffix + /// offset. + pub fn try_instance_point_at( + &self, + offset: usize, + num_vars: usize, + ) -> Result<&[F], SumcheckError> { + let end = offset + .checked_add(num_vars) + .ok_or(SumcheckError::BatchedPointRangeOverflow { offset, num_vars })?; + self.reduction.point.as_slice().get(offset..end).ok_or( + SumcheckError::BatchedPointOutOfRange { + offset, + num_vars, + total: self.reduction.point.len(), + }, + ) + } +} + +impl BatchedCommittedSumcheckConsistency { + #[must_use] + pub const fn round_offset(&self, num_vars: usize) -> usize { + self.max_num_vars - num_vars + } + + /// Returns the front-padding offset for an instance with `num_vars`. + /// + /// Batched committed verification uses the same front-loaded dummy-round + /// layout as clear batched verification, but it exposes only transcript + /// challenges and commitments, not scalar evaluation claims. + pub fn try_round_offset(&self, num_vars: usize) -> Result> { + self.max_num_vars + .checked_sub(num_vars) + .ok_or(SumcheckError::BatchedPointOutOfRange { + offset: 0, + num_vars, + total: self.consistency.rounds.len(), + }) + } + + pub fn challenges(&self) -> Vec + where + F: Copy, + { + self.consistency + .rounds + .iter() + .map(|round| round.challenge) + .collect() + } + + /// Returns the suffix challenge vector for an instance with `num_vars`. + pub fn try_instance_point(&self, num_vars: usize) -> Result, SumcheckError> + where + F: Copy, + { + self.try_instance_point_at(self.try_round_offset(num_vars)?, num_vars) + } + + /// Returns a challenge vector starting at `offset`. + /// + /// This is useful for protocols whose instance point is embedded inside the + /// batched challenge vector but not necessarily at the canonical suffix + /// offset. + pub fn try_instance_point_at( + &self, + offset: usize, + num_vars: usize, + ) -> Result, SumcheckError> + where + F: Copy, + { + let end = offset + .checked_add(num_vars) + .ok_or(SumcheckError::BatchedPointRangeOverflow { offset, num_vars })?; + self.consistency + .rounds + .get(offset..end) + .ok_or(SumcheckError::BatchedPointOutOfRange { + offset, + num_vars, + total: self.consistency.rounds.len(), + }) + .map(|rounds| rounds.iter().map(|round| round.challenge).collect()) + } +} /// Batched sumcheck verifier. /// @@ -22,7 +166,7 @@ use crate::scalar::SumcheckScalar; pub struct BatchedSumcheckVerifier; impl BatchedSumcheckVerifier { - /// Verifies a batched sumcheck proof. + /// Verifies a batched sumcheck proof over `domain`. /// /// Returns an [`EvaluationClaim`] `{ point: r, value: v }` on success, /// where `v` is the combined final evaluation and `r` is the full @@ -32,15 +176,17 @@ impl BatchedSumcheckVerifier { /// /// Returns [`SumcheckError`] if verification fails. #[tracing::instrument(skip_all, name = "BatchedSumcheckVerifier::verify")] - pub fn verify( + pub fn verify( claims: &[SumcheckClaim], round_proofs: &[P], + domain: D, transcript: &mut T, ) -> Result, SumcheckError> where F: SumcheckScalar, T: Transcript, - P: RoundProof, + P: ClearRound, + D: SumcheckDomain, { let (first, rest) = claims.split_first().ok_or(SumcheckError::EmptyClaims)?; let max_num_vars = rest @@ -59,7 +205,7 @@ impl BatchedSumcheckVerifier { let mut alpha_pow = F::one(); let mut combined_sum = F::zero(); for claim in claims { - let scaled = claim.claimed_sum.mul_pow_2(max_num_vars - claim.num_vars); + let scaled = domain.scale_padding(claim.claimed_sum, max_num_vars - claim.num_vars)?; combined_sum += alpha_pow * scaled; alpha_pow *= alpha; } @@ -70,6 +216,171 @@ impl BatchedSumcheckVerifier { claimed_sum: combined_sum, }; - crate::verifier::SumcheckVerifier::verify(&combined_claim, round_proofs, transcript) + crate::verifier::SumcheckVerifier::verify(&combined_claim, round_proofs, domain, transcript) + } + + #[tracing::instrument(skip_all, name = "BatchedSumcheckVerifier::verify_compressed")] + pub fn verify_compressed( + claims: &[SumcheckClaim], + proof: &CompressedSumcheckProof, + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: Field, + T: Transcript, + { + let statement = Self::batch_claim_statement(claims)?; + let max_num_vars = statement.num_vars; + let max_degree = statement.degree; + + for claim in claims { + append_sumcheck_claim(transcript, &claim.claimed_sum); + } + let batching_coefficients = Self::batching_coefficients(claims.len(), transcript); + + let claimed_sum = claims + .iter() + .zip(&batching_coefficients) + .map(|(claim, coefficient)| { + claim.claimed_sum.mul_pow_2(max_num_vars - claim.num_vars) * *coefficient + }) + .sum(); + + let combined_claim = SumcheckClaim { + num_vars: max_num_vars, + degree: max_degree, + claimed_sum, + }; + let reduction = crate::verifier::SumcheckVerifier::verify_compressed( + &combined_claim, + proof, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + transcript, + )?; + + Ok(BatchedEvaluationClaim { + reduction, + batching_coefficients, + max_num_vars, + max_degree, + }) + } + + #[tracing::instrument(skip_all, name = "BatchedSumcheckVerifier::verify_compressed_boolean")] + pub fn verify_compressed_boolean( + claims: &[SumcheckClaim], + proof: &SumcheckProof, + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: Field, + C: Clone + AppendToTranscript, + T: Transcript, + { + match proof { + SumcheckProof::Clear(ClearProof::Compressed(proof)) => { + Self::verify_compressed(claims, proof, transcript) + } + SumcheckProof::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "full clear", + }), + SumcheckProof::Committed(_) => Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "committed", + }), + } + } + + /// Checks batched committed-proof consistency through the transcript. + /// + /// This path is used when BlindFold verifies the hidden claim relations. It + /// takes only public statements, not [`SumcheckClaim`] values, so it never + /// absorbs `claim.claimed_sum` into the transcript. The caller must absorb + /// commitments to those claim scalars before calling this method; batching + /// coefficients are squeezed first here, and without that prior binding the + /// prover could choose claim openings after seeing the batching challenge. + pub fn verify_committed_consistency( + statements: &[SumcheckStatement], + proof: &SumcheckProof, + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: Field, + C: Clone + AppendToTranscript, + T: Transcript, + { + match proof { + SumcheckProof::Committed(proof) => { + Self::verify_committed_consistency_for_proof(statements, proof, transcript) + } + SumcheckProof::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "committed", + got: "full clear", + }), + SumcheckProof::Clear(ClearProof::Compressed(_)) => { + Err(SumcheckError::WrongProofEncoding { + expected: "committed", + got: "compressed clear", + }) + } + } + } + + fn verify_committed_consistency_for_proof( + statements: &[SumcheckStatement], + proof: &CommittedSumcheckProof, + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: Field, + C: Clone + AppendToTranscript, + T: Transcript, + { + let statement = Self::batch_statement(statements)?; + let batching_coefficients = Self::batching_coefficients(statements.len(), transcript); + let consistency = proof.verify_committed_consistency(statement, transcript)?; + + Ok(BatchedCommittedSumcheckConsistency { + consistency, + batching_coefficients, + max_num_vars: statement.num_vars, + max_degree: statement.degree, + }) + } + + fn batch_claim_statement( + claims: &[SumcheckClaim], + ) -> Result> { + let statements = claims + .iter() + .map(SumcheckClaim::statement) + .collect::>(); + Self::batch_statement(&statements) + } + + fn batch_statement( + statements: &[SumcheckStatement], + ) -> Result> { + let (first, rest) = statements.split_first().ok_or(SumcheckError::EmptyClaims)?; + let max_num_vars = rest + .iter() + .fold(first.num_vars, |acc, statement| acc.max(statement.num_vars)); + let max_degree = rest + .iter() + .fold(first.degree, |acc, statement| acc.max(statement.degree)); + + Ok(SumcheckStatement::new(max_num_vars, max_degree)) + } + + fn batching_coefficients(count: usize, transcript: &mut T) -> Vec + where + F: Field, + T: Transcript, + { + (0..count) + .map(|_| transcript.challenge_scalar()) + .collect::>() } } diff --git a/crates/jolt-sumcheck/src/claim.rs b/crates/jolt-sumcheck/src/claim.rs index 64d910f70a..7e9172e818 100644 --- a/crates/jolt-sumcheck/src/claim.rs +++ b/crates/jolt-sumcheck/src/claim.rs @@ -2,6 +2,39 @@ use jolt_field::FieldCore; +pub use jolt_openings::EvaluationClaim; + +/// Round count and degree bound for a sumcheck instance. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SumcheckStatement { + pub num_vars: usize, + pub degree: usize, +} + +impl SumcheckStatement { + /// Construct a sumcheck statement. + /// + /// # Panics + /// + /// Panics if `degree == 0`. + pub fn new(num_vars: usize, degree: usize) -> Self { + assert!( + degree >= 1, + "sumcheck round polynomial must have degree >= 1, got {degree}" + ); + Self { num_vars, degree } + } +} + +impl From<&SumcheckClaim> for SumcheckStatement { + fn from(claim: &SumcheckClaim) -> Self { + Self { + num_vars: claim.num_vars, + degree: claim.degree, + } + } +} + /// A sumcheck claim asserting that /// $\sum_{x \in \{0,1\}^n} g(x) = C$ /// where $g$ is a polynomial of individual degree at most `degree` in each variable. @@ -40,19 +73,8 @@ impl SumcheckClaim { claimed_sum, } } -} -/// Oracle evaluation claim produced by a successful sumcheck reduction. -/// -/// Sumcheck reduces `∑_{x ∈ {0,1}^n} g(x) = C` to a single query -/// `g(r) = v` at a Fiat-Shamir-derived point `r`. The caller MUST -/// discharge this claim against the polynomial oracle (opening proof, -/// BlindFold, etc.) to retain soundness — sumcheck alone does not -/// verify `v` against any commitment. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EvaluationClaim { - /// Challenge point `r = (r_1, ..., r_n)`. - pub point: Vec, - /// Claimed evaluation `g(r) = v`. - pub value: F, + pub fn statement(&self) -> SumcheckStatement { + SumcheckStatement::from(self) + } } diff --git a/crates/jolt-sumcheck/src/committed.rs b/crates/jolt-sumcheck/src/committed.rs new file mode 100644 index 0000000000..956a0a44f5 --- /dev/null +++ b/crates/jolt-sumcheck/src/committed.rs @@ -0,0 +1,106 @@ +//! Committed sumcheck round messages. + +use jolt_crypto::VectorCommitment; +use jolt_field::Field; +use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; +use serde::{Deserialize, Serialize}; + +use crate::error::SumcheckError; +use crate::round_proof::RoundMessage; + +const SUMCHECK_COMMITMENT_LABEL: &[u8] = b"sumcheck_commitment"; +const OUTPUT_CLAIMS_LABEL: &[u8] = b"output_claims_coms"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedRound { + pub commitment: C, + pub degree: usize, +} + +impl RoundMessage for CommittedRound { + fn degree(&self) -> usize { + self.degree + } + + fn append_to_transcript(&self, transcript: &mut T) { + transcript.append(&Label(SUMCHECK_COMMITMENT_LABEL)); + self.commitment.append_to_transcript(transcript); + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedOutputClaims { + pub commitments: Vec, +} + +impl AppendToTranscript for CommittedOutputClaims { + fn append_to_transcript(&self, transcript: &mut T) { + transcript.append(&LabelWithCount( + OUTPUT_CLAIMS_LABEL, + self.commitments.len() as u64, + )); + for commitment in &self.commitments { + commitment.append_to_transcript(transcript); + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedSumcheckProof { + pub rounds: Vec>, + pub output_claims: CommittedOutputClaims, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifiedCommittedRound { + pub commitment: C, + pub degree: usize, + pub challenge: F, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CommittedSumcheckConsistency { + pub rounds: Vec>, +} + +impl CommittedSumcheckConsistency { + pub fn challenges(&self) -> Vec { + self.rounds.iter().map(|round| round.challenge).collect() + } + + pub fn round_degrees(&self) -> Vec { + self.rounds.iter().map(|round| round.degree).collect() + } + + pub fn round_commitments(&self) -> Vec { + self.rounds + .iter() + .map(|round| round.commitment.clone()) + .collect() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommittedRoundWitness { + pub coefficients: Vec, + pub blinding: F, +} + +impl CommittedRoundWitness { + pub fn commit( + &self, + setup: &VC::Setup, + ) -> Result, SumcheckError> + where + VC: VectorCommitment, + { + if self.coefficients.is_empty() { + return Err(SumcheckError::EmptyRoundCoefficients); + } + + Ok(CommittedRound { + commitment: VC::commit(setup, &self.coefficients, &self.blinding), + degree: self.coefficients.len() - 1, + }) + } +} diff --git a/crates/jolt-sumcheck/src/domain.rs b/crates/jolt-sumcheck/src/domain.rs new file mode 100644 index 0000000000..340df285ac --- /dev/null +++ b/crates/jolt-sumcheck/src/domain.rs @@ -0,0 +1,197 @@ +//! Sumcheck round domains. + +use crate::error::SumcheckError; +use crate::round_proof::ClearRound; +use crate::scalar::SumcheckScalar; +use jolt_poly::lagrange::{centered_domain_start, centered_power_sums, CenteredIntegerDomainError}; + +pub trait SumcheckDomain { + fn round_sum_coefficients(&self, degree: usize) -> Result, SumcheckError>; + + #[inline] + fn padding_scale(&self) -> Result> { + let coefficients = self.round_sum_coefficients(0)?; + match coefficients.as_slice() { + [scale] => Ok(*scale), + _ => Err(SumcheckError::RoundSumCoefficientCountMismatch { + round: 0, + expected: 1, + got: coefficients.len(), + }), + } + } + + #[inline] + fn scale_padding(&self, value: F, padding_rounds: usize) -> Result> { + if padding_rounds == 0 { + return Ok(value); + } + Ok(value * pow_usize(self.padding_scale()?, padding_rounds)) + } + + fn check_round_sum( + &self, + round_index: usize, + running_sum: F, + round: &R, + ) -> Result<(), SumcheckError> + where + R: ClearRound, + { + round.check_round_well_formed(round_index)?; + let coefficients = self.round_sum_coefficients(round.degree())?; + let expected = round.degree() + 1; + if coefficients.len() != expected { + return Err(SumcheckError::RoundSumCoefficientCountMismatch { + round: round_index, + expected, + got: coefficients.len(), + }); + } + + let actual = round.coefficient_linear_combination(&coefficients); + if actual != running_sum { + return Err(SumcheckError::RoundCheckFailed { + round: round_index, + expected: running_sum, + actual, + }); + } + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SumcheckDomainSpec { + BooleanHypercube, + CenteredInteger { domain_size: usize }, +} + +impl SumcheckDomainSpec { + pub const fn centered_integer(domain_size: usize) -> Self { + Self::CenteredInteger { domain_size } + } +} + +impl SumcheckDomain for SumcheckDomainSpec +where + F: SumcheckScalar, +{ + fn round_sum_coefficients(&self, degree: usize) -> Result, SumcheckError> { + match *self { + Self::BooleanHypercube => BooleanHypercube.round_sum_coefficients(degree), + Self::CenteredInteger { domain_size } => { + CenteredIntegerDomain::new(domain_size).round_sum_coefficients(degree) + } + } + } + + #[inline] + fn padding_scale(&self) -> Result> { + match *self { + Self::BooleanHypercube => BooleanHypercube.padding_scale(), + Self::CenteredInteger { domain_size } => { + CenteredIntegerDomain::new(domain_size).padding_scale() + } + } + } + + #[inline] + fn scale_padding(&self, value: F, padding_rounds: usize) -> Result> { + match *self { + Self::BooleanHypercube => BooleanHypercube.scale_padding(value, padding_rounds), + Self::CenteredInteger { domain_size } => { + CenteredIntegerDomain::new(domain_size).scale_padding(value, padding_rounds) + } + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct BooleanHypercube; + +impl SumcheckDomain for BooleanHypercube +where + F: SumcheckScalar, +{ + fn round_sum_coefficients(&self, degree: usize) -> Result, SumcheckError> { + let mut coefficients = vec![F::one(); degree + 1]; + coefficients[0] = F::from_u64(2); + Ok(coefficients) + } + + #[inline] + fn padding_scale(&self) -> Result> { + Ok(F::from_u64(2)) + } + + #[inline] + fn scale_padding(&self, value: F, padding_rounds: usize) -> Result> { + Ok(value.mul_pow_2(padding_rounds)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CenteredIntegerDomain { + domain_size: usize, +} + +impl CenteredIntegerDomain { + pub const fn new(domain_size: usize) -> Self { + Self { domain_size } + } + + pub const fn domain_size(self) -> usize { + self.domain_size + } + + pub fn start(self) -> Result { + centered_domain_start(self.domain_size) + } + + pub fn power_sums(self, num_powers: usize) -> Result, CenteredIntegerDomainError> { + centered_power_sums(self.domain_size, num_powers) + } +} + +impl SumcheckDomain for CenteredIntegerDomain +where + F: SumcheckScalar, +{ + fn round_sum_coefficients(&self, degree: usize) -> Result, SumcheckError> { + self.power_sums(degree + 1) + .map_err(|_| SumcheckError::InvalidIntegerDomain { + domain_size: self.domain_size, + }) + .map(|power_sums| power_sums.into_iter().map(F::from_i128).collect()) + } + + #[inline] + fn padding_scale(&self) -> Result> { + if self.domain_size == 0 || self.domain_size > i64::MAX as usize { + return Err(SumcheckError::InvalidIntegerDomain { + domain_size: self.domain_size, + }); + } + Ok(F::from_u64(self.domain_size as u64)) + } +} + +#[inline] +fn pow_usize(mut base: F, mut exponent: usize) -> F +where + F: SumcheckScalar, +{ + let mut result = F::one(); + while exponent > 0 { + if exponent % 2 == 1 { + result *= base; + } + exponent /= 2; + if exponent > 0 { + base = base.square(); + } + } + result +} diff --git a/crates/jolt-sumcheck/src/error.rs b/crates/jolt-sumcheck/src/error.rs index 6a17f358bf..06aba254d6 100644 --- a/crates/jolt-sumcheck/src/error.rs +++ b/crates/jolt-sumcheck/src/error.rs @@ -10,15 +10,15 @@ use jolt_field::FieldCore; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SumcheckError { - /// Round check failed: the sum $s_i(0) + s_i(1)$ did not match the - /// expected value carried forward from the previous round. + /// Round check failed: the domain sum did not match the expected value + /// carried forward from the previous round. #[error("round {round}: expected sum {expected}, got {actual}")] RoundCheckFailed { /// Zero-indexed round number where the check failed. round: usize, /// The expected sum. expected: F, - /// The computed sum $s_i(0) + s_i(1)$. + /// The computed domain sum. actual: F, }, @@ -42,6 +42,25 @@ pub enum SumcheckError { got: usize, }, + /// The domain-sum coefficient vector must have exactly one scalar per + /// round-polynomial coefficient. + #[error("round {round}: expected {expected} round-sum coefficients, got {got}")] + RoundSumCoefficientCountMismatch { + /// Zero-indexed round number where the mismatch appeared. + round: usize, + /// Expected number of coefficients, equal to `degree + 1`. + expected: usize, + /// Actual number of coefficients supplied by the domain. + got: usize, + }, + + /// An integer-domain sumcheck round used an invalid domain size. + #[error("integer sumcheck domain size must be between 1 and i64::MAX, got {domain_size}")] + InvalidIntegerDomain { + /// Number of integer points in the domain. + domain_size: usize, + }, + /// The number of round polynomials in the proof does not match /// the number of variables in the claim. #[error("expected {expected} rounds, proof contains {got}")] @@ -52,7 +71,43 @@ pub enum SumcheckError { got: usize, }, + /// A round witness did not contain any coefficients. + #[error("round polynomial must contain at least one coefficient")] + EmptyRoundCoefficients, + + /// The caller selected a verifier path that is incompatible with the proof + /// wire encoding. + #[error("wrong sumcheck proof encoding: expected {expected}, got {got}")] + WrongProofEncoding { + /// Expected proof encoding. + expected: &'static str, + /// Actual proof encoding. + got: &'static str, + }, + /// Batched verification received an empty claims slice. #[error("batched verification requires at least one claim")] EmptyClaims, + + /// A batched evaluation claim was asked for an impossible point slice. + #[error("batched point range overflow: offset {offset}, num_vars {num_vars}")] + BatchedPointRangeOverflow { + /// Starting index into the batched challenge vector. + offset: usize, + /// Number of variables in the requested instance. + num_vars: usize, + }, + + /// A batched evaluation claim did not contain enough challenges for the requested instance. + #[error( + "batched point out of range: offset {offset}, num_vars {num_vars}, total challenges {total}" + )] + BatchedPointOutOfRange { + /// Starting index into the batched challenge vector. + offset: usize, + /// Number of variables in the requested instance. + num_vars: usize, + /// Total number of available batched challenges. + total: usize, + }, } diff --git a/crates/jolt-sumcheck/src/lib.rs b/crates/jolt-sumcheck/src/lib.rs index a37124ee2f..601ce234f7 100644 --- a/crates/jolt-sumcheck/src/lib.rs +++ b/crates/jolt-sumcheck/src/lib.rs @@ -1,9 +1,6 @@ //! Sumcheck protocol: claims, proofs, and verification. //! -//! Verifier-side types and logic for the sumcheck protocol, used by the Jolt -//! zkVM. This crate is **verifier-only** and **backend-agnostic**: any field and -//! transcript can be plugged in. Proving is handled by `jolt-zkvm`'s runtime, -//! which drives sumcheck rounds via `ComputeBackend` primitives. +//! Verifier-side types and logic for the sumcheck protocol. //! //! # Protocol overview //! @@ -19,19 +16,29 @@ //! | Module | Purpose | //! |--------|---------| //! | [`claim`] | [`SumcheckClaim`] (input statement) and [`EvaluationClaim`] (reduction output) | -//! | [`proof`] | [`SumcheckProof`] — serializable proof | +//! | [`proof`] | [`ClearProof`], [`ClearSumcheckProof`], [`CompressedSumcheckProof`], and [`SumcheckProof`] — serializable proofs | //! | [`verifier`] | [`SumcheckVerifier`] engine | //! | [`batched_verifier`] | [`BatchedSumcheckVerifier`] — batched verification via RLC | -//! | [`round_proof`] | [`RoundProof`] — per-round trait and concrete wire-format impls | +//! | [`domain`] | [`SumcheckDomain`] implementations for round-sum checks | +//! | `r1cs` | R1CS lowering for sumcheck verifier equations (`r1cs` feature) | +//! | [`round_proof`] | [`RoundMessage`] and [`ClearRound`] traits | +//! | [`committed`] | Commitment-backed round messages | //! | [`error`] | [`SumcheckError`] variants | //! //! # Public API //! //! ## Types //! - [`SumcheckClaim`] — the public statement: `num_vars`, `degree`, and `claimed_sum`. +//! - [`SumcheckStatement`] — round count and degree bound without a claimed sum. //! - [`EvaluationClaim`] — the oracle evaluation claim `g(r) = v` produced by a //! successful reduction; the caller MUST discharge it against the polynomial oracle. -//! - [`SumcheckProof`] — a sequence of univariate round polynomials, one per variable. +//! - [`ClearProof`] — clear proof wire representation, either full or compressed. +//! - [`ClearSumcheckProof`] — a sequence of full univariate round polynomials, one per variable. +//! - [`CompressedSumcheckProof`] — owned wire form omitting each linear coefficient. +//! - [`SumcheckProof`] — clear or committed sumcheck proof data. +//! - [`CommittedSumcheckProof`] — committed round messages and output-claim commitments. +//! - [`BooleanHypercube`] — the standard `{0,1}` sumcheck round domain. +//! - [`CenteredIntegerDomain`] — centered consecutive-integer sumcheck round domain. //! - [`SumcheckError`] — error variants: `RoundCheckFailed`, `DegreeBoundExceeded`, //! `WrongNumberOfRounds`, `EmptyClaims`. //! @@ -43,9 +50,8 @@ //! via front-loaded padding. //! //! ## Per-round proof types -//! - [`RoundProof`] — trait implemented by anything the verifier can step -//! through one round at a time: degree bound, sum check, transcript absorb, -//! evaluation at challenge. +//! - [`RoundMessage`] — degree bound and transcript absorption. +//! - [`ClearRound`] — clear round polynomial evaluation and well-formedness. //! - [`UnivariatePoly`](jolt_poly::UnivariatePoly) — raw, unlabelled absorb. //! - [`LabeledRoundPoly`] — borrowed wrapper adding a `LabelWithCount` prefix. //! - [`CompressedLabeledRoundPoly`] — borrowed wrapper using the compressed @@ -56,14 +62,19 @@ //! ```text //! jolt-field ─┐ //! jolt-poly ─┼─> jolt-sumcheck -//! jolt-transcript ─┘ +//! jolt-transcript ─┤ +//! jolt-crypto ─┘ //! ``` //! pub mod batched_verifier; pub mod claim; +pub mod committed; +pub mod domain; pub mod error; pub mod proof; +#[cfg(feature = "r1cs")] +pub mod r1cs; pub mod round_proof; pub mod scalar; pub mod verifier; @@ -71,10 +82,39 @@ pub mod verifier; #[cfg(test)] mod tests; -pub use batched_verifier::BatchedSumcheckVerifier; -pub use claim::{EvaluationClaim, SumcheckClaim}; +/// Transcript label used for ordinary sumcheck round polynomials. +pub const SUMCHECK_ROUND_TRANSCRIPT_LABEL: &[u8] = b"sumcheck_poly"; +/// Transcript label used for univariate-skip round polynomials. +pub const UNISKIP_ROUND_TRANSCRIPT_LABEL: &[u8] = b"uniskip_poly"; +/// Transcript label used when a sumcheck claim scalar is absorbed before batching. +pub const SUMCHECK_CLAIM_TRANSCRIPT_LABEL: &[u8] = b"sumcheck_claim"; + +/// Absorbs a sumcheck claim scalar using Jolt's canonical transcript label. +pub fn append_sumcheck_claim(transcript: &mut T, claim: &A) +where + A: jolt_transcript::AppendToTranscript, + T: jolt_transcript::Transcript, +{ + transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, claim); +} + +pub use batched_verifier::{ + BatchedCommittedSumcheckConsistency, BatchedEvaluationClaim, BatchedSumcheckVerifier, +}; +pub use claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; +pub use committed::{ + CommittedOutputClaims, CommittedRound, CommittedRoundWitness, CommittedSumcheckConsistency, + CommittedSumcheckProof, VerifiedCommittedRound, +}; +pub use domain::{BooleanHypercube, CenteredIntegerDomain, SumcheckDomain, SumcheckDomainSpec}; pub use error::SumcheckError; -pub use proof::SumcheckProof; -pub use round_proof::{CompressedLabeledRoundPoly, LabeledRoundPoly, RoundProof}; +pub use proof::{ClearProof, ClearSumcheckProof, CompressedSumcheckProof, SumcheckProof}; +#[cfg(feature = "r1cs")] +pub use r1cs::{ + allocate_sumcheck_r1cs_layout, append_sumcheck_r1cs_constraints, + append_sumcheck_r1cs_constraints_for_domain, SumcheckR1csError, SumcheckR1csLayout, + SumcheckR1csRound, SumcheckR1csRoundLayout, +}; +pub use round_proof::{ClearRound, CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; pub use scalar::SumcheckScalar; pub use verifier::SumcheckVerifier; diff --git a/crates/jolt-sumcheck/src/proof.rs b/crates/jolt-sumcheck/src/proof.rs index bff701c267..249f94e004 100644 --- a/crates/jolt-sumcheck/src/proof.rs +++ b/crates/jolt-sumcheck/src/proof.rs @@ -1,6 +1,16 @@ //! Proof structures for single and batched sumcheck protocols. -use jolt_poly::UnivariatePoly; +use crate::{ + claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}, + committed::{CommittedSumcheckConsistency, CommittedSumcheckProof}, + domain::{BooleanHypercube, SumcheckDomain}, + error::SumcheckError, + round_proof::LabeledRoundPoly, + verifier::SumcheckVerifier, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, +}; +use jolt_poly::{CompressedPoly, UnivariatePoly}; +use jolt_transcript::{AppendToTranscript, Transcript}; use serde::{Deserialize, Serialize}; /// A sumcheck proof consisting of one univariate round polynomial per variable. @@ -13,9 +23,150 @@ use serde::{Deserialize, Serialize}; /// The proof is complete when all $n$ round polynomials have been sent; /// the verifier is left with a single evaluation claim at the point /// $(r_1, \ldots, r_n)$. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] -pub struct SumcheckProof { +pub struct ClearSumcheckProof { /// Round polynomials $s_1, \ldots, s_n$ in the order they were generated. pub round_polynomials: Vec>, } + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct CompressedSumcheckProof { + /// Boolean-hypercube round polynomials with the linear coefficient omitted. + pub round_polynomials: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub enum ClearProof { + Full(ClearSumcheckProof), + Compressed(CompressedSumcheckProof), +} + +impl Default for ClearProof { + fn default() -> Self { + Self::Full(ClearSumcheckProof::default()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound(serialize = "C: Serialize", deserialize = "C: Deserialize<'de>"))] +pub enum SumcheckProof { + Clear(ClearProof), + Committed(CommittedSumcheckProof), +} + +impl SumcheckProof { + pub fn is_committed(&self) -> bool { + matches!(self, Self::Committed(_)) + } + + pub fn is_clear(&self) -> bool { + matches!(self, Self::Clear(_)) + } + + pub fn as_clear(&self) -> Option<&ClearProof> { + match self { + Self::Clear(proof) => Some(proof), + Self::Committed(_) => None, + } + } + + pub fn as_committed(&self) -> Option<&CommittedSumcheckProof> { + match self { + Self::Clear(_) => None, + Self::Committed(proof) => Some(proof), + } + } + + /// Verifies a full-round clear sumcheck proof over `domain`. + pub fn verify( + &self, + claim: &SumcheckClaim, + domain: D, + round_label: &'static [u8], + transcript: &mut T, + ) -> Result, SumcheckError> + where + T: Transcript, + D: SumcheckDomain, + C: Clone + AppendToTranscript, + { + match self { + Self::Clear(ClearProof::Full(proof)) => { + let rounds = proof + .round_polynomials + .iter() + .map(|poly| LabeledRoundPoly::new(poly, round_label)) + .collect::>(); + SumcheckVerifier::verify(claim, &rounds, domain, transcript) + } + Self::Clear(ClearProof::Compressed(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "full clear", + got: "compressed clear", + }), + Self::Committed(_) => Err(SumcheckError::WrongProofEncoding { + expected: "full clear", + got: "committed", + }), + } + } + + /// Verifies a compressed clear Boolean-hypercube sumcheck proof. + pub fn verify_compressed_boolean( + &self, + claim: &SumcheckClaim, + transcript: &mut T, + ) -> Result, SumcheckError> + where + T: Transcript, + C: Clone + AppendToTranscript, + { + match self { + Self::Clear(ClearProof::Compressed(proof)) => SumcheckVerifier::verify_compressed( + claim, + proof, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + transcript, + ), + Self::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "full clear", + }), + Self::Committed(_) => Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "committed", + }), + } + } + + /// Checks public consistency for a committed sumcheck proof. + /// + /// This path intentionally takes only a [`SumcheckStatement`]. Committed + /// proofs do not reveal the scalar claim, so claim relations are deferred + /// to the BlindFold verifier rather than represented with placeholder + /// values. + pub fn verify_committed_consistency( + &self, + statement: SumcheckStatement, + transcript: &mut T, + ) -> Result, SumcheckError> + where + T: Transcript, + C: Clone + AppendToTranscript, + { + match self { + Self::Committed(proof) => proof.verify_committed_consistency(statement, transcript), + Self::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "committed", + got: "full clear", + }), + Self::Clear(ClearProof::Compressed(_)) => Err(SumcheckError::WrongProofEncoding { + expected: "committed", + got: "compressed clear", + }), + } + } +} diff --git a/crates/jolt-sumcheck/src/r1cs.rs b/crates/jolt-sumcheck/src/r1cs.rs new file mode 100644 index 0000000000..c1214987b9 --- /dev/null +++ b/crates/jolt-sumcheck/src/r1cs.rs @@ -0,0 +1,558 @@ +use jolt_field::Field; +use jolt_r1cs::{LinearCombination, R1csBuilder, Variable}; +use thiserror::Error; + +use crate::{BooleanHypercube, SumcheckDomain, SumcheckStatement, VerifiedCommittedRound}; + +pub trait SumcheckR1csRound { + fn degree(&self) -> usize; + fn challenge(&self) -> F; +} + +impl SumcheckR1csRound for VerifiedCommittedRound { + fn degree(&self) -> usize { + self.degree + } + + fn challenge(&self) -> F { + self.challenge + } +} + +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum SumcheckR1csError { + #[error("sumcheck expects {expected} rounds but input has {actual}")] + WrongNumberOfRounds { expected: usize, actual: usize }, + #[error("round {round_index} has degree {actual}, exceeding bound {bound}")] + DegreeBoundExceeded { + round_index: usize, + bound: usize, + actual: usize, + }, + #[error("layout has {actual} rounds but sumcheck input has {expected}")] + LayoutRoundCountMismatch { expected: usize, actual: usize }, + #[error("round {round_index} layout has no coefficient variables")] + EmptyRoundLayout { round_index: usize }, + #[error( + "round {round_index} layout has degree {actual} but sumcheck input has degree {expected}" + )] + LayoutRoundDegreeMismatch { + round_index: usize, + expected: usize, + actual: usize, + }, + #[error( + "round {round_index} expected input claim variable {expected:?} but layout uses {actual:?}" + )] + RoundClaimLinkMismatch { + round_index: usize, + expected: Variable, + actual: Variable, + }, + #[error("expected output claim variable {expected:?} but layout uses {actual:?}")] + OutputClaimLinkMismatch { + expected: Variable, + actual: Variable, + }, + #[error("layout references variable {variable:?} but builder has {num_vars} variables")] + LayoutVariableOutOfBounds { variable: Variable, num_vars: usize }, + #[error("round {round_index}: failed to derive round-sum coefficients: {reason}")] + RoundSumCoefficientsUnavailable { round_index: usize, reason: String }, + #[error("round {round_index}: expected {expected} round-sum coefficients, got {actual}")] + RoundSumCoefficientCountMismatch { + round_index: usize, + expected: usize, + actual: usize, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SumcheckR1csLayout { + pub input_claim: Variable, + pub rounds: Vec, + pub output_claim: Variable, +} + +impl SumcheckR1csLayout { + pub fn round_count(&self) -> usize { + self.rounds.len() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SumcheckR1csRoundLayout { + pub claim_in: Variable, + pub coefficients: Vec, + pub claim_out: Variable, +} + +impl SumcheckR1csRoundLayout { + pub fn degree(&self) -> usize { + self.coefficients.len().saturating_sub(1) + } +} + +pub fn allocate_sumcheck_r1cs_layout( + builder: &mut R1csBuilder, + statement: SumcheckStatement, + rounds: &[R], +) -> Result +where + F: Field, + R: SumcheckR1csRound, +{ + validate_rounds_statement(statement, rounds)?; + + let input_claim = builder.alloc_unknown(); + let mut claim_in = input_claim; + let mut round_layouts = Vec::with_capacity(rounds.len()); + + for round in rounds { + let coefficients = (0..=round.degree()) + .map(|_| builder.alloc_unknown()) + .collect(); + let claim_out = builder.alloc_unknown(); + + round_layouts.push(SumcheckR1csRoundLayout { + claim_in, + coefficients, + claim_out, + }); + claim_in = claim_out; + } + + Ok(SumcheckR1csLayout { + input_claim, + rounds: round_layouts, + output_claim: claim_in, + }) +} + +pub fn append_sumcheck_r1cs_constraints( + builder: &mut R1csBuilder, + statement: SumcheckStatement, + rounds: &[R], + layout: &SumcheckR1csLayout, +) -> Result<(), SumcheckR1csError> +where + F: Field, + R: SumcheckR1csRound, +{ + append_sumcheck_r1cs_constraints_for_domain( + builder, + statement, + rounds, + layout, + BooleanHypercube, + ) +} + +pub fn append_sumcheck_r1cs_constraints_for_domain( + builder: &mut R1csBuilder, + statement: SumcheckStatement, + rounds: &[R], + layout: &SumcheckR1csLayout, + domain: D, +) -> Result<(), SumcheckR1csError> +where + F: Field, + R: SumcheckR1csRound, + D: SumcheckDomain, +{ + validate_layout(builder.num_vars(), statement, rounds, layout)?; + + for (round_index, (round_layout, round)) in layout.rounds.iter().zip(rounds).enumerate() { + let round_sum_coefficients = + domain + .round_sum_coefficients(round.degree()) + .map_err( + |source| SumcheckR1csError::RoundSumCoefficientsUnavailable { + round_index, + reason: source.to_string(), + }, + )?; + append_round_constraints( + builder, + round_index, + round_layout, + round.challenge(), + &round_sum_coefficients, + )?; + } + + Ok(()) +} + +fn validate_layout( + num_vars: usize, + statement: SumcheckStatement, + rounds: &[R], + layout: &SumcheckR1csLayout, +) -> Result<(), SumcheckR1csError> +where + F: Field, + R: SumcheckR1csRound, +{ + validate_rounds_statement(statement, rounds)?; + validate_variable(layout.input_claim, num_vars)?; + validate_variable(layout.output_claim, num_vars)?; + + if rounds.len() != layout.rounds.len() { + return Err(SumcheckR1csError::LayoutRoundCountMismatch { + expected: rounds.len(), + actual: layout.rounds.len(), + }); + } + + let mut expected_claim_in = layout.input_claim; + for (round_index, (round, round_layout)) in rounds.iter().zip(&layout.rounds).enumerate() { + validate_variable(round_layout.claim_in, num_vars)?; + validate_variable(round_layout.claim_out, num_vars)?; + for &coefficient in &round_layout.coefficients { + validate_variable(coefficient, num_vars)?; + } + + if round_layout.coefficients.is_empty() { + return Err(SumcheckR1csError::EmptyRoundLayout { round_index }); + } + + let layout_degree = round_layout.degree(); + if round.degree() != layout_degree { + return Err(SumcheckR1csError::LayoutRoundDegreeMismatch { + round_index, + expected: round.degree(), + actual: layout_degree, + }); + } + + if round_layout.claim_in != expected_claim_in { + return Err(SumcheckR1csError::RoundClaimLinkMismatch { + round_index, + expected: expected_claim_in, + actual: round_layout.claim_in, + }); + } + expected_claim_in = round_layout.claim_out; + } + + if layout.output_claim != expected_claim_in { + return Err(SumcheckR1csError::OutputClaimLinkMismatch { + expected: expected_claim_in, + actual: layout.output_claim, + }); + } + + Ok(()) +} + +fn validate_rounds_statement( + statement: SumcheckStatement, + rounds: &[R], +) -> Result<(), SumcheckR1csError> +where + R: SumcheckR1csRound, +{ + if statement.num_vars != rounds.len() { + return Err(SumcheckR1csError::WrongNumberOfRounds { + expected: statement.num_vars, + actual: rounds.len(), + }); + } + + for (round_index, round) in rounds.iter().enumerate() { + if round.degree() > statement.degree { + return Err(SumcheckR1csError::DegreeBoundExceeded { + round_index, + bound: statement.degree, + actual: round.degree(), + }); + } + } + + Ok(()) +} + +fn validate_variable(variable: Variable, num_vars: usize) -> Result<(), SumcheckR1csError> { + if variable.index() >= num_vars { + return Err(SumcheckR1csError::LayoutVariableOutOfBounds { variable, num_vars }); + } + + Ok(()) +} + +fn append_round_constraints( + builder: &mut R1csBuilder, + round_index: usize, + round: &SumcheckR1csRoundLayout, + challenge: F, + round_sum_coefficients: &[F], +) -> Result<(), SumcheckR1csError> { + let round_sum = round_sum_lc(round_index, round, round_sum_coefficients)?; + builder.assert_equal(round_sum, round.claim_in); + builder.assert_equal( + polynomial_eval_lc(&round.coefficients, challenge), + round.claim_out, + ); + Ok(()) +} + +fn round_sum_lc( + round_index: usize, + round: &SumcheckR1csRoundLayout, + round_sum_coefficients: &[F], +) -> Result, SumcheckR1csError> { + if round_sum_coefficients.len() != round.coefficients.len() { + return Err(SumcheckR1csError::RoundSumCoefficientCountMismatch { + round_index, + expected: round.coefficients.len(), + actual: round_sum_coefficients.len(), + }); + } + + Ok(round.coefficients.iter().zip(round_sum_coefficients).fold( + LinearCombination::zero(), + |sum, (&variable, &coefficient)| { + sum + LinearCombination::variable(variable).scale(coefficient) + }, + )) +} + +fn polynomial_eval_lc(coefficients: &[Variable], point: F) -> LinearCombination { + let mut result = LinearCombination::zero(); + let mut power = F::one(); + + for &coefficient in coefficients { + result = result + LinearCombination::variable(coefficient).scale(power); + power *= point; + } + + result +} + +#[cfg(test)] +#[expect(clippy::expect_used, reason = "tests may panic on assertion failures")] +mod tests { + use super::*; + use jolt_field::{Fr, FromPrimitiveInt}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + struct Round { + degree: usize, + challenge: Fr, + } + + impl SumcheckR1csRound for Round { + fn degree(&self) -> usize { + self.degree + } + + fn challenge(&self) -> Fr { + self.challenge + } + } + + fn round(degree: usize, challenge: u64) -> Round { + Round { + degree, + challenge: Fr::from_u64(challenge), + } + } + + fn assign(builder: &mut R1csBuilder, variable: Variable, value: u64) { + builder + .assign(variable, Fr::from_u64(value)) + .expect("assignment succeeds"); + } + + fn assign_round( + builder: &mut R1csBuilder, + round: &SumcheckR1csRoundLayout, + coefficients: &[u64], + claim_out: u64, + ) { + for (&variable, &coefficient) in round.coefficients.iter().zip(coefficients) { + assign(builder, variable, coefficient); + } + assign(builder, round.claim_out, claim_out); + } + + #[test] + fn emits_satisfied_round_constraints() { + let statement = SumcheckStatement::new(2, 1); + let rounds = [round(1, 2), round(1, 3)]; + let mut builder = R1csBuilder::::new(); + + let layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect("constraints should build"); + + assign(&mut builder, layout.input_claim, 10); + assign_round(&mut builder, &layout.rounds[0], &[3, 4], 11); + assign_round(&mut builder, &layout.rounds[1], &[5, 1], 8); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn rejects_bad_round_sum() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(1, 2)]; + let mut builder = R1csBuilder::::new(); + + let layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect("constraints should build"); + + assign(&mut builder, layout.input_claim, 10); + assign_round(&mut builder, &layout.rounds[0], &[3, 5], 13); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_err()); + } + + #[test] + fn rejects_bad_challenge_transition() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(1, 2)]; + let mut builder = R1csBuilder::::new(); + + let layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect("constraints should build"); + + assign(&mut builder, layout.input_claim, 10); + assign_round(&mut builder, &layout.rounds[0], &[3, 4], 12); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_err()); + } + + #[test] + fn supports_constant_rounds_under_degree_bound() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(0, 99)]; + let mut builder = R1csBuilder::::new(); + + let layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect("constraints should build"); + + assign(&mut builder, layout.input_claim, 14); + assign_round(&mut builder, &layout.rounds[0], &[7], 7); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn emits_centered_integer_domain_round_sum_constraints() { + let statement = SumcheckStatement::new(1, 2); + let rounds = [round(2, 5)]; + let mut builder = R1csBuilder::::new(); + + let layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + append_sumcheck_r1cs_constraints_for_domain( + &mut builder, + statement, + &rounds, + &layout, + crate::CenteredIntegerDomain::new(4), + ) + .expect("constraints should build"); + + assign(&mut builder, layout.input_claim, 26); + assign_round(&mut builder, &layout.rounds[0], &[1, 2, 3], 86); + + let witness = builder.witness().expect("witness is assigned"); + assert!(builder.into_matrices().check_witness(&witness).is_ok()); + } + + #[test] + fn rejects_wrong_number_of_rounds() { + let statement = SumcheckStatement::new(2, 1); + let rounds = [round(1, 2)]; + let mut builder = R1csBuilder::::new(); + + let error = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect_err("round count differs"); + + assert_eq!( + error, + SumcheckR1csError::WrongNumberOfRounds { + expected: 2, + actual: 1, + } + ); + } + + #[test] + fn rejects_degree_above_statement_bound() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(2, 2)]; + let mut builder = R1csBuilder::::new(); + + let error = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect_err("degree exceeds bound"); + + assert_eq!( + error, + SumcheckR1csError::DegreeBoundExceeded { + round_index: 0, + bound: 1, + actual: 2, + } + ); + } + + #[test] + fn append_rejects_broken_claim_chain() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(1, 2)]; + let mut builder = R1csBuilder::::new(); + + let mut layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + layout.rounds[0].claim_in = layout.rounds[0].claim_out; + + let error = append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect_err("claim chain is broken"); + + assert_eq!( + error, + SumcheckR1csError::RoundClaimLinkMismatch { + round_index: 0, + expected: layout.input_claim, + actual: layout.rounds[0].claim_in, + } + ); + } + + #[test] + fn append_rejects_out_of_bounds_layout_variable() { + let statement = SumcheckStatement::new(1, 1); + let rounds = [round(1, 2)]; + let mut builder = R1csBuilder::::new(); + + let mut layout = allocate_sumcheck_r1cs_layout(&mut builder, statement, &rounds) + .expect("layout should allocate"); + let num_vars = builder.num_vars(); + layout.rounds[0].coefficients[0] = Variable::new(num_vars); + + let error = append_sumcheck_r1cs_constraints(&mut builder, statement, &rounds, &layout) + .expect_err("layout references an unknown variable"); + + assert_eq!( + error, + SumcheckR1csError::LayoutVariableOutOfBounds { + variable: Variable::new(num_vars), + num_vars, + } + ); + } +} diff --git a/crates/jolt-sumcheck/src/round_proof.rs b/crates/jolt-sumcheck/src/round_proof.rs index 54c592119a..9d9ecef3a2 100644 --- a/crates/jolt-sumcheck/src/round_proof.rs +++ b/crates/jolt-sumcheck/src/round_proof.rs @@ -1,10 +1,4 @@ -//! Per-round proof types and the `RoundProof` trait. -//! -//! [`RoundProof`] unifies the four operations the sumcheck verifier performs -//! on each round: degree bound, sum-check consistency, transcript absorption, -//! and evaluation at the Fiat-Shamir challenge. Concrete implementations -//! encode transcript format (raw vs labeled, full vs compressed) and mode -//! (clear vs committed — future). +//! Per-round sumcheck messages. use jolt_field::Field; use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; @@ -12,69 +6,53 @@ use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; use crate::error::SumcheckError; use crate::scalar::SumcheckScalar; +use crate::{SUMCHECK_ROUND_TRANSCRIPT_LABEL, UNISKIP_ROUND_TRANSCRIPT_LABEL}; -/// Per-round proof operations used by the sumcheck verifier. -/// -/// Implementations encode one concrete wire format by pairing a round -/// polynomial (or commitment, in future committed-mode impls) with -/// transcript labelling and compression choices. The single verifier loop -/// in [`crate::SumcheckVerifier::verify`] drives any impl uniformly; future -/// ZK support is a new impl, not a new strategy trait. -pub trait RoundProof { - /// Degree of this round polynomial (for the degree-bound check). +/// Common interface for one sumcheck round message. +pub trait RoundMessage { fn degree(&self) -> usize; - /// Verify round consistency: `poly(0) + poly(1) == running_sum`. - /// - /// Clear-mode impls enforce this; a future committed-mode impl returns - /// `Ok(())` and defers to BlindFold. - fn check_sum(&self, running_sum: F, round: usize) -> Result<(), SumcheckError>; + fn append_to_transcript(&self, transcript: &mut T); +} - /// Evaluate at the Fiat-Shamir challenge to compute the next running sum. - /// - /// Committed-mode impls may return `F::zero()` since BlindFold verifies - /// the reduction separately. +/// A round message whose polynomial is available to the verifier. +pub trait ClearRound: RoundMessage { fn evaluate(&self, challenge: F) -> F; - /// Absorb this round's payload into the transcript. Must match the - /// bytes the prover appended, including any label prefix. - fn append_to_transcript(&self, transcript: &mut impl Transcript); + fn coefficient_linear_combination(&self, coefficients: &[F]) -> F; + + fn check_round_well_formed(&self, _round: usize) -> Result<(), SumcheckError> { + Ok(()) + } } -impl RoundProof for UnivariatePoly { +impl RoundMessage for UnivariatePoly { fn degree(&self) -> usize { UnivariatePolynomial::degree(self) } - fn check_sum(&self, running_sum: F, round: usize) -> Result<(), SumcheckError> { - let sum = - UnivariatePoly::evaluate(self, F::zero()) + UnivariatePoly::evaluate(self, F::one()); - if sum != running_sum { - return Err(SumcheckError::RoundCheckFailed { - round, - expected: running_sum, - actual: sum, - }); + fn append_to_transcript(&self, transcript: &mut T) { + for coeff in self.coefficients() { + coeff.append_to_transcript(transcript); } - Ok(()) } +} +impl ClearRound for UnivariatePoly { fn evaluate(&self, challenge: F) -> F { UnivariatePoly::evaluate(self, challenge) } - fn append_to_transcript(&self, transcript: &mut impl Transcript) { - for coeff in self.coefficients() { - coeff.append_to_transcript(transcript); - } + fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { + self.coefficients() + .iter() + .zip(coefficients) + .map(|(&coefficient, &scale)| coefficient * scale) + .sum() } } /// Round polynomial paired with a Fiat-Shamir domain-separation label. -/// -/// On absorb: prepends `LabelWithCount(label, coeffs.len())` then the -/// coefficients. Degree, sum check, and evaluation delegate to the inner -/// [`UnivariatePoly`]. pub struct LabeledRoundPoly<'a, F: Field> { poly: &'a UnivariatePoly, label: &'static [u8], @@ -84,22 +62,22 @@ impl<'a, F: Field> LabeledRoundPoly<'a, F> { pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { Self { poly, label } } -} -impl RoundProof for LabeledRoundPoly<'_, F> { - fn degree(&self) -> usize { - as RoundProof>::degree(self.poly) + pub fn sumcheck(poly: &'a UnivariatePoly) -> Self { + Self::new(poly, SUMCHECK_ROUND_TRANSCRIPT_LABEL) } - fn check_sum(&self, running_sum: F, round: usize) -> Result<(), SumcheckError> { - as RoundProof>::check_sum(self.poly, running_sum, round) + pub fn uniskip(poly: &'a UnivariatePoly) -> Self { + Self::new(poly, UNISKIP_ROUND_TRANSCRIPT_LABEL) } +} - fn evaluate(&self, challenge: F) -> F { - as RoundProof>::evaluate(self.poly, challenge) +impl RoundMessage for LabeledRoundPoly<'_, F> { + fn degree(&self) -> usize { + as RoundMessage>::degree(self.poly) } - fn append_to_transcript(&self, transcript: &mut impl Transcript) { + fn append_to_transcript(&self, transcript: &mut T) { let coeffs = self.poly.coefficients(); transcript.append(&LabelWithCount(self.label, coeffs.len() as u64)); for coeff in coeffs { @@ -108,15 +86,22 @@ impl RoundProof for LabeledRoundPoly<'_, F> { } } +impl ClearRound for LabeledRoundPoly<'_, F> { + fn evaluate(&self, challenge: F) -> F { + as ClearRound>::evaluate(self.poly, challenge) + } + + fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { + as ClearRound>::coefficient_linear_combination( + self.poly, + coefficients, + ) + } +} + /// Compressed round polynomial with label. Wire format omits the linear /// coefficient `c_1`; the verifier recovers it from the sum-check invariant /// `running_sum = s(0) + s(1) = 2·c_0 + c_1 + c_2 + … + c_d`. -/// -/// Construct over an already-full [`UnivariatePoly`]; compression affects -/// only transcript absorption. -/// -/// The length check (`>= 2` coefficients) lives in [`Self::check_sum`], -/// which the verifier calls before [`Self::append_to_transcript`]. pub struct CompressedLabeledRoundPoly<'a, F: Field> { poly: &'a UnivariatePoly, label: &'static [u8], @@ -126,34 +111,51 @@ impl<'a, F: Field> CompressedLabeledRoundPoly<'a, F> { pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { Self { poly, label } } + + pub fn sumcheck(poly: &'a UnivariatePoly) -> Self { + Self::new(poly, SUMCHECK_ROUND_TRANSCRIPT_LABEL) + } + + pub fn uniskip(poly: &'a UnivariatePoly) -> Self { + Self::new(poly, UNISKIP_ROUND_TRANSCRIPT_LABEL) + } } -impl RoundProof for CompressedLabeledRoundPoly<'_, F> { +impl RoundMessage for CompressedLabeledRoundPoly<'_, F> { fn degree(&self) -> usize { - as RoundProof>::degree(self.poly) + as RoundMessage>::degree(self.poly) } - fn check_sum(&self, running_sum: F, round: usize) -> Result<(), SumcheckError> { + fn append_to_transcript(&self, transcript: &mut T) { let coeffs = self.poly.coefficients(); - if coeffs.len() < 2 { - return Err(SumcheckError::CompressedPolynomialTooShort { - round, - got: coeffs.len(), - }); + transcript.append(&LabelWithCount(self.label, (coeffs.len() - 1) as u64)); + coeffs[0].append_to_transcript(transcript); + for c in coeffs.iter().skip(2) { + c.append_to_transcript(transcript); } - as RoundProof>::check_sum(self.poly, running_sum, round) } +} +impl ClearRound for CompressedLabeledRoundPoly<'_, F> { fn evaluate(&self, challenge: F) -> F { - as RoundProof>::evaluate(self.poly, challenge) + as ClearRound>::evaluate(self.poly, challenge) } - fn append_to_transcript(&self, transcript: &mut impl Transcript) { + fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { + as ClearRound>::coefficient_linear_combination( + self.poly, + coefficients, + ) + } + + fn check_round_well_formed(&self, round: usize) -> Result<(), SumcheckError> { let coeffs = self.poly.coefficients(); - transcript.append(&LabelWithCount(self.label, (coeffs.len() - 1) as u64)); - coeffs[0].append_to_transcript(transcript); - for c in coeffs.iter().skip(2) { - c.append_to_transcript(transcript); + if coeffs.len() < 2 { + return Err(SumcheckError::CompressedPolynomialTooShort { + round, + got: coeffs.len(), + }); } + Ok(()) } } diff --git a/crates/jolt-sumcheck/src/tests.rs b/crates/jolt-sumcheck/src/tests.rs index b3cce06f73..7b58ac7ce9 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -6,16 +6,23 @@ reason = "tests may panic on assertion failures" )] +use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; use jolt_field::{Fr, FromPrimitiveInt}; -use jolt_poly::UnivariatePoly; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; +use jolt_poly::{CompressedPoly, UnivariatePoly}; +use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Label, LabelWithCount, Transcript}; -use crate::claim::{EvaluationClaim, SumcheckClaim}; +use crate::claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; +use crate::committed::{ + CommittedOutputClaims, CommittedRound, CommittedRoundWitness, CommittedSumcheckProof, +}; use crate::error::SumcheckError; -use crate::proof::SumcheckProof; -use crate::round_proof::{CompressedLabeledRoundPoly, LabeledRoundPoly, RoundProof}; +use crate::proof::{ClearProof, ClearSumcheckProof, CompressedSumcheckProof, SumcheckProof}; +use crate::round_proof::{ClearRound, CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; use crate::verifier::SumcheckVerifier; -use crate::BatchedSumcheckVerifier; +use crate::{ + append_sumcheck_claim, BatchedSumcheckVerifier, BooleanHypercube, CenteredIntegerDomain, + SumcheckDomain, SUMCHECK_ROUND_TRANSCRIPT_LABEL, +}; type F = Fr; @@ -29,7 +36,7 @@ fn honest_prove( evals: &[F], num_vars: usize, transcript: &mut Blake2bTranscript, -) -> SumcheckProof { +) -> ClearSumcheckProof { let mut buf = evals.to_vec(); let mut round_polys = Vec::with_capacity(num_vars); @@ -51,7 +58,7 @@ fn honest_prove( let round_poly = UnivariatePoly::new(vec![c0, c1]); // Absorb through the same path the unlabelled verifier uses. - as RoundProof>::append_to_transcript(&round_poly, transcript); + as RoundMessage>::append_to_transcript(&round_poly, transcript); let r: F = transcript.challenge(); round_polys.push(round_poly); @@ -63,11 +70,58 @@ fn honest_prove( buf.truncate(half); } - SumcheckProof { + ClearSumcheckProof { round_polynomials: round_polys, } } +fn honest_prove_compressed_labeled( + evals: &[F], + num_vars: usize, + label: &'static [u8], + transcript: &mut Blake2bTranscript, +) -> (ClearSumcheckProof, CompressedSumcheckProof) { + let mut buf = evals.to_vec(); + let mut round_polys = Vec::with_capacity(num_vars); + let mut compressed_round_polys = Vec::with_capacity(num_vars); + + for _round in 0..num_vars { + let half = buf.len() / 2; + + let mut eval_0 = F::from_u64(0); + let mut eval_1 = F::from_u64(0); + for i in 0..half { + eval_0 += buf[i]; + eval_1 += buf[i + half]; + } + + let round_poly = UnivariatePoly::new(vec![eval_0, eval_1 - eval_0]); + let compressed = CompressedLabeledRoundPoly::new(&round_poly, label); + as RoundMessage>::append_to_transcript( + &compressed, + transcript, + ); + + let r: F = transcript.challenge(); + compressed_round_polys.push(round_poly.compress()); + round_polys.push(round_poly); + + for i in 0..half { + buf[i] = buf[i] + r * (buf[i + half] - buf[i]); + } + buf.truncate(half); + } + + ( + ClearSumcheckProof { + round_polynomials: round_polys, + }, + CompressedSumcheckProof { + round_polynomials: compressed_round_polys, + }, + ) +} + /// Compute the sum over {0,1}^n of multilinear evaluations. fn compute_sum(evals: &[F]) -> F { evals.iter().copied().sum() @@ -92,8 +146,12 @@ fn verify_valid_degree1_proof() { }; let mut verifier_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut verifier_transcript); + let result = SumcheckVerifier::verify( + &claim, + &proof.round_polynomials, + BooleanHypercube, + &mut verifier_transcript, + ); assert!(result.is_ok(), "verification failed: {:?}", result.err()); let EvaluationClaim { @@ -128,13 +186,123 @@ fn verify_single_variable() { let EvaluationClaim { point: challenges, value: final_eval, - } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt).unwrap(); + } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) + .unwrap(); assert_eq!(challenges.len(), 1); let poly = jolt_poly::Polynomial::new(evals); assert_eq!(final_eval, poly.evaluate_and_consume(&challenges)); } +#[test] +fn centered_integer_domain_verifies_round_sum() { + let round_poly = UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)]); + let claim = SumcheckClaim { + num_vars: 1, + degree: 2, + claimed_sum: F::from_u64(16), + }; + + let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); + let result = SumcheckVerifier::verify( + &claim, + std::slice::from_ref(&round_poly), + CenteredIntegerDomain::new(3), + &mut transcript, + ) + .unwrap(); + + assert_eq!(result.point.len(), 1); + assert_eq!(result.value, round_poly.evaluate(result.point[0])); +} + +#[test] +fn centered_integer_domain_uses_core_even_window_convention() { + let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_i64(2), + }; + + let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); + let result = SumcheckVerifier::verify( + &claim, + &[round_poly], + CenteredIntegerDomain::new(4), + &mut transcript, + ); + + assert!(result.is_ok(), "verification failed: {:?}", result.err()); +} + +#[test] +fn centered_integer_domain_rejects_wrong_sum() { + let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_u64(3), + }; + + let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); + let result = SumcheckVerifier::verify( + &claim, + &[round_poly], + CenteredIntegerDomain::new(4), + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::RoundCheckFailed { + round: 0, + expected, + actual, + }) if expected == F::from_u64(3) && actual == F::from_i64(2) + )); +} + +#[test] +fn centered_integer_domain_rejects_empty_domain() { + let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_u64(0), + }; + + let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); + let result = SumcheckVerifier::verify( + &claim, + &[round_poly], + CenteredIntegerDomain::new(0), + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::InvalidIntegerDomain { domain_size: 0 }) + )); +} + +#[test] +fn centered_integer_domain_exposes_power_sums() { + let domain = CenteredIntegerDomain::new(4); + + assert_eq!(domain.start().unwrap(), -1); + assert_eq!(domain.power_sums(4).unwrap(), vec![4, 2, 6, 8]); + assert_eq!( + >::round_sum_coefficients(&domain, 3).unwrap(), + vec![ + F::from_u64(4), + F::from_u64(2), + F::from_u64(6), + F::from_u64(8) + ] + ); +} + #[test] fn verify_round_check_failure() { let evals: Vec = (1..=8).map(F::from_u64).collect(); @@ -154,7 +322,8 @@ fn verify_round_check_failure() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_err()); match result.unwrap_err() { SumcheckError::RoundCheckFailed { round, .. } => assert_eq!(round, 0), @@ -180,7 +349,8 @@ fn verify_wrong_num_rounds() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); match result.unwrap_err() { SumcheckError::WrongNumberOfRounds { expected, got } => { assert_eq!(expected, 3); @@ -209,7 +379,8 @@ fn verify_degree_exceeded() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); match result.unwrap_err() { SumcheckError::DegreeBoundExceeded { got, max } => { assert_eq!(max, 1); @@ -235,7 +406,8 @@ fn verify_wrong_claimed_sum() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -251,7 +423,7 @@ fn clear_round_verifier_with_label_absorbs_label() { let labeled = LabeledRoundPoly::new(&poly, label); let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundProof>::append_to_transcript(&labeled, &mut t1); + as RoundMessage>::append_to_transcript(&labeled, &mut t1); let c1: F = t1.challenge(); // Absorb manually (should match) @@ -270,7 +442,7 @@ fn clear_round_verifier_no_label() { let poly = UnivariatePoly::new(vec![F::from_u64(5), F::from_u64(3)]); let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundProof>::append_to_transcript(&poly, &mut t1); + as RoundMessage>::append_to_transcript(&poly, &mut t1); let c1: F = t1.challenge(); // Manual: just coefficients, no label @@ -292,10 +464,7 @@ fn clear_round_verifier_compressed_matches_manual_absorption() { let compressed = CompressedLabeledRoundPoly::new(&poly, label); let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundProof>::append_to_transcript( - &compressed, - &mut t1, - ); + as RoundMessage>::append_to_transcript(&compressed, &mut t1); let ch1: F = t1.challenge(); // Manual absorb matching the compressed wire format: label_with_count(d), c0, c2..cd. @@ -322,11 +491,7 @@ fn clear_round_verifier_compressed_rejects_wrong_running_sum() { let wrong_running_sum = F::from_u64(999); let compressed = CompressedLabeledRoundPoly::new(&poly, b"compressed_test"); - let result = as RoundProof>::check_sum( - &compressed, - wrong_running_sum, - 0, - ); + let result = BooleanHypercube.check_round_sum(0, wrong_running_sum, &compressed); assert!( matches!( result, @@ -343,11 +508,7 @@ fn clear_round_verifier_compressed_rejects_short_polynomial() { let degree_zero = UnivariatePoly::new(vec![F::from_u64(7)]); let compressed = CompressedLabeledRoundPoly::new(°ree_zero, b"compressed_test"); - let result = as RoundProof>::check_sum( - &compressed, - F::from_u64(14), - 3, - ); + let result = BooleanHypercube.check_round_sum(3, F::from_u64(14), &compressed); assert!( matches!( result, @@ -357,6 +518,196 @@ fn clear_round_verifier_compressed_rejects_short_polynomial() { ); } +#[test] +fn owned_compressed_verify_matches_borrowed_compressed_rounds() { + let evals: Vec = (1..=8).map(F::from_u64).collect(); + let num_vars = 3; + let sum = compute_sum(&evals); + let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; + + let mut prover_transcript = Blake2bTranscript::new(b"sumcheck-test"); + let (clear_proof, compressed_proof) = + honest_prove_compressed_labeled(&evals, num_vars, label, &mut prover_transcript); + let claim = SumcheckClaim { + num_vars, + degree: 1, + claimed_sum: sum, + }; + + let wrapped = clear_proof + .round_polynomials + .iter() + .map(|poly| CompressedLabeledRoundPoly::new(poly, label)) + .collect::>(); + let mut borrowed_transcript = Blake2bTranscript::new(b"sumcheck-test"); + let borrowed = + SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut borrowed_transcript) + .unwrap(); + + let mut owned_transcript = Blake2bTranscript::new(b"sumcheck-test"); + let owned = compressed_proof + .verify(&claim, BooleanHypercube, label, &mut owned_transcript) + .unwrap(); + + assert_eq!(owned, borrowed); + assert_eq!(owned_transcript.state(), borrowed_transcript.state()); + + let poly = jolt_poly::Polynomial::new(evals); + assert_eq!(owned.value, poly.evaluate_and_consume(&owned.point)); +} + +#[test] +fn owned_compressed_verify_rejects_wrong_round_count() { + let proof = CompressedSumcheckProof { + round_polynomials: Vec::new(), + }; + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_u64(0), + }; + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); + let result = proof.verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::WrongNumberOfRounds { + expected: 1, + got: 0, + }) + )); +} + +#[test] +fn owned_compressed_verify_rejects_degree_bound_exceeded() { + let proof = CompressedSumcheckProof { + round_polynomials: vec![CompressedPoly::new(vec![F::from_u64(1), F::from_u64(2)])], + }; + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_u64(3), + }; + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); + let result = proof.verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }) + )); +} + +#[test] +fn owned_compressed_verify_rejects_empty_round_polynomial() { + let proof = CompressedSumcheckProof { + round_polynomials: vec![CompressedPoly::new(Vec::new())], + }; + let claim = SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: F::from_u64(0), + }; + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); + let result = proof.verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::CompressedPolynomialTooShort { round: 0, got: 0 }) + )); +} + +#[test] +fn sumcheck_proof_verify_dispatches_full_clear() { + let proof = SumcheckProof::::Clear(ClearProof::Full(ClearSumcheckProof { + round_polynomials: vec![UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3)])], + })); + let claim = SumcheckClaim::new(1, 1, F::from_u64(7)); + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); + let reduction = proof + .verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ) + .unwrap(); + + assert_eq!(reduction.point.len(), 1); +} + +#[test] +fn sumcheck_proof_verify_rejects_wrong_clear_encoding() { + let proof = SumcheckProof::::Clear(ClearProof::Compressed(CompressedSumcheckProof { + round_polynomials: Vec::new(), + })); + let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); + let result = proof.verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::WrongProofEncoding { + expected: "full clear", + got: "compressed clear", + }) + )); +} + +#[test] +fn sumcheck_proof_verify_rejects_committed_encoding() { + let proof = SumcheckProof::::Committed(CommittedSumcheckProof { + rounds: vec![CommittedRound { + commitment: F::from_u64(11), + degree: 1, + }], + output_claims: CommittedOutputClaims { + commitments: vec![F::from_u64(21)], + }, + }); + let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); + + let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); + let result = proof.verify( + &claim, + BooleanHypercube, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::WrongProofEncoding { + expected: "full clear", + got: "committed", + }) + )); +} + #[test] fn batched_verify_same_size() { // Two polynomials, both 2 variables @@ -394,7 +745,12 @@ fn batched_verify_same_size() { ]; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = BatchedSumcheckVerifier::verify(&claims, &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &claims, + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!(result.is_ok(), "batched verify failed: {:?}", result.err()); let challenges = result.unwrap().point; @@ -448,10 +804,54 @@ fn batched_verify_different_sizes() { ]; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = BatchedSumcheckVerifier::verify(&claims, &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &claims, + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!(result.is_ok(), "batched verify failed: {:?}", result.err()); } +#[test] +fn batched_verify_uses_domain_padding_scale() { + let sum_a = F::from_u64(0); + let sum_b = F::from_u64(1); + let claims = vec![ + SumcheckClaim { + num_vars: 1, + degree: 1, + claimed_sum: sum_a, + }, + SumcheckClaim { + num_vars: 0, + degree: 1, + claimed_sum: sum_b, + }, + ]; + + let mut pt = Blake2bTranscript::new(b"sumcheck-test"); + sum_a.append_to_transcript(&mut pt); + sum_b.append_to_transcript(&mut pt); + let alpha: F = pt.challenge(); + + let proof = ClearSumcheckProof { + round_polynomials: vec![UnivariatePoly::new(vec![alpha])], + }; + + let mut vt = Blake2bTranscript::new(b"sumcheck-test"); + let result = BatchedSumcheckVerifier::verify( + &claims, + &proof.round_polynomials, + CenteredIntegerDomain::new(3), + &mut vt, + ) + .unwrap(); + + assert_eq!(result.value, alpha); + assert_eq!(result.point.len(), 1); +} + #[test] fn batched_single_claim_matches_single_verify() { let evals: Vec = (1..=8).map(F::from_u64).collect(); @@ -475,7 +875,12 @@ fn batched_single_claim_matches_single_verify() { let proof = honest_prove(&evals, 3, &mut pt); let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = BatchedSumcheckVerifier::verify(&[claim], &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &[claim], + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!( result.is_ok(), "single-claim batch failed: {:?}", @@ -488,52 +893,507 @@ fn batched_empty_claims_returns_error() { let claims: &[SumcheckClaim] = &[]; let round_proofs: &[UnivariatePoly] = &[]; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = BatchedSumcheckVerifier::verify(claims, round_proofs, &mut vt); + let result = BatchedSumcheckVerifier::verify(claims, round_proofs, BooleanHypercube, &mut vt); assert!(matches!(result, Err(SumcheckError::EmptyClaims))); } -/// A mock round proof that always accepts and returns a fixed evaluation. -/// Used to verify that `SumcheckVerifier` dispatches through the trait correctly. -struct MockRoundProof { +#[test] +fn batched_compressed_verify_uses_core_batching_statement() { + let evals_a: Vec = (1..=8).map(F::from_u64).collect(); + let evals_b: Vec = (1..=4).map(F::from_u64).collect(); + let sum_a = compute_sum(&evals_a); + let sum_b = compute_sum(&evals_b); + + let claims = vec![ + SumcheckClaim { + num_vars: 3, + degree: 1, + claimed_sum: sum_a, + }, + SumcheckClaim { + num_vars: 2, + degree: 1, + claimed_sum: sum_b, + }, + ]; + + let mut prover_transcript = Blake2bTranscript::new(b"sumcheck-test"); + for claim in &claims { + append_sumcheck_claim(&mut prover_transcript, &claim.claimed_sum); + } + let batching_coefficients = (0..claims.len()) + .map(|_| prover_transcript.challenge_scalar()) + .collect::>(); + + let evals_b_extended: Vec = evals_b.iter().flat_map(|&value| [value, value]).collect(); + let combined: Vec = evals_a + .iter() + .zip(&evals_b_extended) + .map(|(&a, &b)| batching_coefficients[0] * a + batching_coefficients[1] * b) + .collect(); + let (_full_proof, compressed_proof) = honest_prove_compressed_labeled( + &combined, + 3, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut prover_transcript, + ); + + let mut verifier_transcript = Blake2bTranscript::new(b"sumcheck-test"); + let result = BatchedSumcheckVerifier::verify_compressed( + &claims, + &compressed_proof, + &mut verifier_transcript, + ) + .unwrap(); + + assert_eq!(result.batching_coefficients, batching_coefficients); + assert_eq!(result.max_num_vars, 3); + assert_eq!(result.max_degree, 1); + assert_eq!( + result.instance_point(2), + &result.reduction.point.as_slice()[1..] + ); + assert_eq!( + result.try_instance_point(2).unwrap(), + &result.reduction.point.as_slice()[1..] + ); + assert_eq!( + result.try_instance_point_at(0, 3).unwrap(), + result.reduction.point.as_slice() + ); + assert!(matches!( + result.try_instance_point(4), + Err(SumcheckError::BatchedPointOutOfRange { + offset: 0, + num_vars: 4, + total: 3 + }) + )); + assert!(matches!( + result.try_instance_point_at(usize::MAX, 1), + Err(SumcheckError::BatchedPointRangeOverflow { + offset: usize::MAX, + num_vars: 1 + }) + )); +} + +#[test] +fn batched_sumcheck_proof_verify_dispatches_compressed_clear() { + let evals: Vec = (1..=4).map(F::from_u64).collect(); + let claim = SumcheckClaim::new(2, 1, compute_sum(&evals)); + + let mut prover_transcript = Blake2bTranscript::new(b"batched-proof-dispatch"); + append_sumcheck_claim(&mut prover_transcript, &claim.claimed_sum); + let _batching_coefficient = prover_transcript.challenge_scalar(); + let (_full_proof, compressed_proof) = honest_prove_compressed_labeled( + &evals, + 2, + SUMCHECK_ROUND_TRANSCRIPT_LABEL, + &mut prover_transcript, + ); + let proof = SumcheckProof::::Clear(ClearProof::Compressed(compressed_proof)); + + let mut verifier_transcript = Blake2bTranscript::new(b"batched-proof-dispatch"); + let reduction = BatchedSumcheckVerifier::verify_compressed_boolean( + &[claim], + &proof, + &mut verifier_transcript, + ) + .unwrap(); + + assert_eq!(reduction.max_num_vars, 2); + assert_eq!(reduction.max_degree, 1); + assert_eq!(reduction.batching_coefficients.len(), 1); +} + +#[test] +fn batched_sumcheck_proof_verify_rejects_full_clear_encoding() { + let proof = SumcheckProof::::Clear(ClearProof::Full(ClearSumcheckProof { + round_polynomials: Vec::new(), + })); + let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); + + let mut transcript = Blake2bTranscript::::new(b"batched-proof-dispatch"); + let result = + BatchedSumcheckVerifier::verify_compressed_boolean(&[claim], &proof, &mut transcript); + + assert!(matches!( + result, + Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "full clear", + }) + )); +} + +#[test] +fn batched_committed_consistency_uses_statements_without_clear_claims() { + let statements = [SumcheckStatement::new(3, 2), SumcheckStatement::new(1, 1)]; + let proof = SumcheckProof::::Committed(CommittedSumcheckProof { + rounds: vec![ + CommittedRound { + commitment: F::from_u64(11), + degree: 2, + }, + CommittedRound { + commitment: F::from_u64(12), + degree: 1, + }, + CommittedRound { + commitment: F::from_u64(13), + degree: 0, + }, + ], + output_claims: CommittedOutputClaims { + commitments: vec![F::from_u64(21), F::from_u64(34)], + }, + }); + + let mut manual = Blake2bTranscript::::new(b"batched-proof-dispatch"); + let batching_coefficients = (0..statements.len()) + .map(|_| manual.challenge_scalar()) + .collect::>(); + let mut expected_challenges = Vec::new(); + let SumcheckProof::Committed(committed_proof) = &proof else { + panic!("proof must be committed"); + }; + for round in &committed_proof.rounds { + manual.append(&Label(b"sumcheck_commitment")); + round.commitment.append_to_transcript(&mut manual); + expected_challenges.push(manual.challenge()); + } + manual.append(&LabelWithCount(b"output_claims_coms", 2)); + for commitment in &committed_proof.output_claims.commitments { + commitment.append_to_transcript(&mut manual); + } + + let mut verifier = Blake2bTranscript::::new(b"batched-proof-dispatch"); + let consistency = + BatchedSumcheckVerifier::verify_committed_consistency(&statements, &proof, &mut verifier) + .unwrap(); + + assert_eq!(consistency.batching_coefficients, batching_coefficients); + assert_eq!(consistency.max_num_vars, 3); + assert_eq!(consistency.max_degree, 2); + assert_eq!(consistency.challenges(), expected_challenges); + assert_eq!(consistency.try_round_offset(1).unwrap(), 2); + assert_eq!( + consistency.try_instance_point(1).unwrap(), + expected_challenges[2..].to_vec() + ); + assert_eq!( + consistency.try_instance_point_at(0, 3).unwrap(), + expected_challenges + ); + assert!(matches!( + consistency.try_instance_point(4), + Err(SumcheckError::BatchedPointOutOfRange { + offset: 0, + num_vars: 4, + total: 3 + }) + )); + assert!(matches!( + consistency.try_instance_point_at(usize::MAX, 1), + Err(SumcheckError::BatchedPointRangeOverflow { + offset: usize::MAX, + num_vars: 1 + }) + )); + assert_eq!(verifier.state(), manual.state()); +} + +#[test] +fn batched_claim_verifier_rejects_committed_encoding() { + let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); + let proof = SumcheckProof::::Committed(CommittedSumcheckProof { + rounds: vec![CommittedRound { + commitment: F::from_u64(11), + degree: 1, + }], + output_claims: CommittedOutputClaims::default(), + }); + + let mut transcript = Blake2bTranscript::::new(b"batched-proof-dispatch"); + let result = + BatchedSumcheckVerifier::verify_compressed_boolean(&[claim], &proof, &mut transcript); + + assert!(matches!( + result, + Err(SumcheckError::WrongProofEncoding { + expected: "compressed clear", + got: "committed", + }) + )); +} + +#[test] +fn committed_rounds_check_transcript_and_return_public_data() { + let rounds = vec![ + CommittedRound { + commitment: F::from_u64(11), + degree: 1, + }, + CommittedRound { + commitment: F::from_u64(12), + degree: 2, + }, + CommittedRound { + commitment: F::from_u64(13), + degree: 0, + }, + ]; + + let mut manual = Blake2bTranscript::::new(b"committed-sumcheck"); + let mut expected_challenges = Vec::new(); + for round in &rounds { + manual.append(&Label(b"sumcheck_commitment")); + round.commitment.append_to_transcript(&mut manual); + expected_challenges.push(manual.challenge()); + } + + let mut verifier = Blake2bTranscript::::new(b"committed-sumcheck"); + let consistency = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(3, 2), + &rounds, + &mut verifier, + ) + .unwrap(); + + assert_eq!(consistency.challenges(), expected_challenges); + assert_eq!(consistency.round_degrees(), vec![1, 2, 0]); + assert_eq!( + consistency.round_commitments(), + rounds + .iter() + .map(|round| round.commitment) + .collect::>() + ); + assert_eq!(verifier.state(), manual.state()); +} + +#[test] +fn committed_rounds_reject_wrong_round_count() { + let rounds = vec![CommittedRound { + commitment: F::from_u64(11), + degree: 1, + }]; + let mut transcript = Blake2bTranscript::::new(b"committed-sumcheck"); + + let result = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(2, 1), + &rounds, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::WrongNumberOfRounds { + expected: 2, + got: 1 + }) + )); +} + +#[test] +fn committed_rounds_reject_degree_bound_before_absorbing() { + let rounds = vec![CommittedRound { + commitment: F::from_u64(11), + degree: 3, + }]; + let mut transcript = Blake2bTranscript::::new(b"committed-sumcheck"); + let before = *transcript.state(); + + let result = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(1, 2), + &rounds, + &mut transcript, + ); + + assert!(matches!( + result, + Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) + )); + assert_eq!(*transcript.state(), before); +} + +#[test] +fn committed_output_claims_absorb_length_and_order() { + let output_claims = CommittedOutputClaims { + commitments: vec![F::from_u64(3), F::from_u64(5), F::from_u64(8)], + }; + + let mut actual = Blake2bTranscript::::new(b"committed-output"); + output_claims.append_to_transcript(&mut actual); + + let mut expected = Blake2bTranscript::::new(b"committed-output"); + expected.append(&LabelWithCount(b"output_claims_coms", 3)); + for commitment in &output_claims.commitments { + commitment.append_to_transcript(&mut expected); + } + + assert_eq!(actual.state(), expected.state()); + assert_eq!(actual.challenge(), expected.challenge()); +} + +#[test] +fn committed_proof_checks_rounds_then_output_claims() { + let proof = CommittedSumcheckProof { + rounds: vec![ + CommittedRound { + commitment: F::from_u64(11), + degree: 1, + }, + CommittedRound { + commitment: F::from_u64(12), + degree: 2, + }, + ], + output_claims: CommittedOutputClaims { + commitments: vec![F::from_u64(21), F::from_u64(34)], + }, + }; + + let mut manual = Blake2bTranscript::::new(b"committed-proof"); + let mut expected_challenges = Vec::new(); + for round in &proof.rounds { + manual.append(&Label(b"sumcheck_commitment")); + round.commitment.append_to_transcript(&mut manual); + expected_challenges.push(manual.challenge()); + } + manual.append(&LabelWithCount(b"output_claims_coms", 2)); + for commitment in &proof.output_claims.commitments { + commitment.append_to_transcript(&mut manual); + } + + let mut verifier = Blake2bTranscript::::new(b"committed-proof"); + let consistency = proof + .verify_committed_consistency(SumcheckStatement::new(2, 2), &mut verifier) + .unwrap(); + + assert_eq!(consistency.challenges(), expected_challenges); + assert_eq!(consistency.round_degrees(), vec![1, 2]); + assert_eq!(verifier.state(), manual.state()); +} + +#[test] +fn committed_proof_rejects_bad_round_before_output_claims() { + let proof = CommittedSumcheckProof { + rounds: vec![CommittedRound { + commitment: F::from_u64(11), + degree: 3, + }], + output_claims: CommittedOutputClaims { + commitments: vec![F::from_u64(21)], + }, + }; + let mut transcript = Blake2bTranscript::::new(b"committed-proof"); + let before = *transcript.state(); + + let result = proof.verify_committed_consistency(SumcheckStatement::new(1, 2), &mut transcript); + + assert!(matches!( + result, + Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) + )); + assert_eq!(*transcript.state(), before); +} + +#[test] +fn committed_round_witness_commits_with_generic_vector_commitment() { + type VC = Pedersen; + + let generator = Bn254::g1_generator(); + let setup = PedersenSetup::new( + vec![ + generator, + generator.scalar_mul(&F::from_u64(2)), + generator.scalar_mul(&F::from_u64(3)), + ], + generator.scalar_mul(&F::from_u64(99)), + ); + let witness = CommittedRoundWitness { + coefficients: vec![F::from_u64(4), F::from_u64(5), F::from_u64(6)], + blinding: F::from_u64(7), + }; + + let round = witness.commit::(&setup).unwrap(); + + assert_eq!(round.degree, 2); + assert!(VC::verify( + &setup, + &round.commitment, + &witness.coefficients, + &witness.blinding + )); + assert!(!VC::verify( + &setup, + &round.commitment, + &witness.coefficients, + &(witness.blinding + F::from_u64(1)) + )); +} + +#[test] +fn committed_round_witness_rejects_empty_coefficients() { + type VC = Pedersen; + + let generator = Bn254::g1_generator(); + let setup = PedersenSetup::new(vec![generator], generator.scalar_mul(&F::from_u64(99))); + let witness = CommittedRoundWitness { + coefficients: Vec::new(), + blinding: F::from_u64(7), + }; + + let result = witness.commit::(&setup); + + assert!(matches!(result, Err(SumcheckError::EmptyRoundCoefficients))); +} + +/// A mock clear round that returns a fixed evaluation. +/// Used to verify that `SumcheckVerifier` accepts custom clear round messages. +struct MockClearRound { fixed_sum: F, } -impl RoundProof for MockRoundProof { +impl RoundMessage for MockClearRound { fn degree(&self) -> usize { 0 } - fn check_sum(&self, _running_sum: F, _round: usize) -> Result<(), SumcheckError> { - Ok(()) + fn append_to_transcript(&self, transcript: &mut T) { + F::from_u64(42).append_to_transcript(transcript); } +} +impl ClearRound for MockClearRound { fn evaluate(&self, _challenge: F) -> F { self.fixed_sum } - fn append_to_transcript(&self, transcript: &mut impl Transcript) { - // Absorb a deterministic byte so challenges are well-defined. - F::from_u64(42).append_to_transcript(transcript); + fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { + self.fixed_sum * coefficients[0] } } #[test] -fn verify_dispatches_through_round_verifier_trait() { - let fixed = F::from_u64(99); +fn verify_accepts_custom_clear_round_messages() { + let fixed = F::from_u64(0); let round_proofs = [ - MockRoundProof { fixed_sum: fixed }, - MockRoundProof { fixed_sum: fixed }, - MockRoundProof { fixed_sum: fixed }, + MockClearRound { fixed_sum: fixed }, + MockClearRound { fixed_sum: fixed }, + MockClearRound { fixed_sum: fixed }, ]; let claim = SumcheckClaim { num_vars: 3, degree: 1, - claimed_sum: F::from_u64(0), // doesn't matter — mock always accepts + claimed_sum: F::from_u64(0), }; let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &round_proofs, &mut vt); + let result = SumcheckVerifier::verify(&claim, &round_proofs, BooleanHypercube, &mut vt); assert!( result.is_ok(), "mock verifier should accept: {:?}", @@ -554,3 +1414,9 @@ fn verify_dispatches_through_round_verifier_trait() { fn sumcheck_claim_new_rejects_degree_zero() { let _ = SumcheckClaim::::new(3, 0, Fr::from_u64(0)); } + +#[test] +#[should_panic(expected = "degree >= 1")] +fn sumcheck_statement_new_rejects_degree_zero() { + let _ = SumcheckStatement::new(3, 0); +} diff --git a/crates/jolt-sumcheck/src/verifier.rs b/crates/jolt-sumcheck/src/verifier.rs index ae5a1bb3d5..c2e340546c 100644 --- a/crates/jolt-sumcheck/src/verifier.rs +++ b/crates/jolt-sumcheck/src/verifier.rs @@ -1,17 +1,20 @@ //! Sumcheck verifier: checks round polynomials against the claimed sum. -use jolt_transcript::Transcript; +use jolt_field::Field; +use jolt_poly::UnivariatePolynomial; +use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; -use crate::claim::{EvaluationClaim, SumcheckClaim}; +use crate::claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; +use crate::committed::{ + CommittedRound, CommittedSumcheckConsistency, CommittedSumcheckProof, VerifiedCommittedRound, +}; +use crate::domain::{BooleanHypercube, SumcheckDomain}; use crate::error::SumcheckError; -use crate::round_proof::RoundProof; +use crate::proof::CompressedSumcheckProof; +use crate::round_proof::{ClearRound, RoundMessage}; use crate::scalar::SumcheckScalar; /// Stateless sumcheck verifier engine. -/// -/// Replays the Fiat-Shamir transcript and checks each round against -/// the running sum, ultimately producing the final evaluation point -/// and expected value for an oracle query. pub struct SumcheckVerifier; impl SumcheckVerifier { @@ -19,12 +22,10 @@ impl SumcheckVerifier { /// /// For each round $i = 0, \ldots, n-1$: /// 1. The degree bound is enforced against `claim.degree`. - /// 2. The round proof's [`RoundProof::check_sum`] is invoked against the - /// running sum (clear-mode impls verify `s_i(0) + s_i(1) == running_sum`; - /// committed-mode impls defer to BlindFold). - /// 3. The round proof absorbs its payload into the transcript. + /// 2. The round sum is checked against the running sum in `domain`. + /// 3. The round message is absorbed into the transcript. /// 4. A challenge $r_i$ is squeezed from the transcript. - /// 5. The running sum is updated to [`RoundProof::evaluate`] at $r_i$. + /// 5. The running sum is updated to the round polynomial at $r_i$. /// /// On success, returns an [`EvaluationClaim`] `{ point: r, value: v }` /// where `v` is the final evaluation and `r = (r_1, ..., r_n)` is the @@ -39,20 +40,22 @@ impl SumcheckVerifier { /// /// When `claim.num_vars == 0`, this function performs no transcript /// interaction and no checks: it returns - /// `EvaluationClaim { point: vec![], value: claim.claimed_sum }`. + /// `EvaluationClaim { point: Point::default(), value: claim.claimed_sum }`. /// Sumcheck trivially reduces to a single oracle query at that point, /// so the caller MUST verify `claim.claimed_sum` against the /// commitment/oracle layer to retain soundness. #[tracing::instrument(skip_all, name = "SumcheckVerifier::verify")] - pub fn verify( + pub fn verify( claim: &SumcheckClaim, - round_proofs: &[P], + round_proofs: &[R], + domain: D, transcript: &mut T, ) -> Result, SumcheckError> where F: SumcheckScalar, T: Transcript, - P: RoundProof, + R: ClearRound, + D: SumcheckDomain, { if round_proofs.len() != claim.num_vars { return Err(SumcheckError::WrongNumberOfRounds { @@ -71,16 +74,145 @@ impl SumcheckVerifier { max: claim.degree, }); } - round_proof.check_sum(running_sum, round)?; + domain.check_round_sum(round, running_sum, round_proof)?; round_proof.append_to_transcript(transcript); let r: F = transcript.challenge(); running_sum = round_proof.evaluate(r); challenges.push(r); } - Ok(EvaluationClaim { - point: challenges, - value: running_sum, - }) + Ok(EvaluationClaim::new(challenges, running_sum)) + } + + #[tracing::instrument(skip_all, name = "SumcheckVerifier::verify_compressed")] + pub fn verify_compressed( + claim: &SumcheckClaim, + proof: &CompressedSumcheckProof, + domain: BooleanHypercube, + round_label: &'static [u8], + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: Field, + T: Transcript, + { + if proof.round_polynomials.len() != claim.num_vars { + return Err(SumcheckError::WrongNumberOfRounds { + expected: claim.num_vars, + got: proof.round_polynomials.len(), + }); + } + let BooleanHypercube = domain; + + let mut running_sum = claim.claimed_sum; + let mut challenges = Vec::with_capacity(claim.num_vars); + + for (round, round_proof) in proof.round_polynomials.iter().enumerate() { + if round_proof.degree() > claim.degree { + return Err(SumcheckError::DegreeBoundExceeded { + got: round_proof.degree(), + max: claim.degree, + }); + } + let coeffs = round_proof.coeffs_except_linear_term(); + if coeffs.is_empty() { + return Err(SumcheckError::CompressedPolynomialTooShort { round, got: 0 }); + } + + transcript.append(&LabelWithCount(round_label, coeffs.len() as u64)); + for coeff in coeffs { + coeff.append_to_transcript(transcript); + } + let r: F = transcript.challenge(); + running_sum = round_proof.evaluate_with_hint(running_sum, r); + challenges.push(r); + } + + Ok(EvaluationClaim::new(challenges, running_sum)) + } + + /// Checks committed sumcheck rounds and returns the transcript-derived data. + #[tracing::instrument( + skip_all, + name = "SumcheckVerifier::verify_committed_round_consistency" + )] + pub fn verify_committed_round_consistency( + statement: SumcheckStatement, + round_proofs: &[CommittedRound], + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: SumcheckScalar, + T: Transcript, + C: Clone + AppendToTranscript, + { + if round_proofs.len() != statement.num_vars { + return Err(SumcheckError::WrongNumberOfRounds { + expected: statement.num_vars, + got: round_proofs.len(), + }); + } + + let mut rounds = Vec::with_capacity(statement.num_vars); + for round_proof in round_proofs { + if round_proof.degree() > statement.degree { + return Err(SumcheckError::DegreeBoundExceeded { + got: round_proof.degree(), + max: statement.degree, + }); + } + + round_proof.append_to_transcript(transcript); + rounds.push(VerifiedCommittedRound { + commitment: round_proof.commitment.clone(), + degree: round_proof.degree, + challenge: transcript.challenge(), + }); + } + + Ok(CommittedSumcheckConsistency { rounds }) + } +} + +impl CompressedSumcheckProof +where + F: Field, +{ + pub fn verify( + &self, + claim: &SumcheckClaim, + domain: BooleanHypercube, + round_label: &'static [u8], + transcript: &mut T, + ) -> Result, SumcheckError> + where + T: Transcript, + { + SumcheckVerifier::verify_compressed(claim, self, domain, round_label, transcript) + } +} + +impl CommittedSumcheckProof { + /// Checks committed-proof consistency through the Fiat-Shamir transcript. + /// + /// This checks the round count and degree bounds, derives the round challenges, + /// and absorbs the committed output claims after the round transcript. + pub fn verify_committed_consistency( + &self, + statement: SumcheckStatement, + transcript: &mut T, + ) -> Result, SumcheckError> + where + F: SumcheckScalar, + T: Transcript, + C: Clone + AppendToTranscript, + { + let consistency = SumcheckVerifier::verify_committed_round_consistency( + statement, + &self.rounds, + transcript, + )?; + self.output_claims.append_to_transcript(transcript); + Ok(consistency) } } diff --git a/crates/jolt-sumcheck/tests/committed.rs b/crates/jolt-sumcheck/tests/committed.rs new file mode 100644 index 0000000000..af24a1e167 --- /dev/null +++ b/crates/jolt-sumcheck/tests/committed.rs @@ -0,0 +1,174 @@ +#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] + +use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; +use jolt_field::{Fr, FromPrimitiveInt}; +use jolt_sumcheck::round_proof::RoundMessage; +use jolt_sumcheck::{ + CommittedOutputClaims, CommittedRound, CommittedRoundWitness, SumcheckError, SumcheckStatement, + SumcheckVerifier, +}; +use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; + +type F = Fr; +type VC = Pedersen; + +fn pedersen_setup(capacity: usize) -> PedersenSetup { + let generator = Bn254::g1_generator(); + let message_generators = (1..=capacity) + .map(|i| generator.scalar_mul(&F::from_u64(i as u64))) + .collect(); + PedersenSetup::new(message_generators, generator.scalar_mul(&F::from_u64(99))) +} + +fn committed_rounds( + setup: &PedersenSetup, + coefficients: &[Vec], +) -> Vec> { + coefficients + .iter() + .enumerate() + .map(|(round, coefficients)| { + CommittedRoundWitness { + coefficients: coefficients.clone(), + blinding: F::from_u64(round as u64 + 17), + } + .commit::(setup) + .unwrap() + }) + .collect() +} + +#[test] +fn committed_rounds_complete_with_pedersen_commitments() { + let setup = pedersen_setup(3); + let rounds = committed_rounds( + &setup, + &[ + vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], + vec![F::from_u64(7), F::from_u64(11)], + vec![F::from_u64(13), F::from_u64(17), F::from_u64(19)], + ], + ); + + let mut prover_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let mut expected_challenges = Vec::new(); + for round in &rounds { + round.append_to_transcript(&mut prover_transcript); + expected_challenges.push(prover_transcript.challenge()); + } + + let mut verifier_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let consistency = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(rounds.len(), 2), + &rounds, + &mut verifier_transcript, + ) + .unwrap(); + + assert_eq!(consistency.challenges(), expected_challenges); + assert_eq!(consistency.round_degrees(), vec![2, 1, 2]); + assert_eq!(verifier_transcript.state(), prover_transcript.state()); +} + +#[test] +fn committed_rounds_reject_wrong_count_and_degree() { + let setup = pedersen_setup(3); + let rounds = committed_rounds( + &setup, + &[ + vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], + vec![F::from_u64(7), F::from_u64(11)], + ], + ); + + let mut wrong_count_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let wrong_count = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(3, 2), + &rounds, + &mut wrong_count_transcript, + ); + assert!(matches!( + wrong_count, + Err(SumcheckError::WrongNumberOfRounds { + expected: 3, + got: 2 + }) + )); + + let mut degree_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let degree = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(2, 1), + &rounds, + &mut degree_transcript, + ); + assert!(matches!( + degree, + Err(SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }) + )); +} + +#[test] +fn tampered_committed_round_changes_challenges() { + let setup = pedersen_setup(3); + let rounds = committed_rounds( + &setup, + &[ + vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], + vec![F::from_u64(7), F::from_u64(11), F::from_u64(13)], + ], + ); + let mut tampered = rounds.clone(); + tampered[1] = committed_rounds( + &setup, + &[vec![F::from_u64(101), F::from_u64(103), F::from_u64(107)]], + ) + .remove(0); + + let mut original_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let original = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(2, 2), + &rounds, + &mut original_transcript, + ) + .unwrap(); + + let mut tampered_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); + let tampered = SumcheckVerifier::verify_committed_round_consistency( + SumcheckStatement::new(2, 2), + &tampered, + &mut tampered_transcript, + ) + .unwrap(); + + assert_ne!(tampered.challenges(), original.challenges()); + assert_ne!(tampered_transcript.state(), original_transcript.state()); +} + +#[test] +fn committed_output_claims_keep_length_and_order() { + let setup = pedersen_setup(2); + let rounds = committed_rounds( + &setup, + &[ + vec![F::from_u64(2), F::from_u64(3)], + vec![F::from_u64(5), F::from_u64(7)], + ], + ); + let output_claims = CommittedOutputClaims { + commitments: rounds + .iter() + .map(|round| round.commitment) + .collect::>(), + }; + + let mut actual = Blake2bTranscript::::new(b"committed-output"); + output_claims.append_to_transcript(&mut actual); + + let mut expected = Blake2bTranscript::::new(b"committed-output"); + expected.append(&LabelWithCount(b"output_claims_coms", 2)); + for commitment in &output_claims.commitments { + commitment.append_to_transcript(&mut expected); + } + + assert_eq!(actual.state(), expected.state()); +} diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index 4a3142f934..5926c27574 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -19,7 +19,9 @@ use jolt_field::{ FixedBytes, FromPrimitiveInt, Invertible, MulPow2, MulPrimitiveInt, NaiveAccumulator, RandomSampling, ReducingBytes, RingCore, TranscriptChallenge, WithAccumulator, }; -use jolt_sumcheck::{EvaluationClaim, RoundProof, SumcheckClaim, SumcheckError, SumcheckVerifier}; +use jolt_sumcheck::{ + BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, +}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, KeccakTranscript, Transcript}; use num_traits::{One, Zero}; @@ -294,35 +296,28 @@ struct LinearRound { coeffs: [Mersenne61; 2], } -impl RoundProof for LinearRound { +impl RoundMessage for LinearRound { fn degree(&self) -> usize { 1 } - fn check_sum( - &self, - running_sum: Mersenne61, - round: usize, - ) -> Result<(), SumcheckError> { - let actual = self.evaluate(Mersenne61::zero()) + self.evaluate(Mersenne61::one()); - if actual == running_sum { - Ok(()) - } else { - Err(SumcheckError::RoundCheckFailed { - round, - expected: running_sum, - actual, - }) - } + fn append_to_transcript(&self, transcript: &mut T) { + self.coeffs[0].append_to_transcript(transcript); + self.coeffs[1].append_to_transcript(transcript); } +} +impl ClearRound for LinearRound { fn evaluate(&self, challenge: Mersenne61) -> Mersenne61 { self.coeffs[0] + self.coeffs[1] * challenge } - fn append_to_transcript(&self, transcript: &mut impl Transcript) { - self.coeffs[0].append_to_transcript(transcript); - self.coeffs[1].append_to_transcript(transcript); + fn coefficient_linear_combination(&self, coefficients: &[Mersenne61]) -> Mersenne61 { + self.coeffs + .iter() + .zip(coefficients) + .map(|(&coefficient, &scale)| coefficient * scale) + .sum() } } @@ -350,10 +345,7 @@ fn build_rounds() -> ( ( SumcheckClaim::new(4, 1, Mersenne61::from_u64(10)), rounds, - EvaluationClaim { - point, - value: running_sum, - }, + EvaluationClaim::new(point, running_sum), ) } @@ -372,6 +364,8 @@ fn hash_transcripts_accept_mersenne61_without_bn254_field_surface() { fn sumcheck_verifier_accepts_mersenne61_round_proof() { let (claim, rounds, expected) = build_rounds(); let mut verifier_transcript = Blake2bTranscript::::new(b"mersenne61"); - let actual = SumcheckVerifier::verify(&claim, &rounds, &mut verifier_transcript).unwrap(); + let actual = + SumcheckVerifier::verify(&claim, &rounds, BooleanHypercube, &mut verifier_transcript) + .unwrap(); assert_eq!(actual, expected); } diff --git a/crates/jolt-sumcheck/tests/roundtrip.rs b/crates/jolt-sumcheck/tests/roundtrip.rs index 06199c9b4b..19f95f069d 100644 --- a/crates/jolt-sumcheck/tests/roundtrip.rs +++ b/crates/jolt-sumcheck/tests/roundtrip.rs @@ -9,9 +9,11 @@ use jolt_field::{Fr, FromPrimitiveInt, MulPow2}; use jolt_poly::{Polynomial, UnivariatePoly}; use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; -use jolt_sumcheck::proof::SumcheckProof; -use jolt_sumcheck::round_proof::{CompressedLabeledRoundPoly, LabeledRoundPoly, RoundProof}; -use jolt_sumcheck::{BatchedSumcheckVerifier, SumcheckVerifier}; +use jolt_sumcheck::proof::ClearSumcheckProof; +use jolt_sumcheck::round_proof::{CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; +use jolt_sumcheck::{ + BatchedSumcheckVerifier, BooleanHypercube, SumcheckVerifier, SUMCHECK_ROUND_TRANSCRIPT_LABEL, +}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; type F = Fr; @@ -27,7 +29,7 @@ fn prove_product( polys: &[Vec], num_vars: usize, transcript: &mut Blake2bTranscript, -) -> (SumcheckProof, F) { +) -> (ClearSumcheckProof, F) { let degree = polys.len(); let n = 1 << num_vars; assert!(polys.iter().all(|p| p.len() == n)); @@ -73,7 +75,7 @@ fn prove_product( let round_poly = UnivariatePoly::interpolate(&points); // Absorb through the same path the unlabelled verifier uses. - as RoundProof>::append_to_transcript(&round_poly, transcript); + as RoundMessage>::append_to_transcript(&round_poly, transcript); let r: F = transcript.challenge(); round_polys.push(round_poly); @@ -88,7 +90,7 @@ fn prove_product( } ( - SumcheckProof { + ClearSumcheckProof { round_polynomials: round_polys, }, claimed_sum, @@ -114,7 +116,8 @@ fn degree2_product_roundtrip() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_ok(), "degree-2 verify failed: {:?}", result.err()); } @@ -138,7 +141,8 @@ fn degree3_product_roundtrip() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_ok(), "degree-3 verify failed: {:?}", result.err()); } @@ -169,7 +173,8 @@ fn degree3_final_eval_correct() { let EvaluationClaim { point: challenges, value: final_eval, - } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt).unwrap(); + } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) + .unwrap(); // f(r) * g(r) * h(r) should equal the final_eval let f_at_r = Polynomial::new(f_evals).evaluate_and_consume(&challenges); @@ -212,7 +217,8 @@ fn eq_weighted_sumcheck() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!( result.is_ok(), "eq-weighted verify failed: {:?}", @@ -296,7 +302,7 @@ fn batched_heterogeneous_degrees() { .collect(); let round_poly = UnivariatePoly::interpolate(&points); - as RoundProof>::append_to_transcript(&round_poly, &mut pt); + as RoundMessage>::append_to_transcript(&round_poly, &mut pt); let r: F = pt.challenge(); round_polys.push(round_poly); @@ -306,12 +312,17 @@ fn batched_heterogeneous_degrees() { buf.truncate(half); } - let proof = SumcheckProof { + let proof = ClearSumcheckProof { round_polynomials: round_polys, }; let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = BatchedSumcheckVerifier::verify(&claims, &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &claims, + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!( result.is_ok(), "batched heterogeneous verify failed: {:?}", @@ -338,7 +349,8 @@ fn large_num_vars_roundtrip() { }; let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_ok(), "large roundtrip failed: {:?}", result.err()); } @@ -349,7 +361,7 @@ fn compressed_round_verifier_roundtrip() { // single source of truth for the compressed wire format. let num_vars = 3; let n = 1 << num_vars; - let label: &[u8; 13] = b"sumcheck_poly"; + let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; let degree = 2; let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); @@ -387,7 +399,7 @@ fn compressed_round_verifier_roundtrip() { let round_poly = UnivariatePoly::interpolate(&points); let compressed = CompressedLabeledRoundPoly::new(&round_poly, label); - as RoundProof>::append_to_transcript( + as RoundMessage>::append_to_transcript( &compressed, &mut pt, ); @@ -403,7 +415,7 @@ fn compressed_round_verifier_roundtrip() { } } - let proof = SumcheckProof { + let proof = ClearSumcheckProof { round_polynomials: round_polys, }; let claim = SumcheckClaim { @@ -419,7 +431,7 @@ fn compressed_round_verifier_roundtrip() { .collect(); let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &wrapped, &mut vt); + let result = SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut vt); assert!( result.is_ok(), "compressed round verifier roundtrip failed: {:?}", @@ -436,7 +448,7 @@ fn labeled_round_verifier_roundtrip() { let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); let g: Vec = (0..n).map(|i| F::from_u64((i + 5) as u64)).collect(); - let label: &[u8; 13] = b"sumcheck_poly"; + let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; // Prove with labeled absorption let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); @@ -472,7 +484,7 @@ fn labeled_round_verifier_roundtrip() { let round_poly = UnivariatePoly::interpolate(&points); let labeled = LabeledRoundPoly::new(&round_poly, label); - as RoundProof>::append_to_transcript(&labeled, &mut pt); + as RoundMessage>::append_to_transcript(&labeled, &mut pt); let r: F = pt.challenge(); round_polys.push(round_poly); @@ -485,7 +497,7 @@ fn labeled_round_verifier_roundtrip() { } } - let proof = SumcheckProof { + let proof = ClearSumcheckProof { round_polynomials: round_polys, }; @@ -502,7 +514,7 @@ fn labeled_round_verifier_roundtrip() { .collect(); let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &wrapped, &mut vt); + let result = SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut vt); assert!( result.is_ok(), "labeled round verifier roundtrip failed: {:?}", diff --git a/crates/jolt-sumcheck/tests/soundness.rs b/crates/jolt-sumcheck/tests/soundness.rs index b4a78fb8ce..a81574e5e4 100644 --- a/crates/jolt-sumcheck/tests/soundness.rs +++ b/crates/jolt-sumcheck/tests/soundness.rs @@ -10,9 +10,9 @@ use jolt_field::{Fr, FromPrimitiveInt}; use jolt_poly::{Polynomial, UnivariatePoly}; use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; use jolt_sumcheck::error::SumcheckError; -use jolt_sumcheck::proof::SumcheckProof; -use jolt_sumcheck::round_proof::RoundProof; -use jolt_sumcheck::{BatchedSumcheckVerifier, SumcheckVerifier}; +use jolt_sumcheck::proof::ClearSumcheckProof; +use jolt_sumcheck::round_proof::RoundMessage; +use jolt_sumcheck::{BatchedSumcheckVerifier, BooleanHypercube, SumcheckVerifier}; use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; type F = Fr; @@ -42,7 +42,7 @@ fn honest_prove( evals: &[F], num_vars: usize, transcript: &mut Blake2bTranscript, -) -> SumcheckProof { +) -> ClearSumcheckProof { let mut buf = evals.to_vec(); let mut round_polys = Vec::with_capacity(num_vars); @@ -55,7 +55,7 @@ fn honest_prove( eval_1 += buf[i + half]; } let round_poly = UnivariatePoly::new(vec![eval_0, eval_1 - eval_0]); - as RoundProof>::append_to_transcript(&round_poly, transcript); + as RoundMessage>::append_to_transcript(&round_poly, transcript); let r: F = transcript.challenge(); round_polys.push(round_poly); for i in 0..half { @@ -64,7 +64,7 @@ fn honest_prove( buf.truncate(half); } - SumcheckProof { + ClearSumcheckProof { round_polynomials: round_polys, } } @@ -80,21 +80,26 @@ fn compute_sum(evals: &[F]) -> F { /// but the final evaluation doesn't match the intended polynomial. fn verify_with_oracle_check( claim: &SumcheckClaim, - proof: &SumcheckProof, + proof: &ClearSumcheckProof, intended_evals: &[F], ) -> Result, OracleCheckError> { let mut transcript = new_transcript(); let EvaluationClaim { point: challenges, value: final_eval, - } = SumcheckVerifier::verify(claim, &proof.round_polynomials, &mut transcript)?; + } = SumcheckVerifier::verify( + claim, + &proof.round_polynomials, + BooleanHypercube, + &mut transcript, + )?; let expected = Polynomial::new(intended_evals.to_vec()).evaluate_and_consume(&challenges); if final_eval != expected { return Err(OracleCheckError::FinalEvalMismatch); } - Ok(challenges) + Ok(challenges.into_vec()) } #[test] @@ -140,7 +145,8 @@ fn wrong_polynomial_same_sum_fails_oracle_check() { // Round checks pass (proof is internally consistent for g) let mut vt = new_transcript(); - let round_result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let round_result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!( round_result.is_ok(), "round checks should pass for honest g proof" @@ -178,7 +184,8 @@ fn proof_for_different_polynomial_different_sum_fails_round_check() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(matches!( result, Err(SumcheckError::RoundCheckFailed { round: 0, .. }) @@ -204,7 +211,8 @@ fn corrupted_middle_round_detected() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); // Corruption at round 2 may be detected at round 2 (wrong sum) or later // (transcript desync from corrupted absorption). Either way, it must fail. @@ -229,7 +237,8 @@ fn corrupted_last_round_detected() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(result.is_err(), "corrupted last round must be rejected"); } @@ -253,7 +262,8 @@ fn swapped_round_order_rejected() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); // Round 0 now has the wrong s(0)+s(1) (it was computed for a different running sum). // Even if by accident s(0)+s(1) matched, the transcript would desync. @@ -271,7 +281,7 @@ fn replayed_round_polynomial_rejected() { let mut pt = new_transcript(); let proof = honest_prove(&evals, 3, &mut pt); - let replayed = SumcheckProof { + let replayed = ClearSumcheckProof { round_polynomials: vec![proof.round_polynomials[0].clone(); 3], }; @@ -282,7 +292,12 @@ fn replayed_round_polynomial_rejected() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &replayed.round_polynomials, &mut vt); + let result = SumcheckVerifier::verify( + &claim, + &replayed.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!(result.is_err(), "replayed rounds must be rejected"); } @@ -295,7 +310,7 @@ fn all_zero_round_polynomials_rejected_for_nonzero_sum() { assert_ne!(sum, F::from_u64(0)); let zero_poly = UnivariatePoly::new(vec![F::from_u64(0)]); - let proof = SumcheckProof { + let proof = ClearSumcheckProof { round_polynomials: vec![zero_poly; 2], }; @@ -306,7 +321,8 @@ fn all_zero_round_polynomials_rejected_for_nonzero_sum() { }; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(matches!( result, Err(SumcheckError::RoundCheckFailed { round: 0, .. }) @@ -358,7 +374,8 @@ fn verifier_transcript_desync_rejected() { let mut vt = new_transcript(); F::from_u64(0xdead).append_to_transcript(&mut vt); - let result = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt); + let result = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); // Round 0's s(0)+s(1) check passes (it doesn't depend on challenges), // but the challenge r_0 will differ, so round 1's running sum will be wrong. @@ -380,7 +397,7 @@ fn num_vars_zero_accepts_any_claimed_sum() { let round_proofs: &[UnivariatePoly] = &[]; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, round_proofs, &mut vt); + let result = SumcheckVerifier::verify(&claim, round_proofs, BooleanHypercube, &mut vt); assert!(result.is_ok()); let EvaluationClaim { @@ -405,7 +422,7 @@ fn num_vars_zero_no_oracle_check_possible() { let round_proofs: &[UnivariatePoly] = &[]; let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, round_proofs, &mut vt); + let result = SumcheckVerifier::verify(&claim, round_proofs, BooleanHypercube, &mut vt); // Passes — the verifier has nothing to check! // Only the oracle check (comparing 999 against the actual constant) catches this. @@ -435,9 +452,10 @@ fn constant_polynomial_all_same_evals() { // The final eval should be 7 regardless of the challenge point, // since f is constant. let mut vt = new_transcript(); - let final_eval = SumcheckVerifier::verify(&claim, &proof.round_polynomials, &mut vt) - .unwrap() - .value; + let final_eval = + SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) + .unwrap() + .value; assert_eq!(final_eval, F::from_u64(7)); } @@ -478,7 +496,12 @@ fn batched_one_dishonest_claim_rejected() { ]; let mut vt = new_transcript(); - let result = BatchedSumcheckVerifier::verify(&claims, &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &claims, + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!(result.is_err(), "dishonest claim in batch must be rejected"); } @@ -520,7 +543,11 @@ fn batched_swapped_claim_order_rejected() { ]; let mut vt = new_transcript(); - let result = - BatchedSumcheckVerifier::verify(&claims_swapped, &proof.round_polynomials, &mut vt); + let result = BatchedSumcheckVerifier::verify( + &claims_swapped, + &proof.round_polynomials, + BooleanHypercube, + &mut vt, + ); assert!(result.is_err(), "swapped claim order must be rejected"); } diff --git a/crates/jolt-transcript/src/digest.rs b/crates/jolt-transcript/src/digest.rs index 67e9fef4c2..c952f2f9ac 100644 --- a/crates/jolt-transcript/src/digest.rs +++ b/crates/jolt-transcript/src/digest.rs @@ -19,6 +19,11 @@ struct TestState { /// /// Generic over the hash function `D` and field type `F`. Challenges are /// produced as field elements through `F::from_challenge_bytes`. +/// +/// The byte layout intentionally matches `jolt-core`'s hash transcripts: +/// appends hash `state || round || payload`, squeezes hash `state || round`, +/// and standard challenges use the same 125-bit Montgomery-friendly decoding +/// path as `jolt-core`. pub struct DigestTranscript + 'static, F> { state: [u8; 32], n_rounds: u32, @@ -97,11 +102,7 @@ where #[inline] fn challenge_bytes32(&mut self, out: &mut [u8; 32]) { - let hash: [u8; 32] = self - .hasher() - .chain_update([0x01]) // squeeze domain tag - .finalize() - .into(); + let hash: [u8; 32] = self.hasher().finalize().into(); out.copy_from_slice(&hash); self.update_state(hash); } @@ -155,12 +156,7 @@ where } fn append_bytes(&mut self, bytes: &[u8]) { - let hash: [u8; 32] = self - .hasher() - .chain_update([0x00]) // absorb domain tag - .chain_update(bytes) - .finalize() - .into(); + let hash: [u8; 32] = self.hasher().chain_update(bytes).finalize().into(); self.update_state(hash); } @@ -170,6 +166,12 @@ where F::from_challenge_bytes(&buf) } + fn challenge_scalar(&mut self) -> F { + let mut buf = [0u8; 32]; + self.challenge_bytes(&mut buf); + F::from_scalar_challenge_bytes(&buf) + } + #[inline] fn state(&self) -> &[u8; 32] { &self.state @@ -180,3 +182,36 @@ where self.test_state.expected_state_history = Some(other.test_state.state_history.clone()); } } + +#[cfg(test)] +mod tests { + use blake2::{digest::consts::U32, Blake2b}; + use jolt_field::TranscriptChallenge; + + use super::DigestTranscript; + use crate::Transcript; + + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] + struct ChallengeByteLen(usize); + + impl TranscriptChallenge for ChallengeByteLen { + fn from_challenge_bytes(bytes: &[u8]) -> Self { + Self(bytes.len()) + } + + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self(bytes.len()) + } + } + + type TestTranscript = DigestTranscript, ChallengeByteLen>; + + #[test] + fn scalar_challenge_uses_full_digest_width() { + let mut transcript = TestTranscript::new(b"challenge-width"); + assert_eq!(transcript.challenge(), ChallengeByteLen(16)); + + let mut transcript = TestTranscript::new(b"challenge-width"); + assert_eq!(transcript.challenge_scalar(), ChallengeByteLen(32)); + } +} diff --git a/crates/jolt-transcript/src/transcript.rs b/crates/jolt-transcript/src/transcript.rs index cc4d9580cd..49eefffa42 100644 --- a/crates/jolt-transcript/src/transcript.rs +++ b/crates/jolt-transcript/src/transcript.rs @@ -3,6 +3,9 @@ //! This module provides the [`Transcript`] trait for building Fiat-Shamir transcripts //! and the [`AppendToTranscript`] trait for types that can be absorbed into a transcript. +use crate::domain::Label; +use jolt_field::{Field, FromPrimitiveInt}; + /// Fiat-Shamir transcript for non-interactive proofs. /// /// A transcript absorbs data and produces deterministic challenges. Both prover @@ -44,18 +47,48 @@ pub trait Transcript: Default + Clone + Sync + Send + 'static { value.append_to_transcript(self); } + /// Absorbs a domain label followed by a value. + /// + /// Jolt's core proof transcript commonly absorbs scalar payloads as + /// `label || payload`; this method makes that pattern explicit at the + /// transcript API boundary. + fn append_labeled(&mut self, label: &'static [u8], value: &A) { + self.append(&Label(label)); + self.append(value); + } + /// Squeezes a challenge from the transcript. /// /// Each call produces a new challenge and advances the transcript state. #[must_use] fn challenge(&mut self) -> Self::Challenge; + /// Squeezes a non-optimized scalar challenge from the transcript. + #[must_use] + fn challenge_scalar(&mut self) -> Self::Challenge { + self.challenge() + } + /// Squeezes multiple challenges from the transcript. #[must_use] fn challenge_vector(&mut self, len: usize) -> Vec { (0..len).map(|_| self.challenge()).collect() } + /// Squeezes one scalar challenge and returns its powers `[1, gamma, gamma^2, ...]`. + #[must_use] + fn challenge_scalar_powers(&mut self, len: usize) -> Vec + where + Self::Challenge: Field, + { + let gamma = self.challenge_scalar(); + let mut powers = vec![Self::Challenge::from_u64(1); len]; + for index in 1..len { + powers[index] = powers[index - 1] * gamma; + } + powers + } + /// Returns the current 256-bit transcript state. /// /// Useful for debugging and testing transcript synchronization. @@ -78,4 +111,13 @@ pub const MAX_LABEL_LEN: usize = 32; pub trait AppendToTranscript { /// Absorbs this value into the transcript. fn append_to_transcript(&self, transcript: &mut T); + + /// Byte length of the payload absorbed by [`append_to_transcript`]. + /// + /// Types that need to match `jolt-core`'s variable-length labeled + /// transcript methods should override this so callers can prepend the same + /// packed label/length word before absorbing the payload. + fn transcript_payload_len(&self) -> Option { + None + } } diff --git a/examples/hash-bench/guest/Cargo.toml b/examples/hash-bench/guest/Cargo.toml index 877e2109d6..23773a1e97 100644 --- a/examples/hash-bench/guest/Cargo.toml +++ b/examples/hash-bench/guest/Cargo.toml @@ -16,4 +16,4 @@ jolt-inlines-blake3.workspace = true sha2 = { version = "0.11.0", default-features = false } sha3 = { version = "0.10", default-features = false } blake2 = { version = "0.11.0-rc", default-features = false } -blake3 = { version = "1.6", default-features = false } +blake3 = { version = "1.8", default-features = false } diff --git a/jolt-core/Cargo.toml b/jolt-core/Cargo.toml index 63cf24cdf7..fdf76ce837 100644 --- a/jolt-core/Cargo.toml +++ b/jolt-core/Cargo.toml @@ -84,8 +84,8 @@ derive_more.workspace = true dory.workspace = true allocative.workspace = true inferno = { workspace = true, optional = true } -jolt-program = { workspace = true, features = ["image"] } -jolt-riscv.workspace = true +jolt-program = { workspace = true, features = ["image", "serialization"] } +jolt-riscv = { workspace = true, features = ["serialization"] } jolt-inlines-sha2 = { workspace = true, features = ["host"], optional = true } jolt-inlines-keccak256 = { workspace = true, features = [ "host", diff --git a/jolt-core/src/guest/program.rs b/jolt-core/src/guest/program.rs index a801589ce6..e00203488b 100644 --- a/jolt-core/src/guest/program.rs +++ b/jolt-core/src/guest/program.rs @@ -1,6 +1,6 @@ use common::constants::RAM_START_ADDRESS; use common::jolt_device::{JoltDevice, MemoryConfig}; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::{JoltInstructionRow, RV64IMAC_JOLT, RV64IMAC_JOLT_ALL_INLINES}; use std::path::PathBuf; use tracer::emulator::memory::Memory; use tracer::instruction::Cycle; @@ -48,7 +48,7 @@ impl Program { } /// Decode the ELF file into instructions and memory initialization - pub fn decode(&self) -> (Vec, Vec<(u64, u8)>, u64, u64) { + pub fn decode(&self) -> (Vec, Vec<(u64, u8)>, u64, u64) { decode(&self.elf_contents) } @@ -101,14 +101,22 @@ impl crate::host::JoltProgramSource for Program { } } -pub fn decode(elf: &[u8]) -> (Vec, Vec<(u64, u8)>, u64, u64) { - let image = jolt_program::image::decode_elf(elf).expect("program ELF decoding failed"); +pub fn decode(elf: &[u8]) -> (Vec, Vec<(u64, u8)>, u64, u64) { + let image = + jolt_program::image::decode_elf(elf, RV64IMAC_JOLT).expect("program ELF decoding failed"); let program_size = image.program_end - RAM_START_ADDRESS; let mut inline_provider = tracer::TracerInlineExpansionProvider::new(); let instructions = jolt_program::expand::expand_program_with_provider( - image.instructions, + &image.instructions, &mut inline_provider, + RV64IMAC_JOLT_ALL_INLINES, ) + .map(|instructions| { + instructions + .into_iter() + .map(JoltInstructionRow::from) + .collect() + }) .expect("program bytecode expansion failed"); ( diff --git a/jolt-core/src/host/analyze.rs b/jolt-core/src/host/analyze.rs index a8266683f2..2cfdc476e6 100644 --- a/jolt-core/src/host/analyze.rs +++ b/jolt-core/src/host/analyze.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, fs::File, io, path::PathBuf}; use common::jolt_device::JoltDevice; use jolt_program::execution::TraceRow; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::JoltInstructionRow; use serde::{Deserialize, Serialize}; use crate::field::JoltField; @@ -10,7 +10,7 @@ use crate::field::JoltField; #[derive(Serialize, Deserialize)] pub struct ProgramSummary { pub trace: Vec, - pub bytecode: Vec, + pub bytecode: Vec, pub memory_init: Vec<(u64, u8)>, pub io_device: JoltDevice, } diff --git a/jolt-core/src/host/mod.rs b/jolt-core/src/host/mod.rs index 9a1f15cf23..5c46140f20 100644 --- a/jolt-core/src/host/mod.rs +++ b/jolt-core/src/host/mod.rs @@ -13,7 +13,7 @@ pub trait JoltProgramSource { fn decode( &mut self, ) -> ( - Vec, + Vec, Vec<(u64, u8)>, u64, u64, @@ -35,7 +35,7 @@ impl JoltProgramSource for Program { fn decode( &mut self, ) -> ( - Vec, + Vec, Vec<(u64, u8)>, u64, u64, diff --git a/jolt-core/src/host/program.rs b/jolt-core/src/host/program.rs index 2a083612b9..01f5f6e1dc 100644 --- a/jolt-core/src/host/program.rs +++ b/jolt-core/src/host/program.rs @@ -10,7 +10,7 @@ use common::constants::{ use common::jolt_device::{JoltDevice, MemoryConfig}; use jolt_program::execution::{ExecutionBackend, TraceError, TraceInputs, TraceOutput}; use jolt_program::{JoltProgram, ProgramError}; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::{JoltInstructionRow, RV64IMAC_JOLT, RV64IMAC_JOLT_ALL_INLINES}; use std::fs::File; use std::io; use std::io::{Read, Write}; @@ -269,10 +269,14 @@ impl Program { .get_elf_contents() .expect("ELF contents should be available after building the guest"); let mut inline_provider = tracer::TracerInlineExpansionProvider::new(); - jolt_program::build_jolt_program_with_inline_provider(&elf_contents, &mut inline_provider) + jolt_program::build_jolt_program_with_inline_provider( + &elf_contents, + &mut inline_provider, + RV64IMAC_JOLT_ALL_INLINES, + ) } - pub fn decode(&mut self) -> (Vec, Vec<(u64, u8)>, u64, u64) { + pub fn decode(&mut self) -> (Vec, Vec<(u64, u8)>, u64, u64) { let program = self.jolt_program().expect("failed to build Jolt program"); ( program.expanded_bytecode, @@ -317,8 +321,8 @@ impl Program { File::open(elf).unwrap_or_else(|_| panic!("could not open elf file: {elf:?}")); let mut elf_contents = Vec::new(); elf_file.read_to_end(&mut elf_contents).unwrap(); - let image = - jolt_program::image::decode_elf(&elf_contents).expect("program ELF decoding failed"); + let image = jolt_program::image::decode_elf(&elf_contents, RV64IMAC_JOLT) + .expect("program ELF decoding failed"); let memory_config = self.memory_config_with_program_size(image.program_end - RAM_START_ADDRESS); @@ -348,8 +352,8 @@ impl Program { File::open(elf).unwrap_or_else(|_| panic!("could not open elf file: {elf:?}")); let mut elf_contents = Vec::new(); elf_file.read_to_end(&mut elf_contents).unwrap(); - let image = - jolt_program::image::decode_elf(&elf_contents).expect("program ELF decoding failed"); + let image = jolt_program::image::decode_elf(&elf_contents, RV64IMAC_JOLT) + .expect("program ELF decoding failed"); let memory_config = self.memory_config_with_program_size(image.program_end - RAM_START_ADDRESS); diff --git a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs index 08789aa334..fe08972379 100644 --- a/jolt-core/src/poly/commitment/dory/commitment_scheme.rs +++ b/jolt-core/src/poly/commitment/dory/commitment_scheme.rs @@ -252,6 +252,16 @@ impl CommitmentScheme for DoryCommitmentScheme { let mut dory_transcript = JoltToDoryTranscript::::new(transcript); + #[cfg(not(feature = "zk"))] + if proof.e2.is_some() + || proof.y_com.is_some() + || proof.sigma1_proof.is_some() + || proof.sigma2_proof.is_some() + || proof.scalar_product_proof.is_some() + { + return Err(ProofVerifyError::InvalidOpeningProof); + } + dory::verify::( *commitment, ark_eval, diff --git a/jolt-core/src/poly/opening_proof.rs b/jolt-core/src/poly/opening_proof.rs index caaa32e72a..430de9b9a6 100644 --- a/jolt-core/src/poly/opening_proof.rs +++ b/jolt-core/src/poly/opening_proof.rs @@ -151,7 +151,9 @@ pub enum SumcheckId { RegistersClaimReduction, RegistersReadWriteChecking, RegistersValEvaluation, + BytecodeReadRafAddressPhase, BytecodeReadRaf, + BooleanityAddressPhase, Booleanity, AdviceClaimReductionCyclePhase, AdviceClaimReduction, diff --git a/jolt-core/src/subprotocols/booleanity.rs b/jolt-core/src/subprotocols/booleanity.rs index 192fc39e4e..e6f99dd5f0 100644 --- a/jolt-core/src/subprotocols/booleanity.rs +++ b/jolt-core/src/subprotocols/booleanity.rs @@ -1,12 +1,11 @@ -//! Booleanity Sumcheck +//! Booleanity Sumcheck (split into address/cycle phases) //! -//! This module implements a single booleanity sumcheck that handles all three families: -//! - Instruction RA polynomials -//! - Bytecode RA polynomials -//! - RAM RA polynomials +//! This module implements Stage 6 booleanity as two explicit sumcheck instances: +//! - Address phase (`log_k_chunk` rounds) +//! - Cycle phase (`log_t` rounds) //! -//! By combining them into a single sumcheck, all families share the same `r_address` and `r_cycle`, -//! which is required by the HammingWeightClaimReduction sumcheck in Stage 7. +//! Both phases still batch all three families together (InstructionRA, BytecodeRA, RAMRA), +//! so they share the same `r_address` and `r_cycle`, matching what Stage 7 claim reductions expect. //! //! ## Sumcheck Relation //! @@ -51,7 +50,7 @@ use crate::{ sumcheck_verifier::{SumcheckInstanceParams, SumcheckInstanceVerifier}, }, transcripts::Transcript, - utils::{expanding_table::ExpandingTable, thread::drop_in_background_thread}, + utils::expanding_table::ExpandingTable, zkvm::{ bytecode::BytecodePreprocessing, config::OneHotParams, @@ -84,82 +83,6 @@ pub struct BooleanitySumcheckParams { pub one_hot_params: OneHotParams, } -impl SumcheckInstanceParams for BooleanitySumcheckParams { - fn degree(&self) -> usize { - DEGREE_BOUND - } - - fn num_rounds(&self) -> usize { - self.log_k_chunk + self.log_t - } - - fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { - F::zero() - } - - fn normalize_opening_point( - &self, - sumcheck_challenges: &[F::Challenge], - ) -> OpeningPoint { - let mut opening_point = sumcheck_challenges.to_vec(); - opening_point[..self.log_k_chunk].reverse(); - opening_point[self.log_k_chunk..].reverse(); - opening_point.into() - } - - #[cfg(feature = "zk")] - fn input_claim_constraint(&self) -> InputClaimConstraint { - InputClaimConstraint::default() - } - - #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { - Vec::new() - } - - #[cfg(feature = "zk")] - fn output_claim_constraint(&self) -> Option { - let n = self.polynomial_types.len(); - - let mut terms = Vec::with_capacity(2 * n); - for (i, poly_type) in self.polynomial_types.iter().enumerate() { - let opening = OpeningId::committed(*poly_type, SumcheckId::Booleanity); - - terms.push(ProductTerm::scaled( - ValueSource::Challenge(2 * i), - vec![ValueSource::Opening(opening), ValueSource::Opening(opening)], - )); - terms.push(ProductTerm::scaled( - ValueSource::Challenge(2 * i + 1), - vec![ValueSource::Opening(opening)], - )); - } - - Some(OutputClaimConstraint::sum_of_products(terms)) - } - - #[cfg(feature = "zk")] - fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { - let combined_r: Vec = self - .r_address - .iter() - .cloned() - .rev() - .chain(self.r_cycle.iter().cloned().rev()) - .collect(); - - let eq_eval: F = EqPolynomial::::mle(sumcheck_challenges, &combined_r); - - let mut challenges = Vec::with_capacity(2 * self.polynomial_types.len()); - for gamma_2i in &self.gamma_powers_square { - let coeff = eq_eval * *gamma_2i; - challenges.push(coeff); - challenges.push(-coeff); - } - challenges - } -} - impl BooleanitySumcheckParams { /// Create booleanity params by taking r_cycle and r_address from Stage 5. /// @@ -191,19 +114,9 @@ impl BooleanitySumcheckParams { // NOTE: `stage5_point.r` is stored in BIG_ENDIAN format (each segment was reversed by // `normalize_opening_point`). For internal eq evaluations we want LowToHigh (LE) order // because `GruenSplitEqPolynomial` is instantiated with `BindingOrder::LowToHigh`. - debug_assert!( - stage5_point.r.len() == log_k_instruction + log_t, - "InstructionReadRaf opening point length mismatch: got {}, expected {} (= log_k_instruction {} + log_t {})", - stage5_point.r.len(), - log_k_instruction + log_t, - log_k_instruction, - log_t - ); - // Address segment: BE -> LE let mut stage5_addr = stage5_point.r[..log_k_instruction].to_vec(); stage5_addr.reverse(); - // Cycle segment: BE -> LE let mut r_cycle = stage5_point.r[log_k_instruction..].to_vec(); r_cycle.reverse(); @@ -261,49 +174,68 @@ impl BooleanitySumcheckParams { one_hot_params: one_hot_params.clone(), } } + + fn combined_r_big_endian(&self) -> Vec { + self.r_address + .iter() + .cloned() + .rev() + .chain(self.r_cycle.iter().cloned().rev()) + .collect() + } +} + +fn compute_gamma_powers(gamma: F::Challenge, count: usize) -> (Vec, Vec) { + let gamma_f: F = gamma.into(); + let mut powers = Vec::with_capacity(count); + let mut powers_inv = Vec::with_capacity(count); + let mut rho_i = F::one(); + for _ in 0..count { + powers.push(rho_i); + powers_inv.push(rho_i.inverse().expect("gamma powers are nonzero")); + rho_i *= gamma_f; + } + (powers, powers_inv) } -/// Booleanity Sumcheck Prover. #[derive(Allocative)] -pub struct BooleanitySumcheckProver { - /// Per-polynomial powers γ^i (in the base field). - /// Used to pre-scale the address eq tables for phase 2. - gamma_powers: Vec, - /// Per-polynomial inverse powers γ^{-i} (in the base field). - /// Used to unscale cached committed-polynomial openings. - gamma_powers_inv: Vec, +pub struct BooleanityCycleInput { + params: BooleanitySumcheckParams, + ra_indices: Vec, +} + +/// Booleanity address-phase prover. +#[derive(Allocative)] +pub struct BooleanityAddressSumcheckProver { /// B: split-eq over address-chunk variables (phase 1, LowToHigh). B: GruenSplitEqPolynomial, - /// D: split-eq over time/cycle variables (phase 2, LowToHigh). - D: GruenSplitEqPolynomial, - /// G[i][k] = Σ_j eq(r_cycle, j) · ra_i(k, j) for all RA polynomials + /// G[i][k] = Σ_j eq(r_cycle, j) · ra_i(k, j) for all RA polynomials. G: Vec>, - /// Shared H polynomials for phase 2 (initialized at transition) - H: Option>, - /// F: Expanding table for phase 1 - F: ExpandingTable, - /// eq(r_address, r_address) at end of phase 1 - eq_r_r: F, - /// RA indices (non-transposed, one per cycle) + /// RA indices computed alongside `G`, reused by the cycle phase. ra_indices: Vec, - pub params: BooleanitySumcheckParams, + /// F: Expanding table over address bits for phase 1. + F: ExpandingTable, + /// Most recent round polynomial, used to cache the address-phase output claim. + last_round_poly: Option>, + /// Output claim after the final address round (input claim for cycle phase). + address_claim: Option, + /// Address-only `SumcheckInstanceParams` wrapper. + params: BooleanityAddressPhaseParams, } -impl BooleanitySumcheckProver { - /// Initialize a BooleanitySumcheckProver with all three families. +impl BooleanityAddressSumcheckProver { + /// Initialize the address-phase prover. /// - /// All heavy computation is done here: - /// - Compute G polynomials and RA indices in a single pass over the trace - /// - Initialize split-eq polynomials for address (B) and cycle (D) variables - /// - Initialize expanding table for phase 1 - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::initialize")] + /// Heavy precomputation for this phase happens here: + /// - Compute all G-polynomial slices from the trace + /// - Initialize the address split-eq polynomial (`B`) + /// - Initialize the address expanding table (`F`) pub fn initialize( params: BooleanitySumcheckParams, trace: &[Cycle], bytecode: &BytecodePreprocessing, memory_layout: &MemoryLayout, ) -> Self { - // Compute G and RA indices in a single pass over the trace let (G, ra_indices) = compute_all_G_and_ra_indices::( trace, bytecode, @@ -311,55 +243,45 @@ impl BooleanitySumcheckProver { ¶ms.one_hot_params, ¶ms.r_cycle, ); - - // Initialize split-eq polynomials for address and cycle variables let B = GruenSplitEqPolynomial::new(¶ms.r_address, BindingOrder::LowToHigh); - let D = GruenSplitEqPolynomial::new(¶ms.r_cycle, BindingOrder::LowToHigh); - - // Initialize expanding table for phase 1 let k_chunk = 1 << params.log_k_chunk; let mut F_table = ExpandingTable::new(k_chunk, BindingOrder::LowToHigh); F_table.reset(F::one()); - // Compute prover-only fields: gamma_powers (γ^i) and gamma_powers_inv (γ^{-i}) - let num_polys = params.polynomial_types.len(); - let gamma_f: F = params.gamma.into(); - let mut gamma_powers = Vec::with_capacity(num_polys); - let mut gamma_powers_inv = Vec::with_capacity(num_polys); - let mut rho_i = F::one(); - for _ in 0..num_polys { - gamma_powers.push(rho_i); - gamma_powers_inv.push( - rho_i - .inverse() - .expect("gamma_powers[i] is nonzero (gamma != 0)"), - ); - rho_i *= gamma_f; - } - Self { - gamma_powers, - gamma_powers_inv, B, - D, G, ra_indices, - H: None, F: F_table, - eq_r_r: F::zero(), - params, + last_round_poly: None, + address_claim: None, + params: BooleanityAddressPhaseParams::new(params), } } - fn compute_phase1_message(&self, round: usize, previous_claim: F) -> UniPoly { - let m = round + 1; - let B = &self.B; - let N = self.params.polynomial_types.len(); + pub fn into_cycle_input(self) -> BooleanityCycleInput { + BooleanityCycleInput { + params: self.params.into_inner(), + ra_indices: self.ra_indices, + } + } +} + +impl SumcheckInstanceProver + for BooleanityAddressSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } - // Compute quadratic coefficients via generic split-eq fold - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = B + fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { + let m = round + 1; + let n = self.params.common.polynomial_types.len(); + // Compute quadratic coefficients via split-eq folding over the unbound address suffix. + let quadratic_coeffs: [F; DEGREE_BOUND - 1] = self + .B .par_fold_out_in_unreduced::<{ DEGREE_BOUND - 1 }>(&|k_prime| { - let coeffs = (0..N) + (0..n) .into_par_iter() .map(|i| { let G_i = &self.G[i]; @@ -370,7 +292,6 @@ impl BooleanitySumcheckProver { let k_m = k >> (m - 1); let F_k = self.F[k & ((1 << (m - 1)) - 1)]; let G_times_F = G_k * F_k; - let eval_infty = G_times_F * F_k; let eval_0 = if k_m == 0 { eval_infty - G_times_F @@ -393,7 +314,7 @@ impl BooleanitySumcheckProver { |running, new| [running[0] + new[0], running[1] + new[1]], ); - let gamma_2i = self.params.gamma_powers_square[i]; + let gamma_2i = self.params.common.gamma_powers_square[i]; [ gamma_2i * F::reduce_mul_u64(inner_sum[0]), gamma_2i * F::reduce_mul_u64(inner_sum[1]), @@ -402,29 +323,131 @@ impl BooleanitySumcheckProver { .reduce( || [F::zero(); DEGREE_BOUND - 1], |running, new| [running[0] + new[0], running[1] + new[1]], - ); - coeffs + ) }); - B.gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], previous_claim) + let poly = + self.B + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], previous_claim); + self.last_round_poly = Some(poly.clone()); + poly + } + + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + if let Some(poly) = self.last_round_poly.take() { + let claim = poly.evaluate(&r_j); + if round == self.params.common.log_k_chunk - 1 { + self.address_claim = Some(claim); + } + } + self.B.bind(r_j); + self.F.update(r_j); + } + + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + // Cache the intermediate address-phase claim used as input to cycle phase. + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + OpeningPoint::::new(r_address), + self.address_claim + .expect("Booleanity address-phase claim missing"), + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +/// Booleanity cycle-phase prover. +#[derive(Allocative)] +pub struct BooleanityCycleSumcheckProver { + /// D: split-eq over cycle variables (phase 2, LowToHigh). + D: GruenSplitEqPolynomial, + /// Shared RA polynomials, pre-scaled for batched cycle-phase accumulation. + H: SharedRaPolynomials, + /// eq(r_address, r_address), carried from address-phase binding. + eq_r_r: F, + /// Per-polynomial powers γ^i used for pre-scaling. + gamma_powers: Vec, + /// Per-polynomial inverse powers γ^{-i} used to unscale cached openings. + gamma_powers_inv: Vec, + /// Cycle-only `SumcheckInstanceParams` wrapper. + params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckProver { + /// Initialize cycle-phase state from the cached address-phase opening. + pub fn initialize( + input: BooleanityCycleInput, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + let params = BooleanityCyclePhaseParams::new(input.params, opening_accumulator); + let (eq_r_r, base_eq) = Self::compute_bound_address_eq_and_table(¶ms); + let num_polys = params.common.polynomial_types.len(); + let (gamma_powers, gamma_powers_inv) = compute_gamma_powers(params.common.gamma, num_polys); + let tables: Vec> = (0..num_polys) + .into_par_iter() + .map(|i| { + let rho = gamma_powers[i]; + base_eq.iter().map(|v| rho * *v).collect() + }) + .collect(); + + Self { + D: GruenSplitEqPolynomial::new(¶ms.common.r_cycle, BindingOrder::LowToHigh), + H: SharedRaPolynomials::new( + tables, + input.ra_indices, + params.common.one_hot_params.clone(), + ), + eq_r_r, + gamma_powers, + gamma_powers_inv, + params, + } } - fn compute_phase2_message(&self, _round: usize, previous_claim: F) -> UniPoly { - let D = &self.D; - let H = self.H.as_ref().expect("H should be initialized in phase 2"); - let num_polys = H.num_polys(); + fn compute_bound_address_eq_and_table(params: &BooleanityCyclePhaseParams) -> (F, Vec) { + let mut B = GruenSplitEqPolynomial::new(¶ms.common.r_address, BindingOrder::LowToHigh); + let k_chunk = 1 << params.common.log_k_chunk; + let mut F_table = ExpandingTable::new(k_chunk, BindingOrder::LowToHigh); + F_table.reset(F::one()); + for r_j in params.r_address_low_to_high.iter().copied() { + B.bind(r_j); + F_table.update(r_j); + } + (B.get_current_scalar(), F_table.clone_values()) + } +} - // Compute quadratic coefficients via generic split-eq fold (handles both E_in cases). - let quadratic_coeffs: [F; DEGREE_BOUND - 1] = D +impl SumcheckInstanceProver + for BooleanityCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn compute_message(&mut self, _round: usize, previous_claim: F) -> UniPoly { + let num_polys = self.H.num_polys(); + let quadratic_coeffs: [F; DEGREE_BOUND - 1] = self + .D .par_fold_out_in_unreduced::<{ DEGREE_BOUND - 1 }>(&|j_prime| { - // Accumulate in unreduced form to minimize per-term reductions + // Accumulate in unreduced form to minimize per-term reductions. let mut acc_c = F::UnreducedProductAccum::zero(); let mut acc_e = F::UnreducedProductAccum::zero(); for i in 0..num_polys { - let h_0 = H.get_bound_coeff(i, 2 * j_prime); - let h_1 = H.get_bound_coeff(i, 2 * j_prime + 1); + let h_0 = self.H.get_bound_coeff(i, 2 * j_prime); + let h_1 = self.H.get_bound_coeff(i, 2 * j_prime + 1); let b = h_1 - h_0; - // Phase-2 optimization: H is pre-scaled by rho_i = gamma^i, so gamma^{2i} // factors are already accounted for: // gamma^{2i}*h0*(h0-1) = (rho*h0) * (rho*h0 - rho) @@ -438,76 +461,17 @@ impl BooleanitySumcheckProver { F::reduce_product_accum(acc_e), ] }); - // previous_claim is s(0)+s(1) of the scaled polynomial; divide out eq_r_r to get inner claim let adjusted_claim = previous_claim * self.eq_r_r.inverse().unwrap(); let gruen_poly = - D.gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], adjusted_claim); - + self.D + .gruen_poly_deg_3(quadratic_coeffs[0], quadratic_coeffs[1], adjusted_claim); gruen_poly * self.eq_r_r } -} - -impl SumcheckInstanceProver for BooleanitySumcheckProver { - fn get_params(&self) -> &dyn SumcheckInstanceParams { - &self.params - } - - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::compute_message")] - fn compute_message(&mut self, round: usize, previous_claim: F) -> UniPoly { - if round < self.params.log_k_chunk { - self.compute_phase1_message(round, previous_claim) - } else { - self.compute_phase2_message(round, previous_claim) - } - } - #[tracing::instrument(skip_all, name = "BooleanitySumcheckProver::ingest_challenge")] - fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { - if round < self.params.log_k_chunk { - // Phase 1: Bind B and update F - self.B.bind(r_j); - self.F.update(r_j); - - // Transition to phase 2 - if round == self.params.log_k_chunk - 1 { - self.eq_r_r = self.B.get_current_scalar(); - - // Initialize SharedRaPolynomials with per-poly pre-scaled eq tables (by rho_i) - let F_table = std::mem::take(&mut self.F); - let ra_indices = std::mem::take(&mut self.ra_indices); - let base_eq = F_table.clone_values(); - let num_polys = self.params.polynomial_types.len(); - debug_assert!( - num_polys == self.gamma_powers.len(), - "gamma_powers length mismatch: got {}, expected {}", - self.gamma_powers.len(), - num_polys - ); - let tables: Vec> = (0..num_polys) - .into_par_iter() - .map(|i| { - let rho = self.gamma_powers[i]; - base_eq.iter().map(|v| rho * *v).collect() - }) - .collect(); - self.H = Some(SharedRaPolynomials::new( - tables, - ra_indices, - self.params.one_hot_params.clone(), - )); - - // Drop G arrays - let g = std::mem::take(&mut self.G); - drop_in_background_thread(g); - } - } else { - // Phase 2: Bind D and H - self.D.bind(r_j); - if let Some(ref mut h) = self.H { - h.bind_in_place(r_j, BindingOrder::LowToHigh); - } - } + fn ingest_challenge(&mut self, r_j: F::Challenge, _round: usize) { + self.D.bind(r_j); + self.H.bind_in_place(r_j, BindingOrder::LowToHigh); } fn cache_openings( @@ -516,19 +480,15 @@ impl SumcheckInstanceProver for BooleanitySum sumcheck_challenges: &[F::Challenge], ) { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); - let H = self.H.as_ref().expect("H should be initialized"); // H is scaled by rho_i; unscale so cached openings match the committed polynomials. - let claims: Vec = (0..H.num_polys()) - .map(|i| H.final_sumcheck_claim(i) * self.gamma_powers_inv[i]) + let claims: Vec = (0..self.H.num_polys()) + .map(|i| self.H.final_sumcheck_claim(i) * self.gamma_powers_inv[i]) .collect(); - - // All polynomials share the same opening point (r_address, r_cycle) - // Use a single SumcheckId for all accumulator.append_sparse( - self.params.polynomial_types.clone(), + self.params.common.polynomial_types.clone(), SumcheckId::Booleanity, - opening_point.r[..self.params.log_k_chunk].to_vec(), - opening_point.r[self.params.log_k_chunk..].to_vec(), + opening_point.r[..self.params.common.log_k_chunk].to_vec(), + opening_point.r[self.params.common.log_k_chunk..].to_vec(), claims, ); } @@ -539,27 +499,78 @@ impl SumcheckInstanceProver for BooleanitySum } } -/// Booleanity Sumcheck Verifier. -pub struct BooleanitySumcheckVerifier { - params: BooleanitySumcheckParams, +/// Booleanity address-phase verifier. +pub struct BooleanityAddressSumcheckVerifier { + params: BooleanityAddressPhaseParams, } -impl BooleanitySumcheckVerifier { +impl BooleanityAddressSumcheckVerifier { pub fn new(params: BooleanitySumcheckParams) -> Self { - Self { params } + Self { + params: BooleanityAddressPhaseParams::new(params), + } + } + + pub fn into_params(self) -> BooleanitySumcheckParams { + self.params.into_inner() } } impl> - SumcheckInstanceVerifier for BooleanitySumcheckVerifier + SumcheckInstanceVerifier for BooleanityAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn expected_output_claim(&self, accumulator: &A, _sumcheck_challenges: &[F::Challenge]) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + OpeningPoint::::new(r_address), + ); + } +} + +/// Booleanity cycle-phase verifier. +pub struct BooleanityCycleSumcheckVerifier { + params: BooleanityCyclePhaseParams, +} + +impl BooleanityCycleSumcheckVerifier { + pub fn new( + params: BooleanitySumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + Self { + params: BooleanityCyclePhaseParams::new(params, opening_accumulator), + } + } +} + +impl> + SumcheckInstanceVerifier for BooleanityCycleSumcheckVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { + let full_challenges = self.params.full_challenges(sumcheck_challenges); let ra_claims: Vec = self .params + .common .polynomial_types .iter() .map(|poly_type| { @@ -568,28 +579,183 @@ impl> .1 }) .collect(); - - let combined_r: Vec = self - .params - .r_address - .iter() - .cloned() - .rev() - .chain(self.params.r_cycle.iter().cloned().rev()) - .collect(); - - EqPolynomial::::mle(sumcheck_challenges, &combined_r) - * zip(&self.params.gamma_powers_square, ra_claims) - .map(|(gamma_2i, ra)| (ra.square() - ra) * gamma_2i) - .sum::() + EqPolynomial::::mle( + &full_challenges, + &self.params.common.combined_r_big_endian(), + ) * zip(&self.params.common.gamma_powers_square, ra_claims) + .map(|(gamma_2i, ra)| (ra.square() - ra) * gamma_2i) + .sum::() } fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); accumulator.append_sparse( - self.params.polynomial_types.clone(), + self.params.common.polynomial_types.clone(), SumcheckId::Booleanity, opening_point.r, ); } } + +#[derive(Allocative, Clone)] +struct BooleanityAddressPhaseParams { + common: BooleanitySumcheckParams, +} + +impl BooleanityAddressPhaseParams { + fn new(common: BooleanitySumcheckParams) -> Self { + Self { common } + } + + fn into_inner(self) -> BooleanitySumcheckParams { + self.common + } +} + +impl SumcheckInstanceParams for BooleanityAddressPhaseParams { + fn degree(&self) -> usize { + DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.common.log_k_chunk + } + + fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { + F::zero() + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::default() + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ))) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { + Vec::new() + } +} +#[derive(Allocative, Clone)] +pub struct BooleanityCyclePhaseParams { + common: BooleanitySumcheckParams, + r_address_low_to_high: Vec, +} + +impl BooleanityCyclePhaseParams { + pub fn new( + common: BooleanitySumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + let (r_address_point, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); + + Self { + common, + r_address_low_to_high, + } + } + + fn full_challenges(&self, cycle_challenges: &[F::Challenge]) -> Vec { + let mut full = self.r_address_low_to_high.clone(); + full.extend_from_slice(cycle_challenges); + full + } +} + +impl SumcheckInstanceParams for BooleanityCyclePhaseParams { + fn degree(&self) -> usize { + DEGREE_BOUND + } + + fn num_rounds(&self) -> usize { + self.common.log_t + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + ) + .1 + } + + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut opening_point = self.full_challenges(challenges); + opening_point[..self.common.log_k_chunk].reverse(); + opening_point[self.common.log_k_chunk..].reverse(); + opening_point.into() + } + + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BooleanityAddrClaim, + SumcheckId::BooleanityAddressPhase, + )) + } + + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + let mut terms = Vec::with_capacity(2 * self.common.polynomial_types.len()); + for (i, poly_type) in self.common.polynomial_types.iter().enumerate() { + let opening = OpeningId::committed(*poly_type, SumcheckId::Booleanity); + terms.push(ProductTerm::scaled( + ValueSource::Challenge(2 * i), + vec![ValueSource::Opening(opening), ValueSource::Opening(opening)], + )); + terms.push(ProductTerm::scaled( + ValueSource::Challenge(2 * i + 1), + vec![ValueSource::Opening(opening)], + )); + } + Some(OutputClaimConstraint::sum_of_products(terms)) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + let full = self.full_challenges(sumcheck_challenges); + let eq_eval: F = EqPolynomial::::mle(&full, &self.common.combined_r_big_endian()); + let mut challenges = Vec::with_capacity(2 * self.common.polynomial_types.len()); + for gamma_2i in &self.common.gamma_powers_square { + let coeff = eq_eval * *gamma_2i; + challenges.push(coeff); + challenges.push(-coeff); + } + challenges + } +} diff --git a/jolt-core/src/subprotocols/mod.rs b/jolt-core/src/subprotocols/mod.rs index b0c476e4d0..ecd4708549 100644 --- a/jolt-core/src/subprotocols/mod.rs +++ b/jolt-core/src/subprotocols/mod.rs @@ -10,7 +10,3 @@ pub mod sumcheck_claim; pub mod sumcheck_prover; pub mod sumcheck_verifier; pub mod univariate_skip; - -pub use booleanity::{ - BooleanitySumcheckParams, BooleanitySumcheckProver, BooleanitySumcheckVerifier, -}; diff --git a/jolt-core/src/subprotocols/sumcheck.rs b/jolt-core/src/subprotocols/sumcheck.rs index 4360d8c0b0..60256761f9 100644 --- a/jolt-core/src/subprotocols/sumcheck.rs +++ b/jolt-core/src/subprotocols/sumcheck.rs @@ -126,7 +126,7 @@ impl BatchedSumcheck { individual_claims .iter_mut() - .zip(univariate_polys.into_iter()) + .zip(univariate_polys) .for_each(|(claim, poly)| *claim = poly.evaluate(&r_j)); #[cfg(test)] @@ -292,7 +292,7 @@ impl BatchedSumcheck { individual_claims .iter_mut() - .zip(univariate_polys.into_iter()) + .zip(univariate_polys) .for_each(|(claim, poly)| *claim = poly.evaluate(&r_j)); for sumcheck in sumcheck_instances.iter_mut() { diff --git a/jolt-core/src/zkvm/bytecode/mod.rs b/jolt-core/src/zkvm/bytecode/mod.rs index f1666ef859..987102b1d7 100644 --- a/jolt-core/src/zkvm/bytecode/mod.rs +++ b/jolt-core/src/zkvm/bytecode/mod.rs @@ -8,10 +8,25 @@ pub fn get_pc_for_cycle(bytecode: &BytecodePreprocessing, cycle: &Cycle) -> usiz if matches!(cycle, Cycle::NoOp) { return 0; } - let instruction = cycle.instruction().normalize(); - bytecode.get_pc(&instruction).unwrap_or(0) + let instruction = cycle + .instruction() + .try_jolt_instruction_row() + .expect("trace cycle must be a final Jolt instruction row"); + match bytecode.get_pc(&instruction) { + Some(pc) => pc, + None => panic!( + "bytecode preprocessing is missing PC mapping for instruction at address {:#x} with virtual_sequence_remaining {:?}", + instruction.address, instruction.virtual_sequence_remaining + ), + } } pub fn entry_bytecode_index(bytecode: &BytecodePreprocessing) -> usize { - bytecode.entry_bytecode_index().unwrap_or(0) + match bytecode.entry_bytecode_index() { + Some(pc) => pc, + None => panic!( + "bytecode preprocessing is missing entry mapping for address {:#x}", + bytecode.entry_address + ), + } } diff --git a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs index da3c354c4b..358b30c6f7 100644 --- a/jolt-core/src/zkvm/bytecode/read_raf_checking.rs +++ b/jolt-core/src/zkvm/bytecode/read_raf_checking.rs @@ -1,4 +1,9 @@ -use std::{array, iter::once, sync::Arc}; +use std::{ + array, + iter::once, + ops::{Deref, DerefMut}, + sync::Arc, +}; use num_traits::Zero; @@ -47,7 +52,7 @@ use allocative::Allocative; use allocative::FlameGraphBuilder; use common::constants::{REGISTER_COUNT, XLEN}; use itertools::{zip_eq, Itertools}; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::JoltInstructionRow; use rayon::prelude::*; use strum::{EnumCount, IntoEnumIterator}; use tracer::instruction::Cycle; @@ -117,46 +122,26 @@ const N_STAGES: usize = 5; /// in the Stage3 per-stage claim, then the stage itself is folded with an outer factor `γ^2`, /// yielding the advertised `γ^6` overall. #[derive(Allocative)] -pub struct BytecodeReadRafSumcheckProver { +pub struct BytecodeReadRafAddressSumcheckProver { /// Per-stage address MLEs F_i(k) built from eq(r_cycle_stage_i, (chunk_index, j)), /// bound low-to-high during the address-binding phase. F: [MultilinearPolynomial; N_STAGES], - /// Chunked RA polynomials over address variables (one per dimension `d`), used to form - /// the product ∏_i ra_i during the cycle-binding phase. - ra: Vec>, - /// Binding challenges for the first log_K variables of the sumcheck - r_address_prime: Vec, - /// Per-stage Gruen-split eq polynomials over cycle vars (low-to-high binding order). - gruen_eq_polys: [GruenSplitEqPolynomial; N_STAGES], /// Previous-round claims s_i(0)+s_i(1) per stage, needed for degree-(d+1) univariate recovery. prev_round_claims: [F; N_STAGES], /// Round polynomials per stage for advancing to the next claim at r_j. prev_round_polys: Option<[UniPoly; N_STAGES]>, - /// Final sumcheck claims of stage Val polynomials (with RAF Int folded where applicable). - bound_val_evals: Option<[F; N_STAGES]>, /// f_entry_trace[k] = Ra(k, 0): one-hot at PC of cycle 0 (from trace). f_entry_trace: MultilinearPolynomial, /// f_entry_expected[k] = C(k): one-hot at entry_bytecode_index (from preprocessing). /// Product f_entry_trace * f_entry_expected = Ra(k,0) * C(k); sums to 1 iff PC[0] = entry_bytecode_index. f_entry_expected: MultilinearPolynomial, - /// eq_zero(j) indicator (r_cycle = all zeros), used in the cycle phase. - gruen_eq_entry: GruenSplitEqPolynomial, - /// f_entry_expected bound at r_addr after the address phase (cached for output claim). - bound_f_entry: Option, /// Running entry claim over remaining free variables. prev_entry_claim: F, prev_entry_poly: Option>, - /// Trace for computing PCs on the fly in init_log_t_rounds. - #[allocative(skip)] - trace: Arc>, - /// Bytecode preprocessing for computing PCs. - #[allocative(skip)] - bytecode_preprocessing: Arc, - pub params: BytecodeReadRafSumcheckParams, + params: BytecodeReadRafAddressPhaseParams, } -impl BytecodeReadRafSumcheckProver { - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::initialize")] +impl BytecodeReadRafAddressSumcheckProver { pub fn initialize( params: BytecodeReadRafSumcheckParams, trace: Arc>, @@ -305,11 +290,6 @@ impl BytecodeReadRafSumcheckProver { let F = F.map(MultilinearPolynomial::from); - let gruen_eq_polys = params - .r_cycles - .each_ref() - .map(|r_cycle| GruenSplitEqPolynomial::new(r_cycle, BindingOrder::LowToHigh)); - let pc_0 = super::get_pc_for_cycle(&bytecode_preprocessing, &trace[0]); assert!( pc_0 < params.K, @@ -330,120 +310,54 @@ impl BytecodeReadRafSumcheckProver { f_entry_expected_vec[entry_bytecode_index] = F::one(); let f_entry_expected = MultilinearPolynomial::from(f_entry_expected_vec); - // eq_zero(j) = ∏(1 - j_i): indicator for cycle j = 0. r_cycle = all zeros. - let r_cycle_zero = vec![F::Challenge::default(); params.log_T]; - let gruen_eq_entry = GruenSplitEqPolynomial::new(&r_cycle_zero, BindingOrder::LowToHigh); - Self { F, f_entry_trace, f_entry_expected, - gruen_eq_entry, - bound_f_entry: None, // Initial entry claim = Σ_k f_entry_trace[k] * f_entry_expected[k] = 1 for honest prover // (both one-hots at same index when PC[0] = entry_bytecode_index). prev_entry_claim: F::one(), prev_entry_poly: None, - ra: Vec::with_capacity(params.d), - r_address_prime: Vec::with_capacity(params.log_K), - gruen_eq_polys, prev_round_claims: claim_per_stage, prev_round_polys: None, - bound_val_evals: None, - trace, - bytecode_preprocessing, - params, + params: BytecodeReadRafAddressPhaseParams::new(params), } } - fn init_log_t_rounds(&mut self) { - let int_poly = self.params.int_poly.final_sumcheck_claim(); - - // We have a separate Val polynomial for each stage - // Additionally, for stages 1 and 3 we have an Int polynomial for RAF - // So we would have: - // Stage 1: gamma^0 * (Val_1 + gamma^5 * Int) - // Stage 2: gamma^1 * (Val_2) - // Stage 3: gamma^2 * (Val_3 + gamma^4 * Int) - // Stage 4: gamma^3 * (Val_4) - // Stage 5: gamma^4 * (Val_5) - // Which matches with the input claim: - // rv_1 + gamma * rv_2 + gamma^2 * rv_3 + gamma^3 * rv_4 + gamma^4 * rv_5 + gamma^5 * raf_1 + gamma^6 * raf_3 - let bound_val_evals: [F; N_STAGES] = self - .params - .val_polys - .iter() - .zip([ - int_poly * self.params.gamma_powers[5], - F::zero(), - int_poly * self.params.gamma_powers[4], - F::zero(), - F::zero(), - ]) - .map(|(poly, int_poly)| poly.final_sumcheck_claim() + int_poly) - .collect::>() - .try_into() - .unwrap(); - self.bound_val_evals = Some(bound_val_evals); - self.params.bound_val_polys = Some(bound_val_evals); - self.params.bound_int_poly = Some(int_poly); - - let bound_f_entry = self.f_entry_expected.final_sumcheck_claim(); - self.bound_f_entry = Some(bound_f_entry); - self.params.bound_f_entry = Some(bound_f_entry); - - // Reverse r_address_prime to get the correct order (it was built low-to-high) - let mut r_address = std::mem::take(&mut self.r_address_prime); - r_address.reverse(); - - self.F = array::from_fn(|_| MultilinearPolynomial::default()); - self.f_entry_trace = MultilinearPolynomial::default(); - self.f_entry_expected = MultilinearPolynomial::default(); - self.params.val_polys = array::from_fn(|_| MultilinearPolynomial::default()); - self.params.int_poly = IdentityPolynomial::new(0); - - let r_address_chunks = self - .params - .one_hot_params - .compute_r_address_chunks::(&r_address); - - // Build RA polynomials by iterating over trace and computing PCs on the fly - self.ra = r_address_chunks - .iter() - .enumerate() - .map(|(i, r_address_chunk)| { - let ra_i: Vec> = self - .trace - .par_iter() - .map(|cycle| { - let pc = super::get_pc_for_cycle(&self.bytecode_preprocessing, cycle); - Some(self.params.one_hot_params.bytecode_pc_chunk(pc, i)) - }) - .collect(); - RaPolynomial::new(Arc::new(ra_i), EqPolynomial::evals(r_address_chunk)) - }) - .collect(); - - // Drop trace and preprocessing - no longer needed after this - self.trace = Arc::new(Vec::new()); + pub fn into_params(self) -> BytecodeReadRafSumcheckParams { + let mut params = self.params.into_inner(); + params.cycle_initial_round_claims = Some(self.prev_round_claims); + params.cycle_initial_entry_claim = Some(self.prev_entry_claim); + params } } impl SumcheckInstanceProver - for BytecodeReadRafSumcheckProver + for BytecodeReadRafAddressSumcheckProver { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::compute_message")] + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_K + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + self.params.input_claim(accumulator) + } + fn compute_message(&mut self, round: usize, _previous_claim: F) -> UniPoly { - if round < self.params.log_K { - const DEGREE: usize = 2; + debug_assert!(round < self.params.log_K); + const DEGREE: usize = 2; - // Evaluation at [0, 2] for each stage plus the entry term. - let (eval_per_stage, entry_evals): ([[F; DEGREE]; N_STAGES], [F; DEGREE]) = - (0..self.params.val_polys[0].len() / 2) + // Evaluation at [0, 2] for each stage plus the entry term. + let (eval_per_stage, entry_evals): ([[F; DEGREE]; N_STAGES], [F; DEGREE]) = + (0..self.params.val_polys[0].len() / 2) .into_par_iter() .map(|i| { let ra_evals = self.F.each_ref().map(|poly| { @@ -501,135 +415,334 @@ impl SumcheckInstanceProver ), ); - let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); - let mut agg_round_poly = UniPoly::zero(); + let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); + let mut agg_round_poly = UniPoly::zero(); - for (stage, evals) in eval_per_stage.into_iter().enumerate() { - let [eval_at_0, eval_at_2] = evals; - let eval_at_1 = self.prev_round_claims[stage] - eval_at_0; - let round_poly = UniPoly::from_evals(&[eval_at_0, eval_at_1, eval_at_2]); - agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); - round_polys[stage] = round_poly; - } + for (stage, evals) in eval_per_stage.into_iter().enumerate() { + let [eval_at_0, eval_at_2] = evals; + let eval_at_1 = self.prev_round_claims[stage] - eval_at_0; + let round_poly = UniPoly::from_evals(&[eval_at_0, eval_at_1, eval_at_2]); + agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); + round_polys[stage] = round_poly; + } - let [entry_at_0, entry_at_2] = entry_evals; - let entry_at_1 = self.prev_entry_claim - entry_at_0; - let entry_round_poly = UniPoly::from_evals(&[entry_at_0, entry_at_1, entry_at_2]); - agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); - self.prev_entry_poly = Some(entry_round_poly); + let [entry_at_0, entry_at_2] = entry_evals; + let entry_at_1 = self.prev_entry_claim - entry_at_0; + let entry_round_poly = UniPoly::from_evals(&[entry_at_0, entry_at_1, entry_at_2]); + agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); + self.prev_entry_poly = Some(entry_round_poly); - self.prev_round_polys = Some(round_polys); + self.prev_round_polys = Some(round_polys); - agg_round_poly - } else { - let degree = >::degree(self); + agg_round_poly + } - let out_len = self.gruen_eq_polys[0].E_out_current().len(); - let in_len = self.gruen_eq_polys[0].E_in_current().len(); - let in_n_vars = in_len.log_2(); + fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + debug_assert!(round < self.params.log_K); + if let Some(prev_round_polys) = self.prev_round_polys.take() { + self.prev_round_claims = prev_round_polys.map(|poly| poly.evaluate(&r_j)); + } + if let Some(entry_poly) = self.prev_entry_poly.take() { + self.prev_entry_claim = entry_poly.evaluate(&r_j); + } - // Evaluations on [1, ..., degree - 2, inf] (for each stage + entry term). - let (mut evals_per_stage, mut entry_evals_raw): ([Vec; N_STAGES], Vec) = (0 - ..out_len) - .into_par_iter() - .map(|j_hi| { - let mut ra_eval_pairs = vec![(F::zero(), F::zero()); self.ra.len()]; - let mut ra_prod_evals = vec![F::zero(); degree - 1]; - let mut evals_per_stage: [_; N_STAGES] = - array::from_fn(|_| vec![F::UnreducedProductAccum::zero(); degree - 1]); - let mut entry_accum = vec![F::UnreducedProductAccum::zero(); degree - 1]; - - for j_lo in 0..in_len { - let j = j_lo + (j_hi << in_n_vars); - - for (i, ra_i) in self.ra.iter().enumerate() { - let ra_i_eval_at_j_0 = ra_i.get_bound_coeff(j * 2); - let ra_i_eval_at_j_1 = ra_i.get_bound_coeff(j * 2 + 1); - ra_eval_pairs[i] = (ra_i_eval_at_j_0, ra_i_eval_at_j_1); - } - eval_linear_prod_assign(&ra_eval_pairs, &mut ra_prod_evals); + self.params + .val_polys + .iter_mut() + .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.params + .int_poly + .bind_parallel(r_j, BindingOrder::LowToHigh); + self.F + .iter_mut() + .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.f_entry_trace + .bind_parallel(r_j, BindingOrder::LowToHigh); + self.f_entry_expected + .bind_parallel(r_j, BindingOrder::LowToHigh); + if round == self.params.log_K - 1 { + let int_poly = self.params.int_poly.final_sumcheck_claim(); + let bound_val_evals: [F; N_STAGES] = self + .params + .val_polys + .iter() + .zip([ + int_poly * self.params.gamma_powers[5], + F::zero(), + int_poly * self.params.gamma_powers[4], + F::zero(), + F::zero(), + ]) + .map(|(poly, int_poly)| poly.final_sumcheck_claim() + int_poly) + .collect::>() + .try_into() + .unwrap(); + self.params.bound_val_polys = Some(bound_val_evals); + self.params.bound_f_entry = Some(self.f_entry_expected.final_sumcheck_claim()); + } + } - for stage in 0..N_STAGES { - let eq_in_eval = self.gruen_eq_polys[stage].E_in_current()[j_lo]; - for i in 0..degree - 1 { - evals_per_stage[stage][i] += - eq_in_eval.mul_to_product_accum(ra_prod_evals[i]); - } - } + fn cache_openings( + &self, + accumulator: &mut ProverOpeningAccumulator, + sumcheck_challenges: &[F::Challenge], + ) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + let opening_point = OpeningPoint::::new(r_address); + let address_claim = self + .prev_round_claims + .iter() + .zip(self.params.gamma_powers.iter()) + .take(N_STAGES) + .map(|(claim, gamma)| *claim * *gamma) + .sum::() + + self.params.entry_gamma * self.prev_entry_claim; + accumulator.append_virtual( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + opening_point.clone(), + address_claim, + ); + } + + #[cfg(feature = "allocative")] + fn update_flamegraph(&self, flamegraph: &mut FlameGraphBuilder) { + flamegraph.visit_root(self); + } +} + +#[derive(Allocative)] +pub struct BytecodeReadRafCycleSumcheckProver { + /// Chunked RA polynomials over address variables (one per dimension `d`), used to form + /// the product ∏_i ra_i during the cycle-binding phase. + ra: Vec>, + /// Per-stage Gruen-split eq polynomials over cycle vars (low-to-high binding order). + gruen_eq_polys: [GruenSplitEqPolynomial; N_STAGES], + /// Previous-round claims s_i(0)+s_i(1) per stage, needed for degree-(d+1) univariate recovery. + prev_round_claims: [F; N_STAGES], + /// Round polynomials per stage for advancing to the next claim at r_j. + prev_round_polys: Option<[UniPoly; N_STAGES]>, + /// Final sumcheck claims of stage Val polynomials (with RAF Int folded where applicable). + bound_val_evals: [F; N_STAGES], + /// eq_zero(j) indicator (r_cycle = all zeros), used in the cycle phase. + gruen_eq_entry: GruenSplitEqPolynomial, + /// f_entry_expected bound at r_addr after the address phase. + bound_f_entry: F, + /// Running entry claim over remaining free variables. + prev_entry_claim: F, + prev_entry_poly: Option>, + params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckProver { + #[tracing::instrument(skip_all, name = "BytecodeReadRafCycleSumcheckProver::initialize")] + pub fn initialize( + mut params: BytecodeReadRafSumcheckParams, + trace: Arc>, + bytecode_preprocessing: Arc, + accumulator: &ProverOpeningAccumulator, + ) -> Self { + let (r_address_point, _) = accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + let r_address = r_address_point.r; + let mut r_address_low_to_high = r_address.clone(); + r_address_low_to_high.reverse(); + + let r_address_chunks = params + .one_hot_params + .compute_r_address_chunks::(&r_address); + let ra = r_address_chunks + .iter() + .enumerate() + .map(|(i, r_address_chunk)| { + let ra_i: Vec> = trace + .par_iter() + .map(|cycle| { + let pc = super::get_pc_for_cycle(&bytecode_preprocessing, cycle); + Some(params.one_hot_params.bytecode_pc_chunk(pc, i)) + }) + .collect(); + RaPolynomial::new(Arc::new(ra_i), EqPolynomial::evals(r_address_chunk)) + }) + .collect::>(); - let eq_in_entry = self.gruen_eq_entry.E_in_current()[j_lo]; + let gruen_eq_polys = params + .r_cycles + .each_ref() + .map(|r_cycle| GruenSplitEqPolynomial::new(r_cycle, BindingOrder::LowToHigh)); + + let r_cycle_zero = vec![F::Challenge::default(); params.log_T]; + let gruen_eq_entry = GruenSplitEqPolynomial::new(&r_cycle_zero, BindingOrder::LowToHigh); + + let bound_val_evals = params + .bound_val_polys + .take() + .expect("address phase must cache bound Val claims before cycle phase"); + let bound_f_entry = params + .bound_f_entry + .take() + .expect("address phase must cache bound entry claim before cycle phase"); + params.bound_val_polys = Some(bound_val_evals); + params.bound_f_entry = Some(bound_f_entry); + let prev_round_claims = params + .cycle_initial_round_claims + .take() + .expect("address phase must transfer cycle initial round claims before cycle phase"); + let prev_entry_claim = params + .cycle_initial_entry_claim + .take() + .expect("address phase must transfer cycle initial entry claim before cycle phase"); + + Self { + ra, + gruen_eq_polys, + prev_round_claims, + prev_round_polys: None, + bound_val_evals, + gruen_eq_entry, + bound_f_entry, + prev_entry_claim, + prev_entry_poly: None, + params: BytecodeReadRafCyclePhaseParams::new(params, r_address_low_to_high), + } + } +} + +impl SumcheckInstanceProver + for BytecodeReadRafCycleSumcheckProver +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_T + } + + fn input_claim(&self, accumulator: &ProverOpeningAccumulator) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn compute_message(&mut self, _round: usize, _previous_claim: F) -> UniPoly { + let degree = >::degree(self); + + let out_len = self.gruen_eq_polys[0].E_out_current().len(); + let in_len = self.gruen_eq_polys[0].E_in_current().len(); + let in_n_vars = in_len.log_2(); + + // Evaluations on [1, ..., degree - 2, inf] (for each stage + entry term). + let (mut evals_per_stage, mut entry_evals_raw): ([Vec; N_STAGES], Vec) = (0..out_len) + .into_par_iter() + .map(|j_hi| { + let mut ra_eval_pairs = vec![(F::zero(), F::zero()); self.ra.len()]; + let mut ra_prod_evals = vec![F::zero(); degree - 1]; + let mut evals_per_stage: [_; N_STAGES] = + array::from_fn(|_| vec![F::UnreducedProductAccum::zero(); degree - 1]); + let mut entry_accum = vec![F::UnreducedProductAccum::zero(); degree - 1]; + + for j_lo in 0..in_len { + let j = j_lo + (j_hi << in_n_vars); + + for (i, ra_i) in self.ra.iter().enumerate() { + let ra_i_eval_at_j_0 = ra_i.get_bound_coeff(j * 2); + let ra_i_eval_at_j_1 = ra_i.get_bound_coeff(j * 2 + 1); + ra_eval_pairs[i] = (ra_i_eval_at_j_0, ra_i_eval_at_j_1); + } + eval_linear_prod_assign(&ra_eval_pairs, &mut ra_prod_evals); + + for stage in 0..N_STAGES { + let eq_in_eval = self.gruen_eq_polys[stage].E_in_current()[j_lo]; for i in 0..degree - 1 { - entry_accum[i] += eq_in_entry.mul_to_product_accum(ra_prod_evals[i]); + evals_per_stage[stage][i] += + eq_in_eval.mul_to_product_accum(ra_prod_evals[i]); } } - let stage_evals = array::from_fn(|stage| { - let eq_out_eval = self.gruen_eq_polys[stage].E_out_current()[j_hi]; - evals_per_stage[stage] - .iter() - .map(|v| eq_out_eval * F::reduce_product_accum(*v)) - .collect() - }); - let eq_out_entry = self.gruen_eq_entry.E_out_current()[j_hi]; - let entry_evals: Vec = entry_accum + let eq_in_entry = self.gruen_eq_entry.E_in_current()[j_lo]; + for i in 0..degree - 1 { + entry_accum[i] += eq_in_entry.mul_to_product_accum(ra_prod_evals[i]); + } + } + + let stage_evals = array::from_fn(|stage| { + let eq_out_eval = self.gruen_eq_polys[stage].E_out_current()[j_hi]; + evals_per_stage[stage] .iter() - .map(|v| eq_out_entry * F::reduce_product_accum(*v)) - .collect(); + .map(|v| eq_out_eval * F::reduce_product_accum(*v)) + .collect() + }); + let eq_out_entry = self.gruen_eq_entry.E_out_current()[j_hi]; + let entry_evals: Vec = entry_accum + .iter() + .map(|v| eq_out_entry * F::reduce_product_accum(*v)) + .collect(); - (stage_evals, entry_evals) - }) - .reduce( - || { - ( - array::from_fn(|_| vec![F::zero(); degree - 1]), - vec![F::zero(); degree - 1], - ) - }, - |(a_stages, a_entry), (b_stages, b_entry)| { - let stages = array::from_fn(|i| { - zip_eq(&a_stages[i], &b_stages[i]) - .map(|(a, b)| *a + *b) - .collect() - }); - let entry: Vec = - zip_eq(&a_entry, &b_entry).map(|(a, b)| *a + *b).collect(); - (stages, entry) - }, - ); + (stage_evals, entry_evals) + }) + .reduce( + || { + ( + array::from_fn(|_| vec![F::zero(); degree - 1]), + vec![F::zero(); degree - 1], + ) + }, + |(a_stages, a_entry), (b_stages, b_entry)| { + let stages = array::from_fn(|i| { + zip_eq(&a_stages[i], &b_stages[i]) + .map(|(a, b)| *a + *b) + .collect() + }); + let entry: Vec = zip_eq(&a_entry, &b_entry).map(|(a, b)| *a + *b).collect(); + (stages, entry) + }, + ); - // Multiply by bound values. - let bound_val_evals = self.bound_val_evals.as_ref().unwrap(); - for (stage, evals) in evals_per_stage.iter_mut().enumerate() { - evals.iter_mut().for_each(|v| *v *= bound_val_evals[stage]); - } - let bound_f_entry = self.bound_f_entry.unwrap(); - entry_evals_raw.iter_mut().for_each(|v| *v *= bound_f_entry); - - let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); - let mut agg_round_poly = UniPoly::zero(); - - // Obtain round poly for each stage and perform RLC. - for (stage, evals) in evals_per_stage.iter().enumerate() { - let claim = self.prev_round_claims[stage]; - let round_poly = self.gruen_eq_polys[stage].gruen_poly_from_evals(evals, claim); - agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); - round_polys[stage] = round_poly; - } + // Multiply by bound values. + for (stage, evals) in evals_per_stage.iter_mut().enumerate() { + evals + .iter_mut() + .for_each(|v| *v *= self.bound_val_evals[stage]); + } + entry_evals_raw + .iter_mut() + .for_each(|v| *v *= self.bound_f_entry); + + let mut round_polys: [_; N_STAGES] = array::from_fn(|_| UniPoly::zero()); + let mut agg_round_poly = UniPoly::zero(); + + // Obtain round poly for each stage and perform RLC. + for (stage, evals) in evals_per_stage.iter().enumerate() { + let claim = self.prev_round_claims[stage]; + let round_poly = self.gruen_eq_polys[stage].gruen_poly_from_evals(evals, claim); + agg_round_poly += &(&round_poly * self.params.gamma_powers[stage]); + round_polys[stage] = round_poly; + } - let entry_round_poly = self - .gruen_eq_entry - .gruen_poly_from_evals(&entry_evals_raw, self.prev_entry_claim); - agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); - self.prev_entry_poly = Some(entry_round_poly); + let entry_round_poly = self + .gruen_eq_entry + .gruen_poly_from_evals(&entry_evals_raw, self.prev_entry_claim); + agg_round_poly += &(&entry_round_poly * self.params.entry_gamma); + self.prev_entry_poly = Some(entry_round_poly); - self.prev_round_polys = Some(round_polys); + self.prev_round_polys = Some(round_polys); - agg_round_poly - } + agg_round_poly } - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckProver::ingest_challenge")] fn ingest_challenge(&mut self, r_j: F::Challenge, round: usize) { + debug_assert!(round < self.params.log_T); if let Some(prev_round_polys) = self.prev_round_polys.take() { self.prev_round_claims = prev_round_polys.map(|poly| poly.evaluate(&r_j)); } @@ -637,34 +750,13 @@ impl SumcheckInstanceProver self.prev_entry_claim = entry_poly.evaluate(&r_j); } - if round < self.params.log_K { - self.params - .val_polys - .iter_mut() - .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.params - .int_poly - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.F - .iter_mut() - .for_each(|poly| poly.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.f_entry_trace - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.f_entry_expected - .bind_parallel(r_j, BindingOrder::LowToHigh); - self.r_address_prime.push(r_j); - if round == self.params.log_K - 1 { - self.init_log_t_rounds(); - } - } else { - self.ra - .iter_mut() - .for_each(|ra| ra.bind_parallel(r_j, BindingOrder::LowToHigh)); - self.gruen_eq_polys - .iter_mut() - .for_each(|poly| poly.bind(r_j)); - self.gruen_eq_entry.bind(r_j); - } + self.ra + .iter_mut() + .for_each(|ra| ra.bind_parallel(r_j, BindingOrder::LowToHigh)); + self.gruen_eq_polys + .iter_mut() + .for_each(|poly| poly.bind(r_j)); + self.gruen_eq_entry.bind(r_j); } fn cache_openings( @@ -698,41 +790,122 @@ impl SumcheckInstanceProver } } -pub struct BytecodeReadRafSumcheckVerifier { - params: BytecodeReadRafSumcheckParams, +pub struct BytecodeReadRafAddressSumcheckVerifier { + params: BytecodeReadRafAddressPhaseParams, } -impl BytecodeReadRafSumcheckVerifier { - pub fn gen>( +impl BytecodeReadRafAddressSumcheckVerifier { + pub fn new( bytecode_preprocessing: &BytecodePreprocessing, n_cycle_vars: usize, one_hot_params: &OneHotParams, - opening_accumulator: &A, + opening_accumulator: &dyn OpeningAccumulator, transcript: &mut impl Transcript, ) -> Self { + let params = BytecodeReadRafSumcheckParams::gen( + bytecode_preprocessing, + n_cycle_vars, + one_hot_params, + opening_accumulator, + transcript, + ); + Self { + params: BytecodeReadRafAddressPhaseParams::new(params), + } + } + + pub fn into_params(self) -> BytecodeReadRafSumcheckParams { + self.params.into_inner() + } +} + +impl> + SumcheckInstanceVerifier for BytecodeReadRafAddressSumcheckVerifier +{ + fn get_params(&self) -> &dyn SumcheckInstanceParams { + &self.params + } + + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_K + } + + fn input_claim(&self, accumulator: &A) -> F { + self.params.input_claim(accumulator) + } + + fn expected_output_claim(&self, accumulator: &A, _sumcheck_challenges: &[F::Challenge]) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + + fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { + let mut r_address = sumcheck_challenges.to_vec(); + r_address.reverse(); + accumulator.append_virtual( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + OpeningPoint::::new(r_address.clone()), + ); + } +} + +pub struct BytecodeReadRafCycleSumcheckVerifier { + params: BytecodeReadRafCyclePhaseParams, +} + +impl BytecodeReadRafCycleSumcheckVerifier { + pub fn new( + params: BytecodeReadRafSumcheckParams, + opening_accumulator: &dyn OpeningAccumulator, + ) -> Self { + let (r_address_point, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ); + let mut r_address_low_to_high = r_address_point.r; + r_address_low_to_high.reverse(); Self { - params: BytecodeReadRafSumcheckParams::gen( - bytecode_preprocessing, - n_cycle_vars, - one_hot_params, - opening_accumulator, - transcript, - ), + params: BytecodeReadRafCyclePhaseParams::new(params, r_address_low_to_high), } } } impl> - SumcheckInstanceVerifier for BytecodeReadRafSumcheckVerifier + SumcheckInstanceVerifier for BytecodeReadRafCycleSumcheckVerifier { fn get_params(&self) -> &dyn SumcheckInstanceParams { &self.params } + fn degree(&self) -> usize { + self.params.degree() + } + + fn num_rounds(&self) -> usize { + self.params.log_T + } + + fn input_claim(&self, accumulator: &A) -> F { + accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ) + .1 + } + fn expected_output_claim(&self, accumulator: &A, sumcheck_challenges: &[F::Challenge]) -> F { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.params.log_K); - // r_cycle is bound LowToHigh, so reverse let int_poly = self.params.int_poly.evaluate(&r_address_prime.r); @@ -745,64 +918,48 @@ impl> .1 }); - // We have a separate Val polynomial for each stage - // Additionally, for stages 1 and 3 we have an Int polynomial for RAF - // So we would have: - // Stage 1: gamma^0 * (Val_1 + gamma^5 * Int) - // Stage 2: gamma^1 * (Val_2) - // Stage 3: gamma^2 * (Val_3 + gamma^4 * Int) - // Stage 4: gamma^3 * (Val_4) - // Stage 5: gamma^4 * (Val_5) - // Which matches with the input claim: - // rv_1 + gamma * rv_2 + gamma^2 * rv_3 + gamma^3 * rv_4 + gamma^4 * rv_5 + gamma^5 * raf_1 + gamma^6 * raf_3 + let stage_val_claim = + |stage: usize| self.params.val_polys[stage].evaluate(&r_address_prime.r); + let int_poly_contrib_by_stage = [ + int_poly * self.params.gamma_powers[5], + F::zero(), + int_poly * self.params.gamma_powers[4], + F::zero(), + F::zero(), + ]; + let val = self .params - .val_polys + .r_cycles .iter() - .zip(&self.params.r_cycles) .zip(&self.params.gamma_powers) - .zip([ - int_poly * self.params.gamma_powers[5], // RAF for Stage1 - F::zero(), // There's no raf for Stage2 - int_poly * self.params.gamma_powers[4], // RAF for Stage3 - F::zero(), // There's no raf for Stage4 - F::zero(), // There's no raf for Stage5 - ]) - .map(|(((val, r_cycle), gamma), int_poly)| { - (val.evaluate(&r_address_prime.r) + int_poly) + .zip(int_poly_contrib_by_stage) + .enumerate() + .map(|(stage, ((r_cycle, gamma), int_poly))| { + (stage_val_claim(stage) + int_poly) * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma }) .sum::(); - // Entry constraint: γ_entry · eq(r_addr, entry_bits) · eq_zero(r_cycle). - // r_address_prime.r is MSB-first (after normalize_opening_point reversal), - // so entry_bits must also be MSB-first: entry_bits[j] = (e >> (log_K-1-j)) & 1. - let entry_f_at_r_addr = { - let log_k = self.params.log_K; - let e = self.params.entry_bytecode_index; - let entry_bits: Vec = (0..log_k) - .map(|i| F::from_u64(((e >> (log_k - 1 - i)) & 1) as u64)) - .collect(); - EqPolynomial::::mle(&entry_bits, &r_address_prime.r) - }; - // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) - let zeros: Vec = vec![F::Challenge::default(); r_cycle_prime.r.len()]; - let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); - let entry_contrib = self.params.entry_gamma * entry_f_at_r_addr * eq_zero_at_r_cycle; + let entry_bits: Vec = (0..self.params.log_K) + .map(|i| { + F::from_u64( + ((self.params.entry_bytecode_index >> (self.params.log_K - 1 - i)) & 1) as u64, + ) + }) + .collect(); + let entry_contrib = self.params.entry_gamma + * EqPolynomial::::mle(&entry_bits, &r_address_prime.r) + * EqPolynomial::::zero_selector(&r_cycle_prime.r); ra_claims.fold(val + entry_contrib, |running, ra_claim| running * ra_claim) } - fn cache_openings( - &self, - accumulator: &mut A, - sumcheck_challenges: &[::Challenge], - ) { + fn cache_openings(&self, accumulator: &mut A, sumcheck_challenges: &[F::Challenge]) { let opening_point = self.params.normalize_opening_point(sumcheck_challenges); let (r_address, r_cycle) = opening_point.split_at(self.params.log_K); - // Compute r_address_chunks with proper padding let r_address_chunks = self .params .one_hot_params @@ -820,579 +977,217 @@ impl> } #[derive(Allocative, Clone)] -pub struct BytecodeReadRafSumcheckParams { - /// Index `i` stores `gamma^i`. - pub gamma_powers: Vec, - /// Stage-specific gamma powers for input_claim_constraint - pub stage1_gammas: Vec, - pub stage2_gammas: Vec, - pub stage3_gammas: Vec, - pub stage4_gammas: Vec, - pub stage5_gammas: Vec, - /// RLC of stage rv_claims and RAF claims (per Stage1/Stage3) used as the sumcheck LHS. - pub input_claim: F, - /// RaParams - pub one_hot_params: OneHotParams, - /// Bytecode length. - pub K: usize, - /// log2(K) and log2(T) used to determine round counts. - pub log_K: usize, - pub log_T: usize, - /// Number of address chunks (and RA polynomials in the product). - pub d: usize, - /// Stage Val polynomials evaluated over address vars. - pub val_polys: [MultilinearPolynomial; N_STAGES], - /// Stage rv claims. - pub rv_claims: [F; N_STAGES], - pub raf_claim: F, - pub raf_shift_claim: F, - /// Identity polynomial over address vars used to inject RAF contributions. - pub int_poly: IdentityPolynomial, - pub r_cycles: [Vec; N_STAGES], - /// Bound values after log_K rounds (set by prover for output_constraint_challenge_values) - pub bound_int_poly: Option, - pub bound_val_polys: Option<[F; N_STAGES]>, - /// γ_entry = gamma_powers[7]. Weights the entry-point constraint term. - pub entry_gamma: F, - /// Bytecode table index of the ELF entry point. - pub entry_bytecode_index: usize, - /// Prover-cached f_entry(r_addr) after address phase (None in verifier params). - pub bound_f_entry: Option, +struct BytecodeReadRafCyclePhaseParams { + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, } -impl BytecodeReadRafSumcheckParams { - #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] - pub fn gen( - bytecode_preprocessing: &BytecodePreprocessing, - n_cycle_vars: usize, - one_hot_params: &OneHotParams, - opening_accumulator: &dyn OpeningAccumulator, - transcript: &mut impl Transcript, +impl BytecodeReadRafCyclePhaseParams { + fn new( + inner: BytecodeReadRafSumcheckParams, + r_address_low_to_high: Vec, ) -> Self { - let gamma_powers = transcript.challenge_scalar_powers(8); + Self { + inner, + r_address_low_to_high, + } + } - let bytecode = &bytecode_preprocessing.bytecode; + fn full_challenges(&self, cycle_challenges: &[F::Challenge]) -> Vec { + let mut full = self.r_address_low_to_high.clone(); + full.extend_from_slice(cycle_challenges); + full + } +} - // Generate all stage-specific gamma powers upfront (order must match verifier) - let stage1_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_CIRCUIT_FLAGS); - let stage2_gammas: Vec = transcript.challenge_scalar_powers(4); - let stage3_gammas: Vec = transcript.challenge_scalar_powers(9); - let stage4_gammas: Vec = transcript.challenge_scalar_powers(3); - let stage5_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_LOOKUP_TABLES); +impl Deref for BytecodeReadRafCyclePhaseParams { + type Target = BytecodeReadRafSumcheckParams; - // Compute rv_claims (these don't iterate bytecode, just query opening accumulator) - let rv_claim_1 = Self::compute_rv_claim_1(opening_accumulator, &stage1_gammas); - let rv_claim_2 = Self::compute_rv_claim_2(opening_accumulator, &stage2_gammas); - let rv_claim_3 = Self::compute_rv_claim_3(opening_accumulator, &stage3_gammas); - let rv_claim_4 = Self::compute_rv_claim_4(opening_accumulator, &stage4_gammas); - let rv_claim_5 = Self::compute_rv_claim_5(opening_accumulator, &stage5_gammas); - let rv_claims = [rv_claim_1, rv_claim_2, rv_claim_3, rv_claim_4, rv_claim_5]; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - // Pre-compute eq_r_register for stages 4 and 5 (they use different r_register points) - let r_register_4 = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersReadWriteChecking, - ) - .0 - .r; - let eq_r_register_4 = - EqPolynomial::::evals(&r_register_4[..(REGISTER_COUNT as usize).log_2()]); +impl DerefMut for BytecodeReadRafCyclePhaseParams { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} - let r_register_5 = opening_accumulator +impl SumcheckInstanceParams for BytecodeReadRafCyclePhaseParams { + fn degree(&self) -> usize { + self.d + 1 + } + + fn num_rounds(&self) -> usize { + self.inner.log_T + } + + fn input_claim(&self, accumulator: &dyn OpeningAccumulator) -> F { + accumulator .get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, ) - .0 - .r; - let eq_r_register_5 = - EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); + .1 + } - // Fused pass: compute all val polynomials in a single parallel iteration - let val_polys = Self::compute_val_polys( - bytecode, - &eq_r_register_4, - &eq_r_register_5, - &stage1_gammas, - &stage2_gammas, - &stage3_gammas, - &stage4_gammas, - &stage5_gammas, - ); + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = self.full_challenges(challenges); + r[0..self.log_K].reverse(); + r[self.log_K..].reverse(); + OpeningPoint::new(r) + } - let int_poly = IdentityPolynomial::new(one_hot_params.bytecode_k.log_2()); + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + InputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + )) + } - let (_, raf_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanOuter); - let (_, raf_shift_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanShift); - let entry_gamma = gamma_powers[7]; - let entry_bytecode_index = super::entry_bytecode_index(bytecode_preprocessing); - // Both prover and verifier add entry_gamma unconditionally. - // The security comes from the sumcheck: if ra(entry_index, 0) != 1, the sum - // won't match input_claim and the sumcheck fails. - let input_claim: F = [ - rv_claim_1, - rv_claim_2, - rv_claim_3, - rv_claim_4, - rv_claim_5, - raf_claim, - raf_shift_claim, - ] - .iter() - .zip(&gamma_powers) - .map(|(claim, g)| *claim * g) - .sum::() - + entry_gamma; + #[cfg(feature = "zk")] + fn input_constraint_challenge_values( + &self, + _accumulator: &dyn OpeningAccumulator, + ) -> Vec { + Vec::new() + } - let (r_cycle_1, _) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); - let (r_cycle_2, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::Jump), - SumcheckId::SpartanProductVirtualization, - ); - let (r_cycle_3, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanShift, - ); - let (r, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::Rs1Ra, - SumcheckId::RegistersReadWriteChecking, - ); - let (_, r_cycle_4) = r.split_at((REGISTER_COUNT as usize).log_2()); - let (r, _) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, - ); - let (_, r_cycle_5) = r.split_at((REGISTER_COUNT as usize).log_2()); - let r_cycles = [ - r_cycle_1.r, - r_cycle_2.r, - r_cycle_3.r, - r_cycle_4.r, - r_cycle_5.r, - ]; + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + let ra_factors: Vec = (0..self.d) + .map(|i| { + ValueSource::Opening(OpeningId::committed( + CommittedPolynomial::BytecodeRa(i), + SumcheckId::BytecodeReadRaf, + )) + }) + .collect(); - Self { - gamma_powers, - entry_gamma, - entry_bytecode_index, - stage1_gammas, - stage2_gammas, - stage3_gammas, - stage4_gammas, - stage5_gammas, - input_claim, - one_hot_params: one_hot_params.clone(), - K: one_hot_params.bytecode_k, - log_K: one_hot_params.bytecode_k.log_2(), - d: one_hot_params.bytecode_d, - log_T: n_cycle_vars, - val_polys, - rv_claims, - raf_claim, - raf_shift_claim, - int_poly, - r_cycles, - bound_int_poly: None, - bound_val_polys: None, - bound_f_entry: None, - } + let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), ra_factors)]; + Some(OutputClaimConstraint::sum_of_products(terms)) } - /// Fused computation of all Val polynomials in a single parallel pass over bytecode. - /// - /// This computes all 5 stage-specific Val(k) polynomials simultaneously, avoiding - /// 5 separate passes through the bytecode. Each stage has its own gamma powers - /// and formula for Val(k). - #[allow(clippy::too_many_arguments)] - fn compute_val_polys( - bytecode: &[NormalizedInstruction], - eq_r_register_4: &[F], - eq_r_register_5: &[F], - stage1_gammas: &[F], - stage2_gammas: &[F], - stage3_gammas: &[F], - stage4_gammas: &[F], - stage5_gammas: &[F], - ) -> [MultilinearPolynomial; N_STAGES] { - let K = bytecode.len(); + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { + let opening_point = self.normalize_opening_point(sumcheck_challenges); + let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.log_K); - // Pre-allocate output vectors for each stage - let mut vals: [Vec; N_STAGES] = array::from_fn(|_| unsafe_allocate_zero_vec(K)); - let [v0, v1, v2, v3, v4] = &mut vals; + // Prover stores bound values before clearing polys; verifier evaluates directly. + let val: F = if let Some(bound_val_polys) = &self.bound_val_polys { + bound_val_polys + .iter() + .zip(&self.r_cycles) + .zip(&self.gamma_powers) + .map(|((bound_val, r_cycle), gamma)| { + *bound_val * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma + }) + .sum() + } else { + let int_poly = self.int_poly.evaluate(&r_address_prime.r); + self.val_polys + .iter() + .zip(&self.r_cycles) + .zip(&self.gamma_powers) + .zip([ + int_poly * self.gamma_powers[5], + F::zero(), + int_poly * self.gamma_powers[4], + F::zero(), + F::zero(), + ]) + .map(|(((val, r_cycle), gamma), int_poly_contrib)| { + (val.evaluate(&r_address_prime.r) + int_poly_contrib) + * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) + * gamma + }) + .sum() + }; - // Fused parallel iteration: compute all 5 val entries for each instruction - bytecode - .par_iter() - .zip(v0.par_iter_mut()) - .zip(v1.par_iter_mut()) - .zip(v2.par_iter_mut()) - .zip(v3.par_iter_mut()) - .zip(v4.par_iter_mut()) - .for_each(|(((((instruction, o0), o1), o2), o3), o4)| { - let instr = *instruction; - let circuit_flags = instruction.circuit_flags(); - let instr_flags = instruction.instruction_flags(); + let f_entry_at_r_addr = if let Some(v) = self.bound_f_entry { + v + } else { + let log_k = self.log_K; + let e = self.entry_bytecode_index; + let entry_bits: Vec = (0..log_k) + .map(|i| F::from_u64(((e >> (log_k - 1 - i)) & 1) as u64)) + .collect(); + EqPolynomial::::mle(&entry_bits, &r_address_prime.r) + }; + // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) + let zeros: Vec = vec![F::Challenge::default(); r_cycle_prime.r.len()]; + let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); + let entry_contrib = self.entry_gamma * f_entry_at_r_addr * eq_zero_at_r_cycle; - // Stage 1 (Spartan outer sumcheck) - // Val(k) = unexpanded_pc(k) + γ·imm(k) - // + γ²·circuit_flags[0](k) + γ³·circuit_flags[1](k) + ... - // This virtualizes claims output by Spartan's "outer" sumcheck. - { - let mut lc = F::from_u64(instr.address as u64); - lc += instr.operands.imm.field_mul(stage1_gammas[1]); - // sanity check - debug_assert!( - !circuit_flags[CircuitFlags::IsCompressed] - || !circuit_flags[CircuitFlags::DoNotUpdateUnexpandedPC] - ); - for (flag, gamma_power) in circuit_flags.iter().zip(stage1_gammas[2..].iter()) { - if *flag { - lc += *gamma_power; - } - } - *o0 = lc; - } + vec![val + entry_contrib] + } +} - // Stage 2 (product virtualization, de-duplicated factors) - // Val(k) = jump_flag(k) + γ·branch_flag(k) - // + γ²·write_lookup_output_to_rd_flag(k) + γ³·virtual_instruction(k) - // This Val matches the fused product sumcheck. - { - let mut lc = F::zero(); - if circuit_flags[CircuitFlags::Jump] { - lc += stage2_gammas[0]; - } - if instr_flags[InstructionFlags::Branch] { - lc += stage2_gammas[1]; - } - if circuit_flags[CircuitFlags::WriteLookupOutputToRD] { - lc += stage2_gammas[2]; - } - if circuit_flags[CircuitFlags::VirtualInstruction] { - lc += stage2_gammas[3]; - } - *o1 = lc; - } +#[derive(Allocative, Clone)] +struct BytecodeReadRafAddressPhaseParams { + inner: BytecodeReadRafSumcheckParams, +} - // Stage 3 (Shift sumcheck) - // Val(k) = imm(k) + γ·unexpanded_pc(k) - // + γ²·left_operand_is_rs1_value(k) + γ³·left_operand_is_pc(k) - // + γ⁴·right_operand_is_rs2_value(k) + γ⁵·right_operand_is_imm(k) - // + γ⁶·is_noop(k) + γ⁷·virtual_instruction(k) + γ⁸·is_first_in_sequence(k) - // This virtualizes claims output by the ShiftSumcheck. - { - let mut lc = F::from_i128(instr.operands.imm); - lc += stage3_gammas[1].mul_u64(instr.address as u64); - if instr_flags[InstructionFlags::LeftOperandIsRs1Value] { - lc += stage3_gammas[2]; - } - if instr_flags[InstructionFlags::LeftOperandIsPC] { - lc += stage3_gammas[3]; - } - if instr_flags[InstructionFlags::RightOperandIsRs2Value] { - lc += stage3_gammas[4]; - } - if instr_flags[InstructionFlags::RightOperandIsImm] { - lc += stage3_gammas[5]; - } - if instr_flags[InstructionFlags::IsNoop] { - lc += stage3_gammas[6]; - } - if circuit_flags[CircuitFlags::VirtualInstruction] { - lc += stage3_gammas[7]; - } - if circuit_flags[CircuitFlags::IsFirstInSequence] { - lc += stage3_gammas[8]; - } - *o2 = lc; - } +impl BytecodeReadRafAddressPhaseParams { + fn new(inner: BytecodeReadRafSumcheckParams) -> Self { + Self { inner } + } - // Stage 4 (registers read/write checking sumcheck) - // Val(k) = eq(rd(k), r_register) + γ·eq(rs1(k), r_register) + γ²·eq(rs2(k), r_register) - // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, - // and analogously for rs1(k, r) and rs2(k, r). - // This virtualizes claims output by the registers read/write checking sumcheck. - { - let rd_eq = instr - .operands - .rd - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - let rs1_eq = instr - .operands - .rs1 - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - let rs2_eq = instr - .operands - .rs2 - .map_or(F::zero(), |r| eq_r_register_4[r as usize]); - *o3 = rd_eq * stage4_gammas[0] - + rs1_eq * stage4_gammas[1] - + rs2_eq * stage4_gammas[2]; - } + fn into_inner(self) -> BytecodeReadRafSumcheckParams { + self.inner + } +} - // Stage 5 (registers val-evaluation + instruction lookups sumcheck) - // Val(k) = eq(rd(k), r_register) + γ·raf_flag(k) - // + γ²·lookup_table_flag[0](k) + γ³·lookup_table_flag[1](k) + ... - // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, - // and raf_flag(k) = 1 if instruction k is NOT interleaved operands. - // This virtualizes the claim output by the registers val-evaluation sumcheck - // and the instruction lookups sumcheck. - { - let mut lc = instr - .operands - .rd - .map_or(F::zero(), |r| eq_r_register_5[r as usize]); - if !circuit_flags.is_interleaved_operands() { - lc += stage5_gammas[1]; - } - if let Some(table) = InstructionLookup::::lookup_table(instruction) { - let table_index = LookupTables::::enum_index(&table); - lc += stage5_gammas[2 + table_index]; - } - *o4 = lc; - } - }); +impl Deref for BytecodeReadRafAddressPhaseParams { + type Target = BytecodeReadRafSumcheckParams; - vals.map(MultilinearPolynomial::from) + fn deref(&self) -> &Self::Target { + &self.inner } +} - fn compute_rv_claim_1( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, unexpanded_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanOuter, - ); - let (_, imm_claim) = opening_accumulator - .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); +impl DerefMut for BytecodeReadRafAddressPhaseParams { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} - let circuit_flag_claims: Vec = CircuitFlags::iter() - .map(|flag| { - opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(flag), - SumcheckId::SpartanOuter, - ) - .1 - }) - .collect(); +impl SumcheckInstanceParams for BytecodeReadRafAddressPhaseParams { + fn degree(&self) -> usize { + self.d + 1 + } - std::iter::once(unexpanded_pc_claim) - .chain(std::iter::once(imm_claim)) - .chain(circuit_flag_claims) - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() + fn num_rounds(&self) -> usize { + self.inner.log_K } - fn compute_rv_claim_2( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, jump_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::Jump), - SumcheckId::SpartanProductVirtualization, - ); - let (_, branch_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::Branch), - SumcheckId::SpartanProductVirtualization, - ); - let (_, write_lookup_output_to_rd_flag_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), - SumcheckId::SpartanProductVirtualization, - ); - let (_, virtual_instruction_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), - SumcheckId::SpartanProductVirtualization, - ); + fn input_claim(&self, _accumulator: &dyn OpeningAccumulator) -> F { + self.input_claim + } - [ - jump_claim, - branch_claim, - write_lookup_output_to_rd_flag_claim, - virtual_instruction_claim, - ] - .into_iter() - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() + fn normalize_opening_point(&self, challenges: &[F::Challenge]) -> OpeningPoint { + let mut r = challenges.to_vec(); + r.reverse(); + OpeningPoint::new(r) } - fn compute_rv_claim_3( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, imm_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::Imm, - SumcheckId::InstructionInputVirtualization, - ); - let (_, spartan_shift_unexpanded_pc_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanShift, - ); - let (_, instruction_input_unexpanded_pc_claim) = opening_accumulator - .get_virtual_polynomial_opening( - VirtualPolynomial::UnexpandedPC, - SumcheckId::InstructionInputVirtualization, - ); + #[cfg(feature = "zk")] + fn input_claim_constraint(&self) -> InputClaimConstraint { + // input_claim = Σᵢ gamma_powers[i] * rv_claim_i + gamma_powers[5]*raf_claim + gamma_powers[6]*raf_shift_claim + // Each rv_claim_i = Σⱼ stage_i_gamma[j] * opening_ij + let mut terms = Vec::new(); + let mut challenge_idx = 0; - assert_eq!( - spartan_shift_unexpanded_pc_claim, - instruction_input_unexpanded_pc_claim - ); - - let unexpanded_pc_claim = spartan_shift_unexpanded_pc_claim; - let (_, left_is_rs1_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsRs1Value), - SumcheckId::InstructionInputVirtualization, - ); - let (_, left_is_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsPC), - SumcheckId::InstructionInputVirtualization, - ); - let (_, right_is_rs2_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsRs2Value), - SumcheckId::InstructionInputVirtualization, - ); - let (_, right_is_imm_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsImm), - SumcheckId::InstructionInputVirtualization, - ); - let (_, is_noop_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionFlags(InstructionFlags::IsNoop), - SumcheckId::SpartanShift, - ); - let (_, is_virtual_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), - SumcheckId::SpartanShift, - ); - let (_, is_first_in_sequence_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), - SumcheckId::SpartanShift, - ); - - [ - imm_claim, - unexpanded_pc_claim, - left_is_rs1_claim, - left_is_pc_claim, - right_is_rs2_claim, - right_is_imm_claim, - is_noop_claim, - is_virtual_claim, - is_first_in_sequence_claim, - ] - .into_iter() - .zip_eq(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum() - } - - fn compute_rv_claim_4( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - std::iter::empty() - .chain(once(VirtualPolynomial::RdWa)) - .chain(once(VirtualPolynomial::Rs1Ra)) - .chain(once(VirtualPolynomial::Rs2Ra)) - .map(|vp| { - opening_accumulator - .get_virtual_polynomial_opening(vp, SumcheckId::RegistersReadWriteChecking) - .1 - }) - .zip(gamma_powers) - .map(|(claim, gamma)| claim * gamma) - .sum::() - } - - fn compute_rv_claim_5( - opening_accumulator: &dyn OpeningAccumulator, - gamma_powers: &[F], - ) -> F { - let (_, rd_wa_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::RdWa, - SumcheckId::RegistersValEvaluation, - ); - - let (_, raf_flag_claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::InstructionRafFlag, - SumcheckId::InstructionReadRaf, - ); - - let mut sum = rd_wa_claim * gamma_powers[0]; - sum += raf_flag_claim * gamma_powers[1]; - - // Add lookup table flag claims from InstructionReadRaf - for i in 0..LookupTables::::COUNT { - let (_, claim) = opening_accumulator.get_virtual_polynomial_opening( - VirtualPolynomial::LookupTableFlag(i), - SumcheckId::InstructionReadRaf, - ); - sum += claim * gamma_powers[2 + i]; - } - - sum - } -} - -impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams { - fn degree(&self) -> usize { - self.d + 1 - } - - fn num_rounds(&self) -> usize { - self.log_K + self.log_T - } - - fn input_claim(&self, _: &dyn OpeningAccumulator) -> F { - self.input_claim - } - - fn normalize_opening_point( - &self, - sumcheck_challenges: &[::Challenge], - ) -> OpeningPoint { - let mut r = sumcheck_challenges.to_vec(); - r[0..self.log_K].reverse(); - r[self.log_K..].reverse(); - OpeningPoint::new(r) - } - - #[cfg(feature = "zk")] - fn input_claim_constraint(&self) -> InputClaimConstraint { - // input_claim = Σᵢ gamma_powers[i] * rv_claim_i + gamma_powers[5]*raf_claim + gamma_powers[6]*raf_shift_claim - // Each rv_claim_i = Σⱼ stage_i_gamma[j] * opening_ij - // - // Challenge layout: - // - Stage 1 (SpartanOuter): 2 + NUM_CIRCUIT_FLAGS terms - // - Stage 2 (SpartanProductVirtualization): 5 terms - // - Stage 3 (InstructionInputVirtualization + SpartanShift): 10 terms (9 + 1 for split unexpanded_pc) - // - Stage 4 (RegistersReadWriteChecking): 3 terms - // - Stage 5 (RegistersValEvaluation + InstructionReadRaf): 2 + NUM_LOOKUP_TABLES terms - // - raf_claim (PC @ SpartanOuter): 1 term - // - raf_shift_claim (PC @ SpartanShift): 1 term - - let mut terms = Vec::new(); - let mut challenge_idx = 0; - - // Stage 1: SpartanOuter openings - // Order: UnexpandedPC, Imm, then CircuitFlags in order - terms.push(ProductTerm::scaled( - ValueSource::Challenge(challenge_idx), - vec![ValueSource::Opening(OpeningId::virt( - VirtualPolynomial::UnexpandedPC, - SumcheckId::SpartanOuter, - ))], - )); - challenge_idx += 1; + terms.push(ProductTerm::scaled( + ValueSource::Challenge(challenge_idx), + vec![ValueSource::Opening(OpeningId::virt( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanOuter, + ))], + )); + challenge_idx += 1; terms.push(ProductTerm::scaled( ValueSource::Challenge(challenge_idx), @@ -1414,8 +1209,6 @@ impl SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams SumcheckInstanceParams for BytecodeReadRafSumcheckParams, + ) -> Vec { + let mut challenges = Vec::new(); + + for g in &self.stage1_gammas { + challenges.push(self.gamma_powers[0] * *g); + } + + for g in &self.stage2_gammas { + challenges.push(self.gamma_powers[1] * *g); + } + + challenges.push(self.gamma_powers[2] * self.stage3_gammas[0]); + let half = F::from_u64(2).inverse().unwrap(); + challenges.push(self.gamma_powers[2] * self.stage3_gammas[1] * half); + for g in &self.stage3_gammas[2..] { + challenges.push(self.gamma_powers[2] * *g); + } + + for g in &self.stage4_gammas { + challenges.push(self.gamma_powers[3] * *g); + } + + for g in &self.stage5_gammas { + challenges.push(self.gamma_powers[4] * *g); + } + + challenges.push(self.gamma_powers[5]); + challenges.push(self.gamma_powers[6]); + challenges.push(self.entry_gamma); + + challenges + } + + #[cfg(feature = "zk")] + fn output_claim_constraint(&self) -> Option { + Some(OutputClaimConstraint::direct(OpeningId::virt( + VirtualPolynomial::BytecodeReadRafAddrClaim, + SumcheckId::BytecodeReadRafAddressPhase, + ))) + } + + #[cfg(feature = "zk")] + fn output_constraint_challenge_values(&self, _sumcheck_challenges: &[F::Challenge]) -> Vec { + Vec::new() + } +} + +#[derive(Allocative, Clone)] +pub struct BytecodeReadRafSumcheckParams { + /// Index `i` stores `gamma^i`. + pub gamma_powers: Vec, + /// Stage-specific gamma powers for input_claim_constraint + pub stage1_gammas: Vec, + pub stage2_gammas: Vec, + pub stage3_gammas: Vec, + pub stage4_gammas: Vec, + pub stage5_gammas: Vec, + /// RLC of stage rv_claims and RAF claims (per Stage1/Stage3) used as the sumcheck LHS. + pub input_claim: F, + /// RaParams + pub one_hot_params: OneHotParams, + /// Bytecode length. + pub K: usize, + /// log2(K) and log2(T) used to determine round counts. + pub log_K: usize, + pub log_T: usize, + /// Number of address chunks (and RA polynomials in the product). + pub d: usize, + /// Stage Val polynomials evaluated over address vars. + pub val_polys: [MultilinearPolynomial; N_STAGES], + /// Stage rv claims. + pub rv_claims: [F; N_STAGES], + pub raf_claim: F, + pub raf_shift_claim: F, + /// Identity polynomial over address vars used to inject RAF contributions. + pub int_poly: IdentityPolynomial, + pub r_cycles: [Vec; N_STAGES], + /// Bound values after log_K rounds (set by prover for output_constraint_challenge_values) + pub bound_val_polys: Option<[F; N_STAGES]>, + /// γ_entry = gamma_powers[7]. Weights the entry-point constraint term. + pub entry_gamma: F, + /// Bytecode table index of the ELF entry point. + pub entry_bytecode_index: usize, + /// Prover-cached f_entry(r_addr) after address phase (None in verifier params). + pub bound_f_entry: Option, + /// Prover-cached per-stage cycle claims after address binding. + pub cycle_initial_round_claims: Option<[F; N_STAGES]>, + /// Prover-cached entry cycle claim after address binding. + pub cycle_initial_entry_claim: Option, +} + +impl BytecodeReadRafSumcheckParams { + #[tracing::instrument(skip_all, name = "BytecodeReadRafSumcheckParams::gen")] + pub fn gen( + bytecode_preprocessing: &BytecodePreprocessing, + n_cycle_vars: usize, + one_hot_params: &OneHotParams, + opening_accumulator: &dyn OpeningAccumulator, + transcript: &mut impl Transcript, + ) -> Self { + let gamma_powers = transcript.challenge_scalar_powers(8); + + // Generate all stage-specific gamma powers upfront (order must match verifier) + let stage1_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_CIRCUIT_FLAGS); + let stage2_gammas: Vec = transcript.challenge_scalar_powers(4); + let stage3_gammas: Vec = transcript.challenge_scalar_powers(9); + let stage4_gammas: Vec = transcript.challenge_scalar_powers(3); + let stage5_gammas: Vec = transcript.challenge_scalar_powers(2 + NUM_LOOKUP_TABLES); + + // Compute rv_claims (these don't iterate bytecode, just query opening accumulator) + let rv_claim_1 = Self::compute_rv_claim_1(opening_accumulator, &stage1_gammas); + let rv_claim_2 = Self::compute_rv_claim_2(opening_accumulator, &stage2_gammas); + let rv_claim_3 = Self::compute_rv_claim_3(opening_accumulator, &stage3_gammas); + let rv_claim_4 = Self::compute_rv_claim_4(opening_accumulator, &stage4_gammas); + let rv_claim_5 = Self::compute_rv_claim_5(opening_accumulator, &stage5_gammas); + let rv_claims = [rv_claim_1, rv_claim_2, rv_claim_3, rv_claim_4, rv_claim_5]; + + let r_register_4 = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersReadWriteChecking, + ) + .0 + .r; + let eq_r_register_4 = + EqPolynomial::::evals(&r_register_4[..(REGISTER_COUNT as usize).log_2()]); + + let r_register_5 = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ) + .0 + .r; + let eq_r_register_5 = + EqPolynomial::::evals(&r_register_5[..(REGISTER_COUNT as usize).log_2()]); + + let val_polys = Self::compute_val_polys( + &bytecode_preprocessing.bytecode, + &eq_r_register_4, + &eq_r_register_5, + &stage1_gammas, + &stage2_gammas, + &stage3_gammas, + &stage4_gammas, + &stage5_gammas, + ); + + let int_poly = IdentityPolynomial::new(one_hot_params.bytecode_k.log_2()); + + let (_, raf_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanOuter); + let (_, raf_shift_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::PC, SumcheckId::SpartanShift); + let entry_gamma = gamma_powers[7]; + let entry_bytecode_index = super::entry_bytecode_index(bytecode_preprocessing); + // Both prover and verifier add entry_gamma unconditionally. + // The security comes from the sumcheck: if ra(entry_index, 0) != 1, the sum + // won't match input_claim and the sumcheck fails. + let mut input_claim: F = [ + rv_claim_1, + rv_claim_2, + rv_claim_3, + rv_claim_4, + rv_claim_5, + raf_claim, + raf_shift_claim, + ] + .iter() + .zip(&gamma_powers) + .map(|(claim, g)| *claim * g) + .sum::(); + input_claim += entry_gamma; + + let (r_cycle_1, _) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); + let (r_cycle_2, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::Jump), + SumcheckId::SpartanProductVirtualization, + ); + let (r_cycle_3, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanShift, + ); + let (r, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::Rs1Ra, + SumcheckId::RegistersReadWriteChecking, + ); + let (_, r_cycle_4) = r.split_at((REGISTER_COUNT as usize).log_2()); + let (r, _) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ); + let (_, r_cycle_5) = r.split_at((REGISTER_COUNT as usize).log_2()); + let r_cycles = [ + r_cycle_1.r, + r_cycle_2.r, + r_cycle_3.r, + r_cycle_4.r, + r_cycle_5.r, + ]; + + Self { + gamma_powers, + entry_gamma, + entry_bytecode_index, + stage1_gammas, + stage2_gammas, + stage3_gammas, + stage4_gammas, + stage5_gammas, + input_claim, + one_hot_params: one_hot_params.clone(), + K: one_hot_params.bytecode_k, + log_K: one_hot_params.bytecode_k.log_2(), + d: one_hot_params.bytecode_d, + log_T: n_cycle_vars, + val_polys, + rv_claims, + raf_claim, + raf_shift_claim, + int_poly, + r_cycles, + bound_val_polys: None, + bound_f_entry: None, + cycle_initial_round_claims: None, + cycle_initial_entry_claim: None, + } + } + + /// Fused computation of all Val polynomials in a single parallel pass over bytecode. + /// + /// This computes all 5 stage-specific Val(k) polynomials simultaneously, avoiding + /// 5 separate passes through the bytecode. Each stage has its own gamma powers + /// and formula for Val(k). + #[allow(clippy::too_many_arguments)] + fn compute_val_polys( + bytecode: &[JoltInstructionRow], + eq_r_register_4: &[F], + eq_r_register_5: &[F], + stage1_gammas: &[F], + stage2_gammas: &[F], + stage3_gammas: &[F], + stage4_gammas: &[F], + stage5_gammas: &[F], + ) -> [MultilinearPolynomial; N_STAGES] { + let K = bytecode.len(); + + // Pre-allocate output vectors for each stage + let mut vals: [Vec; N_STAGES] = array::from_fn(|_| unsafe_allocate_zero_vec(K)); + let [v0, v1, v2, v3, v4] = &mut vals; + + // Fused parallel iteration: compute all 5 val entries for each instruction + bytecode + .par_iter() + .zip(v0.par_iter_mut()) + .zip(v1.par_iter_mut()) + .zip(v2.par_iter_mut()) + .zip(v3.par_iter_mut()) + .zip(v4.par_iter_mut()) + .for_each(|(((((instruction, o0), o1), o2), o3), o4)| { + let instr = *instruction; + let circuit_flags = instruction.circuit_flags(); + let instr_flags = instruction.instruction_flags(); + + // Stage 1 (Spartan outer sumcheck) + // Val(k) = unexpanded_pc(k) + γ·imm(k) + // + γ²·circuit_flags[0](k) + γ³·circuit_flags[1](k) + ... + // This virtualizes claims output by Spartan's "outer" sumcheck. + { + let mut lc = F::from_u64(instr.address as u64); + lc += instr.operands.imm.field_mul(stage1_gammas[1]); + // sanity check + debug_assert!( + !circuit_flags[CircuitFlags::IsCompressed] + || !circuit_flags[CircuitFlags::DoNotUpdateUnexpandedPC] + ); + for (flag, gamma_power) in circuit_flags.iter().zip(stage1_gammas[2..].iter()) { + if *flag { + lc += *gamma_power; + } + } + *o0 = lc; + } + + // Stage 2 (product virtualization, de-duplicated factors) + // Val(k) = jump_flag(k) + γ·branch_flag(k) + // + γ²·write_lookup_output_to_rd_flag(k) + γ³·virtual_instruction(k) + // This Val matches the fused product sumcheck. + { + let mut lc = F::zero(); + if circuit_flags[CircuitFlags::Jump] { + lc += stage2_gammas[0]; + } + if instr_flags[InstructionFlags::Branch] { + lc += stage2_gammas[1]; + } + if circuit_flags[CircuitFlags::WriteLookupOutputToRD] { + lc += stage2_gammas[2]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + lc += stage2_gammas[3]; + } + *o1 = lc; + } + + // Stage 3 (Shift sumcheck) + // Val(k) = imm(k) + γ·unexpanded_pc(k) + // + γ²·left_operand_is_rs1_value(k) + γ³·left_operand_is_pc(k) + // + γ⁴·right_operand_is_rs2_value(k) + γ⁵·right_operand_is_imm(k) + // + γ⁶·is_noop(k) + γ⁷·virtual_instruction(k) + γ⁸·is_first_in_sequence(k) + // This virtualizes claims output by the ShiftSumcheck. + { + let mut lc = F::from_i128(instr.operands.imm); + lc += stage3_gammas[1].mul_u64(instr.address as u64); + if instr_flags[InstructionFlags::LeftOperandIsRs1Value] { + lc += stage3_gammas[2]; + } + if instr_flags[InstructionFlags::LeftOperandIsPC] { + lc += stage3_gammas[3]; + } + if instr_flags[InstructionFlags::RightOperandIsRs2Value] { + lc += stage3_gammas[4]; + } + if instr_flags[InstructionFlags::RightOperandIsImm] { + lc += stage3_gammas[5]; + } + if instr_flags[InstructionFlags::IsNoop] { + lc += stage3_gammas[6]; + } + if circuit_flags[CircuitFlags::VirtualInstruction] { + lc += stage3_gammas[7]; + } + if circuit_flags[CircuitFlags::IsFirstInSequence] { + lc += stage3_gammas[8]; + } + *o2 = lc; + } + + // Stage 4 (registers read/write checking sumcheck) + // Val(k) = eq(rd(k), r_register) + γ·eq(rs1(k), r_register) + γ²·eq(rs2(k), r_register) + // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, + // and analogously for rs1(k, r) and rs2(k, r). + // This virtualizes claims output by the registers read/write checking sumcheck. + { + let rd_eq = instr + .operands + .rd + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + let rs1_eq = instr + .operands + .rs1 + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + let rs2_eq = instr + .operands + .rs2 + .map_or(F::zero(), |r| eq_r_register_4[r as usize]); + *o3 = rd_eq * stage4_gammas[0] + + rs1_eq * stage4_gammas[1] + + rs2_eq * stage4_gammas[2]; + } + + // Stage 5 (registers val-evaluation + instruction lookups sumcheck) + // Val(k) = eq(rd(k), r_register) + γ·raf_flag(k) + // + γ²·lookup_table_flag[0](k) + γ³·lookup_table_flag[1](k) + ... + // where rd(k, r) = 1 if the k'th instruction in the bytecode has rd = r, + // and raf_flag(k) = 1 if instruction k is NOT interleaved operands. + // This virtualizes the claim output by the registers val-evaluation sumcheck + // and the instruction lookups sumcheck. + { + let mut lc = instr + .operands + .rd + .map_or(F::zero(), |r| eq_r_register_5[r as usize]); + if !circuit_flags.is_interleaved_operands() { + lc += stage5_gammas[1]; + } + if let Some(table) = InstructionLookup::::lookup_table(instruction) { + let table_index = LookupTables::::enum_index(&table); + lc += stage5_gammas[2 + table_index]; + } + *o4 = lc; + } + }); + + vals.map(MultilinearPolynomial::from) } - #[cfg(feature = "zk")] - fn input_constraint_challenge_values(&self, _: &dyn OpeningAccumulator) -> Vec { - // Compute coefficients: gamma_powers[stage] * stage_gammas[idx] - // The order must match input_claim_constraint terms. - - let mut challenges = Vec::new(); - - // Stage 1: gamma_powers[0] * stage1_gammas[i] - for g in &self.stage1_gammas { - challenges.push(self.gamma_powers[0] * *g); - } + fn compute_rv_claim_1( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, unexpanded_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanOuter, + ); + let (_, imm_claim) = opening_accumulator + .get_virtual_polynomial_opening(VirtualPolynomial::Imm, SumcheckId::SpartanOuter); - // Stage 2: gamma_powers[1] * stage2_gammas[i] - for g in &self.stage2_gammas { - challenges.push(self.gamma_powers[1] * *g); - } + let circuit_flag_claims: Vec = CircuitFlags::iter() + .map(|flag| { + opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(flag), + SumcheckId::SpartanOuter, + ) + .1 + }) + .collect(); - // Stage 3: gamma_powers[2] * stage3_gammas[i] - // Special handling for index 1 (unexpanded_pc) which is split between two openings - challenges.push(self.gamma_powers[2] * self.stage3_gammas[0]); // imm - // unexpanded_pc: split between SpartanShift and InstructionInputVirtualization - // Each gets half the coefficient - let half = F::from_u64(2).inverse().unwrap(); - challenges.push(self.gamma_powers[2] * self.stage3_gammas[1] * half); - // Continue with the rest of stage3 (indices 2..9) - for g in &self.stage3_gammas[2..] { - challenges.push(self.gamma_powers[2] * *g); - } + std::iter::once(unexpanded_pc_claim) + .chain(std::iter::once(imm_claim)) + .chain(circuit_flag_claims) + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() + } - // Stage 4: gamma_powers[3] * stage4_gammas[i] - for g in &self.stage4_gammas { - challenges.push(self.gamma_powers[3] * *g); - } + fn compute_rv_claim_2( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, jump_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::Jump), + SumcheckId::SpartanProductVirtualization, + ); + let (_, branch_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::Branch), + SumcheckId::SpartanProductVirtualization, + ); + let (_, write_lookup_output_to_rd_flag_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::WriteLookupOutputToRD), + SumcheckId::SpartanProductVirtualization, + ); + let (_, virtual_instruction_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + SumcheckId::SpartanProductVirtualization, + ); - // Stage 5: gamma_powers[4] * stage5_gammas[i] - for g in &self.stage5_gammas { - challenges.push(self.gamma_powers[4] * *g); - } + [ + jump_claim, + branch_claim, + write_lookup_output_to_rd_flag_claim, + virtual_instruction_claim, + ] + .into_iter() + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() + } - // raf_claim: gamma_powers[5] - challenges.push(self.gamma_powers[5]); + fn compute_rv_claim_3( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, imm_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::Imm, + SumcheckId::InstructionInputVirtualization, + ); + let (_, spartan_shift_unexpanded_pc_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::SpartanShift, + ); + let (_, instruction_input_unexpanded_pc_claim) = opening_accumulator + .get_virtual_polynomial_opening( + VirtualPolynomial::UnexpandedPC, + SumcheckId::InstructionInputVirtualization, + ); - // raf_shift_claim: gamma_powers[6] - challenges.push(self.gamma_powers[6]); + assert_eq!( + spartan_shift_unexpanded_pc_claim, + instruction_input_unexpanded_pc_claim + ); - // entry constraint - challenges.push(self.entry_gamma); + let unexpanded_pc_claim = spartan_shift_unexpanded_pc_claim; + let (_, left_is_rs1_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsRs1Value), + SumcheckId::InstructionInputVirtualization, + ); + let (_, left_is_pc_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::LeftOperandIsPC), + SumcheckId::InstructionInputVirtualization, + ); + let (_, right_is_rs2_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsRs2Value), + SumcheckId::InstructionInputVirtualization, + ); + let (_, right_is_imm_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::RightOperandIsImm), + SumcheckId::InstructionInputVirtualization, + ); + let (_, is_noop_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionFlags(InstructionFlags::IsNoop), + SumcheckId::SpartanShift, + ); + let (_, is_virtual_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::VirtualInstruction), + SumcheckId::SpartanShift, + ); + let (_, is_first_in_sequence_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::OpFlags(CircuitFlags::IsFirstInSequence), + SumcheckId::SpartanShift, + ); - challenges + [ + imm_claim, + unexpanded_pc_claim, + left_is_rs1_claim, + left_is_pc_claim, + right_is_rs2_claim, + right_is_imm_claim, + is_noop_claim, + is_virtual_claim, + is_first_in_sequence_claim, + ] + .into_iter() + .zip_eq(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum() } - #[cfg(feature = "zk")] - fn output_claim_constraint(&self) -> Option { - let factors: Vec = (0..self.d) - .map(|i| { - let opening = OpeningId::committed( - CommittedPolynomial::BytecodeRa(i), - SumcheckId::BytecodeReadRaf, - ); - ValueSource::Opening(opening) + fn compute_rv_claim_4( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + std::iter::empty() + .chain(once(VirtualPolynomial::RdWa)) + .chain(once(VirtualPolynomial::Rs1Ra)) + .chain(once(VirtualPolynomial::Rs2Ra)) + .map(|vp| { + opening_accumulator + .get_virtual_polynomial_opening(vp, SumcheckId::RegistersReadWriteChecking) + .1 }) - .collect(); - - let terms = vec![ProductTerm::scaled(ValueSource::Challenge(0), factors)]; - - Some(OutputClaimConstraint::sum_of_products(terms)) + .zip(gamma_powers) + .map(|(claim, gamma)| claim * gamma) + .sum::() } - #[cfg(feature = "zk")] - fn output_constraint_challenge_values(&self, sumcheck_challenges: &[F::Challenge]) -> Vec { - let opening_point = self.normalize_opening_point(sumcheck_challenges); - let (r_address_prime, r_cycle_prime) = opening_point.split_at(self.log_K); + fn compute_rv_claim_5( + opening_accumulator: &dyn OpeningAccumulator, + gamma_powers: &[F], + ) -> F { + let (_, rd_wa_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::RdWa, + SumcheckId::RegistersValEvaluation, + ); - // Prover stores bound values before clearing polys; verifier evaluates directly - let val: F = if let Some(bound_val_polys) = &self.bound_val_polys { - bound_val_polys - .iter() - .zip(&self.r_cycles) - .zip(&self.gamma_powers) - .map(|((bound_val, r_cycle), gamma)| { - *bound_val * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) * gamma - }) - .sum() - } else { - let int_poly = self.int_poly.evaluate(&r_address_prime.r); - self.val_polys - .iter() - .zip(&self.r_cycles) - .zip(&self.gamma_powers) - .zip([ - int_poly * self.gamma_powers[5], - F::zero(), - int_poly * self.gamma_powers[4], - F::zero(), - F::zero(), - ]) - .map(|(((val, r_cycle), gamma), int_poly_contrib)| { - (val.evaluate(&r_address_prime.r) + int_poly_contrib) - * EqPolynomial::::mle(r_cycle, &r_cycle_prime.r) - * gamma - }) - .sum() - }; + let (_, raf_flag_claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::InstructionRafFlag, + SumcheckId::InstructionReadRaf, + ); - // r_address_prime.r is MSB-first (after normalize_opening_point reversal), - // so entry_bits must also be MSB-first: entry_bits[j] = (e >> (log_K-1-j)) & 1. - let f_entry_at_r_addr = if let Some(v) = self.bound_f_entry { - v - } else { - let log_k = self.log_K; - let e = self.entry_bytecode_index; - let entry_bits: Vec = (0..log_k) - .map(|i| F::from_u64(((e >> (log_k - 1 - i)) & 1) as u64)) - .collect(); - EqPolynomial::::mle(&entry_bits, &r_address_prime.r) - }; - // eq_zero(r_cycle) = ∏_i (1 - r_cycle_prime.r[i]) - let zeros: Vec = vec![F::Challenge::default(); r_cycle_prime.r.len()]; - let eq_zero_at_r_cycle = EqPolynomial::::mle(&zeros, &r_cycle_prime.r); - let entry_contrib = self.entry_gamma * f_entry_at_r_addr * eq_zero_at_r_cycle; + let mut sum = rd_wa_claim * gamma_powers[0]; + sum += raf_flag_claim * gamma_powers[1]; - vec![val + entry_contrib] + // Add lookup table flag claims from InstructionReadRaf + for i in 0..LookupTables::::COUNT { + let (_, claim) = opening_accumulator.get_virtual_polynomial_opening( + VirtualPolynomial::LookupTableFlag(i), + SumcheckId::InstructionReadRaf, + ); + sum += claim * gamma_powers[2 + i]; + } + + sum } } diff --git a/jolt-core/src/zkvm/claim_reductions/advice.rs b/jolt-core/src/zkvm/claim_reductions/advice.rs index 56b8668ab0..d6f9aea87e 100644 --- a/jolt-core/src/zkvm/claim_reductions/advice.rs +++ b/jolt-core/src/zkvm/claim_reductions/advice.rs @@ -569,17 +569,8 @@ impl SumcheckInstanceProver for AdviceClaimRe } } - fn round_offset(&self, max_num_rounds: usize) -> usize { - match self.params.phase { - ReductionPhase::CycleVariables => { - // Align to the *start* of Booleanity's cycle segment, so local rounds correspond - // to low Dory column bits in the unified point ordering. - let booleanity_rounds = self.params.log_k_chunk + self.params.log_t; - let booleanity_offset = max_num_rounds - booleanity_rounds; - booleanity_offset + self.params.log_k_chunk - } - ReductionPhase::AddressVariables => 0, - } + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } #[cfg(feature = "allocative")] @@ -672,16 +663,8 @@ impl> } } - fn round_offset(&self, max_num_rounds: usize) -> usize { - let params = self.params.borrow(); - match params.phase { - ReductionPhase::CycleVariables => { - let booleanity_rounds = params.log_k_chunk + params.log_t; - let booleanity_offset = max_num_rounds - booleanity_rounds; - booleanity_offset + params.log_k_chunk - } - ReductionPhase::AddressVariables => 0, - } + fn round_offset(&self, _max_num_rounds: usize) -> usize { + 0 } } diff --git a/jolt-core/src/zkvm/instruction/ebreak.rs b/jolt-core/src/zkvm/instruction/ebreak.rs index 8b0d7d75b8..ecd1cab5ce 100644 --- a/jolt-core/src/zkvm/instruction/ebreak.rs +++ b/jolt-core/src/zkvm/instruction/ebreak.rs @@ -1,28 +1,6 @@ -use crate::zkvm::instruction::NUM_INSTRUCTION_FLAGS; use tracer::instruction::{ebreak::EBREAK, RISCVCycle}; -use crate::zkvm::lookup_table::LookupTables; - -use super::{CircuitFlags, Flags, InstructionLookup, LookupQuery, NUM_CIRCUIT_FLAGS}; - -impl InstructionLookup for EBREAK { - fn lookup_table(&self) -> Option> { - None - } -} - -impl Flags for EBREAK { - fn circuit_flags(&self) -> [bool; NUM_CIRCUIT_FLAGS] { - let mut flags = [false; NUM_CIRCUIT_FLAGS]; - flags[CircuitFlags::IsFirstInSequence] = self.is_first_in_sequence; - flags[CircuitFlags::IsCompressed] = self.is_compressed; - flags - } - - fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS] { - [false; NUM_INSTRUCTION_FLAGS] - } -} +use super::LookupQuery; impl LookupQuery for RISCVCycle { fn to_instruction_inputs(&self) -> (u64, i128) { diff --git a/jolt-core/src/zkvm/instruction/ecall.rs b/jolt-core/src/zkvm/instruction/ecall.rs index d6025a42e0..9d28ef2c17 100644 --- a/jolt-core/src/zkvm/instruction/ecall.rs +++ b/jolt-core/src/zkvm/instruction/ecall.rs @@ -1,28 +1,6 @@ -use crate::zkvm::instruction::NUM_INSTRUCTION_FLAGS; use tracer::instruction::{ecall::ECALL, RISCVCycle}; -use crate::zkvm::lookup_table::LookupTables; - -use super::{CircuitFlags, Flags, InstructionLookup, LookupQuery, NUM_CIRCUIT_FLAGS}; - -impl InstructionLookup for ECALL { - fn lookup_table(&self) -> Option> { - None - } -} - -impl Flags for ECALL { - fn circuit_flags(&self) -> [bool; NUM_CIRCUIT_FLAGS] { - let mut flags = [false; NUM_CIRCUIT_FLAGS]; - flags[CircuitFlags::IsFirstInSequence] = self.is_first_in_sequence; - flags[CircuitFlags::IsCompressed] = self.is_compressed; - flags - } - - fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS] { - [false; NUM_INSTRUCTION_FLAGS] - } -} +use super::LookupQuery; impl LookupQuery for RISCVCycle { fn to_instruction_inputs(&self) -> (u64, i128) { diff --git a/jolt-core/src/zkvm/instruction/mod.rs b/jolt-core/src/zkvm/instruction/mod.rs index c4bc9c2b08..ec498021a3 100644 --- a/jolt-core/src/zkvm/instruction/mod.rs +++ b/jolt-core/src/zkvm/instruction/mod.rs @@ -1,11 +1,10 @@ use std::ops::{Index, IndexMut}; use allocative::Allocative; -use common::constants::XLEN; use jolt_riscv::{ CircuitFlagSet as RiscvCircuitFlagSet, Flags as RiscvFlags, - InstructionFlagSet as RiscvInstructionFlagSet, InstructionKind, JoltInstructions, - NormalizedInstruction, + InstructionFlagSet as RiscvInstructionFlagSet, JoltInstruction, JoltInstructionKind, + JoltInstructionRow, SourceInstructionKind, }; use strum::EnumCount; use strum_macros::{EnumCount as EnumCountMacro, EnumIter, FromRepr}; @@ -47,6 +46,44 @@ pub trait LookupQuery { fn to_lookup_output(&self) -> u64; } +/// Proof-facing view of a tracer cycle whose instruction is backed by a final +/// Jolt bytecode row. +/// +/// A tracer [`Cycle`] still owns dynamic witness data such as register reads, +/// RAM accesses, lookup operands, and lookup outputs. Static proof metadata such +/// as circuit flags, instruction flags, and lookup-table routing must come from +/// the final [`JoltInstructionRow`] stored here. Constructing this adapter is the +/// phase-boundary check: decoded source-only instructions are rejected before +/// proving code can ask proof-metadata questions about them. +#[derive(Clone, Copy, Debug)] +pub struct JoltTraceCycle<'a> { + cycle: &'a Cycle, + instruction: JoltInstructionRow, +} + +impl<'a> JoltTraceCycle<'a> { + #[inline(always)] + pub fn try_new(cycle: &'a Cycle) -> Result { + let instruction = cycle.instruction().try_jolt_instruction_row()?; + Ok(Self { cycle, instruction }) + } + + #[inline(always)] + pub fn cycle(&self) -> &'a Cycle { + self.cycle + } + + #[inline(always)] + pub fn instruction(&self) -> &JoltInstructionRow { + &self.instruction + } + + #[inline(always)] + pub fn into_instruction(self) -> JoltInstructionRow { + self.instruction + } +} + /// Boolean flags used in Jolt's R1CS constraints (`opflags` in the Jolt paper). /// Note that the flags below deviate somewhat from those described in Appendix A.1 /// of the Jolt paper. @@ -175,117 +212,193 @@ pub trait Flags { fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS]; } -impl Flags for NormalizedInstruction { +impl Flags for JoltInstructionRow { + #[expect( + clippy::expect_used, + reason = "JoltInstructionRow is generated from the final JoltInstruction universe" + )] + #[inline(always)] fn circuit_flags(&self) -> [bool; NUM_CIRCUIT_FLAGS] { - JoltInstructions::try_from(*self) + JoltInstruction::try_from(*self) .map(|instruction| circuit_flags_from_riscv(instruction.circuit_flags())) - .unwrap_or([false; NUM_CIRCUIT_FLAGS]) + .expect("JoltInstructionRow kinds are exhaustive over JoltInstruction") } + #[expect( + clippy::expect_used, + reason = "JoltInstructionRow is generated from the final JoltInstruction universe" + )] + #[inline(always)] fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS] { - JoltInstructions::try_from(*self) + JoltInstruction::try_from(*self) .map(|instruction| instruction_flags_from_riscv(instruction.instruction_flags())) - .unwrap_or([false; NUM_INSTRUCTION_FLAGS]) + .expect("JoltInstructionRow kinds are exhaustive over JoltInstruction") } } -impl InstructionLookup for NormalizedInstruction { +impl InstructionLookup for JoltInstructionRow { + #[inline(always)] fn lookup_table(&self) -> Option> { Some(match self.instruction_kind { - InstructionKind::ADD - | InstructionKind::ADDI - | InstructionKind::AUIPC - | InstructionKind::JAL - | InstructionKind::LUI - | InstructionKind::MUL - | InstructionKind::SUB - | InstructionKind::VirtualAdvice - | InstructionKind::VirtualAdviceLen - | InstructionKind::VirtualAdviceLoad - | InstructionKind::VirtualMULI => LookupTables::RangeCheck(Default::default()), - InstructionKind::JALR => LookupTables::RangeCheckAligned(Default::default()), - InstructionKind::AND | InstructionKind::ANDI => LookupTables::And(Default::default()), - InstructionKind::ANDN => LookupTables::Andn(Default::default()), - InstructionKind::OR | InstructionKind::ORI => LookupTables::Or(Default::default()), - InstructionKind::XOR | InstructionKind::XORI => LookupTables::Xor(Default::default()), - InstructionKind::BEQ | InstructionKind::VirtualAssertEQ => { + JoltInstructionKind::ADD + | JoltInstructionKind::ADDI + | JoltInstructionKind::AUIPC + | JoltInstructionKind::JAL + | JoltInstructionKind::LUI + | JoltInstructionKind::MUL + | JoltInstructionKind::SUB + | JoltInstruction::VirtualAdvice(_) + | JoltInstruction::VirtualAdviceLen(_) + | JoltInstruction::VirtualAdviceLoad(_) + | JoltInstructionKind::VirtualMULI => LookupTables::RangeCheck(Default::default()), + JoltInstructionKind::JALR => LookupTables::RangeCheckAligned(Default::default()), + JoltInstructionKind::AND | JoltInstructionKind::ANDI => { + LookupTables::And(Default::default()) + } + JoltInstructionKind::ANDN => LookupTables::Andn(Default::default()), + JoltInstructionKind::OR | JoltInstructionKind::ORI => { + LookupTables::Or(Default::default()) + } + JoltInstructionKind::XOR | JoltInstructionKind::XORI => { + LookupTables::Xor(Default::default()) + } + JoltInstructionKind::BEQ | JoltInstructionKind::VirtualAssertEQ => { LookupTables::Equal(Default::default()) } - InstructionKind::BGE => LookupTables::SignedGreaterThanEqual(Default::default()), - InstructionKind::BGEU => LookupTables::UnsignedGreaterThanEqual(Default::default()), - InstructionKind::BNE => LookupTables::NotEqual(Default::default()), - InstructionKind::BLT | InstructionKind::SLT | InstructionKind::SLTI => { + JoltInstructionKind::BGE => LookupTables::SignedGreaterThanEqual(Default::default()), + JoltInstructionKind::BGEU => LookupTables::UnsignedGreaterThanEqual(Default::default()), + JoltInstructionKind::BNE => LookupTables::NotEqual(Default::default()), + JoltInstructionKind::BLT | JoltInstructionKind::SLT | JoltInstructionKind::SLTI => { LookupTables::SignedLessThan(Default::default()) } - InstructionKind::BLTU | InstructionKind::SLTU | InstructionKind::SLTIU => { + JoltInstructionKind::BLTU | JoltInstructionKind::SLTU | JoltInstructionKind::SLTIU => { LookupTables::UnsignedLessThan(Default::default()) } - InstructionKind::VirtualMovsign => LookupTables::Movsign(Default::default()), - InstructionKind::MULHU => LookupTables::UpperWord(Default::default()), - InstructionKind::VirtualAssertLTE => LookupTables::LessThanEqual(Default::default()), - InstructionKind::VirtualAssertValidUnsignedRemainder => { + JoltInstructionKind::VirtualMovsign => LookupTables::Movsign(Default::default()), + JoltInstructionKind::MULHU => LookupTables::UpperWord(Default::default()), + JoltInstructionKind::VirtualAssertLTE => { + LookupTables::LessThanEqual(Default::default()) + } + JoltInstructionKind::VirtualAssertValidUnsignedRemainder => { LookupTables::ValidUnsignedRemainder(Default::default()) } - InstructionKind::VirtualAssertValidDiv0 => LookupTables::ValidDiv0(Default::default()), - InstructionKind::VirtualAssertHalfwordAlignment => { + JoltInstructionKind::VirtualAssertValidDiv0 => { + LookupTables::ValidDiv0(Default::default()) + } + JoltInstructionKind::VirtualAssertHalfwordAlignment => { LookupTables::HalfwordAlignment(Default::default()) } - InstructionKind::VirtualAssertWordAlignment => { + JoltInstructionKind::VirtualAssertWordAlignment => { LookupTables::WordAlignment(Default::default()) } - InstructionKind::VirtualZeroExtendWord => { + JoltInstruction::VirtualZeroExtendWord(_) => { LookupTables::LowerHalfWord(Default::default()) } - InstructionKind::VirtualSignExtendWord => { + JoltInstruction::VirtualSignExtendWord(_) => { LookupTables::SignExtendHalfWord(Default::default()) } - InstructionKind::VirtualPow2 | InstructionKind::VirtualPow2I => { + JoltInstructionKind::VirtualPow2 | JoltInstructionKind::VirtualPow2I => { LookupTables::Pow2(Default::default()) } - InstructionKind::VirtualPow2W | InstructionKind::VirtualPow2IW => { + JoltInstructionKind::VirtualPow2W | JoltInstructionKind::VirtualPow2IW => { LookupTables::Pow2W(Default::default()) } - InstructionKind::VirtualShiftRightBitmask - | InstructionKind::VirtualShiftRightBitmaskI => { + JoltInstruction::VirtualShiftRightBitmask(_) + | JoltInstructionKind::VirtualShiftRightBitmaskI => { LookupTables::ShiftRightBitmask(Default::default()) } - InstructionKind::VirtualRev8W => LookupTables::VirtualRev8W(Default::default()), - InstructionKind::VirtualSRL | InstructionKind::VirtualSRLI => { + JoltInstruction::VirtualRev8W(_) => LookupTables::VirtualRev8W(Default::default()), + JoltInstructionKind::VirtualSRL | JoltInstructionKind::VirtualSRLI => { LookupTables::VirtualSRL(Default::default()) } - InstructionKind::VirtualSRA | InstructionKind::VirtualSRAI => { + JoltInstructionKind::VirtualSRA | JoltInstructionKind::VirtualSRAI => { LookupTables::VirtualSRA(Default::default()) } - InstructionKind::VirtualROTRI => LookupTables::VirtualROTR(Default::default()), - InstructionKind::VirtualROTRIW => LookupTables::VirtualROTRW(Default::default()), - InstructionKind::VirtualChangeDivisor => { + JoltInstructionKind::VirtualROTRI => LookupTables::VirtualROTR(Default::default()), + JoltInstructionKind::VirtualROTRIW => LookupTables::VirtualROTRW(Default::default()), + JoltInstruction::VirtualChangeDivisor(_) => { LookupTables::VirtualChangeDivisor(Default::default()) } - InstructionKind::VirtualChangeDivisorW => { + JoltInstruction::VirtualChangeDivisorW(_) => { LookupTables::VirtualChangeDivisorW(Default::default()) } - InstructionKind::VirtualAssertMulUNoOverflow => { + JoltInstructionKind::VirtualAssertMulUNoOverflow => { LookupTables::MulUNoOverflow(Default::default()) } - InstructionKind::VirtualXORROT32 => LookupTables::VirtualXORROT32(Default::default()), - InstructionKind::VirtualXORROT24 => LookupTables::VirtualXORROT24(Default::default()), - InstructionKind::VirtualXORROT16 => LookupTables::VirtualXORROT16(Default::default()), - InstructionKind::VirtualXORROT63 => LookupTables::VirtualXORROT63(Default::default()), - InstructionKind::VirtualXORROTW16 => LookupTables::VirtualXORROTW16(Default::default()), - InstructionKind::VirtualXORROTW12 => LookupTables::VirtualXORROTW12(Default::default()), - InstructionKind::VirtualXORROTW8 => LookupTables::VirtualXORROTW8(Default::default()), - InstructionKind::VirtualXORROTW7 => LookupTables::VirtualXORROTW7(Default::default()), - InstructionKind::LD - | InstructionKind::SD - | InstructionKind::EBREAK - | InstructionKind::ECALL - | InstructionKind::FENCE - | InstructionKind::VirtualHostIO => return None, - _ => return None, + JoltInstructionKind::VirtualXORROT32 => { + LookupTables::VirtualXORROT32(Default::default()) + } + JoltInstructionKind::VirtualXORROT24 => { + LookupTables::VirtualXORROT24(Default::default()) + } + JoltInstructionKind::VirtualXORROT16 => { + LookupTables::VirtualXORROT16(Default::default()) + } + JoltInstructionKind::VirtualXORROT63 => { + LookupTables::VirtualXORROT63(Default::default()) + } + JoltInstructionKind::VirtualXORROTW16 => { + LookupTables::VirtualXORROTW16(Default::default()) + } + JoltInstructionKind::VirtualXORROTW12 => { + LookupTables::VirtualXORROTW12(Default::default()) + } + JoltInstructionKind::VirtualXORROTW8 => { + LookupTables::VirtualXORROTW8(Default::default()) + } + JoltInstructionKind::VirtualXORROTW7 => { + LookupTables::VirtualXORROTW7(Default::default()) + } + JoltInstructionKind::NoOp + | JoltInstructionKind::LD + | JoltInstructionKind::SD + | JoltInstructionKind::FENCE + | JoltInstruction::VirtualHostIO(_) => return None, }) } } +impl InstructionLookup for JoltTraceCycle<'_> { + #[inline(always)] + fn lookup_table(&self) -> Option> { + self.instruction.lookup_table() + } +} + +impl Flags for JoltTraceCycle<'_> { + #[inline(always)] + fn circuit_flags(&self) -> [bool; NUM_CIRCUIT_FLAGS] { + self.instruction.circuit_flags() + } + + #[inline(always)] + fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS] { + self.instruction.instruction_flags() + } +} + +impl LookupQuery for JoltTraceCycle<'_> { + #[inline(always)] + fn to_instruction_inputs(&self) -> (u64, i128) { + LookupQuery::::to_instruction_inputs(self.cycle) + } + + #[inline(always)] + fn to_lookup_index(&self) -> u128 { + LookupQuery::::to_lookup_index(self.cycle) + } + + #[inline(always)] + fn to_lookup_operands(&self) -> (u64, u128) { + LookupQuery::::to_lookup_operands(self.cycle) + } + + #[inline(always)] + fn to_lookup_output(&self) -> u64 { + LookupQuery::::to_lookup_output(self.cycle) + } +} + fn circuit_flags_from_riscv(flags: RiscvCircuitFlagSet) -> [bool; NUM_CIRCUIT_FLAGS] { let mut converted = [false; NUM_CIRCUIT_FLAGS]; for (index, value) in converted.iter_mut().enumerate() { @@ -302,59 +415,10 @@ fn instruction_flags_from_riscv(flags: RiscvInstructionFlagSet) -> [bool; NUM_IN converted } -macro_rules! define_rv32im_trait_impls { +macro_rules! define_rv64imac_trait_impls { ( instructions: [$($instr:ident),* $(,)?] ) => { - impl InstructionLookup for Instruction { - fn lookup_table(&self) -> Option> { - match self { - Instruction::NoOp => None, - $( - Instruction::$instr(instr) => instr.lookup_table(), - )* - Instruction::UNIMPL => None, - _ => panic!("Unexpected instruction: {:?}", self), - } - } - } - - impl Flags for Instruction { - fn circuit_flags(&self) -> [bool; NUM_CIRCUIT_FLAGS] { - let mut flags = match self { - Instruction::NoOp => { - let mut flags = [false; NUM_CIRCUIT_FLAGS]; - flags[CircuitFlags::DoNotUpdateUnexpandedPC] = true; - flags - }, - $( - Instruction::$instr(instr) => instr.circuit_flags(), - )* - Instruction::UNIMPL => [false; NUM_CIRCUIT_FLAGS], - _ => panic!("Unexpected instruction: {:?}", self), - }; - if self.normalize().virtual_sequence_remaining == Some(0) { - flags[CircuitFlags::IsLastInSequence] = true; - } - flags - } - - fn instruction_flags(&self) -> [bool; NUM_INSTRUCTION_FLAGS] { - match self { - Instruction::NoOp => { - let mut flags = [false; NUM_INSTRUCTION_FLAGS]; - flags[InstructionFlags::IsNoop] = true; - flags - }, - $( - Instruction::$instr(instr) => instr.instruction_flags(), - )* - Instruction::UNIMPL => [false; NUM_INSTRUCTION_FLAGS], - _ => panic!("Unexpected instruction: {:?}", self), - } - } - } - impl SupportedInstruction for Instruction { fn is_supported_instruction(&self) -> bool { match self { @@ -366,18 +430,6 @@ macro_rules! define_rv32im_trait_impls { } } - impl InstructionLookup for Cycle { - fn lookup_table(&self) -> Option> { - match self { - Cycle::NoOp => None, - $( - Cycle::$instr(cycle) => cycle.instruction.lookup_table(), - )* - _ => panic!("Unexpected instruction: {:?}", self), - } - } - } - impl LookupQuery for Cycle { fn to_instruction_inputs(&self) -> (u64, i128) { match self { @@ -422,7 +474,7 @@ macro_rules! define_rv32im_trait_impls { }; } -define_rv32im_trait_impls! { +define_rv64imac_trait_impls! { instructions: [ ADD, ADDI, AND, ANDI, ANDN, AUIPC, BEQ, BGE, BGEU, BLT, BLTU, BNE, EBREAK, ECALL, FENCE, JAL, JALR, LUI, LD, MUL, MULHU, OR, ORI, diff --git a/jolt-core/src/zkvm/instruction/test.rs b/jolt-core/src/zkvm/instruction/test.rs index 708b007641..f7dce38a9e 100644 --- a/jolt-core/src/zkvm/instruction/test.rs +++ b/jolt-core/src/zkvm/instruction/test.rs @@ -1,11 +1,11 @@ use crate::{field::JoltField, zkvm::instruction::LookupQuery}; use common::constants::XLEN; +use jolt_riscv::JoltInstructionRowData; use rand::prelude::*; use tracer::{ emulator::{cpu::Cpu, terminal::DummyTerminal}, instruction::{ - self, format::InstructionRegisterState, Cycle, NormalizedInstruction, RISCVCycle, - RISCVInstruction, RISCVTrace, + self, format::InstructionRegisterState, Cycle, RISCVCycle, RISCVInstruction, RISCVTrace, }, }; @@ -33,119 +33,109 @@ where /// Test that certain combinations of circuit flags are exclusive. mod flags { - use std::panic; - - use crate::zkvm::instruction::{Flags, InstructionFlags, SupportedInstruction}; + use crate::zkvm::instruction::{Flags, InstructionFlags, JoltTraceCycle}; use super::CircuitFlags; - use jolt_riscv::{InstructionKind, NormalizedInstruction, NormalizedOperands}; + use jolt_riscv::{JoltInstructionKind, JoltInstructionRow, NormalizedOperands}; use strum::IntoEnumIterator; - use tracer::instruction::{Cycle, Instruction}; + use tracer::instruction::Cycle; #[test] fn left_operand_exclusive() { for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; - } - let instr = cycle.instruction(); - if let Ok(flags) = panic::catch_unwind(|| instr.instruction_flags()) { - assert!( - !(flags[InstructionFlags::LeftOperandIsPC] - && flags[InstructionFlags::LeftOperandIsRs1Value]), - "Left operand flags not exclusive for {instr:?}", - ); - } + }; + let flags = jolt_cycle.instruction_flags(); + assert!( + !(flags[InstructionFlags::LeftOperandIsPC] + && flags[InstructionFlags::LeftOperandIsRs1Value]), + "Left operand flags not exclusive for {:?}", + jolt_cycle.instruction(), + ); } } #[test] fn right_operand_exclusive() { for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; - } - let instr = cycle.instruction(); - if let Ok(flags) = panic::catch_unwind(|| instr.instruction_flags()) { - assert!( - !(flags[InstructionFlags::RightOperandIsRs2Value] - && flags[InstructionFlags::RightOperandIsImm]), - "Right operand flags not exclusive for {instr:?}", - ); - } + }; + let flags = jolt_cycle.instruction_flags(); + assert!( + !(flags[InstructionFlags::RightOperandIsRs2Value] + && flags[InstructionFlags::RightOperandIsImm]), + "Right operand flags not exclusive for {:?}", + jolt_cycle.instruction(), + ); } } #[test] fn lookup_shape_exclusive() { for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; - } - let instr = cycle.instruction(); - if let Ok(flags) = panic::catch_unwind(|| instr.circuit_flags()) { - let num_true = [ - flags[CircuitFlags::AddOperands], - flags[CircuitFlags::SubtractOperands], - flags[CircuitFlags::MultiplyOperands], - flags[CircuitFlags::Advice], - ] - .iter() - .filter(|&&b| b) - .count(); - assert!( - num_true <= 1, - "Lookup shaping flags not exclusive for {instr:?}", - ); - } + }; + let flags = jolt_cycle.circuit_flags(); + let num_true = [ + flags[CircuitFlags::AddOperands], + flags[CircuitFlags::SubtractOperands], + flags[CircuitFlags::MultiplyOperands], + flags[CircuitFlags::Advice], + ] + .iter() + .filter(|&&b| b) + .count(); + assert!( + num_true <= 1, + "Lookup shaping flags not exclusive for {:?}", + jolt_cycle.instruction(), + ); } } #[test] fn load_store_exclusive() { for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; - } - let instr = cycle.instruction(); - if let Ok(flags) = panic::catch_unwind(|| instr.circuit_flags()) { - assert!( - !(flags[CircuitFlags::Load] && flags[CircuitFlags::Store]), - "Load/Store flags not exclusive for {instr:?}", - ); - } + }; + let flags = jolt_cycle.circuit_flags(); + assert!( + !(flags[CircuitFlags::Load] && flags[CircuitFlags::Store]), + "Load/Store flags not exclusive for {:?}", + jolt_cycle.instruction(), + ); } } #[test] - fn normalized_flags_match_concrete_instruction_flags() { + fn jolt_trace_cycle_flags_match_final_row_flags() { for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; - } - let instr = cycle.instruction(); - if !instr.is_supported_instruction() { - continue; - } - - let normalized = instr.normalize(); + }; assert_eq!( - normalized.circuit_flags(), - instr.circuit_flags(), - "circuit flags differ for {instr:?}" + jolt_cycle.instruction().circuit_flags(), + jolt_cycle.circuit_flags(), + "circuit flags differ for {:?}", + jolt_cycle.instruction(), ); assert_eq!( - normalized.instruction_flags(), - instr.instruction_flags(), - "instruction flags differ for {instr:?}" + jolt_cycle.instruction().instruction_flags(), + jolt_cycle.instruction_flags(), + "instruction flags differ for {:?}", + jolt_cycle.instruction(), ); } } #[test] - fn concrete_terminal_virtual_flags_match_normalized_flags() { - let normalized = NormalizedInstruction { - instruction_kind: InstructionKind::ADDI, + fn terminal_virtual_flags_are_final_row_metadata() { + let normalized = JoltInstructionRow { + instruction_kind: JoltInstructionKind::ADDI, address: 0x8000_0000, operands: NormalizedOperands { rd: Some(1), @@ -157,11 +147,7 @@ mod flags { is_first_in_sequence: false, is_compressed: false, }; - let concrete = Instruction::try_from_normalized(normalized) - .expect("ADDI should convert from normalized form"); - - assert_eq!(normalized.circuit_flags(), concrete.circuit_flags()); - assert!(concrete.circuit_flags()[CircuitFlags::IsLastInSequence]); + assert!(normalized.circuit_flags()[CircuitFlags::IsLastInSequence]); } #[cfg(feature = "host")] @@ -171,18 +157,8 @@ mod flags { let (bytecode, _, _, _) = program.decode(); for normalized in bytecode { - let concrete = Instruction::try_from_normalized(normalized) - .expect("expanded bytecode should convert to a concrete instruction"); - assert_eq!( - normalized.circuit_flags(), - concrete.circuit_flags(), - "circuit flags differ for {normalized:?}" - ); - assert_eq!( - normalized.instruction_flags(), - concrete.instruction_flags(), - "instruction flags differ for {normalized:?}" - ); + let _ = normalized.circuit_flags(); + let _ = normalized.instruction_flags(); } } @@ -192,18 +168,30 @@ mod flags { use common::constants::XLEN; for cycle in Cycle::iter() { - if let Cycle::INLINE(_) = cycle { + let Ok(jolt_cycle) = JoltTraceCycle::try_new(&cycle) else { continue; + }; + let instr_flags = jolt_cycle.instruction_flags(); + if instr_flags[InstructionFlags::Branch] { + let out = LookupQuery::::to_lookup_output(&jolt_cycle); + assert!( + out == 0 || out == 1, + "Branch lookup output not boolean for {:?}: got {out}", + jolt_cycle.instruction(), + ); } - let instr = cycle.instruction(); - if let Ok(instr_flags) = panic::catch_unwind(|| instr.instruction_flags()) { - if instr_flags[InstructionFlags::Branch] { - let out = LookupQuery::::to_lookup_output(&cycle); - assert!( - out == 0 || out == 1, - "Branch lookup output not boolean for {instr:?}: got {out}", - ); - } + } + } + + #[test] + fn source_only_cycles_are_not_jolt_trace_cycles() { + for cycle in Cycle::iter() { + let instruction = cycle.instruction(); + if instruction.try_jolt_instruction_row().is_err() { + assert!( + JoltTraceCycle::try_new(&cycle).is_err(), + "source-only instruction unexpectedly constructed a JoltTraceCycle: {instruction:?}", + ); } } } @@ -228,7 +216,9 @@ mod r1cs_consistency { use strum::IntoEnumIterator; use tracer::instruction::Cycle; - use crate::zkvm::instruction::{Flags, InstructionFlags, LookupQuery, SupportedInstruction}; + use crate::zkvm::instruction::{ + Flags, InstructionFlags, JoltTraceCycle, LookupQuery, SupportedInstruction, + }; #[test] fn instruction_inputs_match_constraint() { @@ -244,14 +234,18 @@ mod r1cs_consistency { if !default_instr.is_supported_instruction() { continue; } + if default_instr.try_jolt_instruction_row().is_err() { + continue; + } let variant: &'static str = (&default_instr).into(); let mut first_failure_for_variant: Option = None; for _ in 0..10_000 { let cycle = default_cycle.random(&mut rng); - let instr = cycle.instruction(); - let flags = instr.instruction_flags(); - let norm = instr.normalize(); + let jolt_cycle = JoltTraceCycle::try_new(&cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let flags = jolt_cycle.instruction_flags(); + let norm = jolt_cycle.instruction(); let rs1 = cycle.rs1_read().map(|(_, v)| v).unwrap_or(0); let rs2 = cycle.rs2_read().map(|(_, v)| v).unwrap_or(0); @@ -274,7 +268,7 @@ mod r1cs_consistency { }; let (left_actual, right_actual) = - LookupQuery::::to_instruction_inputs(&cycle); + LookupQuery::::to_instruction_inputs(&jolt_cycle); if left_actual != left_expected || right_actual != right_expected { first_failure_for_variant.get_or_insert_with(|| { @@ -305,14 +299,20 @@ mod r1cs_consistency { pub fn lookup_output_matches_trace_test() where - T: InstructionLookup + RISCVInstruction + RISCVTrace + Default + Flags + 'static, + T: InstructionLookup + + RISCVInstruction + + RISCVTrace + + Default + + Flags + + JoltInstructionRowData + + 'static, RISCVCycle: LookupQuery + Into, { let cycle: RISCVCycle = Default::default(); let mut rng = StdRng::seed_from_u64(12345); for _ in 0..10000 { let random_cycle = cycle.random(&mut rng); - let normalized_instr: NormalizedInstruction = random_cycle.instruction.into(); + let normalized_instr = random_cycle.instruction.jolt_instruction_row(); let normalized_operands = normalized_instr.operands; let mut cpu = Cpu::new(Box::new(DummyTerminal::default())); diff --git a/jolt-core/src/zkvm/instruction_lookups/read_raf_checking.rs b/jolt-core/src/zkvm/instruction_lookups/read_raf_checking.rs index 9d2b14e037..987fba2242 100644 --- a/jolt-core/src/zkvm/instruction_lookups/read_raf_checking.rs +++ b/jolt-core/src/zkvm/instruction_lookups/read_raf_checking.rs @@ -43,7 +43,9 @@ use crate::{ }, zkvm::{ config::{self, OneHotParams}, - instruction::{Flags, InstructionLookup, InterleavedBitsMarker, LookupQuery}, + instruction::{ + Flags, InstructionLookup, InterleavedBitsMarker, JoltTraceCycle, LookupQuery, + }, lookup_table::{ prefixes::{PrefixCheckpoint, PrefixEval, Prefixes}, suffixes::Suffixes, @@ -404,12 +406,12 @@ impl InstructionReadRafSumcheckProver { .par_iter() .enumerate() .map(|(idx, cycle)| { - let bits = LookupBits::new(LookupQuery::::to_lookup_index(cycle), LOG_K); - let is_interleaved = cycle - .instruction() - .circuit_flags() - .is_interleaved_operands(); - let table = cycle.lookup_table(); + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let bits = + LookupBits::new(LookupQuery::::to_lookup_index(&jolt_cycle), LOG_K); + let is_interleaved = jolt_cycle.circuit_flags().is_interleaved_operands(); + let table = jolt_cycle.lookup_table(); CycleData { idx, @@ -809,7 +811,9 @@ impl InstructionReadRafSumcheckProver { .zip(std::mem::take(&mut self.is_interleaved_operands)) .for_each(|((val, cycle), is_interleaved_operands)| { // Add lookup table value (Val_j(k)) - derive table from trace - if let Some(table) = cycle.lookup_table() { + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + if let Some(table) = jolt_cycle.lookup_table() { let t_idx = LookupTables::::enum_index(&table); *val += table_values_at_r_addr[t_idx]; } @@ -1246,17 +1250,15 @@ impl InstructionReadRafSumcheckProver { let e_lo_unreduced = E_lo[c_lo].to_unreduced(); // Accumulate table flag - if let Some(table) = cycle.lookup_table() { + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + if let Some(table) = jolt_cycle.lookup_table() { let t_idx = LookupTables::::enum_index(&table); local_flags[t_idx] += e_lo_unreduced; } // Accumulate RAF flag (identity = not interleaved) - if !cycle - .instruction() - .circuit_flags() - .is_interleaved_operands() - { + if !jolt_cycle.circuit_flags().is_interleaved_operands() { local_raf += e_lo_unreduced; } } @@ -1539,8 +1541,10 @@ mod tests { let mut right_operand_claim = Fr::zero(); for (i, cycle) in trace.iter().enumerate() { - let lookup_index = LookupQuery::::to_lookup_index(cycle); - let table: Option> = cycle.lookup_table(); + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let lookup_index = LookupQuery::::to_lookup_index(&jolt_cycle); + let table: Option> = jolt_cycle.lookup_table(); if let Some(table) = table { rv_claim += JoltField::mul_u64(&eq_r_cycle[i], table.materialize_entry(lookup_index)); diff --git a/jolt-core/src/zkvm/lookup_table/prefixes/sign_extension.rs b/jolt-core/src/zkvm/lookup_table/prefixes/sign_extension.rs index dc0cd15546..57e7d6a31e 100644 --- a/jolt-core/src/zkvm/lookup_table/prefixes/sign_extension.rs +++ b/jolt-core/src/zkvm/lookup_table/prefixes/sign_extension.rs @@ -27,11 +27,9 @@ impl SparseDensePrefix for SignExtensionPref let _ = b.pop_msb(); let (_, mut y) = b.uninterleave(); let mut result = F::zero(); - let mut index = 1; - for _ in 0..y.len() { + for index in 1..=y.len() { let y_i = y.pop_msb() as u64; result += F::from_u64((1 - y_i) << index); - index += 1; } return result * sign_bit; } @@ -39,11 +37,9 @@ impl SparseDensePrefix for SignExtensionPref let sign_bit = r_x.unwrap(); let (_, mut y) = b.uninterleave(); let mut result = F::zero(); - let mut index = 1; - for _ in 0..y.len() { + for index in 1..=y.len() { let y_i = y.pop_msb() as u64; result += F::from_u64((1 - y_i) << index); - index += 1; } return result * sign_bit; } diff --git a/jolt-core/src/zkvm/proof_serialization.rs b/jolt-core/src/zkvm/proof_serialization.rs index 2dea06bd78..71b17a1d69 100644 --- a/jolt-core/src/zkvm/proof_serialization.rs +++ b/jolt-core/src/zkvm/proof_serialization.rs @@ -48,7 +48,8 @@ pub struct JoltProof< pub stage3_sumcheck_proof: SumcheckInstanceProof, pub stage4_sumcheck_proof: SumcheckInstanceProof, pub stage5_sumcheck_proof: SumcheckInstanceProof, - pub stage6_sumcheck_proof: SumcheckInstanceProof, + pub stage6a_sumcheck_proof: SumcheckInstanceProof, + pub stage6b_sumcheck_proof: SumcheckInstanceProof, pub stage7_sumcheck_proof: SumcheckInstanceProof, #[cfg(feature = "zk")] pub blindfold_proof: BlindFoldProof, @@ -77,7 +78,8 @@ impl, PCS: CommitmentScheme, FS: Tr && self.stage3_sumcheck_proof.is_zk() == zk_mode && self.stage4_sumcheck_proof.is_zk() == zk_mode && self.stage5_sumcheck_proof.is_zk() == zk_mode - && self.stage6_sumcheck_proof.is_zk() == zk_mode + && self.stage6a_sumcheck_proof.is_zk() == zk_mode + && self.stage6b_sumcheck_proof.is_zk() == zk_mode && self.stage7_sumcheck_proof.is_zk() == zk_mode; if !consistent { @@ -403,6 +405,8 @@ impl CanonicalSerialize for VirtualPolynomial { 38u8.serialize_with_mode(&mut writer, compress)?; (u8::try_from(*flag).unwrap()).serialize_with_mode(&mut writer, compress) } + Self::BytecodeReadRafAddrClaim => 39u8.serialize_with_mode(&mut writer, compress), + Self::BooleanityAddrClaim => 40u8.serialize_with_mode(&mut writer, compress), } } @@ -442,7 +446,9 @@ impl CanonicalSerialize for VirtualPolynomial { | Self::RamValInit | Self::RamValFinal | Self::RamHammingWeight - | Self::UnivariateSkip => 1, + | Self::UnivariateSkip + | Self::BytecodeReadRafAddrClaim + | Self::BooleanityAddrClaim => 1, Self::InstructionRa(_) | Self::OpFlags(_) | Self::InstructionFlags(_) @@ -520,6 +526,8 @@ impl CanonicalDeserialize for VirtualPolynomial { let flag = u8::deserialize_with_mode(&mut reader, compress, validate)?; Self::LookupTableFlag(flag as usize) } + 39 => Self::BytecodeReadRafAddrClaim, + 40 => Self::BooleanityAddrClaim, _ => return Err(SerializationError::InvalidData), }, ) diff --git a/jolt-core/src/zkvm/prover.rs b/jolt-core/src/zkvm/prover.rs index 99bbab3450..253bd2ddcd 100644 --- a/jolt-core/src/zkvm/prover.rs +++ b/jolt-core/src/zkvm/prover.rs @@ -47,7 +47,10 @@ use crate::{ }, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckProver}, + booleanity::{ + BooleanityAddressSumcheckProver, BooleanityCycleInput, BooleanityCycleSumcheckProver, + BooleanitySumcheckParams, + }, streaming_schedule::LinearOnlySchedule, sumcheck::{BatchedSumcheck, SumcheckInstanceProof}, sumcheck_prover::SumcheckInstanceProver, @@ -99,7 +102,9 @@ use crate::{ use crate::{ poly::commitment::commitment_scheme::CommitmentScheme, zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckProver, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckProver, BytecodeReadRafCycleSumcheckProver, + }, fiat_shamir_preamble, instruction_lookups::{ ra_virtual::InstructionRaSumcheckProver as LookupsRaSumcheckProver, @@ -247,8 +252,11 @@ impl< // Count the cycle if the instruction is not part of a inline sequence // (`virtual_sequence_remaining` is `None`) or if it's the first instruction // in a inline sequence (`virtual_sequence_remaining` is `Some(0)`) - if let Some(virtual_sequence_remaining) = - cycle.instruction().normalize().virtual_sequence_remaining + if let Some(virtual_sequence_remaining) = cycle + .instruction() + .try_jolt_instruction_row() + .expect("trace cycle must be a final Jolt instruction row") + .virtual_sequence_remaining { if virtual_sequence_remaining > 0 { return 0; @@ -529,7 +537,10 @@ impl< let (stage3_sumcheck_proof, r_stage3) = self.prove_stage3(); let (stage4_sumcheck_proof, r_stage4) = self.prove_stage4(); let (stage5_sumcheck_proof, r_stage5) = self.prove_stage5(); - let (stage6_sumcheck_proof, r_stage6) = self.prove_stage6(); + let (stage6a_sumcheck_proof, bytecode_read_raf_params, booleanity_cycle_input) = + self.prove_stage6a(); + let (stage6b_sumcheck_proof, r_stage6) = + self.prove_stage6b(bytecode_read_raf_params, booleanity_cycle_input); let (stage7_sumcheck_proof, r_stage7) = self.prove_stage7(); let _sumcheck_challenges = [ @@ -576,7 +587,8 @@ impl< stage3_sumcheck_proof, stage4_sumcheck_proof, stage5_sumcheck_proof, - stage6_sumcheck_proof, + stage6a_sumcheck_proof, + stage6b_sumcheck_proof, stage7_sumcheck_proof, #[cfg(feature = "zk")] blindfold_proof, @@ -1215,14 +1227,15 @@ impl< } #[tracing::instrument(skip_all)] - fn prove_stage6( + fn prove_stage6a( &mut self, ) -> ( SumcheckInstanceProof, - Vec, + BytecodeReadRafSumcheckParams, + BooleanityCycleInput, ) { #[cfg(not(target_arch = "wasm32"))] - print_current_memory_usage("Stage 6 baseline"); + print_current_memory_usage("Stage 6a baseline"); let bytecode_read_raf_params = BytecodeReadRafSumcheckParams::gen( &self.preprocessing.shared.bytecode, @@ -1232,9 +1245,6 @@ impl< &mut self.transcript, ); - let ram_hamming_booleanity_params = - HammingBooleanitySumcheckParams::new(&self.opening_accumulator); - let booleanity_params = BooleanitySumcheckParams::new( self.trace.len().log_2(), &self.one_hot_params, @@ -1242,6 +1252,65 @@ impl< &mut self.transcript, ); + let mut bytecode_read_raf = BytecodeReadRafAddressSumcheckProver::initialize( + bytecode_read_raf_params.clone(), + Arc::clone(&self.trace), + Arc::clone(&self.preprocessing.shared.bytecode), + ); + let mut booleanity = BooleanityAddressSumcheckProver::initialize( + booleanity_params.clone(), + &self.trace, + &self.preprocessing.shared.bytecode, + &self.program_io.memory_layout, + ); + + #[cfg(feature = "allocative")] + { + print_data_structure_heap_usage( + "BytecodeReadRafAddressSumcheckProver", + &bytecode_read_raf, + ); + print_data_structure_heap_usage("BooleanityAddressSumcheckProver", &booleanity); + } + + let mut instances: Vec<&mut dyn SumcheckInstanceProver<_, _>> = + vec![&mut bytecode_read_raf, &mut booleanity]; + + #[cfg(feature = "allocative")] + write_instance_flamegraph_svg(&instances, "stage6a_start_flamechart.svg"); + tracing::info!("Stage 6a proving"); + + let (sumcheck_proof, _r_stage6a, _initial_claim) = + self.prove_batched_sumcheck(instances.iter_mut().map(|v| &mut **v as _).collect()); + + #[cfg(feature = "allocative")] + write_instance_flamegraph_svg(&instances, "stage6a_end_flamechart.svg"); + drop(instances); + + let booleanity_cycle_input = booleanity.into_cycle_input(); + + ( + sumcheck_proof, + bytecode_read_raf.into_params(), + booleanity_cycle_input, + ) + } + + #[tracing::instrument(skip_all)] + fn prove_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_cycle_input: BooleanityCycleInput, + ) -> ( + SumcheckInstanceProof, + Vec, + ) { + #[cfg(not(target_arch = "wasm32"))] + print_current_memory_usage("Stage 6b baseline"); + + let ram_hamming_booleanity_params = + HammingBooleanitySumcheckParams::new(&self.opening_accumulator); + let ram_ra_virtual_params = RamRaVirtualParams::new( self.trace.len(), &self.one_hot_params, @@ -1258,7 +1327,7 @@ impl< &mut self.transcript, ); - // Advice claim reduction (Phase 1 in Stage 6): trusted and untrusted are separate instances. + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.advice.trusted_advice_polynomial.is_some() { let trusted_advice_params = AdviceClaimReductionParams::new( AdviceKind::Trusted, @@ -1303,21 +1372,19 @@ impl< }; } - let mut bytecode_read_raf = BytecodeReadRafSumcheckProver::initialize( + let mut bytecode_read_raf = BytecodeReadRafCycleSumcheckProver::initialize( bytecode_read_raf_params, Arc::clone(&self.trace), Arc::clone(&self.preprocessing.shared.bytecode), + &self.opening_accumulator, + ); + let mut booleanity = BooleanityCycleSumcheckProver::initialize( + booleanity_cycle_input, + &self.opening_accumulator, ); let mut ram_hamming_booleanity = HammingBooleanitySumcheckProver::initialize(ram_hamming_booleanity_params, &self.trace); - let mut booleanity = BooleanitySumcheckProver::initialize( - booleanity_params, - &self.trace, - &self.preprocessing.shared.bytecode, - &self.program_io.memory_layout, - ); - let mut ram_ra_virtual = RamRaVirtualSumcheckProver::initialize( ram_ra_virtual_params, &self.trace, @@ -1331,8 +1398,11 @@ impl< #[cfg(feature = "allocative")] { - print_data_structure_heap_usage("BytecodeReadRafSumcheckProver", &bytecode_read_raf); - print_data_structure_heap_usage("BooleanitySumcheckProver", &booleanity); + print_data_structure_heap_usage( + "BytecodeReadRafCycleSumcheckProver", + &bytecode_read_raf, + ); + print_data_structure_heap_usage("BooleanityCycleSumcheckProver", &booleanity); print_data_structure_heap_usage( "ram HammingBooleanitySumcheckProver", &ram_hamming_booleanity, @@ -1367,13 +1437,13 @@ impl< } #[cfg(feature = "allocative")] - write_instance_flamegraph_svg(&instances, "stage6_start_flamechart.svg"); - tracing::info!("Stage 6 proving"); + write_instance_flamegraph_svg(&instances, "stage6b_start_flamechart.svg"); + tracing::info!("Stage 6b proving"); - let (sumcheck_proof, r_stage6, _initial_claim) = + let (sumcheck_proof, r_stage6b, _initial_claim) = self.prove_batched_sumcheck(instances.iter_mut().map(|v| &mut **v as _).collect()); #[cfg(feature = "allocative")] - write_instance_flamegraph_svg(&instances, "stage6_end_flamechart.svg"); + write_instance_flamegraph_svg(&instances, "stage6b_end_flamechart.svg"); drop_in_background_thread(bytecode_read_raf); drop_in_background_thread(booleanity); drop_in_background_thread(ram_hamming_booleanity); @@ -1384,7 +1454,7 @@ impl< self.advice_reduction_prover_trusted = advice_trusted; self.advice_reduction_prover_untrusted = advice_untrusted; - (sumcheck_proof, r_stage6) + (sumcheck_proof, r_stage6b) } #[tracing::instrument(skip_all)] @@ -1409,8 +1479,8 @@ impl< let zk_stages = self.blindfold_accumulator.take_stage_data(); assert_eq!( zk_stages.len(), - 7, - "Expected 7 ZK stages, got {}", + 8, + "Expected 8 ZK stages, got {}", zk_stages.len() ); @@ -3182,7 +3252,7 @@ mod tests { ("Stage 5 (Value+Lookup)", &jolt_proof.stage5_sumcheck_proof), ( "Stage 6 (OneHot+Hamming)", - &jolt_proof.stage6_sumcheck_proof, + &jolt_proof.stage6b_sumcheck_proof, ), ( "Stage 7 (HammingWeight+ClaimReduction)", diff --git a/jolt-core/src/zkvm/r1cs/inputs.rs b/jolt-core/src/zkvm/r1cs/inputs.rs index 7bbd942345..42b62e91fd 100644 --- a/jolt-core/src/zkvm/r1cs/inputs.rs +++ b/jolt-core/src/zkvm/r1cs/inputs.rs @@ -16,7 +16,7 @@ use crate::poly::opening_proof::{OpeningId, SumcheckId}; use crate::zkvm::bytecode::BytecodePreprocessing; use crate::zkvm::instruction::{ - CircuitFlags, Flags, InstructionFlags, LookupQuery, NUM_CIRCUIT_FLAGS, + CircuitFlags, Flags, InstructionFlags, JoltTraceCycle, LookupQuery, NUM_CIRCUIT_FLAGS, }; use crate::zkvm::witness::VirtualPolynomial; @@ -264,21 +264,23 @@ impl R1CSCycleInputs { F: JoltField, { let len = trace.len(); - let cycle = &trace[t]; - let instr = cycle.instruction(); - let flags_view = instr.circuit_flags(); - let instruction_flags = instr.instruction_flags(); - let norm = instr.normalize(); + let cycle = JoltTraceCycle::try_new(&trace[t]) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let flags_view = cycle.circuit_flags(); + let instruction_flags = cycle.instruction_flags(); + let norm = cycle.instruction(); - // Next-cycle context let next_cycle = if t + 1 < len { - Some(&trace[t + 1]) + Some( + JoltTraceCycle::try_new(&trace[t + 1]) + .expect("trace cycle must be backed by a final Jolt instruction row"), + ) } else { None }; // Instruction inputs and product - let (left_input, right_i128) = LookupQuery::::to_instruction_inputs(cycle); + let (left_input, right_i128) = LookupQuery::::to_instruction_inputs(&cycle); let left_s64: S64 = S64::from_u64(left_input); let right_mag = right_i128.unsigned_abs(); debug_assert!( @@ -290,35 +292,33 @@ impl R1CSCycleInputs { let product: S128 = left_s64.mul_trunc::<2, 2>(&right_s128); // Lookup operands and output - let (left_lookup, right_lookup) = LookupQuery::::to_lookup_operands(cycle); - let lookup_output = LookupQuery::::to_lookup_output(cycle); + let (left_lookup, right_lookup) = LookupQuery::::to_lookup_operands(&cycle); + let lookup_output = LookupQuery::::to_lookup_output(&cycle); // Registers - let rs1_read_value = cycle.rs1_read().unwrap_or_default().1; - let rs2_read_value = cycle.rs2_read().unwrap_or_default().1; - let rd_write_value = cycle.rd_write().unwrap_or_default().2; + let rs1_read_value = cycle.cycle().rs1_read().unwrap_or_default().1; + let rs2_read_value = cycle.cycle().rs2_read().unwrap_or_default().1; + let rd_write_value = cycle.cycle().rd_write().unwrap_or_default().2; // RAM - let ram_addr = cycle.ram_access().address() as u64; - let (ram_read_value, ram_write_value) = match cycle.ram_access() { + let ram_addr = cycle.cycle().ram_access().address() as u64; + let (ram_read_value, ram_write_value) = match cycle.cycle().ram_access() { tracer::instruction::RAMAccess::Read(r) => (r.value, r.value), tracer::instruction::RAMAccess::Write(w) => (w.pre_value, w.post_value), tracer::instruction::RAMAccess::NoOp => (0u64, 0u64), }; // PCs - let pc = crate::zkvm::bytecode::get_pc_for_cycle(bytecode_preprocessing, cycle) as u64; - let next_pc = if let Some(nc) = next_cycle { - crate::zkvm::bytecode::get_pc_for_cycle(bytecode_preprocessing, nc) as u64 - } else { - 0u64 - }; + let pc = + crate::zkvm::bytecode::get_pc_for_cycle(bytecode_preprocessing, cycle.cycle()) as u64; + let next_pc = next_cycle.as_ref().map_or(0, |next_cycle| { + crate::zkvm::bytecode::get_pc_for_cycle(bytecode_preprocessing, next_cycle.cycle()) + as u64 + }); let unexpanded_pc = norm.address as u64; - let next_unexpanded_pc = if let Some(nc) = next_cycle { - nc.instruction().normalize().address as u64 - } else { - 0u64 - }; + let next_unexpanded_pc = next_cycle + .as_ref() + .map_or(0, |next_cycle| next_cycle.instruction().address as u64); // Immediate let imm_i128 = norm.operands.imm; @@ -334,23 +334,22 @@ impl R1CSCycleInputs { for flag in CircuitFlags::iter() { flags[flag] = flags_view[flag]; } - let next_is_noop = if let Some(nc) = next_cycle { - nc.instruction().instruction_flags()[InstructionFlags::IsNoop] - } else { - false // There is no next cycle, so cannot be a noop - }; + let next_is_noop = next_cycle + .as_ref() + .is_some_and(|next_cycle| next_cycle.instruction_flags()[InstructionFlags::IsNoop]); let should_jump = flags_view[CircuitFlags::Jump] && !next_is_noop; let should_branch = instruction_flags[InstructionFlags::Branch] && (lookup_output == 1); - let (next_is_virtual, next_is_first_in_sequence) = if let Some(nc) = next_cycle { - let flags = nc.instruction().circuit_flags(); - ( - flags[CircuitFlags::VirtualInstruction], - flags[CircuitFlags::IsFirstInSequence], - ) - } else { - (false, false) - }; + let (next_is_virtual, next_is_first_in_sequence) = + if let Some(next_cycle) = next_cycle.as_ref() { + let flags = next_cycle.circuit_flags(); + ( + flags[CircuitFlags::VirtualInstruction], + flags[CircuitFlags::IsFirstInSequence], + ) + } else { + (false, false) + }; Self { left_input, @@ -465,16 +464,16 @@ impl ProductCycleInputs { F: JoltField, { let len = trace.len(); - let cycle = &trace[t]; - let instr = cycle.instruction(); - let flags_view = instr.circuit_flags(); - let instruction_flags = instr.instruction_flags(); + let cycle = JoltTraceCycle::try_new(&trace[t]) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let flags_view = cycle.circuit_flags(); + let instruction_flags = cycle.instruction_flags(); // Instruction inputs - let (left_input, right_input) = LookupQuery::::to_instruction_inputs(cycle); + let (left_input, right_input) = LookupQuery::::to_instruction_inputs(&cycle); // Lookup output - let lookup_output = LookupQuery::::to_lookup_output(cycle); + let lookup_output = LookupQuery::::to_lookup_output(&cycle); // Jump and Branch flags let jump_flag = flags_view[CircuitFlags::Jump]; @@ -483,7 +482,9 @@ impl ProductCycleInputs { // Next-is-noop and its complement (1 - NextIsNoop) let not_next_noop = { if t + 1 < len { - !trace[t + 1].instruction().instruction_flags()[InstructionFlags::IsNoop] + let next_cycle = JoltTraceCycle::try_new(&trace[t + 1]) + .expect("trace cycle must be backed by a final Jolt instruction row"); + !next_cycle.instruction_flags()[InstructionFlags::IsNoop] } else { // Needs final not_next_noop to be false for the shift sumcheck // (since EqPlusOne does not do overflow) @@ -520,14 +521,15 @@ pub struct ShiftSumcheckCycleState { impl ShiftSumcheckCycleState { pub fn new(cycle: &Cycle, bytecode_preprocessing: &BytecodePreprocessing) -> Self { - let instruction = cycle.instruction(); - let circuit_flags = instruction.circuit_flags(); + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let circuit_flags = jolt_cycle.circuit_flags(); Self { - unexpanded_pc: instruction.normalize().address as u64, + unexpanded_pc: jolt_cycle.instruction().address as u64, pc: crate::zkvm::bytecode::get_pc_for_cycle(bytecode_preprocessing, cycle) as u64, is_virtual: circuit_flags[CircuitFlags::VirtualInstruction], is_first_in_sequence: circuit_flags[CircuitFlags::IsFirstInSequence], - is_noop: instruction.instruction_flags()[InstructionFlags::IsNoop], + is_noop: jolt_cycle.instruction_flags()[InstructionFlags::IsNoop], } } } diff --git a/jolt-core/src/zkvm/registers/val_evaluation.rs b/jolt-core/src/zkvm/registers/val_evaluation.rs index 17d8380532..8c334756c9 100644 --- a/jolt-core/src/zkvm/registers/val_evaluation.rs +++ b/jolt-core/src/zkvm/registers/val_evaluation.rs @@ -176,7 +176,10 @@ impl ValEvaluationSumcheckProver { let wa: Vec> = trace .par_iter() .map(|cycle| { - let instr = cycle.instruction().normalize(); + let instr = cycle + .instruction() + .try_jolt_instruction_row() + .expect("trace cycle must be a final Jolt instruction row"); instr.operands.rd }) .collect(); diff --git a/jolt-core/src/zkvm/spartan/instruction_input.rs b/jolt-core/src/zkvm/spartan/instruction_input.rs index e66f2cbf02..ef4436b4f6 100644 --- a/jolt-core/src/zkvm/spartan/instruction_input.rs +++ b/jolt-core/src/zkvm/spartan/instruction_input.rs @@ -32,7 +32,7 @@ use crate::{ }, transcripts::Transcript, zkvm::{ - instruction::{Flags, InstructionFlags}, + instruction::{Flags, InstructionFlags, JoltTraceCycle}, witness::VirtualPolynomial, }, }; @@ -271,17 +271,17 @@ impl InstructionInputSumcheckProver { unexpanded_pc_eval, cycle, )| { - let instruction = cycle.instruction(); - let instruction_norm = instruction.normalize(); - let flags = instruction.instruction_flags(); + let jolt_cycle = JoltTraceCycle::try_new(cycle) + .expect("trace cycle must be backed by a final Jolt instruction row"); + let flags = jolt_cycle.instruction_flags(); *left_is_rs1_eval = flags[InstructionFlags::LeftOperandIsRs1Value]; *left_is_pc_eval = flags[InstructionFlags::LeftOperandIsPC]; *right_is_rs2_eval = flags[InstructionFlags::RightOperandIsRs2Value]; *right_is_imm_eval = flags[InstructionFlags::RightOperandIsImm]; *rs1_value_eval = cycle.rs1_read().unwrap_or_default().1; *rs2_value_eval = cycle.rs2_read().unwrap_or_default().1; - *imm_eval = instruction_norm.operands.imm; - *unexpanded_pc_eval = instruction_norm.address as u64; + *imm_eval = jolt_cycle.instruction().operands.imm; + *unexpanded_pc_eval = jolt_cycle.instruction().address as u64; }, ); diff --git a/jolt-core/src/zkvm/transpilable_verifier.rs b/jolt-core/src/zkvm/transpilable_verifier.rs index f376762f43..92c2925647 100644 --- a/jolt-core/src/zkvm/transpilable_verifier.rs +++ b/jolt-core/src/zkvm/transpilable_verifier.rs @@ -50,7 +50,10 @@ use crate::zkvm::claim_reductions::{ }; use crate::zkvm::config::OneHotParams; use crate::zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckVerifier, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckVerifier, BytecodeReadRafCycleSumcheckVerifier, + BytecodeReadRafSumcheckParams, + }, claim_reductions::{ IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, RamRaClaimReductionSumcheckVerifier, @@ -87,7 +90,10 @@ use crate::{ poly::opening_proof::{AbstractVerifierOpeningAccumulator, VerifierOpeningAccumulator}, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckVerifier}, + booleanity::{ + BooleanityAddressSumcheckVerifier, BooleanityCycleSumcheckVerifier, + BooleanitySumcheckParams, + }, sumcheck_verifier::SumcheckInstanceVerifier, }, transcripts::Transcript, @@ -554,25 +560,60 @@ impl< } fn verify_stage6(&mut self) -> Result<(), ProofVerifyError> { + let (bytecode_read_raf_params, booleanity_params) = self.verify_stage6a()?; + self.verify_stage6b(bytecode_read_raf_params, booleanity_params) + } + + fn verify_stage6a( + &mut self, + ) -> Result< + ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + ), + ProofVerifyError, + > { let n_cycle_vars = self.proof.trace_length.log_2(); - let bytecode_read_raf = BytecodeReadRafSumcheckVerifier::gen( + let bytecode_read_raf = BytecodeReadRafAddressSumcheckVerifier::new( &self.preprocessing.shared.bytecode, n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, ); - - let ram_hamming_booleanity = - HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); - let booleanity_params = BooleanitySumcheckParams::new( + let booleanity = BooleanityAddressSumcheckVerifier::new(BooleanitySumcheckParams::new( n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, - ); + )); + + let instances: Vec<&dyn SumcheckInstanceVerifier> = + vec![&bytecode_read_raf, &booleanity]; - let booleanity = BooleanitySumcheckVerifier::new(booleanity_params); + let _r_stage6a = BatchedSumcheck::verify_standard::( + extract_clear_proof(&self.proof.stage6a_sumcheck_proof), + instances, + &mut self.opening_accumulator, + &mut self.transcript, + )?; + + Ok((bytecode_read_raf.into_params(), booleanity.into_params())) + } + + fn verify_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_params: BooleanitySumcheckParams, + ) -> Result<(), ProofVerifyError> { + let bytecode_read_raf = BytecodeReadRafCycleSumcheckVerifier::new( + bytecode_read_raf_params, + &self.opening_accumulator, + ); + let ram_hamming_booleanity = + HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); + let booleanity = + BooleanityCycleSumcheckVerifier::new(booleanity_params, &self.opening_accumulator); let ram_ra_virtual = RamRaVirtualSumcheckVerifier::new( self.proof.trace_length, &self.one_hot_params, @@ -590,7 +631,7 @@ impl< &mut self.transcript, ); - // Advice claim reduction (Phase 1 in Stage 6): trusted and untrusted are separate instances. + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.trusted_advice_commitment.is_some() { self.advice_reduction_verifier_trusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Trusted, @@ -623,8 +664,8 @@ impl< instances.push(advice); } - let _r_stage6 = BatchedSumcheck::verify_standard::( - extract_clear_proof(&self.proof.stage6_sumcheck_proof), + let _r_stage6b = BatchedSumcheck::verify_standard::( + extract_clear_proof(&self.proof.stage6b_sumcheck_proof), instances, &mut self.opening_accumulator, &mut self.transcript, diff --git a/jolt-core/src/zkvm/verifier.rs b/jolt-core/src/zkvm/verifier.rs index 254807e37b..6f3cbebaa2 100644 --- a/jolt-core/src/zkvm/verifier.rs +++ b/jolt-core/src/zkvm/verifier.rs @@ -42,7 +42,10 @@ use crate::zkvm::ram::RAMPreprocessing; use crate::zkvm::witness::all_committed_polynomials; use crate::zkvm::Serializable; use crate::zkvm::{ - bytecode::read_raf_checking::BytecodeReadRafSumcheckVerifier, + bytecode::read_raf_checking::{ + BytecodeReadRafAddressSumcheckVerifier, BytecodeReadRafCycleSumcheckVerifier, + BytecodeReadRafSumcheckParams, + }, claim_reductions::{ AdviceClaimReductionVerifier, AdviceKind, HammingWeightClaimReductionVerifier, IncClaimReductionSumcheckVerifier, InstructionLookupsClaimReductionSumcheckVerifier, @@ -85,7 +88,10 @@ use crate::{ }, pprof_scope, subprotocols::{ - booleanity::{BooleanitySumcheckParams, BooleanitySumcheckVerifier}, + booleanity::{ + BooleanityAddressSumcheckVerifier, BooleanityCycleSumcheckVerifier, + BooleanitySumcheckParams, + }, sumcheck_verifier::SumcheckInstanceVerifier, }, transcripts::Transcript, @@ -216,7 +222,7 @@ fn scale_batching_coefficients< } use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use common::jolt_device::MemoryLayout; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::{JoltInstructionRow, RV64IMAC_JOLT}; use tracer::JoltDevice; pub struct JoltVerifier< @@ -459,7 +465,7 @@ impl< let stage5_result = self .verify_stage5() .inspect_err(|e| tracing::error!("Stage 5: {e}"))?; - let stage6_result = self + let (stage6a_result, stage6b_result) = self .verify_stage6() .inspect_err(|e| tracing::error!("Stage 6: {e}"))?; let stage7_result = self @@ -478,7 +484,8 @@ impl< stage3_result.challenges.clone(), stage4_result.challenges.clone(), stage5_result.challenges.clone(), - stage6_result.challenges.clone(), + stage6a_result.challenges.clone(), + stage6b_result.challenges.clone(), stage7_result.challenges.clone(), ]; let uniskip_challenges = [uniskip_challenge1, uniskip_challenge2]; @@ -489,7 +496,8 @@ impl< stage3_result.batched_output_constraint, stage4_result.batched_output_constraint, stage5_result.batched_output_constraint, - stage6_result.batched_output_constraint, + stage6a_result.batched_output_constraint, + stage6b_result.batched_output_constraint, stage7_result.batched_output_constraint, ]; @@ -499,7 +507,8 @@ impl< stage3_result.batched_input_constraint.clone(), stage4_result.batched_input_constraint.clone(), stage5_result.batched_input_constraint.clone(), - stage6_result.batched_input_constraint.clone(), + stage6a_result.batched_input_constraint.clone(), + stage6b_result.batched_input_constraint.clone(), stage7_result.batched_input_constraint.clone(), ]; @@ -513,17 +522,19 @@ impl< stage3_result.input_constraint_challenge_values.clone(), stage4_result.input_constraint_challenge_values.clone(), stage5_result.input_constraint_challenge_values.clone(), - stage6_result.input_constraint_challenge_values.clone(), + stage6a_result.input_constraint_challenge_values.clone(), + stage6b_result.input_constraint_challenge_values.clone(), stage7_result.input_constraint_challenge_values.clone(), ]; - let output_constraint_challenge_values: [Vec; 7] = [ + let output_constraint_challenge_values: [Vec; 8] = [ stage1_result.output_constraint_challenge_values.clone(), stage2_result.output_constraint_challenge_values.clone(), stage3_result.output_constraint_challenge_values.clone(), stage4_result.output_constraint_challenge_values.clone(), stage5_result.output_constraint_challenge_values.clone(), - stage6_result.output_constraint_challenge_values.clone(), + stage6a_result.output_constraint_challenge_values.clone(), + stage6b_result.output_constraint_challenge_values.clone(), stage7_result.output_constraint_challenge_values.clone(), ]; @@ -533,7 +544,8 @@ impl< oc_blocks.extend(stage3_result.oc_block_ids); oc_blocks.extend(stage4_result.oc_block_ids); oc_blocks.extend(stage5_result.oc_block_ids); - oc_blocks.extend(stage6_result.oc_block_ids); + oc_blocks.extend(stage6a_result.oc_block_ids); + oc_blocks.extend(stage6b_result.oc_block_ids); oc_blocks.extend(stage7_result.oc_block_ids); let uniskip_output_constraints = [ @@ -1020,26 +1032,114 @@ impl< } #[cfg_attr(not(feature = "zk"), allow(unused_variables))] - fn verify_stage6(&mut self) -> Result, ProofVerifyError> { + fn verify_stage6( + &mut self, + ) -> Result<(StageVerifyResult, StageVerifyResult), ProofVerifyError> { + let (bytecode_read_raf_params, booleanity_params, stage6a_result) = + self.verify_stage6a()?; + let stage6b_result = self.verify_stage6b(bytecode_read_raf_params, booleanity_params)?; + Ok((stage6a_result, stage6b_result)) + } + + #[expect(clippy::type_complexity)] + fn verify_stage6a( + &mut self, + ) -> Result< + ( + BytecodeReadRafSumcheckParams, + BooleanitySumcheckParams, + StageVerifyResult, + ), + ProofVerifyError, + > { let n_cycle_vars = self.proof.trace_length.log_2(); - let bytecode_read_raf = BytecodeReadRafSumcheckVerifier::gen( + let bytecode_read_raf = BytecodeReadRafAddressSumcheckVerifier::new( &self.preprocessing.shared.bytecode, n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, ); - - let ram_hamming_booleanity = - HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); - let booleanity_params = BooleanitySumcheckParams::new( + let booleanity = BooleanityAddressSumcheckVerifier::new(BooleanitySumcheckParams::new( n_cycle_vars, &self.one_hot_params, &self.opening_accumulator, &mut self.transcript, - ); + )); + + let instances: Vec< + &dyn SumcheckInstanceVerifier>, + > = vec![&bytecode_read_raf, &booleanity]; + let (batching_coefficients, r_stage6a) = BatchedSumcheck::verify( + &self.proof.stage6a_sumcheck_proof, + instances.clone(), + &mut self.opening_accumulator, + &mut self.transcript, + )?; + #[cfg(not(feature = "zk"))] + let _ = &batching_coefficients; + #[cfg(feature = "zk")] + { + let regular_oc_ids = self.opening_accumulator.take_pending_claim_ids(); + let batched_output_constraint = batch_output_constraints(&instances); + let batched_input_constraint = batch_input_constraints(&instances); + let max_num_rounds = instances.iter().map(|i| i.num_rounds()).max().unwrap(); + let mut output_constraint_challenge_values: Vec = batching_coefficients.clone(); + let mut input_constraint_challenge_values: Vec = + scale_batching_coefficients(&batching_coefficients, &instances); + for instance in &instances { + let num_rounds = instance.num_rounds(); + let offset = instance.round_offset(max_num_rounds); + let r_slice = &r_stage6a[offset..offset + num_rounds]; + output_constraint_challenge_values.extend( + instance + .get_params() + .output_constraint_challenge_values(r_slice), + ); + input_constraint_challenge_values.extend( + instance + .get_params() + .input_constraint_challenge_values(&self.opening_accumulator), + ); + } + let stage_result = StageVerifyResult::new( + r_stage6a, + batched_output_constraint, + output_constraint_challenge_values, + batched_input_constraint, + input_constraint_challenge_values, + vec![regular_oc_ids], + ); + Ok(( + bytecode_read_raf.into_params(), + booleanity.into_params(), + stage_result, + )) + } + #[cfg(not(feature = "zk"))] + Ok(( + bytecode_read_raf.into_params(), + booleanity.into_params(), + StageVerifyResult { + challenges: r_stage6a, + }, + )) + } - let booleanity = BooleanitySumcheckVerifier::new(booleanity_params); + #[cfg_attr(not(feature = "zk"), allow(unused_variables))] + fn verify_stage6b( + &mut self, + bytecode_read_raf_params: BytecodeReadRafSumcheckParams, + booleanity_params: BooleanitySumcheckParams, + ) -> Result, ProofVerifyError> { + let bytecode_read_raf = BytecodeReadRafCycleSumcheckVerifier::new( + bytecode_read_raf_params, + &self.opening_accumulator, + ); + let ram_hamming_booleanity = + HammingBooleanitySumcheckVerifier::new(&self.opening_accumulator); + let booleanity = + BooleanityCycleSumcheckVerifier::new(booleanity_params, &self.opening_accumulator); let ram_ra_virtual = RamRaVirtualSumcheckVerifier::new( self.proof.trace_length, &self.one_hot_params, @@ -1057,7 +1157,7 @@ impl< &mut self.transcript, ); - // Advice claim reduction (Phase 1 in Stage 6): trusted and untrusted are separate instances. + // Advice claim reduction (Phase 1 in Stage 6b): trusted and untrusted are separate instances. if self.trusted_advice_commitment.is_some() { self.advice_reduction_verifier_trusted = Some(AdviceClaimReductionVerifier::new( AdviceKind::Trusted, @@ -1092,8 +1192,8 @@ impl< instances.push(advice); } - let (batching_coefficients, r_stage6) = BatchedSumcheck::verify( - &self.proof.stage6_sumcheck_proof, + let (batching_coefficients, r_stage6b) = BatchedSumcheck::verify( + &self.proof.stage6b_sumcheck_proof, instances.clone(), &mut self.opening_accumulator, &mut self.transcript, @@ -1111,7 +1211,7 @@ impl< for instance in &instances { let num_rounds = instance.num_rounds(); let offset = instance.round_offset(max_num_rounds); - let r_slice = &r_stage6[offset..offset + num_rounds]; + let r_slice = &r_stage6b[offset..offset + num_rounds]; output_constraint_challenge_values.extend( instance .get_params() @@ -1124,7 +1224,7 @@ impl< ); } Ok(StageVerifyResult::new( - r_stage6, + r_stage6b, batched_output_constraint, output_constraint_challenge_values, batched_input_constraint, @@ -1134,7 +1234,7 @@ impl< } #[cfg(not(feature = "zk"))] Ok(StageVerifyResult { - challenges: r_stage6, + challenges: r_stage6b, }) } @@ -1142,12 +1242,12 @@ impl< #[allow(clippy::too_many_arguments)] fn verify_blindfold( &mut self, - sumcheck_challenges: &[Vec; 7], + sumcheck_challenges: &[Vec; 8], uniskip_challenges: [F::Challenge; 2], - stage_output_constraints: &[Option; 7], - output_constraint_challenge_values: &[Vec; 7], - stage_input_constraints: &[InputClaimConstraint; 7], - input_constraint_challenge_values: &[Vec; 7], + stage_output_constraints: &[Option; 8], + output_constraint_challenge_values: &[Vec; 8], + stage_input_constraints: &[InputClaimConstraint; 8], + input_constraint_challenge_values: &[Vec; 8], // For stages 0-1: batched input constraint for regular rounds (different from uni-skip) stage1_batched_input: &InputClaimConstraint, stage2_batched_input: &InputClaimConstraint, @@ -1166,7 +1266,8 @@ impl< &self.proof.stage3_sumcheck_proof, &self.proof.stage4_sumcheck_proof, &self.proof.stage5_sumcheck_proof, - &self.proof.stage6_sumcheck_proof, + &self.proof.stage6a_sumcheck_proof, + &self.proof.stage6b_sumcheck_proof, &self.proof.stage7_sumcheck_proof, ]; @@ -1183,7 +1284,7 @@ impl< let mut stage_configs = Vec::new(); // Track which stage_config index corresponds to uni-skip and regular first rounds let mut uniskip_indices: Vec = Vec::new(); // Only 2 elements for stages 0-1 - let mut regular_first_round_indices: Vec = Vec::new(); // 7 elements for all stages + let mut regular_first_round_indices: Vec = Vec::new(); // 8 elements for all stages let mut last_round_indices: Vec = Vec::new(); for (stage_idx, proof) in stage_proofs.iter().enumerate() { @@ -1274,7 +1375,7 @@ impl< } } - // Add initial_input configurations for regular first rounds (all 7 stages) + // Add initial_input configurations for regular first rounds (all 8 stages) // These use the batched input constraints from the stage results let regular_constraints = [ stage1_batched_input.clone(), // Stage 0 regular @@ -1282,8 +1383,9 @@ impl< stage_input_constraints[2].clone(), // Stage 2 stage_input_constraints[3].clone(), // Stage 3 stage_input_constraints[4].clone(), // Stage 4 - stage_input_constraints[5].clone(), // Stage 5 - stage_input_constraints[6].clone(), // Stage 6 + stage_input_constraints[5].clone(), // Stage 5 (6a) + stage_input_constraints[6].clone(), // Stage 6 (6b) + stage_input_constraints[7].clone(), // Stage 7 ]; for (i, constraint) in regular_constraints.iter().enumerate() { let idx = regular_first_round_indices[i]; @@ -1311,7 +1413,7 @@ impl< } } - let all_input_challenge_values: [&[F]; 9] = [ + let all_input_challenge_values: [&[F]; 10] = [ &input_constraint_challenge_values[0], stage1_batched_input_values, &input_constraint_challenge_values[1], @@ -1321,6 +1423,7 @@ impl< &input_constraint_challenge_values[4], &input_constraint_challenge_values[5], &input_constraint_challenge_values[6], + &input_constraint_challenge_values[7], ]; let mut baked_input_challenges: Vec = Vec::new(); for expected_values in all_input_challenge_values.iter() { @@ -1812,13 +1915,17 @@ impl ark_serialize::Valid for JoltSharedPreprocessing { impl JoltSharedPreprocessing { #[tracing::instrument(skip_all, name = "JoltSharedPreprocessing::new")] pub fn new( - bytecode: Vec, + bytecode: Vec, memory_layout: MemoryLayout, memory_init: Vec<(u64, u8)>, max_padded_trace_length: usize, entry_address: u64, ) -> Result { - let bytecode = Arc::new(BytecodePreprocessing::preprocess(bytecode, entry_address)?); + let bytecode = Arc::new(BytecodePreprocessing::preprocess( + bytecode, + entry_address, + RV64IMAC_JOLT, + )?); let ram = RAMPreprocessing::preprocess(memory_init); Ok(Self { bytecode, diff --git a/jolt-core/src/zkvm/witness.rs b/jolt-core/src/zkvm/witness.rs index 581ff454f1..bfd45f97b4 100644 --- a/jolt-core/src/zkvm/witness.rs +++ b/jolt-core/src/zkvm/witness.rs @@ -271,4 +271,6 @@ pub enum VirtualPolynomial { OpFlags(CircuitFlags), InstructionFlags(InstructionFlags), LookupTableFlag(usize), + BytecodeReadRafAddrClaim, + BooleanityAddrClaim, } diff --git a/jolt-eval/Cargo.toml b/jolt-eval/Cargo.toml index b100b4a505..5abc7fbde9 100644 --- a/jolt-eval/Cargo.toml +++ b/jolt-eval/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [dependencies] jolt-core = { workspace = true, features = ["host"] } +jolt-program = { workspace = true, features = ["serialization"] } +jolt-riscv = { workspace = true, features = ["serialization"] } jolt-field = { workspace = true } common = { workspace = true, features = ["std"] } tracer = { workspace = true } @@ -18,13 +20,15 @@ tracing = { workspace = true } clap = { workspace = true, features = ["derive"] } rand = { workspace = true } tracing-subscriber = { workspace = true } +hex = { workspace = true } +sha2 = { workspace = true } arbitrary = { version = "1", features = ["derive"] } enumset = "1" schemars = "1.2" tempfile = "3" -rust-code-analysis = "0.0.24" +rust-code-analysis = "0.0.25" jolt-inlines-secp256k1 = { workspace = true, features = ["host"] } jolt-inlines-sha2 = { workspace = true, features = ["host"] } diff --git a/jolt-eval/src/invariant/mod.rs b/jolt-eval/src/invariant/mod.rs index 9fcb997390..8bc464082a 100644 --- a/jolt-eval/src/invariant/mod.rs +++ b/jolt-eval/src/invariant/mod.rs @@ -2,6 +2,7 @@ pub mod field_mul_scalar; #[cfg(test)] mod macro_tests; pub mod soundness; +pub mod source_to_jolt_expansion_equivalence; pub mod split_eq_bind; pub mod synthesis; @@ -136,6 +137,9 @@ pub enum JoltInvariants { SplitEqBindHighLow(split_eq_bind::SplitEqBindHighLowInvariant), FieldMulScalar(field_mul_scalar::FieldMulScalarInvariant), Soundness(soundness::SoundnessInvariant), + SourceToJoltExpansionEquivalence( + source_to_jolt_expansion_equivalence::SourceToJoltExpansionEquivalenceInvariant, + ), } macro_rules! dispatch { @@ -145,6 +149,7 @@ macro_rules! dispatch { JoltInvariants::SplitEqBindHighLow($inv) => $body, JoltInvariants::FieldMulScalar($inv) => $body, JoltInvariants::Soundness($inv) => $body, + JoltInvariants::SourceToJoltExpansionEquivalence($inv) => $body, } }; } @@ -156,6 +161,9 @@ impl JoltInvariants { Self::SplitEqBindHighLow(split_eq_bind::SplitEqBindHighLowInvariant), Self::FieldMulScalar(field_mul_scalar::FieldMulScalarInvariant), Self::Soundness(soundness::SoundnessInvariant), + Self::SourceToJoltExpansionEquivalence( + source_to_jolt_expansion_equivalence::SourceToJoltExpansionEquivalenceInvariant, + ), ] } diff --git a/jolt-eval/src/invariant/source_to_jolt_expansion_equivalence.rs b/jolt-eval/src/invariant/source_to_jolt_expansion_equivalence.rs new file mode 100644 index 0000000000..281e9d0c5c --- /dev/null +++ b/jolt-eval/src/invariant/source_to_jolt_expansion_equivalence.rs @@ -0,0 +1,116 @@ +use arbitrary::{Arbitrary, Unstructured}; +use jolt_program::expand::{expand_instruction, ExpansionAllocator}; +use jolt_riscv::{JoltInstructionRow, SourceInstruction, RV64IMAC_JOLT}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; + +use crate::invariant::{CheckError, Invariant, InvariantViolation}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +pub struct SourceToJoltExpansionInput { + pub case_index: u32, +} + +impl<'a> Arbitrary<'a> for SourceToJoltExpansionInput { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + Ok(Self { + case_index: u.arbitrary()?, + }) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ExpansionParityCase { + pub name: String, + pub input: SourceInstruction, + pub output_sha256: String, +} + +fn fixture_cases() -> Result, serde_json::Error> { + serde_json::from_str(include_str!( + "../../../crates/jolt-program/src/expand/fixtures/main_expand_parity_hashes.json" + )) +} + +#[jolt_eval_macros::invariant(Test)] +#[derive(Default)] +pub struct SourceToJoltExpansionEquivalenceInvariant; + +impl Invariant for SourceToJoltExpansionEquivalenceInvariant { + type Setup = Result, String>; + type Input = SourceToJoltExpansionInput; + + fn name(&self) -> &str { + "source_to_jolt_expansion_equivalence" + } + + fn description(&self) -> String { + "Decoded source instructions must expand to the same canonical Jolt rows \ + as the checked baseline parity corpus." + .to_string() + } + + fn setup(&self) -> Self::Setup { + fixture_cases().map_err(|error| error.to_string()) + } + + fn check( + &self, + setup: &Self::Setup, + input: SourceToJoltExpansionInput, + ) -> Result<(), CheckError> { + let cases = setup.as_ref().map_err(|error| { + CheckError::Violation(InvariantViolation::with_details( + "expansion parity fixture failed to deserialize", + error, + )) + })?; + + if cases.is_empty() { + return Err(CheckError::InvalidInput( + "expansion parity corpus is empty".into(), + )); + } + + let case = &cases[input.case_index as usize % cases.len()]; + let mut allocator = ExpansionAllocator::new(); + let expanded = + expand_instruction(&case.input, &mut allocator, RV64IMAC_JOLT).map_err(|error| { + CheckError::Violation(InvariantViolation::with_details( + "source expansion failed", + format!("case={}, error={error}", case.name), + )) + })?; + let expanded_rows: Vec = + expanded.into_iter().map(JoltInstructionRow::from).collect(); + let encoded = serde_json::to_vec(&expanded_rows).map_err(|error| { + CheckError::Violation(InvariantViolation::with_details( + "expanded row serialization failed", + format!("case={}, error={error}", case.name), + )) + })?; + let output_sha256 = hex::encode(Sha256::digest(encoded)); + + if output_sha256 != case.output_sha256 { + return Err(CheckError::Violation(InvariantViolation::with_details( + "source expansion parity hash mismatch", + format!( + "case={}, got={}, expected={}", + case.name, output_sha256, case.output_sha256 + ), + ))); + } + + Ok(()) + } + + fn seed_corpus(&self) -> Vec { + fixture_cases() + .map(|cases| { + (0..cases.len() as u32) + .map(|case_index| SourceToJoltExpansionInput { case_index }) + .collect() + }) + .unwrap_or_else(|_| vec![SourceToJoltExpansionInput { case_index: 0 }]) + } +} diff --git a/jolt-inlines/bigint/src/lib.rs b/jolt-inlines/bigint/src/lib.rs index f7d7b3bf44..181ce45cd5 100644 --- a/jolt-inlines/bigint/src/lib.rs +++ b/jolt-inlines/bigint/src/lib.rs @@ -6,5 +6,6 @@ pub use multiplication::*; #[cfg(feature = "host")] jolt_inlines_sdk::register_inlines! { trace_file: "bigint_mul256_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::BigInt256, ops: [multiplication::sequence_builder::BigintMul256], } diff --git a/jolt-inlines/bigint/src/multiplication/test_utils.rs b/jolt-inlines/bigint/src/multiplication/test_utils.rs index 90a723d0b4..46082c2199 100644 --- a/jolt-inlines/bigint/src/multiplication/test_utils.rs +++ b/jolt-inlines/bigint/src/multiplication/test_utils.rs @@ -1,5 +1,4 @@ use super::{BIGINT256_MUL_FUNCT3, BIGINT256_MUL_FUNCT7, INLINE_OPCODE, INPUT_LIMBS, OUTPUT_LIMBS}; -use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; pub type BigIntInput = ([u64; INPUT_LIMBS], [u64; INPUT_LIMBS]); @@ -7,7 +6,7 @@ pub type BigIntOutput = [u64; OUTPUT_LIMBS]; pub fn create_bigint_harness() -> InlineTestHarness { let layout = InlineMemoryLayout::two_inputs(32, 32, 64); - InlineTestHarness::new(layout, Xlen::Bit64) + InlineTestHarness::new(layout) } pub fn instruction() -> tracer::instruction::inline::INLINE { diff --git a/jolt-inlines/blake2/src/host.rs b/jolt-inlines/blake2/src/host.rs index 97e4081de0..ba8405e065 100644 --- a/jolt-inlines/blake2/src/host.rs +++ b/jolt-inlines/blake2/src/host.rs @@ -2,5 +2,6 @@ use crate::sequence_builder::Blake2bCompression; jolt_inlines_sdk::register_inlines! { trace_file: "blake2_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Blake2, ops: [Blake2bCompression], } diff --git a/jolt-inlines/blake2/src/test_utils.rs b/jolt-inlines/blake2/src/test_utils.rs index cec52c4154..f321197e96 100644 --- a/jolt-inlines/blake2/src/test_utils.rs +++ b/jolt-inlines/blake2/src/test_utils.rs @@ -1,10 +1,9 @@ use crate::{BLAKE2_FUNCT3, BLAKE2_FUNCT7, INLINE_OPCODE}; -use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; pub fn create_blake2_harness() -> InlineTestHarness { let layout = InlineMemoryLayout::single_input(144, 64); - InlineTestHarness::new(layout, Xlen::Bit64) + InlineTestHarness::new(layout) } pub fn load_blake2_data( diff --git a/jolt-inlines/blake3/src/host.rs b/jolt-inlines/blake3/src/host.rs index fdf263c8ff..21728acf1d 100644 --- a/jolt-inlines/blake3/src/host.rs +++ b/jolt-inlines/blake3/src/host.rs @@ -2,5 +2,6 @@ use crate::sequence_builder::{Blake3Compression, Blake3Keyed64Compression}; jolt_inlines_sdk::register_inlines! { trace_file: "blake3_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Blake3, ops: [Blake3Compression, Blake3Keyed64Compression], } diff --git a/jolt-inlines/blake3/src/test_utils.rs b/jolt-inlines/blake3/src/test_utils.rs index 8d7de9b7da..86512f4b9e 100644 --- a/jolt-inlines/blake3/src/test_utils.rs +++ b/jolt-inlines/blake3/src/test_utils.rs @@ -1,5 +1,4 @@ use crate::{BLAKE3_FUNCT3, BLAKE3_FUNCT7, BLAKE3_KEYED64_FUNCT3, INLINE_OPCODE}; -use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; pub type ChainingValue = [u32; crate::CHAINING_VALUE_LEN]; @@ -9,7 +8,7 @@ pub fn create_blake3_harness() -> InlineTestHarness { // Blake3 needs message block (64 bytes) + params (16 bytes) contiguous at rs2 // and state (32 bytes) at rs1 let layout = InlineMemoryLayout::single_input(80, 32); // 80 bytes for message+params, 32-byte state - InlineTestHarness::new(layout, Xlen::Bit64) + InlineTestHarness::new(layout) } /// Create harness for Keyed64 instruction (Merkle tree merge) @@ -20,7 +19,7 @@ pub fn create_blake3_keyed64_harness() -> InlineTestHarness { // - rs2: right CV (32 bytes) -> input2 // - rd: IV (32 bytes, in/out) -> output let layout = InlineMemoryLayout::two_inputs(32, 32, 32); // left, right, iv - InlineTestHarness::new(layout, Xlen::Bit64) + InlineTestHarness::new(layout) } pub fn load_blake3_keyed64_data( diff --git a/jolt-inlines/grumpkin/src/host.rs b/jolt-inlines/grumpkin/src/host.rs index d63c160c4d..5ce81e1423 100644 --- a/jolt-inlines/grumpkin/src/host.rs +++ b/jolt-inlines/grumpkin/src/host.rs @@ -2,5 +2,6 @@ use crate::sequence_builder::{GrumpkinDivQAdv, GrumpkinDivRAdv}; jolt_inlines_sdk::register_inlines! { trace_file: "grumpkin_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Grumpkin, ops: [GrumpkinDivQAdv, GrumpkinDivRAdv], } diff --git a/jolt-inlines/grumpkin/src/tests.rs b/jolt-inlines/grumpkin/src/tests.rs index c272e20598..546108555e 100644 --- a/jolt-inlines/grumpkin/src/tests.rs +++ b/jolt-inlines/grumpkin/src/tests.rs @@ -7,7 +7,6 @@ mod sequence_tests { use ark_ff::{BigInt, Field}; use ark_grumpkin::{Fq, Fr}; use std::ops::Mul; - use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; fn assert_divq_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { @@ -22,7 +21,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -70,7 +69,7 @@ mod sequence_tests { .0; let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); diff --git a/jolt-inlines/keccak256/src/host.rs b/jolt-inlines/keccak256/src/host.rs index c480b4b84b..82d22ba48a 100644 --- a/jolt-inlines/keccak256/src/host.rs +++ b/jolt-inlines/keccak256/src/host.rs @@ -2,6 +2,7 @@ use crate::sequence_builder::Keccak256Permutation; jolt_inlines_sdk::register_inlines! { trace_file: "keccak256_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Keccak256, ops: [Keccak256Permutation], } diff --git a/jolt-inlines/keccak256/src/test_utils.rs b/jolt-inlines/keccak256/src/test_utils.rs index adf3f2d35e..d5f48bd03f 100644 --- a/jolt-inlines/keccak256/src/test_utils.rs +++ b/jolt-inlines/keccak256/src/test_utils.rs @@ -1,7 +1,6 @@ use crate::exec::execute_keccak_f; use crate::test_constants::{self, TestVectors}; use crate::Keccak256State; -use tracer::emulator::cpu::Xlen; use tracer::instruction::inline::INLINE; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; @@ -12,9 +11,9 @@ pub struct KeccakTestCase { pub description: &'static str, } -pub fn create_keccak_harness(xlen: Xlen) -> InlineTestHarness { +pub fn create_keccak_harness() -> InlineTestHarness { let layout = InlineMemoryLayout::single_input(136, 200); - InlineTestHarness::new(layout, xlen) + InlineTestHarness::new(layout) } pub fn instruction() -> INLINE { diff --git a/jolt-inlines/keccak256/src/tests.rs b/jolt-inlines/keccak256/src/tests.rs index 6e895b7ddc..e9999c67b5 100644 --- a/jolt-inlines/keccak256/src/tests.rs +++ b/jolt-inlines/keccak256/src/tests.rs @@ -2,12 +2,11 @@ mod exec { use crate::test_utils::*; - use tracer::emulator::cpu::Xlen; #[test] fn test_keccak256_direct_execution() { for (i, test_case) in keccak_test_vectors().iter().enumerate() { - let mut harness = create_keccak_harness(Xlen::Bit64); + let mut harness = create_keccak_harness(); harness.setup_registers(); harness.load_state64(&test_case.input); let instruction = instruction(); @@ -50,13 +49,12 @@ mod exec { mod exec_trace_equivalence { use crate::test_constants::*; use crate::test_utils::*; - use tracer::emulator::cpu::Xlen; #[test] fn test_keccak_against_reference() { let initial_state = [0u64; 25]; let expected_final_state = xkcp_vectors::AFTER_ONE_PERMUTATION; - let mut harness = create_keccak_harness(Xlen::Bit64); + let mut harness = create_keccak_harness(); harness.setup_registers(); harness.load_state64(&initial_state); let instruction = instruction(); diff --git a/jolt-inlines/p256/src/host.rs b/jolt-inlines/p256/src/host.rs index 34ad1f70d1..2523243844 100644 --- a/jolt-inlines/p256/src/host.rs +++ b/jolt-inlines/p256/src/host.rs @@ -4,6 +4,7 @@ use crate::sequence_builder::{ jolt_inlines_sdk::register_inlines! { trace_file: "p256_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::P256, ops: [ P256MulQ, P256SquareQ, diff --git a/jolt-inlines/p256/src/tests.rs b/jolt-inlines/p256/src/tests.rs index e4f7701a2c..06d7852151 100644 --- a/jolt-inlines/p256/src/tests.rs +++ b/jolt-inlines/p256/src/tests.rs @@ -6,7 +6,6 @@ mod p256_tests { }; use crate::{P256_CURVE_B, P256_GENERATOR_X, P256_GENERATOR_Y, P256_MODULUS, P256_ORDER}; use num_bigint::BigUint; - use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; // Helper: convert [u64; 4] little-endian limbs to BigUint @@ -55,7 +54,7 @@ mod p256_tests { fn assert_mulq_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { let expected = bigint_mulmod(a, b, &P256_MODULUS); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -73,7 +72,7 @@ mod p256_tests { fn assert_squareq_trace_equiv(a: &[u64; 4]) { let expected = bigint_mulmod(a, a, &P256_MODULUS); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.execute_inline(InlineTestHarness::create_default_instruction( @@ -90,7 +89,7 @@ mod p256_tests { fn assert_divq_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { let expected = bigint_divmod(a, b, &P256_MODULUS); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -108,7 +107,7 @@ mod p256_tests { fn assert_mulr_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { let expected = bigint_mulmod(a, b, &P256_ORDER); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -126,7 +125,7 @@ mod p256_tests { fn assert_squarer_trace_equiv(a: &[u64; 4]) { let expected = bigint_mulmod(a, a, &P256_ORDER); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.execute_inline(InlineTestHarness::create_default_instruction( @@ -143,7 +142,7 @@ mod p256_tests { fn assert_divr_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { let expected = bigint_divmod(a, b, &P256_ORDER); let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); diff --git a/jolt-inlines/sdk/src/host.rs b/jolt-inlines/sdk/src/host.rs index bc3fc233c6..22fd48745b 100644 --- a/jolt-inlines/sdk/src/host.rs +++ b/jolt-inlines/sdk/src/host.rs @@ -4,7 +4,7 @@ pub use num_bigint::BigUint as NBigUint; use tracer::utils::inline_sequence_writer::{write_inline_trace, InlineDescriptor, SequenceInputs}; pub use inventory; -pub use tracer::emulator::cpu::{Cpu, Xlen}; +pub use tracer::emulator::cpu::Cpu; pub use tracer::instruction; pub use tracer::instruction::format::format_inline::FormatInline; pub use tracer::instruction::inline::InlineRegistration; @@ -12,6 +12,7 @@ pub use tracer::instruction::Instruction; pub use tracer::utils::inline_helpers::{InstrAssembler, Value}; pub use tracer::utils::inline_sequence_writer::AppendMode; pub use tracer::utils::virtual_registers::VirtualRegisterGuard; +pub use tracer::InlineExtension; /// Convert a slice of `u64` limbs (little-endian) to `NBigUint`. pub fn limbs_to_nbiguint(limbs: &[u64]) -> NBigUint { @@ -336,6 +337,7 @@ impl MulAccExt for InstrAssembler { /// ```ignore /// register_inlines! { /// trace_file: "sha256_trace.joltinline", +/// extension: jolt_inlines_sdk::host::InlineExtension::Sha2, /// ops: [Sha256Compression, Sha256CompressionInitial], /// } /// ``` @@ -343,6 +345,7 @@ impl MulAccExt for InstrAssembler { macro_rules! register_inlines { ( trace_file: $trace_file:expr, + extension: $extension:expr, ops: [$first:ty $(, $rest:ty)*$(,)?]$(,)? ) => { pub fn store_inlines() -> Result<(), String> { @@ -357,15 +360,15 @@ macro_rules! register_inlines { Ok(()) } - $crate::__submit_inline_op!($first); - $($crate::__submit_inline_op!($rest);)* + $crate::__submit_inline_op!($first, $extension); + $($crate::__submit_inline_op!($rest, $extension);)* }; } /// Helper macro to submit a single `InlineOp` to inventory. #[macro_export] macro_rules! __submit_inline_op { - ($op:ty) => { + ($op:ty, $extension:expr) => { const _: () = { assert!( <$op as $crate::host::InlineOp>::OPCODE == 0x0B @@ -381,6 +384,7 @@ macro_rules! __submit_inline_op { opcode: <$op as $crate::host::InlineOp>::OPCODE, funct3: <$op as $crate::host::InlineOp>::FUNCT3, funct7: <$op as $crate::host::InlineOp>::FUNCT7, + extension: $extension, name: <$op as $crate::host::InlineOp>::NAME, build_sequence: <$op as $crate::host::InlineOp>::build_sequence, build_advice: <$op as $crate::host::InlineOp>::build_advice, diff --git a/jolt-inlines/secp256k1/src/host.rs b/jolt-inlines/secp256k1/src/host.rs index 7ef951e650..685f361056 100644 --- a/jolt-inlines/secp256k1/src/host.rs +++ b/jolt-inlines/secp256k1/src/host.rs @@ -5,6 +5,7 @@ use crate::sequence_builder::{ jolt_inlines_sdk::register_inlines! { trace_file: "secp256k1_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Secp256k1, ops: [ Secp256k1MulQ, Secp256k1SquareQ, diff --git a/jolt-inlines/secp256k1/src/tests.rs b/jolt-inlines/secp256k1/src/tests.rs index b10f599ede..b940d6f291 100644 --- a/jolt-inlines/secp256k1/src/tests.rs +++ b/jolt-inlines/secp256k1/src/tests.rs @@ -7,7 +7,6 @@ mod sequence_tests { }; use ark_ff::{BigInt, Field, PrimeField}; use ark_secp256k1::{Fq, Fr}; - use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; fn assert_divq_trace_equiv(a: &[u64; 4], b: &[u64; 4]) { @@ -22,7 +21,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -68,7 +67,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -117,7 +116,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.execute_inline(InlineTestHarness::create_default_instruction( @@ -161,7 +160,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -207,7 +206,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.load_input2_64(b); @@ -256,7 +255,7 @@ mod sequence_tests { // rs1=input1 (32 bytes), rs2=input2 (32 bytes), rs3=output (32 bytes) let layout = InlineMemoryLayout::two_inputs(32, 32, 32); - let mut harness = InlineTestHarness::new(layout, Xlen::Bit64); + let mut harness = InlineTestHarness::new(layout); harness.setup_registers(); harness.load_input64(a); harness.execute_inline(InlineTestHarness::create_default_instruction( diff --git a/jolt-inlines/sha2/src/host.rs b/jolt-inlines/sha2/src/host.rs index 4c1fb4919f..29a114280e 100644 --- a/jolt-inlines/sha2/src/host.rs +++ b/jolt-inlines/sha2/src/host.rs @@ -2,5 +2,6 @@ use crate::sequence_builder::{Sha256Compression, Sha256CompressionInitial}; jolt_inlines_sdk::register_inlines! { trace_file: "sha256_trace.joltinline", + extension: jolt_inlines_sdk::host::InlineExtension::Sha2, ops: [Sha256Compression, Sha256CompressionInitial], } diff --git a/jolt-inlines/sha2/src/sequence_builder.rs b/jolt-inlines/sha2/src/sequence_builder.rs index 5019a2a347..5c33e789cd 100644 --- a/jolt-inlines/sha2/src/sequence_builder.rs +++ b/jolt-inlines/sha2/src/sequence_builder.rs @@ -1,10 +1,10 @@ use core::array; use jolt_inlines_sdk::host::{ - instruction::{andn::ANDN, lw::LW, sw::SW}, + instruction::andn::ANDN, FormatInline, InlineOp, InstrAssembler, InstrAssemblerExt, Instruction, Value::{self, Imm, Reg}, - VirtualRegisterGuard, Xlen, + VirtualRegisterGuard, }; /// SHA-256 initial hash values @@ -72,60 +72,37 @@ impl Sha256SequenceBuilder { if !self.initial { // Load initial hash values from memory when using custom IV // Load all A-H into initial_state registers (used both for initial values and final addition) - if self.asm.xlen == Xlen::Bit32 { - (0..8).for_each(|i| { - self.asm - .emit_ld::(*self.iv[i], self.operands.rs1, (i * 4) as i64) - }); - } else { - (0..4).for_each(|i| { - self.asm.load_paired_u32_dirty( - self.operands.rs1, - (i as i64) * 8, - *self.iv[i * 2], - *self.iv[i * 2 + 1], - ); - }); - } - } - // Load input words into message registers - if self.asm.xlen == Xlen::Bit32 { - (0..16).for_each(|i| { - self.asm - .emit_ld::(*self.message[i], self.operands.rs2, (i * 4) as i64) - }); - } else { - (0..8).for_each(|i| { + (0..4).for_each(|i| { self.asm.load_paired_u32_dirty( - self.operands.rs2, + self.operands.rs1, (i as i64) * 8, - *self.message[i * 2], - *self.message[i * 2 + 1], + *self.iv[i * 2], + *self.iv[i * 2 + 1], ); }); } + // Load input words into message registers + (0..8).for_each(|i| { + self.asm.load_paired_u32_dirty( + self.operands.rs2, + (i as i64) * 8, + *self.message[i * 2], + *self.message[i * 2 + 1], + ); + }); // Run 64 rounds (0..64).for_each(|_| self.round()); self.final_add_iv(); // Store output values to rs1 location // Store output A..H in-order using the current VR mapping after all rotations - if self.asm.xlen == Xlen::Bit32 { - let outs = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; - for (i, ch) in outs.iter().enumerate() { - let src = self.vr(*ch); - self.asm - .emit_s::(self.operands.rs1, src, (i as i64) * 4); - } - } else { - let outs = [('A', 'B'), ('C', 'D'), ('E', 'F'), ('G', 'H')]; - for (i, (ch1, ch2)) in outs.iter().enumerate() { - self.asm.store_paired_u32( - self.operands.rs1, - (i as i64) * 8, - self.vr(*ch1), - self.vr(*ch2), - ); - } + let outs = [('A', 'B'), ('C', 'D'), ('E', 'F'), ('G', 'H')]; + for (i, (ch1, ch2)) in outs.iter().enumerate() { + self.asm.store_paired_u32( + self.operands.rs1, + (i as i64) * 8, + self.vr(*ch1), + self.vr(*ch2), + ); } // Total allocated: 8 (state) + 16 (message) + 8 (initial_state) + 4 (temps per round) = 36 // The temps are allocated/deallocated per round, but we need to reserve space for them diff --git a/jolt-inlines/sha2/src/tests.rs b/jolt-inlines/sha2/src/tests.rs index 82f3dea7d4..dfeea70fc1 100644 --- a/jolt-inlines/sha2/src/tests.rs +++ b/jolt-inlines/sha2/src/tests.rs @@ -1,10 +1,9 @@ use crate::{INLINE_OPCODE, SHA256_FUNCT3, SHA256_FUNCT7, SHA256_INIT_FUNCT3, SHA256_INIT_FUNCT7}; -use tracer::emulator::cpu::Xlen; use tracer::utils::inline_test_harness::{InlineMemoryLayout, InlineTestHarness}; -pub fn create_sha256_harness(xlen: Xlen) -> InlineTestHarness { +pub fn create_sha256_harness() -> InlineTestHarness { let layout = InlineMemoryLayout::single_input(64, 32); - InlineTestHarness::new(layout, xlen) + InlineTestHarness::new(layout) } pub fn instruction_sha256() -> tracer::instruction::inline::INLINE { @@ -105,13 +104,11 @@ mod exec_functions { mod sequence_tests { use super::*; use crate::test_constants::TestVectors; - use tracer::emulator::cpu::Xlen; #[test] fn test_sha256_direct_execution() { for (desc, block, initial_state, expected) in TestVectors::get_standard_test_vectors() { - let xlen = Xlen::Bit64; - let mut harness = create_sha256_harness(xlen); + let mut harness = create_sha256_harness(); harness.setup_registers(); harness.load_input32(&block); harness.load_state32(&initial_state); @@ -121,7 +118,7 @@ mod sequence_tests { assert_eq!( &expected, &result, - "SHA256 direct execution for {xlen:?}: {desc}, expected: {expected:08x?}, actual: {result:08x?}", + "SHA256 direct execution: {desc}, expected: {expected:08x?}, actual: {result:08x?}", ); } } @@ -129,8 +126,7 @@ mod sequence_tests { #[test] fn test_sha256init_direct_execution() { for (desc, block, _initial_state, expected) in TestVectors::get_standard_test_vectors() { - let xlen = Xlen::Bit64; - let mut harness = create_sha256_harness(xlen); + let mut harness = create_sha256_harness(); harness.setup_registers(); harness.load_input32(&block); harness.execute_inline(instruction_sha256init()); @@ -140,7 +136,7 @@ mod sequence_tests { assert_eq!( &expected, &result, - "SHA256INIT direct execution for {xlen:?}: {desc}, expected: {expected:08x?}, actual: {result:08x?}", + "SHA256INIT direct execution: {desc}, expected: {expected:08x?}, actual: {result:08x?}", ); } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 071e10e31e..3d21c0695b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.94" +channel = "1.95" targets = ["riscv32imac-unknown-none-elf", "riscv64imac-unknown-none-elf"] profile = "minimal" components = ["cargo", "rustc", "clippy", "rustfmt"] diff --git a/specs/1344-committed-bytecode-program-image.md b/specs/1344-committed-bytecode-program-image.md new file mode 100644 index 0000000000..b271b2263e --- /dev/null +++ b/specs/1344-committed-bytecode-program-image.md @@ -0,0 +1,414 @@ +# Spec: Committed Bytecode And Program Image Openings + +| Field | Value | +|-------------|------------------------------------------------------------------------| +| Author(s) | Amirhossein Khajehpour, Quang Dao | +| Created | 2026-05-11 | +| Status | draft | +| PR | [#1344](https://github.com/a16z/jolt/pull/1344) | + +## Summary + +This PR adds a committed program mode for bytecode and program-image openings. +In full mode, verifier preprocessing contains enough bytecode and RAM preprocessing to evaluate the bytecode and initial program image directly. +In committed mode, prover and verifier preprocessing instead agree on Dory commitments to bytecode chunks and to the initial program-image polynomial, then the prover proves all required bytecode/program-image openings through claim reductions that feed the existing Stage 8 batched Dory opening proof. + +The motivation is recursive and verifier-side efficiency. +Bytecode and program-image data are program constants, so a verifier should not have to repeatedly materialize and directly evaluate those tables when a preprocessing commitment and a succinct opening proof can bind the same data. + +## Intent + +### Goal + +Introduce a committed program mode that commits bytecode chunks and the initial program image, reduces all execution-derived claims about those committed polynomials into Stage 8, and preserves the same zkVM execution relation in both full and committed modes. + +The new proving-system surface is: + +- `ProgramMode::{Full, Committed}` in `jolt-core/src/zkvm/config.rs`. +- `ProgramPreprocessing::{Full, Committed}` in `jolt-core/src/zkvm/program.rs`. +- `CommittedPolynomial::BytecodeChunk(i)` and `CommittedPolynomial::ProgramImageInit` in `jolt-core/src/zkvm/witness.rs`. +- `VirtualPolynomial::BytecodeValStage(i)`, `VirtualPolynomial::BytecodeClaimReductionIntermediate`, and `VirtualPolynomial::ProgramImageInitContributionRw`. +- `SumcheckId::{BytecodeReadRafAddressPhase, BooleanityAddressPhase, BytecodeClaimReductionCyclePhase, BytecodeClaimReduction, ProgramImageClaimReductionCyclePhase, ProgramImageClaimReduction}`. +- Shared precommitted scheduling through `PrecommittedClaimReduction` in `jolt-core/src/zkvm/claim_reductions/precommitted.rs`. + +### Invariants + +- Full and committed program modes must prove the same guest execution relation. +- Committed mode must not let the verifier accept a proof for bytecode or program-image data that differs from the committed preprocessing. +- Prover and verifier must derive the same `ProgramMetadata`, bytecode chunk count, bytecode chunk geometry, program-image geometry, Dory layout, and precommitted scheduling reference. +- `bytecode_chunk_count` must be nonzero, at most `256`, a power of two, and must divide `bytecode_len`. +- The committed bytecode lane layout must encode the same values read by bytecode read-RAF: `rs1`, `rs2`, `rd`, unexpanded PC, immediate, circuit flags, instruction flags, lookup-table selector, and RAF flag. +- Every committed bytecode chunk polynomial must have length `committed_lanes() * (bytecode_len / bytecode_chunk_count)`. +- The program-image polynomial must be the RAM preprocessing bytecode-word slice padded to a power of two, with no semantic rewriting. +- Stage 4 program-image virtual claims must use the same remapped bytecode start address and RAM address challenge as the later program-image claim reduction. +- The precommitted opening-point permutation must be identical for prover, verifier, and Stage 8 RLC construction. +- A precommitted polynomial that does not participate in some cycle or address rounds must contribute exactly one factor of `1/2` per skipped round. +- The cycle-phase handoff scale and full final scale must not be conflated. +- In ZK mode, every `input_claim`, `output_claim_constraint`, and `*_constraint_challenge_values` formula for bytecode, program image, and advice reductions must match the non-ZK claim formula exactly. +- Stage 8 must use the same ordered opening IDs and the same `gamma_i * lagrange_i` coefficients in the prover's BlindFold opening-proof data and in the verifier's BlindFold constraints. + +Keep the implementation PR focused on direct prover/verifier tests and targeted unit tests. +New `jolt-eval` invariants for committed-program equivalence and Stage 8 opening-order consistency are useful follow-up work, but are not required to merge this PR. + +### Non-Goals + +- Do not redesign bytecode expansion or move program construction into `jolt-program`. + That work is covered by `specs/bytecode-expansion-crate.md` and PR [#1490](https://github.com/a16z/jolt/pull/1490). +- Do not change RISC-V instruction semantics, bytecode row semantics, RAM semantics, or advice semantics. +- Do not remove full program mode. +- Do not add separate Dory opening proofs for bytecode or program image. + Committed mode must batch these openings into the existing Stage 8 proof. +- Do not introduce compatibility shims for the old single-stage-6 proof serialization format. +- Do not make committed bytecode the default SDK path in this PR. +- Do not require external consumers to adopt a stable public committed-program API beyond the SDK helpers added for this branch. + +## Evaluation + +### Acceptance Criteria + +- [x] `ProgramPreprocessing::preprocess` still builds full bytecode and RAM preprocessing from decoded program data. +- [x] `ProgramPreprocessing::commit` converts full preprocessing into committed preprocessing by deriving bytecode chunk commitments, program-image commitments, and prover hints. +- [x] Serialized committed verifier preprocessing contains metadata and commitments, not prover-only full program data or opening hints. +- [x] `JoltSharedPreprocessing::new_committed` validates chunking, computes the committed maximum Dory variable count, derives a prover setup, commits program preprocessing, and updates `program_meta`. +- [x] Full mode continues to use the existing bytecode/RAM preprocessing semantics. +- [x] Committed mode appends `BytecodeChunk(i)` and `ProgramImageInit` to the expected proof commitment list. +- [x] Stage 4 caches `ProgramImageInitContributionRw` as a virtual opening when committed program mode is active. +- [x] Stage 6a verifies address-phase bytecode read-RAF and booleanity claims without needing full bytecode materialization on the verifier path. +- [x] Stage 6b includes bytecode and program-image claim reduction instances only in committed mode. +- [x] Stage 6b bytecode claim reduction converts staged `BytecodeValStage(i)` claims into committed bytecode chunk openings. +- [x] Stage 6b program-image claim reduction converts `ProgramImageInitContributionRw` into a committed `ProgramImageInit` opening. +- [x] Stage 7 address-phase reductions resume from the cycle-phase intermediate claims for all precommitted reductions that have address rounds. +- [x] Stage 8 includes bytecode chunks and program image in the random linear combination exactly when `ProgramMode::Committed` is used. +- [x] ZK mode BlindFold constraints include bytecode and program-image opening IDs with coefficients matching Stage 8 RLC coefficients. +- [x] Proof serialization includes `stage6a_sumcheck_proof`, `stage6b_sumcheck_proof`, and the new committed/virtual polynomial tags. +- [x] SDK macro output includes committed prover/shared preprocessing helpers that accept `bytecode_chunk_count`. +- [x] At least one end-to-end Dory test proves and verifies in committed program mode. + +### Testing Strategy + +Run standard and ZK e2e coverage: + +```bash +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk +cargo nextest run -p jolt-core muldiv_e2e_dory_committed_program_commitments --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv_e2e_dory_committed_program_commitments --cargo-quiet --features host,zk +``` + +Run advice/precommitted regression coverage because advice shares the same precommitted scheduling path: + +```bash +cargo nextest run -p jolt-core advice_e2e_dory --cargo-quiet --features host +RUST_MIN_STACK=33554432 cargo nextest run -p jolt-core advice_e2e_dory --cargo-quiet --features host,zk +cargo nextest run -p jolt-core final_advice_output_scale --cargo-quiet --features host +``` + +Run strict linting: + +```bash +cargo clippy --all --features host -q --all-targets -- -D warnings +cargo clippy --all --features host,zk -q --all-targets -- -D warnings +cargo fmt -q +``` + +Targeted tests should cover: + +- invalid bytecode chunk counts, +- bytecode chunk coefficient layout for representative instructions, +- `ProgramPreprocessing::{Full, Committed}` serialization roundtrips, +- proof serialization roundtrips for `BytecodeChunk(i)` and `ProgramImageInit`, +- `PrecommittedClaimReduction` identity and non-identity permutations, +- skipped-round scaling in cycle-only and cycle-plus-address cases, +- Stage 8 opening ID ordering in full mode and committed mode. + +### Performance + +Committed mode should reduce verifier and recursive-verifier work by replacing direct bytecode/program-image evaluation with committed openings. +The prover may pay extra preprocessing and Stage 8 work, but those costs must be batched into the existing Dory opening proof rather than paid as separate PCS proofs. + +Performance-sensitive paths: + +- `build_committed_bytecode_chunk_coeffs` must remain linear in nonzero bytecode lane work and avoid dense per-lane overhead beyond the committed chunk vector itself. +- Program-image commitment derivation should be linear in the padded program-image length. +- Stage 8 streaming RLC construction must consume committed bytecode chunks and the program image through `PrecommittedPolynomial` without regenerating unrelated witness polynomials. +- Verifier Stage 8 must combine commitments homomorphically and must not require committed bytecode or program-image coefficients. + +Benchmarks should compare full mode and committed mode on at least `muldiv`, `sha2`, and one larger bytecode example. +The expected verifier-side direction is improvement for committed mode; prover preprocessing may increase. + +## Design + +### Architecture + +Committed mode extends the program preprocessing pipeline: + +```text +Decoded program data + -> FullProgramPreprocessing { + bytecode: BytecodePreprocessing, + ram: RAMPreprocessing, + } + -> ProgramPreprocessing::Committed { + meta: ProgramMetadata, + bytecode_commitments: TrustedBytecodeCommitments, + program_commitments: TrustedProgramCommitments, + prover_data: Option, + } +``` + +`ProgramMetadata` records the verifier-facing program shape: entry address, minimum bytecode address, program-image word length, and bytecode length. +Committed verifier preprocessing keeps only metadata and commitments. +Committed prover preprocessing may retain full preprocessing and opening hints so the prover can construct witnesses and Stage 8 opening hints. + +### Program Mode And Proof Surface + +`ProgramMode::Full` is the legacy behavior: verifier preprocessing has the full bytecode and program image available. +`ProgramMode::Committed` means bytecode chunks and program image are committed preprocessing objects, and all claims about them must be proven through the protocol. + +The proof and preprocessing surface changes in three places: + +- `JoltSharedPreprocessing` stores `ProgramPreprocessing` and `bytecode_chunk_count`. +- `JoltProof` stores separate `stage6a_sumcheck_proof` and `stage6b_sumcheck_proof`. +- `CommittedPolynomial` and `VirtualPolynomial` serialization add the tags needed for committed bytecode/program-image reductions. + +This is a wire-format change for proof serialization. +Old proofs with a single Stage 6 field are not expected to deserialize. + +### Committed Bytecode Polynomial + +Committed bytecode is represented as one or more chunk polynomials. +Each chunk has a fixed lane capacity: + +```text +committed_lanes() + = next_power_of_two( + 3 * REGISTER_COUNT + + 2 + + NUM_CIRCUIT_FLAGS + + NUM_INSTRUCTION_FLAGS + + number_of_lookup_tables + + 1 + ) +``` + +The lane layout is: + +- one-hot `rs1`, +- one-hot `rs2`, +- one-hot `rd`, +- scalar unexpanded PC, +- scalar immediate, +- circuit flags, +- instruction flags, +- lookup table selector, +- RAF flag. + +For a bytecode table of length `T_bc` split into `C` chunks, each chunk has cycle length `T_bc / C`. +The implementation caps `C` at `256`, matching the `u8` serialization used for `BytecodeChunk(i)`. +The chunk polynomial has dimensions `committed_lanes() * (T_bc / C)`. +The coefficient index is derived through the active Dory layout so that commitment-time layout and opening-time layout agree. + +### Committed Program Image Polynomial + +The program-image polynomial is the initial RAM bytecode-word region from `RAMPreprocessing`. +It is padded to a power of two and committed as `CommittedPolynomial::ProgramImageInit`. + +The verifier does not hold the full program-image word slice in committed mode. +Instead, Stage 4 caches the scalar inner product: + +```text +C_rw = sum_j ProgramWord[j] * eq(r_address_rw, start_index + j) +``` + +The prover computes this from RAM preprocessing and appends it as `VirtualPolynomial::ProgramImageInitContributionRw` under `SumcheckId::RamValCheck`. +The verifier appends the same virtual opening point without the value. +The later program-image claim reduction proves that this staged scalar is consistent with the committed `ProgramImageInit` polynomial. + +### Shared Precommitted Geometry + +Bytecode chunks, program image, trusted advice, and untrusted advice are precommitted polynomials. +They may have fewer or more variables than the main trace-domain polynomials. +`PrecommittedClaimReduction::scheduling_reference` computes a shared reference domain from: + +- main trace-domain total variables `log_T + log_K_chunk`, +- committed bytecode chunk variables, +- committed program-image variables, +- trusted advice variables, +- untrusted advice variables. + +The reference domain determines: + +- `reference_total_vars`, +- cycle alignment rounds, +- address rounds, +- joint column variables, +- each precommitted polynomial's projected Dory opening-round permutation, +- active cycle and address rounds for each precommitted polynomial. + +When a precommitted polynomial is smaller than the reference domain, inactive rounds are skipped by multiplying the running claim by `1/2`. +When a precommitted polynomial dominates the main geometry, Stage 8 is anchored by the dominant precommitted opening point. +If multiple dominant precommitted openings exist, prover and verifier require them to agree. + +### Proving-System Stage Changes + +#### Preprocessing + +Full preprocessing still produces bytecode preprocessing and RAM preprocessing. +Committed preprocessing starts from the same full preprocessing, then: + +1. derives Dory commitments for bytecode chunks, +2. derives a Dory commitment for the program image, +3. stores metadata and commitments for verifier preprocessing, +4. stores full preprocessing and Dory opening hints only for prover preprocessing. + +Bytecode chunk commitments are derived sequentially under one Dory context because Dory context selection is process-global. +The default chunk count is `1`, so this does not remove parallelism from the default committed path. + +The shared preprocessing digest binds the serialized committed preprocessing to the Fiat-Shamir transcript. + +#### Stage 4: RAM Val Check + +Committed mode adds a program-image virtual claim to the RAM val-check flow. +After `RamVal` is opened at the read-write point, prover and verifier split out the RAM address component. +The prover evaluates the initial program-image word slice against that address equality polynomial and appends the scalar as `ProgramImageInitContributionRw`. +The verifier appends the same opening point so later constraints can refer to it. + +This does not replace the RAM val check. +It stages a program-image scalar that will be bound to the committed program-image polynomial by a later claim reduction. + +#### Stage 6a: Address-Phase Bytecode RAF And Booleanity + +Stage 6 is split into `stage6a` and `stage6b`. +Stage 6a handles address-phase work for bytecode read-RAF and booleanity. + +In committed mode, bytecode read-RAF verifier construction does not require full bytecode preprocessing. +Instead, address-phase bytecode read-RAF stages the `BytecodeValStage(i)` virtual claims that summarize the bytecode value columns needed later. +These staged values become the input claims to `BytecodeClaimReduction`. + +#### Stage 6b: Cycle-Phase Work And Precommitted Claim Reductions + +Stage 6b batches the remaining cycle-oriented sumchecks and all precommitted claim reductions. +The base Stage 6b batch still includes: + +- bytecode read-RAF cycle phase, +- booleanity cycle phase, +- RAM hamming booleanity, +- RAM RA virtualization, +- instruction RA virtualization, +- increment claim reduction. + +When advice commitments are present, Stage 6b also includes trusted and/or untrusted `AdviceClaimReduction`. +When committed program mode is active, Stage 6b also includes: + +- `BytecodeClaimReduction`, +- `ProgramImageClaimReduction`. + +`BytecodeClaimReduction` input in cycle phase is: + +```text +sum_i eta^i * BytecodeValStage(i) +``` + +where `eta` is sampled from the transcript. +The output is either an intermediate virtual claim at `BytecodeClaimReductionCyclePhase` or final committed bytecode chunk openings if no address phase remains. + +`ProgramImageClaimReduction` input in cycle phase is `ProgramImageInitContributionRw`. +The output is either an intermediate committed claim under `ProgramImageClaimReductionCyclePhase` or the final committed `ProgramImageInit` opening if no address phase remains. + +#### Stage 7: Address-Phase Claim Reduction Completion + +Stage 7 completes address-phase rounds for reductions that still have address variables. +For bytecode, the address phase reduces the intermediate claim into openings of `CommittedPolynomial::BytecodeChunk(i)`. +For program image, the address phase reduces the intermediate claim into an opening of `CommittedPolynomial::ProgramImageInit`. +For advice, the address phase reduces the cycle handoff claim into the final advice opening. + +All three use the same precommitted scheduling abstraction, so their opening points are normalized consistently for Stage 8. + +#### Stage 8: Batched Dory Opening + +Stage 8 constructs one unified opening point. +It then gathers claims for: + +- `RamInc`, +- `RdInc`, +- instruction RA polynomials, +- bytecode RA polynomials, +- RAM RA polynomials, +- optional trusted advice, +- optional untrusted advice, +- committed bytecode chunks in committed mode, +- committed program image in committed mode. + +Each claim whose natural opening point is smaller than the unified Dory point is multiplied by `compute_lagrange_factor(unified_point, polynomial_point)`. +The transcript samples `gamma` powers after non-ZK claims are absorbed. +The prover constructs the streaming RLC polynomial with all main and precommitted polynomials and proves one Dory opening at the unified point. +The verifier computes the same joint commitment homomorphically from proof commitments, trusted advice commitments, bytecode chunk commitments, and program-image commitments. + +In ZK mode, the Stage 8 claim values remain hidden. +Instead, BlindFold receives: + +```text +OpeningProofData { + opening_ids, + constraint_coeffs = gamma_i * lagrange_i, + joint_claim, + y_blinding, +} +``` + +The verifier builds the same `opening_ids` list through `stage8_opening_ids`. +In committed mode, this list appends each `BytecodeChunk(i)` at `SumcheckId::BytecodeClaimReduction` and `ProgramImageInit` at `SumcheckId::ProgramImageClaimReduction`. + +### Alternatives Considered + +The verifier could keep direct access to full bytecode and program-image data. +That preserves the old implementation but does not reduce recursive verifier cost. + +The prover could produce separate Dory openings for bytecode and program image. +That is simpler locally but loses Stage 8 batching and adds verifier work. + +Bytecode, program image, and advice could each use bespoke scheduling. +The PR instead uses one precommitted scheduling abstraction so all non-main committed objects share the same Dory embedding logic. + +## Documentation + +The Jolt book should document: + +- the difference between full and committed program modes, +- why bytecode and program image are precommitted polynomials, +- how precommitted geometry changes the Stage 8 Dory opening point, +- how dominant precommitted polynomials anchor Stage 8, +- how committed bytecode chunk count affects preprocessing. + +This PR already expands `book/src/how/architecture/opening-proof.md` with a precommitted geometry section. +Follow-up documentation should add a user-facing example for `--committed-bytecode ` and guidance for choosing the chunk count. + +The module comments in `jolt-core/src/zkvm/transpilable_verifier.rs` should also be updated to describe Stage 6a and Stage 6b instead of the old monolithic Stage 6. + +## Execution + +Implementation is organized as: + +1. Add committed program metadata and preprocessing in `jolt-core/src/zkvm/program.rs`. +2. Add committed bytecode lane layout and chunk coefficient construction in `jolt-core/src/zkvm/bytecode/chunks.rs`. +3. Add `BytecodeChunk(i)` and `ProgramImageInit` committed polynomial variants. +4. Add bytecode and program-image virtual polynomial IDs for staged claims. +5. Add shared precommitted scheduling in `jolt-core/src/zkvm/claim_reductions/precommitted.rs`. +6. Add `BytecodeClaimReduction` over staged bytecode val claims and committed bytecode chunks. +7. Add `ProgramImageClaimReduction` over staged program-image RAM contribution and committed program-image opening. +8. Wire committed-mode reductions into Stage 6b and Stage 7. +9. Wire committed-mode openings into Stage 8 RLC and BlindFold opening-proof constraints. +10. Add SDK committed preprocessing helpers and canonical `fibonacci` / `muldiv` example CLI paths. +11. Add committed-mode e2e tests and precommitted scheduling regression tests. + +## References + +- PR [#1344](https://github.com/a16z/jolt/pull/1344) +- Related program preprocessing spec: `specs/bytecode-expansion-crate.md` +- Spec template: `specs/TEMPLATE.md` +- Opening proof documentation: `book/src/how/architecture/opening-proof.md` +- Core committed program code: `jolt-core/src/zkvm/program.rs` +- Committed bytecode code: `jolt-core/src/zkvm/bytecode/chunks.rs` +- Precommitted scheduling: `jolt-core/src/zkvm/claim_reductions/precommitted.rs` +- Bytecode claim reduction: `jolt-core/src/zkvm/claim_reductions/bytecode.rs` +- Program-image claim reduction: `jolt-core/src/zkvm/claim_reductions/program_image.rs` diff --git a/specs/bytecode-expansion-crate.md b/specs/bytecode-expansion-crate.md index 1ac0d2adb4..f6f5ebe79b 100644 --- a/specs/bytecode-expansion-crate.md +++ b/specs/bytecode-expansion-crate.md @@ -5,7 +5,7 @@ | Author(s) | Quang Dao | | Created | 2026-05-01 | | Status | in review | -| PR | [#1490](https://github.com/a16z/jolt/pull/1490) | +| PR | [#1518](https://github.com/a16z/jolt/pull/1518), follow-up to [#1490](https://github.com/a16z/jolt/pull/1490) | ## Summary @@ -60,10 +60,9 @@ Keep the implementation PR's new checks focused on targeted `jolt-program`, `tra - This spec does not require redesigning bytecode commitments, lookup tables, or prover constraints. - This spec does not require moving PCS setup, commitment derivation, bytecode/program-image opening hints, BlindFold setup, `JoltProverPreprocessing`, or `JoltVerifierPreprocessing` out of `jolt-core`. - This spec does not require integrating bytecode-commitment PR [#1344](https://github.com/a16z/jolt/pull/1344). That work is a future integration constraint, not part of this implementation scope. -- This spec does not require supporting RV32 in `jolt-program`; RV32/ELF32 should be rejected by the new program pipeline. -- This spec does not require deleting historical RV32 branches from `tracer`; that cleanup can happen separately. -- TODO(tracer RV32 cleanup): when RV32 support is fully removed from `tracer`, update stale comments and docs that still describe `Xlen`, RV32, ELF32, or dual-width expansion behavior as live supported paths. -- TODO(ISA profiles): keep `InstructionKind` as the canonical flat row/discriminant for bytecode, serialization, and proof indexing, but move the source declaration behind it to a hierarchical instruction-family/capability table. That table should generate the flat enum plus metadata such as `instruction_family`, `required_capability`, and `is_supported_by(profile)`, so future profiles like RV64IM, RV64IMA, RV64IMAC, and later floating-point extensions can be selected cleanly without destabilizing committed bytecode rows. +- This spec does not require supporting RV32 in `jolt-program` or `tracer`; RV32/ELF32 should be rejected by the new program pipeline and the historical tracer RV32/SV32 execution paths should be deleted. +- This spec does not complete the deeper source-program versus final-Jolt instruction-kind split. `SourceInstructionKind` remains a decoder-facing mirror of the broad current instruction set, including Jolt custom source opcodes, and `JoltInstructionKind` remains the canonical flat row/discriminant used by `NormalizedInstruction`. A follow-up should split tracer's decoded-source and expanded-row APIs before shrinking `JoltInstructionKind`. +- TODO(ISA profiles): keep `JoltInstructionKind` as the canonical flat row/discriminant for bytecode, serialization, and proof indexing in this PR, but move the source declaration behind it to a hierarchical instruction-family/capability table. That table should generate the flat enum plus metadata such as `instruction_family`, `required_capability`, and `is_supported_by(profile)`, so future profiles like RV64IM, RV64IMA, RV64IMAC, and later floating-point extensions can be selected cleanly without destabilizing committed bytecode rows. - Pivot from the original Claude-approved spec: this PR should add an `InlineExpansionProvider` hook now, even though the earlier plan left registered custom inline handling in tracer as future work. The hook is worthwhile long-term because it lets `jolt-program::expand` remain the canonical expansion entry point for source programs that contain Jolt custom inline opcodes, while keeping the inline registry, advice computation, and tracer CPU state outside `jolt-program`. - This spec does not require exposing a stable public API for third-party consumers outside the Jolt workspace. - This spec does not require moving CPU execution, lazy trace iteration, advice tapes, or memory-device emulation into verifier-facing crates. Only the backend-neutral execution contract and output row types belong in `jolt-program`; concrete execution remains in `tracer`. @@ -84,8 +83,8 @@ Keep the implementation PR's new checks focused on targeted `jolt-program`, `tra - [x] `tracer` implements `jolt_program::execution::ExecutionBackend` inside the `tracer` crate and adapts its concrete CPU/cycle/lazy-trace machinery to the neutral trace contract at its boundary. - [x] SDK host-facing analyze/trace entry points invoke execution through a generic `B: ExecutionBackend` path, with the default tracer backend selected in `jolt-sdk` rather than in `jolt-trace`, `jolt-program`, or proof crates. Proof generation remains intentionally on the prover's concrete trace path in this PR because witness generation still requires lazy tracer checkpoints, advice tape output, and final-memory data that are not part of the neutral trace-row contract. - [x] Tracer-internal implementation changes do not require changes to `jolt-riscv`, `jolt-program`, `jolt-core`, or SDK macros unless the stable execution contract or normalized program row semantics intentionally change. -- [x] `NormalizedInstruction` includes an `instruction_kind: InstructionKind` field plus normalized operands, address, virtual sequence metadata, and compressed-instruction metadata. -- [x] `jolt-riscv` documents that `InstructionKind` is the canonical flat row identity, while ISA/profile hierarchy is expressed as generated metadata rather than nested row variants. +- [x] `NormalizedInstruction` includes an `instruction_kind: JoltInstructionKind` field plus normalized operands, address, virtual sequence metadata, and compressed-instruction metadata. +- [x] `jolt-riscv` documents that `JoltInstructionKind` is the canonical flat row identity in this PR, while `SourceInstructionKind` is only a decoder-facing mirror and the true source/final split remains follow-up work. - [x] `jolt-program::preprocess` owns materialized bytecode/RAM/program preprocessing artifacts consumed by both prover and verifier setup. - [x] `JoltInstruction` is a marker/conversion trait equivalent to `Into + TryFrom`, not a second accessor abstraction over the same fields. - [x] Any `JoltInstruction` impls for tracer's concrete instruction structs live in `tracer` as adapter impls, generated from the shared instruction-kind list where practical. @@ -100,7 +99,7 @@ Keep the implementation PR's new checks focused on targeted `jolt-program`, `tra - [x] `InstrAssembler` borrows `&mut ExpansionAllocator` during emission and does not own, clone, or hide allocator state behind shared ownership. - [x] Expansion APIs return explicit errors for allocation or malformed-expansion failures instead of introducing new panics in the core expansion path. - [x] `jolt-program::expand` exposes an `InlineExpansionProvider` trait and provider-taking expansion entry points for registered Jolt inline source opcodes, without depending on `tracer`, `inventory`, `Cpu`, advice tape internals, or concrete inline crates. -- [x] The default/provider-free expansion path returns an explicit unsupported-inline error for `InstructionKind::Inline` rows instead of silently treating them as already-expanded bytecode. +- [x] The default/provider-free expansion path returns an explicit unsupported-inline error for `JoltInstructionKind::Inline` rows instead of silently treating them as already-expanded bytecode. - [x] `tracer` implements the provider by adapting its existing inline registry and sequence builders to normalized `jolt-program` rows; inline advice generation remains trace-time execution behavior for this PR. - [x] A program-preprocessing path is implemented as `ELF bytes -> Rv64ProgramImage -> expanded bytecode -> JoltProgramPreprocessing`, and prover/verifier setup wraps that program preprocessing without re-decoding or re-expanding the program. - [x] The verifier-facing path does not depend on CPU execution, lazy tracing, memory-device emulation, advice I/O, or prover-only witness generation. @@ -277,12 +276,12 @@ The implementation should resolve this by moving the shared instruction vocabula The concrete execution data structures should remain in `tracer` unless implementation work proves that moving a small piece is clearly better. In particular, `tracer::instruction::Instruction`, `Cycle`, `RISCVCycle`, per-instruction structs such as `ADD`/`LW`, register-state types, RAM-access types, and execution traits stay in `tracer`. `jolt-riscv` should own the canonical bytecode/program row type: ```rust -pub enum InstructionKind { Add, Lw, /* ... */ } +pub enum JoltInstructionKind { ADD, LW, /* ... */ } pub struct NormalizedOperands { /* rs1, rs2, rd, imm */ } pub struct NormalizedInstruction { - pub instruction_kind: InstructionKind, + pub instruction_kind: JoltInstructionKind, pub operands: NormalizedOperands, pub address: u64, pub virtual_sequence_remaining: Option, @@ -293,6 +292,8 @@ pub struct NormalizedInstruction { `jolt-program::image`, `jolt-program::expand`, and `jolt-program::preprocess` should use `NormalizedInstruction`, not tracer's concrete instruction structs. `tracer` should provide conversion at its boundary, such as `From<&tracer::instruction::Instruction> for NormalizedInstruction` and `TryFrom for tracer::instruction::Instruction`, so execution can keep concrete structs while verifier-facing program preprocessing stays independent of `tracer`. +This PR intentionally keeps `NormalizedInstruction` backed by `JoltInstructionKind`, a broad flat row identity that still contains some source-only expansion inputs. `SourceInstructionKind` names decode results, including Jolt custom source opcodes, but mirrors that same broad set. This is not a clean source/final separation yet; doing that well requires changing tracer so decoded guest instructions and expanded trace/proof rows stop sharing the same concrete conversion path. + Expansion APIs should return concrete `NormalizedInstruction` rows rather than something parameterized as `Vec`. Expansion is a heterogeneous row-producing operation: one source instruction may emit different real and virtual instruction kinds, plus sequence metadata. `NormalizedInstruction` is the canonical representation of those rows. `JoltInstruction` should not duplicate the field-access API of `NormalizedInstruction`. If it remains, it should be only a narrow marker/conversion trait: @@ -388,7 +389,7 @@ The file list is intentionally concrete. Implementers may split individual instr `crates/jolt-riscv` owns the data model that must be shared by decoding, expansion, tracing, bytecode preprocessing, and verifier checks. It already contains Jolt instruction kind wrappers, `JoltInstruction`, `JoltInstructions`, and circuit/instruction flag metadata; this PR should remove its current `tracer` dependency by making it own the abstract instruction vocabulary, normalized row, and conversion marker rather than any tracer execution types: -- `src/kind.rs`: own `InstructionKind`, the canonical names of real and virtual Jolt instructions, plus static metadata such as side-effect classification. +- `src/kind.rs`: own `JoltInstructionKind`, the canonical names of real and virtual Jolt instructions, plus static metadata such as side-effect classification. It also owns the preparatory `SourceInstructionKind` decode mirror until a later tracer-aware source/final split can shrink the target enum. - `src/operands.rs`: own `NormalizedOperands` and operand accessors. - `src/normalized.rs`: own `NormalizedInstruction`, including `instruction_kind`, normalized operands, address, virtual sequence metadata, and compressed-instruction metadata. - `src/jolt_instruction.rs`: define the marker trait as `Into + TryFrom`; do not implement it blanket-style for `tracer::instruction::RISCVInstruction`. @@ -396,9 +397,9 @@ The file list is intentionally concrete. Implementers may split individual instr - `src/uncompress.rs`: own RV64 compressed RISC-V decompression helpers used by ELF decode. - `src/traits.rs`: define pure traits needed by instruction metadata. Execution-specific traits must remain outside this crate. -The shared instruction list and macro input should move into `jolt-riscv`. Today `tracer/src/instruction/mod.rs` generates `Instruction` and `Cycle` from the same instruction list. After the split, `jolt-riscv` should own the canonical list and use it to generate `InstructionKind` plus pure metadata dispatch. `tracer` should reuse that same list by invoking an exported `macro_rules!` token tree from `jolt-riscv`, for example `jolt_riscv::for_each_instruction_kind!`, to define its concrete `Instruction`, `Cycle`, `RISCVCycle`, and execution/trace dispatch. The implementation must not duplicate the long instruction list across crates. +The shared instruction list and macro input should move into `jolt-riscv`. Today `tracer/src/instruction/mod.rs` generates `Instruction` and `Cycle` from the same instruction list. After the split, `jolt-riscv` should own the canonical list and use it to generate `JoltInstructionKind` plus pure metadata dispatch. `tracer` should reuse that same list by invoking an exported `macro_rules!` token tree from `jolt-riscv`, for example `jolt_riscv::for_each_instruction_kind!`, to define its concrete `Instruction`, `Cycle`, `RISCVCycle`, and execution/trace dispatch. The implementation must not duplicate the long instruction list across crates. -The canonical instruction list should not stay permanently flat at the declaration level. The row/discriminant type should remain flat because it is used in serialization, bytecode commitments, lookup indexing, and proof code, but the source list should be grouped by capability/family, for example `rv64i`, `rv64m`, `rv64a`, `zicsr`, `system`, `jolt_virtual`, and `jolt_inline`. That grouped declaration should generate the flat `InstructionKind` enum, family/capability metadata, profile checks, and tracer dispatch lists from one source of truth. Compressed instructions should be represented as an input decoding capability rather than as separate bytecode instruction kinds: `C.ADDI` decodes to `ADDI` with `is_compressed = true`, so disabling RV64C should reject 16-bit encodings at `jolt-program::image` rather than remove an `InstructionKind` variant. +The canonical instruction list should not stay permanently flat at the declaration level. The row/discriminant type should remain flat because it is used in serialization, bytecode commitments, lookup indexing, and proof code, but the source list should be grouped by capability/family, for example `rv64i`, `rv64m`, `rv64a`, `zicsr`, `system`, `jolt_virtual`, and `jolt_inline`. That grouped declaration should generate the flat `JoltInstructionKind` enum, family/capability metadata, profile checks, and tracer dispatch lists from one source of truth. Compressed instructions should be represented as an input decoding capability rather than as separate bytecode instruction kinds: `C.ADDI` decodes to `ADDI` with `is_compressed = true`, so disabling RV64C should reject 16-bit encodings at `jolt-program::image` rather than remove a row-kind variant. The profile API can be a follow-up, but the intended shape is: @@ -419,13 +420,13 @@ pub enum IsaProfile { Rv64IMAC, } -impl InstructionKind { +impl JoltInstructionKind { pub const fn family(self) -> InstructionFamily { /* generated */ } } impl IsaProfile { pub const fn supports_compressed(self) -> bool { /* generated */ } - pub const fn supports_kind(self, kind: InstructionKind) -> bool { /* generated */ } + pub const fn supports_kind(self, kind: JoltInstructionKind) -> bool { /* generated */ } } ``` @@ -443,14 +444,14 @@ pub trait InlineExpansionProvider { } ``` -and expose provider-taking entry points such as `expand_instruction_with_provider` and `expand_program_with_provider`. The provider-free `expand_instruction`/`expand_program` functions should still exist for ordinary RV64/Jolt-virtual expansion, but must reject source `InstructionKind::Inline` rows explicitly. `tracer` can then implement the provider by calling the existing inventory-backed `(opcode, funct3, funct7)` inline registry and normalizing the returned sequence. This is a deliberate pivot from leaving inlines entirely in tracer: it keeps the program construction pipeline unified while avoiding a larger refactor of `jolt-inlines-sdk::InlineOp`, old typed `InstrAssembler`, and `build_advice` in this PR. +and expose provider-taking entry points such as `expand_instruction_with_provider` and `expand_program_with_provider`. The provider-free `expand_instruction`/`expand_program` functions should still exist for ordinary RV64/Jolt-virtual expansion, but must reject source `JoltInstructionKind::Inline` rows explicitly. `tracer` can then implement the provider by calling the existing inventory-backed `(opcode, funct3, funct7)` inline registry and normalizing the returned sequence. This is a deliberate pivot from leaving inlines entirely in tracer: it keeps the program construction pipeline unified while avoiding a larger refactor of `jolt-inlines-sdk::InlineOp`, old typed `InstrAssembler`, and `build_advice` in this PR. `crates/jolt-program` owns program image decoding, bytecode expansion, materialized program preprocessing, and the backend-neutral execution contract. The first three are one package because they are one program-construction pipeline; the execution module is adjacent because it defines how a built program is handed to an execution backend. The internal modules should remain separate enough that dependency and formalization boundaries are still visible. `jolt-program::image` owns deterministic ELF parsing: - `image/elf.rs`: move the non-execution part of `tracer::decode`: parse ELF, reject ELF32/RV32, compute entry address, collect RAM image bytes, decode `.text` into RV64 `NormalizedInstruction` values, and compute `program_end`. -- `image/decode.rs` or an equivalent module: own RV64 opcode decoding into `NormalizedInstruction` using `jolt-riscv`'s `InstructionKind`, `NormalizedOperands`, and decode helpers. +- `image/decode.rs` or an equivalent module: own RV64 opcode decoding into `NormalizedInstruction` using `jolt-riscv`'s `SourceInstructionKind`, `JoltInstructionKind`, `NormalizedOperands`, and decode helpers. - `image/mod.rs`: expose `Rv64ProgramImage` and `decode_elf`. - `error.rs`: replace warnings/panics in decode with explicit `ProgramError` or `DecodeError` values where possible. If current behavior must preserve `UNIMPL` insertion for invalid words, encode that policy explicitly. @@ -724,13 +725,13 @@ jolt-core or future committed-program crate During the initial cutover, moving bytecode preprocessing to `NormalizedInstruction` exposed one proof-semantics subtlety that was previously hidden by `jolt-core`'s per-instruction concrete `Flags` impls. `virtual_sequence_remaining == Some(0)` does not by itself mean the R1CS `IsLastInSequence` flag should be set. In current proof semantics, `IsLastInSequence` is only used to skip `NextPCEqPCPlusOneIfInline` for `JALR` at the end of trap-related inline sequences, where the next PC may jump to a trap handler instead of advancing to the next virtual bytecode row. Other final helper instructions, such as an `ADDI` with `virtual_sequence_remaining == Some(0)`, must remain `VirtualInstruction` rows but must not set `IsLastInSequence`; otherwise the `NextPCEqPCPlusOneIfInline` constraint is incorrectly suppressed. -The canonical flag behavior should therefore live in `jolt-riscv`: `VirtualInstruction` is derived from `virtual_sequence_remaining.is_some()`, `DoNotUpdateUnexpandedPC` is derived from `virtual_sequence_remaining.unwrap_or(0) != 0`, and `IsLastInSequence` is derived from `instruction_kind == InstructionKind::JALR && virtual_sequence_remaining == Some(0)`. The implementation should keep an expanded-bytecode parity test that compares normalized flags against the existing concrete instruction flags so future changes to this behavior are intentional. +The canonical flag behavior should therefore live in `jolt-riscv`: `VirtualInstruction` is derived from `virtual_sequence_remaining.is_some()`, `DoNotUpdateUnexpandedPC` is derived from `virtual_sequence_remaining.unwrap_or(0) != 0`, and `IsLastInSequence` is derived from `instruction_kind == JoltInstructionKind::JALR && virtual_sequence_remaining == Some(0)`. The implementation should keep an expanded-bytecode parity test that compares normalized flags against the existing concrete instruction flags so future changes to this behavior are intentional. The allocator cutover also exposed several non-obvious expansion invariants that should stay covered by fixture consistency tests. First, helper instructions emitted by an expansion must themselves be canonicalized through `jolt-program::expand`; for example an emitted `SLLI` row may become `VirtualMULI`, and skipping recursive canonicalization changes the bytecode. Second, some signed DIV/REM expansions must delay temporary-register allocation until after nested multiplication helpers return; otherwise the existing eight-register virtual pool is exhausted. Third, `REMUW` intentionally reuses its advice register as scratch while `DIVUW` needs a separate quotient/temp split. Fourth, RV64 `SCW` spills through the reservation register before expanding the nested store because `SW` consumes almost the whole temporary pool. These are behavioral compatibility constraints, not merely implementation details. The CSR cutover exposed one intentional behavior correction rather than a compatibility constraint. The historical tracer expansion path returned `NormalizedInstruction::default()` for `CSRRW`/`CSRRS` when the decoded CSR address was `0`. In the normalized `jolt-program` path, that default row has address `0`; `BytecodePCMapper` skips address-zero bytecode rows, so a decoded CSR-zero instruction can fail to receive a bytecode PC entry. The implementation now rejects these rows as `ExpansionError::UnsupportedCsr(0)`. This is the narrow exception to byte-for-byte parity with the pre-cutover tracer behavior, and it should remain covered by direct expansion tests plus preprocessing/PC-mapping tests if future code paths admit arbitrary decoded rows. -RV64 ELF decode has one additional boundary to keep explicit. Registered custom inline opcodes carry dispatch metadata in `opcode`, `funct3`, and `funct7`; a generic `InstructionKind::Inline` row alone is not enough to reconstruct the concrete tracer instruction. The normalized inline row should therefore preserve this dispatch metadata, while the registered-inline sequence/advice registry can remain a separate execution-boundary question until it is intentionally moved out of tracer. +RV64 ELF decode has one additional boundary to keep explicit. Registered custom inline opcodes carry dispatch metadata in `opcode`, `funct3`, and `funct7`; a generic `JoltInstructionKind::Inline` row alone is not enough to reconstruct the concrete tracer instruction. The normalized inline row should therefore preserve this dispatch metadata, while the registered-inline sequence/advice registry can remain a separate execution-boundary question until it is intentionally moved out of tracer. The execution-backend cutover should proceed incrementally. The implementation now provides the stable lower hook: `jolt-core::host::Program` can build a `jolt_program::JoltProgram` and run it through any `jolt_program::execution::ExecutionBackend`, while existing tracer-returning host convenience APIs remain available for compatibility. A follow-up SDK pass should move generated default host tracing/proving conveniences onto that hook, constructing `tracer::TracerBackend` in SDK wiring rather than asking lower program/preprocessing layers to name tracer internals. @@ -788,7 +789,7 @@ Update the Jolt book only if the crate is exposed to users or changes contributo - `tracer/src/utils/inline_helpers.rs`: move expansion helpers to `jolt-program::expand`; delete or replace with imports during the same full cutover. - `tracer/src/utils/virtual_registers.rs`: move allocator to `jolt-program::expand`; delete or replace imports. - `crates/jolt-riscv/Cargo.toml`: remove `tracer` and add any lower-level dependencies needed by moved instruction data. -- `crates/jolt-riscv/src/lib.rs`: expose the canonical instruction-kind list, `InstructionKind`, `NormalizedInstruction`, `NormalizedOperands`, `JoltInstruction`, pure traits, and flags without referencing `tracer` or `jolt-program`. +- `crates/jolt-riscv/src/lib.rs`: expose the canonical instruction-kind list, `JoltInstructionKind`, `SourceInstructionKind`, `NormalizedInstruction`, `NormalizedOperands`, `JoltInstruction`, pure traits, and flags without referencing `tracer` or `jolt-program`. - `crates/jolt-riscv/src/instructions/**`: keep existing Jolt instruction kind wrappers and pure instruction metadata; do not import tracer concrete instruction structs. - `crates/jolt-riscv/src/normalized.rs`: define `NormalizedInstruction` with an `instruction_kind` field. - `crates/jolt-riscv/src/jolt_instruction.rs`: remove the blanket impl over `tracer::instruction::RISCVInstruction`; define only the marker/conversion trait without depending on tracer. @@ -798,7 +799,7 @@ Update the Jolt book only if the crate is exposed to users or changes contributo - `jolt-core/src/zkvm/ram/mod.rs`: move or reexport pure RAM preprocessing from `jolt-program::preprocess`; leave prover/verifier sumcheck modules in `jolt-core`. - `jolt-core/src/zkvm/verifier.rs`: use program preprocessing types from `jolt-program::preprocess`, while keeping `JoltVerifierPreprocessing` if PCS setup remains in `jolt-core`. - `jolt-core/src/zkvm/prover.rs`: update imports for program preprocessing, bytecode preprocessing, RAM preprocessing, and program instruction types. -- `jolt-core/src/poly/**`, `jolt-core/src/subprotocols/**`, `jolt-core/src/zkvm/**`: update imports from `tracer::instruction` to `jolt-riscv` where the code needs `NormalizedInstruction`, `InstructionKind`, `JoltInstruction`, or static instruction data. +- `jolt-core/src/poly/**`, `jolt-core/src/subprotocols/**`, `jolt-core/src/zkvm/**`: update imports from `tracer::instruction` to `jolt-riscv` where the code needs `NormalizedInstruction`, `JoltInstructionKind`, `JoltInstruction`, or static instruction data. - `jolt-sdk/macros/src/lib.rs`: generated preprocessing functions should call the modular decode/expand/program-preprocess path; generated prove/analyze/trace entry points should accept an execution backend through `jolt_program::execution::ExecutionBackend` or construct the default `tracer::TracerBackend` in SDK host wiring, without naming tracer internals in lower-level generated types. - `book/**` or developer docs: add an architecture page or section if maintainers want crate-boundary docs in the book. @@ -810,14 +811,14 @@ Remove these only after all call sites are cut over: - `tracer/src/utils/virtual_registers.rs` - decode/uncompress helpers under `tracer/src/instruction/` once `jolt-program::image` owns RV64 decode into `NormalizedInstruction` -Do not delete tracer's concrete instruction structs, instruction-format structs, execution-specific trace implementations, or RV32 cleanup code in this PR. +Do not delete tracer's concrete instruction structs, instruction-format structs, or execution-specific trace implementations in this PR. Historical RV32/SV32 support code is in scope for removal. ### Implementation Checklist 1. [x] Add empty `jolt-program` crate under `crates/`, workspace member, workspace dependency, and minimal module skeletons. 2. [x] Refactor existing `jolt-riscv` so it no longer depends on `tracer`. -3. [x] Add `InstructionKind`, `NormalizedInstruction`, normalized operands, marker `JoltInstruction`, and local static instruction metadata to `jolt-riscv` while keeping tracer execution types out of `jolt-riscv`. -4. [x] Add an `instruction_kind: InstructionKind` field to `NormalizedInstruction` and use it as the row type returned by decode/expansion and consumed by preprocessing. +3. [x] Add `JoltInstructionKind`, preparatory `SourceInstructionKind`, `NormalizedInstruction`, normalized operands, marker `JoltInstruction`, and local static instruction metadata to `jolt-riscv` while keeping tracer execution types out of `jolt-riscv`. +4. [x] Add an `instruction_kind: JoltInstructionKind` field to `NormalizedInstruction` and use it as the row type returned by decode/expansion and consumed by preprocessing. 5. [x] Move RV64 opcode decode into `jolt-program::image` and RV64 compressed-instruction decompression helpers into `jolt-riscv`; reject RV32/ELF32 in the new program pipeline. 6. [x] Export the canonical instruction-kind list from `jolt-riscv` as a macro such as `jolt_riscv::for_each_instruction_kind!`, and use it from `tracer` to generate its concrete `Instruction`, `Cycle`, and `RISCVCycle`. 7. [x] Remove the `JoltInstruction` blanket impl over `tracer::instruction::RISCVInstruction`; add concrete tracer adapter impls only in `tracer` if needed. diff --git a/specs/compiler-native-bytecode-expansion.md b/specs/compiler-native-bytecode-expansion.md index e6cb4be332..a8ae2754ed 100644 --- a/specs/compiler-native-bytecode-expansion.md +++ b/specs/compiler-native-bytecode-expansion.md @@ -1,1882 +1,449 @@ -# Spec: Compiler-Native Bytecode Expansion +# Spec: Compiler-Native RV64 Bytecode Expansion -| Field | Value | -|-------------|-----------------------------------------------------------------------| -| Author(s) | Quang Dao | -| Created | 2026-05-05 | -| Status | draft | -| Related PR | [#1490](https://github.com/a16z/jolt/pull/1490) | -| Baseline | `quang/bytecode-expand-spec` at `a3448e6da44f` | -| Depends on | `specs/bytecode-expansion-crate.md` | +| Field | Value | +|------------|-------| +| Author(s) | Quang Dao | +| Created | 2026-05-05 | +| Revised | 2026-05-13 | +| Status | implemented design | +| Related PR | [#1490](https://github.com/a16z/jolt/pull/1490), [#1518](https://github.com/a16z/jolt/pull/1518) | ## Summary -PR #1490 moves bytecode expansion out of `tracer` and into `jolt-program::expand`. That crate boundary is the right direction for formal verification, but the current implementation at `a3448e6da44f` still uses an idiomatic recursive Rust assembler shape that is hard for Hax and Aeneas to extract: - -- family expanders build `Vec` values; -- `InstrAssembler<'a>` owns a sequence while borrowing `&'a mut ExpansionAllocator`; -- `InstrAssembler::emit` recursively calls `expand_instruction`; -- temporary-register release is encoded in Rust call-stack/control-flow order; -- metadata is stamped by mutating a finished slice; -- inline expansion is a trait callback inside the core dispatch path. - -This spec proposes a second-phase rewrite of `jolt-program::expand` into a compiler-native, extraction-friendly production implementation. The goal is not to add a proof-only model next to production code. The production expander itself should become a first-order lowering pipeline over explicit data transitions, while preserving byte-for-byte output and keeping runtime performance the same or better. - -This rewrite should also align with the MLIR-shaped Jolt work in the -`refactor/crates` branch, which currently treats Bolt as a compiler pipeline -over explicit dialects, passes, schema validation, typed plans, and generated -artifacts. Bytecode expansion is smaller than Bolt's prover/verifier pipeline, -but it has the same compiler shape: a source IR, a target IR, legality -constraints, lowering rules, resource materialization, validation, and -emission into production Rust data structures. - -The target design: +`jolt-program::expand` lowers decoded RV64 source rows into final Jolt bytecode +rows through a compiler-style pipeline: ```text -Decoded NormalizedInstruction - -> Source(NormalizedInstruction) - -> syntactic expansion recipe / shallow lowerer - -> rd=x0 normalization - -> depth-first work stack of ExpansionOp - -> explicit temp release/reset operations - -> bounded per-source output buffer - -> Expanded(NormalizedInstruction) - -> Stamped(NormalizedInstruction) - -> top-level Vec extension -``` +decoded RV64 source row + -> readable per-instruction lowering + -> ExpandedInstructionSequence recipe + -> ExpansionState materializer + -> stamped Jolt bytecode rows +``` + +Concrete lowerers are written as assembly-like recipes using +`ExpansionBuilder`. The builder records what should be emitted, expanded, +allocated, and released. It does not own allocator state and does not recursively +call the public expander. One central materializer interprets the recipe, +resolves symbolic temps to real virtual registers, recursively expands helper +rows, validates target legality, and stamps sequence metadata. + +The design favors production Rust readability and exact register-budget +invariants. Hax/Aeneas extraction is useful as an audit path, but current +extractor limitations do not dictate Rust style. + +## Scope + +This PR combines four related cleanups that reinforce the same boundary: + +- RV64-only program construction and tracing. Historical RV32/SV32 execution + paths are removed from tracer code, and ELF32/RV32 inputs are rejected at the + `jolt-program::image` boundary. +- lookup-backed instruction identity is split out of the flat instruction enum. + The decoded-source versus expanded-Jolt boundary now uses + `SourceInstruction` for decode/expansion input and + `JoltInstruction` for finalized expansion output. +- provider-free expansion is moved from a recursive assembler with borrowed + allocator state to a recipe/materializer pass. +- serialization derives in `jolt-riscv` and `jolt-program` are feature-gated so + the no-default expansion surface does not pull serde or ark serialization into + extraction experiments. + +Word instructions such as `ADDW`, `LW`, and `AMO*W` remain RV64 word +operations. They are not RV32 execution support. ## Goals -- Preserve expansion behavior exactly relative to PR #1490 at `a3448e6da44f`. -- Preserve recursive expansion order exactly. -- Preserve `rd = x0` behavior for all source and helper rows. -- Preserve virtual-register numbering, allocation reuse, reserved registers, and inline reset behavior. -- Preserve sequence metadata: source address, `virtual_sequence_remaining`, `is_first_in_sequence`, and compressed-instruction metadata. -- Make expansion recipes syntactic and inspectable enough to drive production Rust, Lean extraction/model generation, docs, and parity fixtures from one source of truth. -- Keep the recipe representation MLIR-ready: represent instruction emission, temps, metadata, and reset behavior as typed ops/attrs/regions that can later be moved into an MLIR dialect without changing expansion behavior. -- Keep `jolt-program::expand` free of tracer, CPU, memory-device, advice-tape, prover, transcript, ELF parser, and PCS dependencies. -- Make Hax/Aeneas extraction of provider-free RV64 expansion straightforward enough that the first real extraction target is the production core, not a hand-written mirror. -- Avoid performance regressions by removing recursive per-instruction heap allocation and using bounded stack-resident buffers for per-source expansion. +- Keep program construction and bytecode expansion RV64-only. +- Preserve expansion behavior relative to the post-#1490 baseline. +- Keep source rows and final Jolt rows phase-typed while removing the separate + lookup-kind enum. +- Preserve tracer ownership of concrete execution semantics while making its + source and final conversion paths explicit. +- Keep concrete lowerers readable enough for humans to add or revise opcodes. +- Centralize allocator ownership, recursive helper expansion, target-legality + validation, metadata stamping, recursion-depth checks, and `rd = x0` rewriting. +- Keep registered inline expansion behind an explicit provider boundary. +- Keep provider-free expansion independent of tracer CPU state, advice tapes, + prover internals, transcript code, PCS code, and ELF parsing. +- Keep serialization dependencies out of the no-default extraction surface. ## Non-Goals -- Do not change instruction semantics. -- Do not define a structured grammar for instruction execution semantics in Pass 1. The expansion grammar is only a syntactic lowering language: it says that a source row such as `DIV` maps to a sequence of bytecode rows, but it does not define the operational meaning of `DIV` or of those target rows. A separate semantics track should handle execution meaning and expansion-correctness theorems. -- Do not change bytecode preprocessing, RAM preprocessing, or proof-system APIs except for call-site adjustments needed by the new expansion API. -- Do not formalize tracer custom inline registries in this phase. -- Do not port registered `jolt-inlines` recipes or advice builders into the grammar in the next implementation pass. Advice handling is explicitly unresolved for the grammar; provider-owned inline/advice behavior stays behind the adapter boundary in this phase. -- Do not make Aeneas/Hax extraction a hard CI requirement in the implementation PR unless maintainers explicitly ask for it. -- Do not keep the current recursive `InstrAssembler<'a>` implementation as a compatibility layer once the rewrite lands. This branch owns the new `jolt-program` implementation, so the rewrite should be a full cutover. - -## Baseline: Current PR Shape At `a3448e6da44f` - -The current PR implementation lives under: - -- `crates/jolt-program/src/expand/mod.rs` -- `crates/jolt-program/src/expand/allocator.rs` -- `crates/jolt-program/src/expand/assembler.rs` -- per-family modules such as `arithmetic.rs`, `memory.rs`, `division.rs`, `shifts.rs`, and `control_flow.rs` - -Current public entry points: - -```rust -pub fn expand_instruction( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError>; - -pub fn expand_instruction_with_provider( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, - inline_provider: &mut P, -) -> Result, ExpansionError>; - -pub fn expand_program( - instructions: impl IntoIterator, -) -> Result, ExpansionError>; -``` - -Current family expanders are shaped like: - -```rust -pub(super) fn expand_addiw( - instruction: &NormalizedInstruction, - allocator: &mut ExpansionAllocator, -) -> Result, ExpansionError> { - let mut asm = - assembler::InstrAssembler::new(instruction.address, instruction.is_compressed, allocator); - asm.emit_i( - InstructionKind::ADDI, - rd(instruction)?, - rs1(instruction)?, - instruction.operands.imm, - )?; - asm.emit_i( - InstructionKind::VirtualSignExtendWord, - rd(instruction)?, - rd(instruction)?, - 0, - )?; - asm.finalize() -} -``` - -This looks small, but `asm.emit_i` calls `expand_instruction` recursively. Aeneas/Hax do not see "ADDIW lowers to two rows"; they see ADDIW lower into a borrowed assembler that recursively invokes the full dispatch path. - -Current `InstrAssembler` shape: - -```rust -pub struct InstrAssembler<'a> { - address: usize, - is_compressed: bool, - has_inline_instr_format: bool, - sequence: Vec, - allocator: &'a mut ExpansionAllocator, -} -``` - -The lifetime-bearing field is the biggest extraction smell. It lets normal Rust code write ergonomic expansion snippets, but it gives extraction tools a long-lived mutable borrow stored inside another mutable structure. - -### Baseline Extraction Experiment - -The smallest metadata-only slice works: - -```bash -cargo hax -C -p jolt-program \; into \ - -i '+!jolt_program::expand::metadata::set_sequence_metadata' \ - --output-dir /tmp/jolt-hax-bytecode-expand-metadata lean - -/Users/quang.dao/Documents/Lean/aeneas/charon/charon/target/release/charon cargo \ - --preset=aeneas \ - --hide-allocator \ - --start-from crate::expand::metadata::set_sequence_metadata \ - --dest-file /tmp/jolt-program-metadata.llbc \ - -- -p jolt-program - -/Users/quang.dao/Documents/Lean/aeneas/src/_build/install/default/bin/aeneas \ - -backend lean \ - -dest /tmp/jolt-aeneas-metadata-lean \ - -split-files \ - -namespace JoltProgram \ - /tmp/jolt-program-metadata.llbc -``` - -The next slice, `expand::arithmetic::expand_addiw`, already exposes the structural issues: - -```bash -/Users/quang.dao/Documents/Lean/aeneas/charon/charon/target/release/charon cargo \ - --preset=aeneas \ - --hide-allocator \ - --start-from crate::expand::arithmetic::expand_addiw \ - --dest-file /tmp/jolt-program-addiw.llbc \ - -- -p jolt-program - -/Users/quang.dao/Documents/Lean/aeneas/src/_build/install/default/bin/aeneas \ - -backend lean \ - -dest /tmp/jolt-aeneas-addiw-lean \ - -split-files \ - -namespace JoltProgram \ - /tmp/jolt-program-addiw.llbc -``` - -Observed Charon warning: - -```text -warning: Could not reconstruct `Box` initialization; branching during `Box` initialization is not supported. -``` - -Observed Aeneas errors included: - -```text -Unsupported operation: shallow-init-box(move v@...) -The input arguments don't have the proper type -The pushed variables and their values do not have the same type -Internal error, please file an issue -Expected an arrow type -Unreachable -``` - -These errors come from the shape of the extracted call graph, not from bytecode expansion semantics. - -## Current Vs Target Shape - -| Concern | Current PR at `a3448e6da44f` | Target shape | -|---------|-------------------------------|--------------| -| Source of truth | Arbitrary Rust functions using an imperative assembler API | Inspectable syntactic expansion recipes or shallow lowerers | -| Expansion state | Split between `InstrAssembler<'a>` and borrowed `&mut ExpansionAllocator` | One owned `ExpansionState` containing allocator, work stack, and output buffer | -| Family lowering | Expander calls `asm.emit_*`; each emit recursively expands immediately | Expander appends shallow `ExpansionOp` values only | -| Recursion | Hidden inside `InstrAssembler::emit` | One central depth-first driver | -| Temp lifetime | Encoded by Rust control flow and post-`finalize()` releases | Explicit `ExpansionOp::Release(register)` markers | -| Output allocation | Per-source `Vec` plus recursive helper `Vec`s | Bounded per-source buffer plus one top-level program `Vec` | -| Metadata | Mutate `&mut [NormalizedInstruction]` after synthetic sequences are built; non-expanded source rows pass through unchanged | Explicit stage policy: source pass-through, expanded helper rows, stamped synthetic sequence | -| Allocator | `[bool; N]` plus `Vec` reset list | Bitsets for live registers and inline reset set | -| Inline expansion | Trait callback inside core dispatcher | Adapter outside provider-free core | -| API ergonomics | `impl IntoIterator`, trait generic provider path | Concrete slice/state core; ergonomic wrappers outside | -| Extracted call graph | Pulls in much of `jolt-program` and serde/ark derives | Small `expand` core module graph | - -## Target Module Layout - -Suggested module layout: - -```text -crates/jolt-program/src/expand/ - mod.rs public adapters and compatibility API - grammar.rs syntactic recipe data and checked helpers - core.rs RowStage, ExpansionOp, ExpansionState, driver - lower.rs dispatch from helper rows to shallow lowerers/recipe interpreter - lower/ - arithmetic.rs - control_flow.rs - division.rs - memory.rs - shifts.rs - allocator.rs bitset allocator transitions - buffer.rs fixed-capacity WorkStack and ExpansionBuffer - metadata.rs metadata stamping and sequence invariants - operands.rs total operand projection helpers - inline.rs provider adapter; outside extraction-critical core - error.rs small core error enum; display impls can be feature-gated -``` - -The important separation is `grammar + core + lower + allocator + buffer + metadata + operands` versus `inline + public ergonomic wrappers`. The extraction target should be the first group. - -## Compiler And MLIR Alignment - -The `refactor/crates` branch at `4e6c4a635` contains Markos's Bolt-shaped view -of Jolt as an explicit compiler pipeline. Bolt's current docs describe the -generic lowering path as: - -```text -protocol -> concrete -> party -> compute -> cpu -> Rust -``` - -The key rule is that compiler facts live in dialect ops, validators, lowering -passes, or typed plan data before Rust emission. Generated Rust is the final -artifact, not where protocol meaning should be rediscovered by string matching -or ad hoc helper logic. The Bolt paper draft in -`/Users/quang.dao/Documents/SNARKs/bolt` makes the same architectural point: -dialects are domain vocabularies of typed operations and attributes, passes are -partial functions between modules, and each lowering pass should state the -invariants it preserves. - -Bytecode expansion should use the same mental model: - -```text -rv64.source - -> expand.canonical - -> expand.lowered - -> expand.allocated - -> jolt.bytecode - -> Rust Vec -``` - -These names are conceptual phase markers, not a request to introduce MLIR now. -They should guide the Rust design so that a future MLIR dialect can be a -mechanical lift rather than another rewrite. - -### Source And Target - -Today source and target are both represented by `NormalizedInstruction`, and -that should stay true. The compiler boundary should be defined by legality -predicates and stage policy rather than by a new family of row wrapper types: - -```rust -pub(crate) fn is_source_row(row: NormalizedInstruction) -> bool; -pub(crate) fn is_target_bytecode_row(row: NormalizedInstruction) -> bool; -``` - -The source IR is a decoded RV64 program row with address, compressed flag, and -source operands. It may contain instructions that are not legal final Jolt -bytecode rows, such as `ADDIW`, `MULH`, `LB`, `SCW`, `DIV`, `Inline`, and CSR -operations. - -The target IR is the bytecode stream consumed by preprocessing and the proof -system: every row is legal for the final bytecode relation, internal virtual -register use has been materialized, sequence metadata is correct, and -pass-through or literal rows preserve their baseline metadata policy. - -The expansion contract is syntactic. This module says which bytecode rows are -emitted for a decoded source row and how virtual registers, recursive helper -expansion, and sequence metadata are materialized. It does not define the -execution semantics of the source instruction or of the target instructions. -Those semantics can be hand-modeled separately if needed. For this PR branch, -row-for-row parity against the current Rust output remains the first acceptance -test except where this PR intentionally fixes a documented baseline bug. - -### Execution Semantics Track - -Execution semantics should be a separate artifact from bytecode expansion. -Today production execution lives in `tracer`, but `tracer` is an emulator with -CPU state, memory devices, advice plumbing, inline registries, and host -conveniences. It is the implementation to test against, not the cleanest -semantic source of truth. - -The coherent split is: - -```text -Expansion recipe: - source NormalizedInstruction -> target bytecode rows - -Execution semantics: - instruction + abstract machine state -> next abstract machine state - -Expansion correctness: - executing the expanded target sequence refines executing the source row -``` - -The recommended first semantics pass is hand-modeled Lean, not MLIR and not -extraction from `tracer`. Define a small abstract machine state and a transition -relation for a provider-free instruction slice: - -```text -step : NormalizedInstruction -> MachineState -> Result MachineState Trap -``` - -Then prove or test the expansion theorem for that slice: - -```text -exec_seq (expand instr) (extend state) - projects_to -step instr state -``` - -Start with a narrow family such as `ADDI`, `ADD`, shifts, loads/stores, and one -virtual helper instruction. That slice should reveal whether hand-modeled Lean -stays manageable or whether a separate semantic DSL is worth building. - -If a semantic DSL becomes useful, it should be a different language from the -expansion grammar. It would describe effects such as register reads/writes, -memory reads/writes, branches, traps, and advice reads, and could generate a -Rust reference interpreter, Lean definitions, and MLIR op interfaces or -documentation. MLIR should act as a carrier for dialect structure, legality, -lowering, and op metadata; it should not be the first source of proof -semantics. - -Existing RISC-V formal models such as Sail may be useful for the base ISA, but -they will not cover Jolt virtual helper instructions or Jolt-specific execution -conventions. Even if Sail is used for RV64 semantics, Jolt still needs its own -semantic layer for target bytecode rows. - -### Advice Boundary - -Advice should also be split by phase. The current code uses "advice" for three -different channels, and the compiler-native rewrite should preserve them while -naming the boundaries more precisely. - -1. **Advice-load source instructions.** - `AdviceLB`, `AdviceLH`, `AdviceLW`, and `AdviceLD` are decoded guest/source - instructions. Provider-free expansion owns their syntactic lowering: - - ```text - AdviceLB rd -> VirtualAdviceLoad rd, 1; SLLI rd, rd, 56; SRAI rd, rd, 56 - AdviceLH rd -> VirtualAdviceLoad rd, 2; SLLI rd, rd, 48; SRAI rd, rd, 48 - AdviceLW rd -> VirtualAdviceLoad rd, 4; SLLI rd, rd, 32; SRAI rd, rd, 32 - AdviceLD rd -> VirtualAdviceLoad rd, 8 - ``` - - `VirtualAdviceLoad` execution reads bytes from the runtime advice tape and - writes the result to `rd`. Expansion emits the row and byte length; it does - not produce the tape contents. - -2. **Trace-time advice payloads.** - `VirtualAdvice` is a target bytecode row whose concrete tracer instruction - carries an extra `advice: u64` payload. That payload is not part of - `NormalizedInstruction`. Today it is patched by tracer-side logic, including - registered inline `build_advice` functions and LR/SC success-bit handling for - `SCW`/`SCD`. - - Expansion may emit `VirtualAdvice`, but provider-free expansion must not - assign its payload. Inline/provider code may continue returning finalized - rows for this phase, and tracer may continue patching concrete - `VirtualAdvice` instructions during trace construction. - -3. **Committed advice memory.** - `trusted_advice` and `untrusted_advice` are byte arrays placed in Jolt device - memory and committed as advice polynomials by the prover. This is - preprocessing/proof behavior, not bytecode expansion behavior. The expansion - rewrite should not move or reinterpret those commitments. - -The better long-term shape is: - -```text -ExpansionRecipe: - emits bytecode rows, including VirtualAdvice and VirtualAdviceLoad - -AdvicePlan: - describes how trace-time VirtualAdvice payloads or advice-tape bytes are produced - -ExecutionSemantics: - defines how advice-consuming rows read those payloads/tape bytes -``` - -For the semantics follow-up, advice should first be modeled abstractly as an -oracle or tape in the Lean machine state. A concrete `AdvicePlan` or advice DSL -should be introduced only after one advice-bearing slice shows what state the -plan must observe and how slots/tape positions should be named. MLIR can carry -effect metadata such as `reads_advice_tape(byte_len)` or -`requires_trace_advice(slot)`, but it should not hide the source of advice -values inside expansion recipes. - -### Lowering Pipeline - -The proposed recipe/driver shape should map onto ordinary compiler passes: - -```text -decode/uncompress - produces rv64.source rows - -canonicalize-source - normalizes root-level rd=x0 behavior, pass-through rows, literal metadata policies, and documented baseline fixes - -validate-source - rejects malformed or unsupported source rows such as CSRRW/CSRRS with CSR address 0 - -legalize-expansion - repeatedly rewrites illegal source/helper ops with instruction-family recipes - -inline-fragments - expands recipe fragments such as amo_pre64/amo_post64 with alpha-renamed temps - -allocate-temps - materializes symbolic temps into concrete virtual registers using deterministic first-fit allocation - -reset-inline-temps - emits reset rows for touched inline registers after checking no inline temps are live - -materialize-metadata - stamps synthetic sequence metadata and preserves pass-through/literal metadata - -verify-target - checks target legality, no live temps, bounded sequence size, and dependency hygiene - -emit-rust - appends final rows to Vec -``` - -The current implementation conflates most of these passes inside -`InstrAssembler::emit`. The compiler-native rewrite should separate them in -the data model even if the initial Rust implementation fuses several passes for -performance. - -### Compiler Concepts In This Rewrite - -The rewrite is using standard compiler ideas under different names: - -| Bytecode expansion concept | Compiler concept | Why it matters here | -|----------------------------|------------------|---------------------| -| `InstructionKind` families | Dialect operation set | Each instruction is an op with operand shape, side effects, and legality. | -| Source-only vs final kinds | Legalization target | Expansion is a conversion from illegal source ops to legal target ops. | -| `LowerStmt::Emit` | Rewrite pattern / conversion pattern | A recipe replaces one op with a sequence of lower-level ops. | -| Recursive helper expansion | Conversion driver / worklist legalization | Emitted helper ops must themselves be legalized until all target ops are legal. | -| Work fuel / recursion depth | Termination guard | Legalization needs a simple bound so accidental cycles become errors. | -| `Seq`, `If`, `WithTemp` | Structured regions/control flow | Recipes need regions instead of arbitrary Rust control flow. | -| `Fragment` | Symbolic helper / inlining | Shared lowering snippets should be named regions with explicit arguments and alpha-renamed locals. | -| Operand refs and conditions | Pure row syntax | Operand selection should be separate from stateful expansion effects. | -| `TempId` | Virtual register / temporary SSA value | Recipes should name symbolic temps before concrete register allocation. | -| `ExpansionAllocator` | Register allocation / bufferization | Concrete virtual-register numbers are resource materialization, not semantic lowering. | -| `Release` | Lifetime end / deallocation | Reuse requires explicit lifetime boundaries and validation. | -| `RowStage::{Source, Expanded, Stamped}` | Attribute/materialization policy | Metadata is an emitted attribute policy, not an incidental mutation. | -| Recipe checks | IR verifier/schema validation | Bad recipes should be rejected structurally where practical. | -| `expand_program_slice` | Compiler driver | The public API orchestrates passes and emits the final artifact. | - -The most important adjustment to the previous grammar proposal is to treat -concrete virtual-register assignment as a materialization pass. Recipes should -prefer symbolic temps: - -```rust -WithTemp { - temp: T0, - pool: RegisterPool::Instruction, - body: Seq(&[ - Emit(i(ADDI, Temp(T0), SourceRs1, imm(0))), - Emit(i(ADDI, SourceRd, Reserved(CsrTarget), imm(0))), - Emit(i(ADDI, Reserved(CsrTarget), Temp(T0), imm(0))), - ]), -} -``` - -The driver may still allocate the concrete register at `WithTemp` execution -time for performance and exact parity. Conceptually, however, the grammar has a -symbolic temporary whose live range is the `WithTemp` body. This is much closer -to MLIR/SSA form, and it makes the future MLIR lowering natural: - -```mlir -%t0 = expand.alloc_temp {pool = "instruction"} : !expand.vreg -expand.emit "ADDI"(%t0, %rs1) {imm = 0} -expand.emit "ADDI"(%rd, %csr_target) {imm = 0} -expand.emit "ADDI"(%csr_target, %t0) {imm = 0} -expand.release_temp %t0 -``` - -That IR can later be lowered to concrete Jolt bytecode by a register-allocation -pass that uses the same first-fit policy as today's Rust allocator. - -### MLIR-Ready Shape - -The Rust recipe surface should avoid choices that would be awkward in MLIR: - -- Prefer named ops with typed operands/attrs over free-form closures. -- Prefer explicit regions (`Seq`, `If`, `WithTemp`) over Rust call-stack - effects. -- Prefer symbolic temps plus a deterministic allocation pass over hard-coded - register numbers in recipes. -- Store operand shapes, side-effect classes, and target legality as typed data - or traits that a verifier can inspect. -- Keep baseline quirks and intentional fixes as explicit ops, attrs, or verifier - rules. A true literal row can still be represented as - `expand.literal_default_row`, but the CSR-zero fix should be represented as a - validation failure such as `expand.fail unsupported_csr`, not hidden inside a - Rust branch. -- Treat metadata stamping as attribute materialization at a phase boundary. -- Keep schema/validator tests close to the grammar, the same way Bolt validates - transcript threading, party projection, kernel-free verifier IR, and import - boundaries. - -Possible future dialect split: +- Do not change RISC-V or Jolt instruction semantics. +- Do not reintroduce RV32 or ELF32 support. +- Do not add compatibility aliases for renamed instruction kinds. +- Do not model execution semantics in the expander. Expansion defines bytecode + lowering, not operational execution. +- Do not move registered `jolt-inlines` implementations into the provider-free + grammar. +- Do not require Hax/Aeneas extraction in CI. +- Do not move all tracer concrete execution types into `jolt-riscv`. + `jolt-riscv` owns shared row identity/profile facts; tracer owns execution. + +## Instruction Phases + +Instruction identity is now separated at the typed row boundary: + +- `SourceInstruction` is the decode-facing source row. The enum + variant is the source identity; the row payload carries address, operands, + inline metadata, and compression state. +- `JoltInstruction` is the finalized bytecode row view. Its variants + omit source-only rows such as word/narrow memory lowerings, atomics, CSRs, + traps, and inline dispatch. +- `JoltInstructionProfile` defines positive source legality, target legality, + inline-extension availability, profile-local dense indexes, and a profile + fingerprint. +- Bytecode and program preprocessing record the selected profile fingerprint so + serialized preprocessing artifacts carry the profile identity used for + legality and dense-index derivation. +- Decode, expansion, sequence stamping, and bytecode preprocessing receive the + selected profile explicitly. Registered inline inventory entries declare their + `InlineExtension`, and `InlineExpansionProvider` rejects registered keys whose + extension is not enabled by the active profile. +- `LookupInstructionKind` has been removed. Lookup/proving metadata remains + owned by lookup/proving code and is keyed by final instruction identities. + +The remaining compatibility caveat is the legacy bare `JoltInstructionKind` +tag enum: it is still broad while the typed final enum and profile legality are +final-only. That lets existing row serialization and some tracer internals keep +working during this PR. Source-only recipes now use `SourceInstructionRow` as their source +context, so the bare tag bridge is no longer needed merely to pass source +operands, address, or compressed-row metadata into expansion recipes. + +## Expansion Pipeline + +Each source-only lowerer returns an `ExpandedInstructionSequence`: + +```rust +let mut asm = ExpansionBuilder::new(*instruction.row()); + +let tmp = asm.allocate()?; +asm.expand_i( + SourceInstructionKind::VirtualPow2, + tmp.operand(), + reg(rs2(instruction)?), + 0, +); +asm.emit_r( + JoltInstructionKind::MUL, + reg(rd(instruction)?), + reg(rs1(instruction)?), + tmp.operand(), +); +asm.release(tmp); -```text -riscv.norm decoded RV64 source rows and operand formats -jolt.bytecode final legal Jolt bytecode ops and sequence attrs -expand temporary lowering ops: emit, alloc_temp, release_temp, reset_inline, literal +asm.finalize() ``` -The near-term Rust implementation does not need to expose these names publicly. -It should, however, make each concept visible enough that moving to MLIR's -dialect conversion infrastructure later would mostly replace the driver and -grammar interpreter, not the specification. - -### Literature Pointers - -Useful compiler concepts to read alongside this design: - -- **SSA form.** The classic reference is Cytron et al., - "Efficiently Computing Static Single Assignment Form and the Control - Dependence Graph" (TOPLAS 1991). Bolt's paper draft uses SSA as the default - representation style; our symbolic `TempId` discipline is the bytecode - expansion analogue. -- **MLIR dialects and multi-level lowering.** Lattner et al., - "[MLIR: Scaling Compiler Infrastructure for Domain Specific Computation](https://research.google/pubs/mlir-scaling-compiler-infrastructure-for-domain-specific-computation/)" - (CGO 2021) is the main reference. The relevant lesson is not "use MLIR now"; - it is "keep each abstraction level explicit and lower through typed dialects." -- **Dialect conversion / legalization.** MLIR's - [Dialect Conversion](https://mlir.llvm.org/docs/DialectConversion/) - documentation describes conversion targets, rewrite patterns, type - conversion, and legality. Our `is_target_bytecode_row`, lowering recipes, and - worklist driver are the same idea in smaller Rust form. -- **Pattern rewriting.** MLIR's docs index points to generic DAG rewriting and - table-driven declarative rewrite rules. Our recipes should be declarative - rewrite patterns where possible, with Rust builders used only to construct - inspectable recipe data. -- **Register allocation, liveness, and bufferization.** Temp materialization and - explicit `Release` markers are a small register-allocation problem. The Bolt - paper's `bufferize`/`cpu` discussion is the closest local analogy: pure SSA - values become explicit mutable resources only near the target boundary. - -## Declarative Expansion Grammar - -Ari's [`Lost in Translation`](https://randomwalks.xyz/blog/translations/) reaches the right general conclusion for Jolt verification: arbitrary Rust is the wrong long-term source of truth. His article applies that idea to instruction execution semantics. This spec is narrower. It does not propose a CPU-state semantics AST or an execution-semantics DSL. The bytecode expansion grammar is an expansion-time syntax transformer: it turns one `NormalizedInstruction` source row into zero or more `NormalizedInstruction` bytecode rows while preserving allocation, recursive helper expansion, and metadata behavior. - -The recipe surface must be expressive enough to model the current implementation, including the inconvenient parts: - -- Emitted helper rows are not final output immediately. They go back through the central expansion driver, exactly like `InstrAssembler::emit` currently calls `expand_instruction`. -- Some source rows are pass-through rows, not synthetic sequences. A source `ADD` currently returns the input `NormalizedInstruction` unchanged; it does not get `virtual_sequence_remaining = Some(0)` or `is_first_in_sequence = true`. -- Synthetic sequences do get finalized metadata. Current family expanders use `InstrAssembler::finalize`, which rewrites every row's sequence metadata and puts `is_compressed` only on the last row. -- `rd = x0` has two separate behaviors: side-effect-free rows become a direct ADDI no-op, while side-effecting rows are recursively expanded after rewriting `rd` to a temporary register and releasing that temporary after expansion. -- Temporary-register allocation order is observable through emitted register numbers. Recipes or lowerer outputs must expose temp lifetimes at precise points, and the materialization pass must preserve today's deterministic allocation order. -- Temporary-register release timing matters for reuse inside longer expansions such as `SCD`, `SCW`, CSR updates, and division. -- Some branches depend on decoded operands or expansion parameters, for example `rd == rs1`, `rs1 == x0`, `csr == 0`, `word`, `signed`, `min`, and `remainder_output`. -- Shared snippets such as `amo_pre64` and `amo_post64` are real grammar fragments with parameters, not arbitrary Rust helper functions. -- Errors are expansion outcomes. Missing operands, unsupported CSRs, virtual-register exhaustion, and inline-provider requirements should be explicit recipe/driver results. -- Baseline quirks and deliberate baseline fixes must both be made visible. The historical `CSRRW`/`CSRRS` behavior for `csr == 0` returned `NormalizedInstruction::default()` directly, bypassing assembler finalization and producing an address-zero row that could be skipped by bytecode PC mapping. This PR intentionally rejects that source row as `UnsupportedCsr(0)`, so the compiler-native rewrite should model the case as a source validation failure or recipe `Fail`, not as a literal default row to preserve. - -The key design rule is to keep the recipe surface first-order and inspectable, -not to pre-commit to a large expression language. Separate pure operand -selection from expansion-time statements where that buys clarity, but introduce -only the expression forms that real expansion families need. - -The minimal durable grammar is likely closer to: - -```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum RegRef { - Zero, - SourceRd, - SourceRs1, - SourceRs2, - Temp(TempId), - Reserved(ReservedReg), - Const(u8), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ImmRef { - SourceImm, - Const(i128), - Derived(DerivedImm), -} +Builder methods have precise meaning: -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Cond { - RdEqZero, - Rs1EqZero, - RdEqRs1, - CsrEq(u16), - CsrSupported, - Param(ExpansionParam), -} -``` +- `emit_*` records a row that is already target-legal. +- `expand_*` records a helper row that must pass through provider-free + expansion before it is appended to the source row's final bytecode sequence. +- `allocate()` creates a symbolic `TempId`. +- `release(temp)` records the end of a symbolic temp lifetime. +- `finalize()` returns the recipe for materialization. -`RegRef`, `ImmRef`, and `Cond` are pure. They may inspect the current source -row, recipe parameters, named temps, and reserved-register map, but they cannot -allocate, release, emit rows, or mutate output. +Pure recording operations are infallible. Fallible operations are limited to +places where failure can actually occur: operand decoding, symbolic temp +allocation, recursive materialization, target validation, capacity checks, and +real virtual-register allocation/release. -If a family truly needs compound immediate or condition expressions, add the -smallest first-order representation that covers that family. Avoid heap-backed -`Box` trees and Rust closures in the extraction-critical core. Constructor -helpers can still make recipes readable, but `finish()` should return ordinary -data that tests and extractors can inspect. +### Recipe Data Model -Rows are also pure specifications: +`ExpandedInstructionSequence` is a recipe, not finalized bytecode: ```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum RowTemplate { - R { - kind: InstructionKind, - rd: RegRef, - rs1: RegRef, - rs2: RegRef, - }, - I { - kind: InstructionKind, - rd: RegRef, - rs1: RegRef, - imm: ImmRef, - }, - // Add S/B/J/U/Align variants only if direct row construction helpers are - // not enough for the families being ported. +pub(super) struct ExpandedInstructionSequence { + source: SourceInstructionRow, + ops: Vec, } -``` -Expansion-time statements are the only layer that changes allocator or output state: - -```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum LowerStmt { - Seq(&'static [LowerStmt]), +pub(super) enum ExpansionOp { Emit(RowTemplate), - WithTemp { - temp: TempId, - pool: RegisterPool, - body: &'static LowerStmt, - }, - If { - cond: Cond, - then_body: &'static LowerStmt, - else_body: &'static LowerStmt, - }, - Fragment { - fragment: FragmentId, - args: FragmentArgs, - }, + Expand(SourceInstructionRowTemplate), + Allocate(TempId), Release(TempId), - ReturnLiteral(LiteralRow), - Fail(ExpansionErrorKind), - ResetInlineRegisters, -} -``` - -This enum is a sketch, not a required minimum surface. During implementation, -pare it down aggressively. `Seq`, `Emit`, `WithTemp`, `If`, `Fragment`, -`ReturnLiteral`, `Fail`, and reset/release markers are the concepts that seem -stable; the exact helper enums should follow the code. - -`Emit(RowTemplate)` means "push a helper row back through the expansion -driver." It does not mean "append this final instruction to output." This -distinction is what preserves current recursive behavior without recursive Rust -calls. - -`WithTemp` introduces a symbolic temp whose live range is exactly the statement body. The current Rust interpreter may materialize the concrete virtual register when entering the statement and lower to an explicit `Release` operation after the body. Nested `WithTemp` blocks are the grammar form for current mid-sequence release/reuse patterns: - -```rust -// Shape of the current CSRRW rd == rs1 branch. -WithTemp { - temp: T0, - pool: RegisterPool::Instruction, - body: Seq(&[ - Emit(i(ADDI, Temp(T0), SourceRs1, Const(0))), - Emit(i(ADDI, SourceRd, Reserved(CsrTarget), Const(0))), - Emit(i(ADDI, Reserved(CsrTarget), Temp(T0), Const(0))), - ]), -} -``` - -The grammar compiler/interpreter should materialize `T0` at this point and emit the `Release(T0)` after the third row has recursively expanded and before any following statement. That gives the same observable allocation reuse as the current hand-written Rust while keeping the recipe itself in symbolic-temp form. - -A simple lowerer becomes data: - -```rust -pub(crate) const ADDIW_RECIPE: LowerStmt = Seq(&[ - Emit(i(ADDI, SourceRd, SourceRs1, SourceImm)), - Emit(i(VirtualSignExtendWord, SourceRd, SourceRd, Const(0))), -]); -``` - -A parameterized family becomes either a recipe with parameters or a small recipe builder whose output is still grammar, not arbitrary emit calls: - -```rust -pub(crate) fn load_recipe( - kind: InstructionKind, - sign_extension_shift: Option, -) -> Recipe { - let mut recipe = RecipeBuilder::new(); - recipe.emit(i(kind, SourceRd, SourceRs1, SourceImm)); - - if let Some(shift) = sign_extension_shift { - let shift = recipe.imm_const(shift); - recipe.emit(i(SLLI, SourceRd, SourceRd, shift)); - recipe.emit(i(SRAI, SourceRd, SourceRd, shift)); - } - - recipe.finish() } ``` -The exact Rust surface may differ, but the extraction requirement is that the -output is still inspected as recipe data or shallow operations. A builder that -can perform arbitrary Rust-side emission recreates the current problem. +The operation meanings are fixed: -Complex branches should be visible either as recipe data or as shallow lowerer -control flow that produces inspectable operations. For example, CSR update -behavior needs to preserve the current branch structure: +- `Emit` appends a row that is already legal final Jolt bytecode. +- `Expand` records a helper row that must be recursively lowered by the central + provider-free driver. +- `Allocate` creates a symbolic temp lifetime. +- `Release` ends a symbolic temp lifetime. -```rust -pub(crate) const CSRRW_RECIPE: LowerStmt = If { - cond: CsrEq(0), - then_body: &Fail(ExpansionErrorKind::UnsupportedCsr), - else_body: &If { - cond: CsrSupported, - then_body: &If { - cond: RdEqZero, - then_body: &Emit(i(ADDI, Reserved(CsrTarget), SourceRs1, Const(0))), - else_body: &If { - cond: RdEqRs1, - then_body: &CSRRW_SWAP_THROUGH_TEMP, - else_body: &Seq(&[ - Emit(i(ADDI, SourceRd, Reserved(CsrTarget), Const(0))), - Emit(i(ADDI, Reserved(CsrTarget), SourceRs1, Const(0))), - ]), - }, - }, - else_body: &Fail(ExpansionErrorKind::UnsupportedCsr), - }, -}; -``` - -The `csr == 0` branch is intentionally a failure now. The earlier branch that returned `LiteralRow::DefaultNormalizedInstruction` was a useful warning sign because it bypassed normal source metadata; the current PR resolves that warning by rejecting the decoded row instead of preserving the literal. A grammar interpreter should attach the source CSR value to the `UnsupportedCsr` error so this case produces `UnsupportedCsr(0)` in the public API. - -Shared fragments should be grammar fragments with typed arguments: - -```rust -pub(crate) const AMO_PRE64: Fragment = Fragment::new( - FragmentId::AmoPre64, - &[Arg::Rs1, Arg::VRd, Arg::VDword, Arg::VShift], - Seq(&[ - Emit(align(VirtualAssertWordAlignment, ArgReg(Rs1), Const(0))), - Emit(i(ANDI, ArgReg(VShift), ArgReg(Rs1), FormatIImm(Const(-8)))), - Emit(i(LD, ArgReg(VDword), ArgReg(VShift), Const(0))), - Emit(i(SLLI, ArgReg(VShift), ArgReg(Rs1), Const(3))), - Emit(r(SRL, ArgReg(VRd), ArgReg(VDword), ArgReg(VShift))), - ]), -); -``` - -This keeps `amo_pre64` reusable without making extraction depend on arbitrary Rust helper control flow. - -Recipe checks should run in normal Rust tests where practical: - -- all temp uses are inside a live temp scope; -- no temp is released twice; -- every `Emit` row has the operands required by its row shape; -- fragments are acyclic when that can be checked from recipe data; -- every `ReturnLiteral` is named in a baseline-quirks test; -- maximum shallow ops, work-stack depth, and final rows stay within fixed capacities for the curated corpus. - -There are two plausible implementation strategies: - -1. Ordinary Rust recipe definitions interpreted by `expand::lower`, or shallow - lowerers that return bounded `ExpansionOp` buffers. This is the best first - implementation because Hax/Aeneas see a small driver plus data-shaped - recipes/ops, not proc-macro syntax. -2. A proc-macro DSL for ergonomics, later. This can use `syn` the way Ari - describes, but the macro must emit the same checked recipe definitions rather - than opaque hand-coded lowerers. Proc macros parse syntax, not Rust types, so - any type-dependent behavior must be explicit in the recipe data or delegated - to small typed interfaces outside the extraction-critical core. - -The production Rust path can be an interpreter, generated -interpreter-specialized code, or shallow lowerers that return the same operation -data. The Lean path should consume the same syntactic expansion definitions. -Hax/Aeneas then become one extraction path for the driver and core state -machine, not the only way to recover expansion behavior from arbitrary Rust. - -## Core Data Model - -Keep the runtime data model smaller than the first draft. `NormalizedInstruction` -is already the canonical row type, and most proposed types were wrappers around -it. The core should distinguish row stages and metadata policy without creating -a parallel instruction representation. - -```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum RowStage { - Source(NormalizedInstruction), - Expanded(NormalizedInstruction), - Stamped(NormalizedInstruction), -} -``` - -The exact enum name can change, but the phase distinction should stay explicit: - -- `Source` rows are decoded inputs or helper rows that still need expansion - canonicalization. -- `Expanded` rows are final bytecode rows before sequence metadata has been - stamped. -- `Stamped` rows are ready to append to the top-level bytecode vector. - -The current implementation does not stamp every input row. It stamps only -synthetic sequences built through `InstrAssembler::finalize`; rows that do not -expand are returned unchanged. Preserve that policy explicitly. It can be -represented by a small result enum, but avoid introducing separate `SourceRow`, -`RawRow`, `SequenceRow`, `ExpandedRows`, and `SourcePlan` types unless the -implementation demonstrates that the extra names buy real clarity. - -A minimal result enum is enough: - -```rust -pub(crate) enum ExpansionResult { - PassThrough(NormalizedInstruction), - Literal(NormalizedInstruction), - Synthetic(ExpansionBuffer), -} - -pub(crate) enum InitialExpansion { - Direct(ExpansionResult), - Work(OpBuffer), -} -``` - -Family lowerers produce operations, not recursively finalized `Vec`s: +Rows inside recipes use explicit operand provenance: ```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ExpansionOp { - Row(NormalizedInstruction), - Release(u8), - ResetInlineRegisters, -} -``` - -`ExpansionOp::Row` carries a row that must go back through the central expansion -driver. It is not final output until the driver decides the kind is legal target -bytecode and the metadata policy has been applied. Historical CSR-zero -default-row behavior is excluded from the stage model because the PR now rejects -that row before materialization. - -The driver owns all mutable state: +pub(super) struct TempId(u8); -```rust -pub(crate) struct ExpansionState { - allocator: ExpansionAllocator, - work: WorkStack, - output: ExpansionBuffer, - fuel: u32, -} -``` - -No production expansion struct should contain a borrowed allocator or a lifetime parameter. - -## Driver Design - -The driver is the only recursive component. It should be implemented as an iterative depth-first work stack: - -```rust -pub(crate) fn expand_one_core( - source: NormalizedInstruction, - state: &mut ExpansionState, -) -> Result { - state.reset_for_source(); - - let initial_ops = match prepare_source(source, state)? { - InitialExpansion::Direct(rows) => return Ok(rows), - InitialExpansion::Work(initial_ops) => initial_ops, - }; - state.start_synthetic_sequence(); - state.push_ops_reversed(initial_ops.as_slice())?; - - while let Some(op) = state.pop_work() { - state.consume_fuel()?; - match op { - ExpansionOp::Row(row) => process_row(source, row, state)?, - ExpansionOp::Release(register) => state.allocator.release(register)?, - ExpansionOp::ResetInlineRegisters => emit_inline_resets(source, state)?, - } - } - - Ok(ExpansionResult::Synthetic(state.output.clone())) -} -``` - -`process_row` handles the rules currently embedded in `expand_instruction`: - -```rust -fn process_row( - source: NormalizedInstruction, - row: NormalizedInstruction, - state: &mut ExpansionState, -) -> Result<(), ExpansionError> { - if row.operands.rd == Some(0) && !handles_rd_zero_internally(row.instruction_kind) { - if has_side_effects(row.instruction_kind) { - let tmp = state.allocator.allocate_instruction()?; - let rewritten = row.with_rd(tmp); - state.push_ops_reversed(&[ - ExpansionOp::Release(tmp), - ExpansionOp::Row(rewritten), - ])?; - return Ok(()); - } - state.output.push(noop_for_source(source))?; - return Ok(()); - } - - if is_final_kind(row.instruction_kind) { - state.output.push(strip_sequence_metadata(row))?; - } else { - let ops = lower::lower(row, &mut state.allocator)?; - state.push_ops_reversed(ops.as_slice())?; - } - Ok(()) -} -``` - -`prepare_source` handles source-only pass-through before the synthetic driver starts: - -```rust -fn prepare_source( - source: NormalizedInstruction, - state: &mut ExpansionState, -) -> Result { - if source.operands.rd == Some(0) && !handles_rd_zero_internally(source.instruction_kind) { - if has_side_effects(source.instruction_kind) { - let tmp = state.allocator.allocate_instruction()?; - let rewritten = source.with_rd(tmp); - if is_final_kind(rewritten.instruction_kind) { - let row = rewritten; - state.allocator.release(tmp)?; - return Ok(InitialExpansion::Direct(ExpansionResult::Literal(row))); - } - return Ok(InitialExpansion::Work(ops(&[ - ExpansionOp::Row(rewritten), - ExpansionOp::Release(tmp), - ]))); - } - return Ok(InitialExpansion::Direct(ExpansionResult::Literal(noop_for_source(source)))); - } - - if is_final_kind(source.instruction_kind) { - return Ok(InitialExpansion::Direct(ExpansionResult::PassThrough(source))); - } - - Ok(InitialExpansion::Work(ops(&[ExpansionOp::Row(source)]))) -} -``` - -This distinction is necessary for exact parity with the current PR. It also makes any future decision to stamp final source rows a conscious behavior change instead of an accidental consequence of the new architecture. - -The stack must preserve current recursive order. If a lowerer emits `[A, B, C]`, the driver should process the full recursive expansion of `A`, then `B`, then `C`. With a LIFO stack, `push_ops_reversed` pushes `C`, then `B`, then `A`. - -## Shallow Lowerers - -Every family expander should become a shallow lowerer. It must not call the public `expand_instruction` or the central driver. - -Current ADDIW: - -```rust -asm.emit_i(InstructionKind::ADDI, rd, rs1, imm)?; -asm.emit_i(InstructionKind::VirtualSignExtendWord, rd, rd, 0)?; -``` - -Target ADDIW: - -```rust -pub(crate) fn lower_addiw( - row: NormalizedInstruction, - out: &mut OpBuffer, -) -> Result<(), ExpansionError> { - let rd = operands::rd(row)?; - let rs1 = operands::rs1(row)?; - out.row(i(InstructionKind::ADDI, rd, rs1, row.operands.imm))?; - out.row(i(InstructionKind::VirtualSignExtendWord, rd, rd, 0))?; - Ok(()) -} -``` - -For functions with temporary registers, releases become explicit operations at -the same sequence point: - -```rust -pub(crate) fn lower_mulh( - row: NormalizedInstruction, - allocator: &mut ExpansionAllocator, - out: &mut OpBuffer, -) -> Result<(), ExpansionError> { - let rd = operands::rd(row)?; - let rs1 = operands::rs1(row)?; - let rs2 = operands::rs2(row)?; - - let v_sx = allocator.allocate_instruction()?; - let v_sy = allocator.allocate_instruction()?; - let v_tmp = allocator.allocate_instruction()?; - - out.row(i(InstructionKind::VirtualMovsign, v_sx, rs1, 0))?; - out.row(i(InstructionKind::VirtualMovsign, v_sy, rs2, 0))?; - out.row(r(InstructionKind::MUL, v_sx, v_sx, rs2))?; - out.row(r(InstructionKind::MUL, v_sy, v_sy, rs1))?; - out.row(r(InstructionKind::MULHU, v_tmp, rs1, rs2))?; - out.row(r(InstructionKind::ADD, v_tmp, v_tmp, v_sx))?; - out.row(r(InstructionKind::ADD, rd, v_tmp, v_sy))?; - - out.release(v_sx)?; - out.release(v_sy)?; - out.release(v_tmp)?; - Ok(()) -} -``` - -This preserves the current behavior: releases happen after the recursively expanded helper rows that use the temps. - -Some current functions release temps mid-sequence, for example CSR and SC flows. Those become mid-sequence `Release` operations: - -```rust -out.row(i(InstructionKind::ADDI, temp, rs1, 0))?; -out.row(i(InstructionKind::ADDI, rd, virtual_reg, 0))?; -out.row(i(InstructionKind::ADDI, virtual_reg, temp, 0))?; -out.release(temp)?; -out.row(...)?; -``` - -The driver processes that release after the preceding rows have recursively finalized and before subsequent rows. - -## Allocator Design - -The current allocator: - -```rust -allocated: [bool; NUM_VIRTUAL_REGISTERS], -pending_clearing_inline: Vec, -``` - -should become bitset-based: - -```rust -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct ExpansionAllocator { - live: u128, - inline_touched: u128, -} -``` - -There are at most 96 virtual registers, so `u128` is enough. Register index `i` corresponds to virtual register `RISCV_REGISTER_BASE + i`. - -Allocation: - -```rust -pub(crate) fn allocate_in_range( - &mut self, - start: u8, - end: u8, - pool: RegisterPool, -) -> Result { - let mut index = start; - while index < end { - let bit = 1u128 << index; - if self.live & bit == 0 { - self.live |= bit; - if matches!(pool, RegisterPool::Inline) { - self.inline_touched |= bit; - } - return Ok(RISCV_REGISTER_BASE + index); - } - index += 1; - } - Err(ExpansionError::VirtualRegisterExhausted { pool }) -} -``` - -Release: - -```rust -pub(crate) fn release(&mut self, register: u8) -> Result<(), ExpansionError> { - let index = virtual_index(register)?; - let bit = 1u128 << index; - if self.live & bit == 0 { - return Err(ExpansionError::UnallocatedVirtualRegister { register }); - } - self.live &= !bit; - Ok(()) -} -``` - -Inline reset: - -```rust -pub(crate) fn inline_resets(&mut self, out: &mut OpBuffer) -> Result<(), ExpansionError> { - let inline_mask = inline_register_mask(); - if self.live & inline_mask != 0 { - return Err(ExpansionError::InlineRegistersStillAllocated); - } - - let mut pending = self.inline_touched & inline_mask; - while pending != 0 { - let index = pending.trailing_zeros() as u8; - pending &= !(1u128 << index); - out.row(i(InstructionKind::ADDI, RISCV_REGISTER_BASE + index, 0, 0))?; - } - self.inline_touched &= !inline_mask; - Ok(()) -} -``` - -This design is both faster and easier to prove than `[bool; N] + Vec`. - -## Buffer Design - -Use local fixed-capacity buffers for per-source expansion: - -```rust -pub(crate) struct FixedVec { - len: usize, - data: [T; N], -} -``` - -`ExpansionOp` and `NormalizedInstruction` are `Copy`, so this can avoid -`MaybeUninit` in the extraction-critical core. Overflow returns -`ExpansionError::ExpansionBufferExceeded { capacity: N }`. - -Suggested buffers: - -```rust -pub(crate) type WorkStack = FixedVec; -pub(crate) type OpBuffer = FixedVec; -pub(crate) type ExpansionBuffer = FixedVec; -``` - -The exact capacities should be set from observed maximum expansion lengths plus margin, then guarded by tests over the curated parity corpus. This is not merely for extraction: bounded buffers remove recursive heap allocation from the runtime hot path. - -The top-level program API can still return a heap-backed `Vec`: - -```rust -pub fn expand_program_slice( - instructions: &[NormalizedInstruction], -) -> Result, ExpansionError> { - let mut state = ExpansionState::new(); - let mut expanded = Vec::with_capacity(estimate_expanded_len(instructions)); - for instruction in instructions { - let rows = core::expand_one_core(*instruction, &mut state)?; - metadata::append_expanded_rows(rows, &mut expanded)?; - } - Ok(expanded) +pub(super) enum RegisterOperand { + Register(u8), + Temp(TempId), } -``` -Ergonomic `impl IntoIterator` wrappers can remain outside the extraction target. - -## Metadata Stamping - -Rows inside synthetic sequences should not be partially initialized with -placeholder metadata. The finalizer should construct or update -`NormalizedInstruction` rows from expanded rows: - -```rust -pub(crate) fn stamp_row( - source: NormalizedInstruction, - mut row: NormalizedInstruction, - index: usize, - len: usize, -) -> Result { - let remaining = len - .checked_sub(index + 1) - .ok_or(ExpansionError::MalformedExpansion)?; - let remaining = u16::try_from(remaining) - .map_err(|_| ExpansionError::ExpansionTooLong { len })?; - - row.address = source.address; - row.virtual_sequence_remaining = Some(remaining); - row.is_first_in_sequence = index == 0; - row.is_compressed = index + 1 == len && source.is_compressed; - Ok(row) +pub(super) const fn reg(register: u8) -> RegisterOperand { + RegisterOperand::Register(register) } -``` - -This stamping function is for `ExpansionResult::Synthetic` only. -`ExpansionResult::PassThrough` returns the original source row unchanged, -matching the current behavior for already-final rows. Root-level -`ExpansionResult::Literal` returns an explicitly constructed literal row without -stamping and must be covered by a baseline-quirk or normalization test. -For a side-effect-free `rd = x0` source no-op, current behavior returns an ADDI -no-op with the source address and compressed flag, but with -`virtual_sequence_remaining = None` and `is_first_in_sequence = false`. That -should be represented as `ExpansionResult::Literal(noop_for_source(source))`, -not as a one-row synthetic sequence, unless maintainers intentionally approve -the metadata behavior change. - -## Inline Handling - -The provider-free core should treat `InstructionKind::Inline` as unsupported: - -```rust -fn process_row(...) -> Result<(), ExpansionError> { - if row.kind == InstructionKind::Inline { - return Err(ExpansionError::InlineProviderRequired); +impl TempId { + pub(super) const fn operand(self) -> RegisterOperand { + RegisterOperand::Temp(self) } - ... -} -``` - -The provider-taking API should sit outside the extracted core: - -```rust -pub trait InlineExpansionProvider { - fn expand_inline( - &mut self, - source: NormalizedInstruction, - ) -> Result; -} - -pub struct InlineExpansion { - rows: InlineRowBuffer, } ``` -The adapter can intercept inline source rows and ask tracer's registry for the -normalized inline sequence. For this phase, that sequence may remain finalized -provider output rather than `ExpansionOp` data. If provider-owned inline -allocation needs reset rows, the provider should include those rows before -returning. The provider-free core should not call a trait object while -processing ordinary RV64 rows. - -This preserves the dependency boundary from PR #1490: `jolt-program` still does not depend on `tracer`, inventory, CPU state, or advice tapes. +This keeps lowerers close to assembly while still making the register phase +visible at every row. -## Termination And Recursion Bound +## Register Budget -The current recursive implementation relies on the absence of cycles in helper -expansion. The new driver should make accidental cycles explicit without -requiring a full rank system in Pass 1. +Jolt has 32 architectural RISC-V registers and 96 virtual registers: -The driver should enforce a fuel or recursion-depth bound: +```text +x0..x31 architectural RISC-V registers +vr32..vr39 reserved persistent virtual registers +vr40..vr47 instruction expansion temps +vr48..vr127 registered-inline temps +``` + +The expander enforces this split exactly: + +- `NUM_RESERVED_VIRTUAL_REGISTERS = 8` +- `NUM_VIRTUAL_INSTRUCTION_REGISTERS = 8` +- `ExpansionAllocator::allocate()` uses only `vr40..vr47`. +- `ExpansionAllocator::allocate_for_inline()` uses only `vr48..vr127`. + +`TempId` is symbolic recipe syntax, but each live temp is materialized through +`ExpansionAllocator::allocate()`. The symbolic temp namespace is therefore +bounded by `NUM_VIRTUAL_INSTRUCTION_REGISTERS`, not by the full `u8` range. A +ninth symbolic temp is rejected while building the recipe. + +The explicit `reg(x)` and `temp.operand()` spelling is intentional. It makes +operand provenance visible in lowerers: decoded architectural registers and +symbolic allocator temps have different lifetimes and different side-effect +implications. + +## Materialization + +`ExpansionState` owns the real `ExpansionAllocator` while interpreting +`ExpansionOp` recipes. It is responsible for: + +- allocating real virtual registers for symbolic temps; +- rejecting duplicate, unallocated, and leaked symbolic temps; +- recursively expanding helper rows recorded with `expand_*`; +- enforcing recursion depth; +- preserving inline-provider isolation; +- stamping `is_first_in_sequence`, `virtual_sequence_remaining`, and + compressed-tail metadata; +- rejecting source-only rows in final bytecode; +- enforcing final sequence capacity. + +The lowerers stay declarative. The materializer owns stateful behavior. + +### Metadata Policy + +Sequence metadata is owned by the materializer: + +- pass-through rows remain unchanged; +- synthetic sequences are stamped as one source-row expansion; +- recursive helper expansions participate in the outer source sequence; +- `is_first_in_sequence` and `virtual_sequence_remaining` are derived centrally; +- `is_compressed` is attached to the final row of the expanded sequence according + to the existing compressed-tail policy. + +Concrete lowerers should not mutate sequence metadata directly. + +## Inline Provider Boundary + +Registered inlines are expanded through `InlineExpansionProvider`. + +Provider output is intentionally outside the provider-free grammar because +registered inlines may need tracer-side registration, advice generation, and +large inline-specific virtual-register use. The public expansion entry point +still passes the selected profile to the provider, validates provider rows, +appends reset rows for inline registers that must be cleared, and stamps the +resulting sequence. + +Inline register clearing uses the same allocator partition as tracer: +instruction expansion temps do not borrow from the inline register pool, and +inline registers do not consume `vr40..vr47`. + +### Follow-Up: Systematic Inline Expansion + +Registered inlines are still the least systematic part of the expansion stack. +Today they are built through tracer-side `InstrAssembler` and `InlineOp` +registrations: + +- inline metadata is registered by `(opcode, funct3, funct7)`; +- each registered inline declares its `InlineExtension` so the active profile + can reject linked but disabled inline packages; +- sequence builders emit tracer `Instruction` values through generic + `emit_r::`, `emit_i::`, `emit_s::`, and similar helpers; +- inline builders allocate from `allocate_for_inline()`, not from the eight + instruction-temp registers used by provider-free source-row expansion; +- `finalize_inline()` appends zeroing rows for the inline virtual registers that + were allocated; +- advice-producing inlines separately provide advice values during tracing. + +The current PR should keep that provider boundary. A follow-up PR can make +inlines systematic without mixing them into provider-free expansion all at once. +The clean direction is: + +1. Define an inline recipe layer parallel to `ExpandedInstructionSequence`. + It should use explicit inline virtual-register operands and inline-local + allocation, but keep the larger `vr48..vr127` register pool. +2. Split inline declarations into metadata, bytecode recipe construction, and + advice production. Metadata should remain registration-friendly; bytecode + construction should be testable without a live CPU; advice production can + keep the CPU/MMU dependency. +3. Reuse the same final-row validation and metadata stamping policy as + provider-free expansion. Inline recipes should not hand-stamp sequence fields. +4. Preserve `finalize_inline()` semantics: every inline register allocated from + the inline pool must be zeroed exactly once at the end of the inline sequence. +5. Add parity fixtures per inline family using compact hashes, not huge expanded + row JSON. +6. Move one small inline first, preferably an advice-store style inline or a + compact cryptographic helper, before attempting SHA2, Keccak, Blake, or field + arithmetic inlines. + +This would give inlines the same compiler-style shape as provider-free +expansion while preserving the important distinction between source-row +expansion temps and large inline working sets. + +## Serialization Boundary + +`jolt-riscv` and `jolt-program` expose a default `serialization` feature for +normal workspace builds. `serde` and `ark-serialize` derives are gated behind +that feature. + +No-default builds keep the provider-free expansion core independent of +serialization crates: -```rust -const MAX_EXPANSION_OPS_PER_SOURCE: u32 = 4096; +```bash +cargo clippy -p jolt-program --no-default-features -q --all-targets -- -D warnings ``` -Fuel exhaustion should be treated as an internal malformed-expansion error and should never occur in parity fixtures. - -If the recipe surface later becomes declarative enough to build a useful -dependency graph, a rank or acyclicity validator can be added as a follow-up. -It is not required for the first compiler-native rewrite. - -## Performance Expectations - -This rewrite should not regress performance. It should improve or preserve it for three reasons: - -- per-source recursive `Vec` allocation disappears; -- allocator operations become bit operations and bounded scans; -- metadata stamping becomes a single construction pass instead of mutate-after-build. +Workspace crates that need serialized preprocessing, tracing, or proving data +opt back into the feature explicitly. -Potential cost: +## Validation -- fixed buffers use stack space; -- the central work-stack driver adds an explicit dispatch loop. +The expander is validated by focused unit tests and by compact parity checks: -The explicit loop replaces recursive Rust calls and should be comparable or cheaper. Stack size should be bounded by chosen capacities and checked in review. +- allocator partition tests for instruction temps and inline temps; +- symbolic temp tests for duplicate allocation, unallocated use, unallocated + release, leak detection, and the exact eight-temp recipe bound; +- metadata stamping and source-only target-legality tests; +- `rd = x0` behavior for side-effecting and side-effect-free instructions; +- inline-provider validation and reset-row tests; +- LR/SC RAM-range and CSR-zero rejection tests; +- a compact hash fixture covering provider-free expansion parity. -Benchmark expectations: +When the compact parity fixture changes, treat it as a semantic review event: -- no measurable regression in decode-plus-expansion for representative guests; -- no measurable regression in trace length accounting; -- allocation count during expansion should drop relative to `a3448e6da44f`. +- record the baseline commit or intended semantic change; +- inspect row-level diffs for each affected instruction family; +- regenerate from deterministic serialized `Vec>` + bytes; +- rerun the dedicated parity test and the `muldiv` e2e checks. -Suggested measurements: +The primary e2e correctness checks remain: ```bash -cargo nextest run -p jolt-program --cargo-quiet -cargo nextest run -p tracer --cargo-quiet cargo nextest run -p jolt-core muldiv --cargo-quiet --features host cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk - -# Optional follow-up benchmark if maintainers want measured evidence: -cargo run --release -p jolt-core profile --name sha3 --format chrome -``` - -## API Shape - -The extraction-critical API should be concrete: - -```rust -pub(crate) fn expand_one_core( - source: NormalizedInstruction, - state: &mut ExpansionState, -) -> Result; - -pub fn expand_program_slice( - instructions: &[NormalizedInstruction], -) -> Result, ExpansionError>; -``` - -Compatibility wrappers can keep call sites pleasant: - -```rust -pub fn expand_program( - instructions: impl IntoIterator, -) -> Result, ExpansionError> { - let collected: Vec<_> = instructions.into_iter().collect(); - expand_program_slice(&collected) -} -``` - -If that collection is too costly for a hot call site, add a concrete streaming adapter outside the extracted module: - -```rust -pub fn expand_program_iter( - instructions: I, -) -> Result, ExpansionError> -where - I: Iterator, -{ - let mut state = ExpansionState::new(); - let mut expanded = Vec::new(); - for instruction in instructions { - let rows = core::expand_one_core(instruction, &mut state)?; - metadata::append_expanded_rows(rows, &mut expanded)?; - } - Ok(expanded) -} ``` -The extracted core should not be generic over `IntoIterator`. - -## Cargo Feature Shape - -The extraction-critical module graph should avoid serialization and host-only dependencies in its call graph, but the first rewrite should not also change workspace feature defaults. Current `jolt-riscv` serialization derives are unconditional; feature-gating them is a useful follow-up only if Hax/Aeneas still pull those impls after the grammar/core rewrite. - -For this phase, the practical rule is: - -- `expand::grammar`, `expand::core`, `expand::allocator`, `expand::buffer`, and `expand::metadata` should not call serialization APIs; -- `image = ["dep:object"]` should remain outside the extraction target; -- Hax/Aeneas experiments should start from provider-free `expand` core functions, not public image/execution adapters. - -## Next Implementation Pass Scope - -Decision: the next implementation pass should rewrite provider-free -`jolt-program::expand` only. It should design and preserve the inline seam, but -it should not migrate registered `jolt-inlines` recipes, advice generation, or -guest SDK registration into the grammar. - -This is one compiler design, but two implementation passes: - -1. **Pass 1: provider-free bytecode expansion.** Convert the current - `jolt-program::expand` families into syntactic recipes or shallow lowerers, - a central worklist driver, explicit temp materialization, bounded buffers, - and explicit metadata policy. -2. **Pass 2: inline extension/advice design.** Decide how generic inline - infrastructure, selected concrete inline recipes, and advice generation - should relate to the same expansion machinery. This may become an extension - dialect, but that is not settled in this spec. - -Do not combine these passes. Inlines have a different dependency shape from -ordinary bytecode expansion: they involve guest SDK encoding, link-time -registration, host advice computation, tracer CPU/memory access, inline-only -virtual-register reset policy, and much larger sequence capacities. Combining -them with the provider-free rewrite would make review and parity debugging too -wide. The bytecode pass should instead build the target recipe surface and -resource materialization rules that a later inline pass can reuse if that proves -to be the right design. - -### In Scope For Pass 1 - -Pass 1 should include the following production changes: - -- Use compiler-native vocabulary in docs and code where appropriate. - Extraction remains a key acceptance signal, but the implementation shape is a - compiler lowering pipeline. -- Add a small `expand::grammar` or equivalent recipe module with inspectable - syntactic expansion data: - - use `NormalizedInstruction` as the row representation, with a small - `RowStage`/`ExpansionResult` policy for source, expanded, and stamped rows; - - include only the operand-reference, condition, and statement forms that real - expansion families need; - - keep `WithTemp`/nested temp scopes or an equivalent explicit lifetime - construct for recipes that need more than one temporary register; - - keep `RegisterPool::{Instruction, Inline}` only if it is useful for the - provider-free core and the inline adapter boundary. -- Add `expand::core` with concrete, non-generic entry points: - - `expand_one_core(source: NormalizedInstruction, state: &mut ExpansionState)`; - - a depth-first work stack of `ExpansionOp`; - - source and target legality predicates; - - a fuel or recursion-depth bound that returns an explicit error on runaway - expansion. -- Replace the production recursive `InstrAssembler<'a>` with shallow family - lowerers that only return recipe data or bounded `ExpansionOp` buffers. -- Materialize temps through `ExpansionAllocator` at explicit lifetime - boundaries, preserving current first-fit virtual-register numbering and - release order. Nested `WithTemp` scopes are the default representation for - multi-temp sequences, but the implementation may use an equivalent explicit - operation form if it is simpler. -- Replace heap-backed per-source sequence construction in the core with bounded - buffers and explicit overflow errors. -- Preserve current metadata behavior exactly, including pass-through rows, - synthetic sequence stamping, `is_compressed` placement, and any remaining - literal rows. Also preserve the PR's documented baseline fixes, including - rejection of `CSRRW`/`CSRRS` with CSR address `0`. -- Keep public expansion APIs available to current call sites, but route them - through the new core. Ergonomic iterator wrappers may stay outside the - extraction-critical module graph. -- Preserve inline support as an adapter outside provider-free core: - `InstructionKind::Inline` remains illegal for `expand_one_core`, while - `expand_instruction_with_provider` can still delegate to an - `InlineExpansionProvider` that returns finalized `NormalizedInstruction` rows. -- Preserve current advice-channel behavior: - provider-free expansion lowers `AdviceLB/LH/LW/LD` into `VirtualAdviceLoad` - sequences, does not assign `VirtualAdvice` payloads, and does not move - trusted/untrusted advice commitments out of preprocessing/proof code. - -Pass 1 should also include the following tests and checks: - -- A test-only parity harness that compares the new provider-free expander - against the current PR output while porting families. The final production - code should have one expander; the old assembler may remain only as a - temporary test reference during the rewrite. Once tracer delegates to - `jolt-program::expand`, tracer bridge tests are circular and should not be - treated as expansion oracles. -- Fixture coverage for every provider-free source-only instruction kind with - representative operand aliases, `rd = x0`, compressed metadata, CSR edge - cases, branch/control-flow immediates, load/store offsets, AMO/LR/SC cases, - advice-load cases, and division/remainder variants. -- Capacity tests that assert observed final row count, shallow op count, and - work-stack depth stay below the chosen constants. -- Dependency checks showing no `tracer` dependency from `jolt-program` or - `jolt-riscv`. -- Hax/Aeneas reruns on metadata stamping, allocator transitions, ADDIW shallow - lowering, and provider-free `expand_one_core`. -- Standard repo verification for code changes: - - `cargo fmt -q`; - - `cargo clippy --all --features host -q --all-targets -- -D warnings`; - - `cargo clippy --all --features host,zk -q --all-targets -- -D warnings`; - - focused `cargo nextest run -p jolt-program --cargo-quiet`; - - `cargo nextest run -p jolt-core muldiv --cargo-quiet --features host`; - - `cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk`. - -### Concrete Pass 1 Milestones - -The next implementation pass should be reviewable as one focused production -rewrite of `jolt-program::expand`, with the following milestones: - -1. **Freeze the behavioral baseline.** - - Add golden/parity coverage for the current PR behavior before deleting the - recursive assembler path. - - Treat the old assembler as a temporary test oracle only while the rewrite is - in flight. It should not survive as a compatibility layer or second - production expander. - - Capture row-for-row parity for metadata, virtual register allocation, - helper-row recursion order, `rd = x0`, compressed source rows, remaining - quirks, and documented fixes such as CSR address `0` rejection. - -2. **Introduce the compiler data model.** - - Keep `NormalizedInstruction` as the row type and add only the minimal - stage/result wrappers needed to distinguish source rows, expanded helper - rows, stamped rows, pass-through rows, and deliberate literals. - - Add syntactic recipe data or shallow operation buffers with explicit temp - ids, register-pool ids where useful, and recipe fragments where they - reduce duplication. - - Keep the model first-order: no borrowed assembler state, no recursive - callback into public expansion APIs, no trait-object dispatch in the - provider-free core. - - Add validators/checks for source legality, target legality, fuel/depth, - temp liveness where represented as recipe data, and bounded row capacity. - -3. **Build the new core driver behind the existing public API.** - - Implement `ExpansionState`, the bitset allocator, bounded row buffers, the - explicit work stack, and the single metadata materialization pass. - - Route public provider-free expansion through the new core as soon as the - first family is ported, while keeping test-only parity checks available for - families not yet ported. - - Keep iterator conveniences and inline-provider adapters outside the - extraction-critical modules. - -4. **Port one small family end to end.** - - Start with ADDIW/ADDW/SUBW or a similarly small arithmetic slice. - - Validate byte-for-byte output parity against the old path. - - Run Hax/Aeneas on the new allocator transitions, metadata stamping, - shallow lowering, and `expand_one_core` slice before porting larger - families. - -5. **Port all provider-free expansion families.** - - Move arithmetic, shifts, memory, division, control-flow, fragment helpers, - AMO/LR/SC, CSR, and literal/pass-through behavior onto recipes or shallow - lowerers that return inspectable operations. - - Delete the recursive production assembler once the last provider-free family - has parity coverage. - - Preserve the inline boundary as an adapter returning finalized rows, with - `InstructionKind::Inline` still illegal in provider-free `expand_one_core`. - -6. **Close with extraction and repo verification.** - - Re-run the narrow Hax/Aeneas experiments and record which modules now - extract, which still fail, and whether failures are due to expansion-core - structure or extractor/tool limitations. - - Run the repo checks listed above. - - Land the pass only if production behavior is single-path, parity-tested, - dependency-light, and no slower on the expansion hot path. - -The expected review scope is therefore: "replace the provider-free expansion -engine with a compiler-native lowering core." It is not: "also redesign all -inline packages." - -### Out Of Scope For Pass 1 - -Pass 1 should not: - -- introduce a Melior/MLIR dependency; -- create `jolt-inline-ir` or a new inline registry crate; -- port SHA2, Keccak, Blake, BigInt, Secp, Grumpkin, or P-256 inline recipes; -- replace `inventory` registration; -- redesign guest inline SDK assembly macros; -- model inline advice as a grammar; -- assign `VirtualAdvice` payloads in provider-free expansion; -- change runtime advice-tape behavior for `VirtualAdviceLoad` or - `VirtualAdviceLen`; -- move trusted/untrusted advice memory commitments into `jolt-program::expand`; -- change tracer execution semantics; -- change bytecode/RAM preprocessing semantics; -- feature-gate `serde` or `ark-serialize` unless extraction still pulls those - impls after the compiler-native rewrite. - -### Pass 1 File-Level Shape - -The intended end state inside `crates/jolt-program/src/expand` is: +## Extraction Status -```text -allocator.rs deterministic bitset allocator and reset tracking -buffer.rs bounded buffers used by core lowering -core.rs worklist driver, legality, staged row policy -grammar.rs small syntactic recipe data and validators/checks -metadata.rs sequence metadata materialization -operands.rs operand decoding helpers -lower/ - arithmetic.rs shallow recipe/lowering definitions - control_flow.rs - division.rs - memory.rs - shifts.rs - fragments.rs shared helper recipes such as amo_pre64/amo_post64 -inline.rs adapter boundary only, outside provider-free core -mod.rs public API glue -``` +Extraction is informational. It is useful for finding hidden coupling and +dependency creep, but production Rust should remain idiomatic. -The exact file names can shift during implementation, but ownership should not: -`core + grammar/recipe + allocator + buffer + metadata + lower` form the -compiler-native target; `inline + public ergonomic wrappers` stay outside the -first extraction target. - -## Follow-Up Inline Extension Pass - -This section is separate from the execution-semantics track above. Inlines add -both expansion complexity and advice/execution questions, so their design should -not be used as the first semantics modeling slice. - -The inline pass should start after Pass 1 has stabilized the target bytecode -recipe surface and materialization rules. Its goal is not "make current Rust -inline builders extract." The exact advice model is still unresolved. A future -inline spec should decide whether inlines become extension dialects, remain -adapter-provided finalized rows, or split bytecode expansion from advice -generation more explicitly. - -One possible crate split is: - -```text -jolt-inline-ir - Generic InlineId, InlineOpDef, InlineRecipe, placeholder AdviceModel, validators. +Current Hax command: -jolt-program - Consumes inline definitions and lowers source rows to final bytecode. - Still has no tracer dependency. - -jolt-inlines-{sha2,keccak256,bigint,...} - Concrete extension dialects: guest SDK encoding plus typed inline defs. - -jolt-inline-registry - Optional feature-selected registry crate collecting chosen inline packages. - -tracer - Runtime adapter: executes final bytecode and evaluates advice plans against Cpu. -``` - -The first inline implementation slice should port one representative inline, -not all of them. A good first candidate is an inline with large but regular -memory/register structure, such as `bigint.mul256`, because it stresses static -loops, symbolic inline temp arrays, memory loads/stores, and reset rows without -also requiring the full hash/advice surface. A later slice can port an -advice-bearing modular arithmetic inline, then a hash compression inline with -static round schedules. - -A future inline grammar may need: - -- `InlineOpDef { id, name, operands, effects, recipe, advice }`; -- `InlineOperandSpec` that makes `rs3` a memory output pointer, not a normal - architectural `rd`; -- static loops and recipe fragments with alpha-renamed temps; -- symbolic inline temp arrays over `RegisterPool::Inline`; -- explicit memory effect metadata; -- explicit `VirtualAdvice(slot)` rows tied to whatever advice model the follow-up spec chooses; -- reset policy that checks all inline temps are dead before materializing - `ADDI temp, x0, 0` rows. - -In MLIR terms, inlines should become extension ops such as -`jolt.inline.sha2.compress` or `jolt.inline.bigint.mul256`, not opaque Rust -callbacks. The compiler should legalize those ops into `expand` ops and then -into final `jolt.bytecode` rows using the same target legality, metadata, and -resource materialization machinery as provider-free expansion. - -## Migration Plan - -1. Add the small syntactic expansion recipe surface, compiler-phase vocabulary, and baseline-quirk fixtures. -2. Add new `core`, `buffer`, and bitset `allocator` internals under `jolt-program::expand`. -3. Represent temp lifetimes explicitly and materialize them through the allocator at `WithTemp` or equivalent boundaries. -4. Port one small family, such as ADDIW/ADDW/SUBW, to recipe-backed shallow lowering and prove parity against the current output. -5. Port arithmetic, shifts, memory, division, and control-flow families. -6. Replace `InstrAssembler<'a>` in production expansion code. -7. Preserve tracer inline adapter support as finalized rows outside provider-free core. -8. Delete the old recursive assembler once all parity tests pass. -9. Run Hax/Aeneas again on: - - metadata stamping, - - allocator transitions, - - ADDIW shallow lowering, - - provider-free `expand_one_core`. -10. Record the separate semantics follow-up: a hand-modeled Lean transition - relation for a small provider-free slice, plus an expansion-correctness - statement comparing source-row execution with target-sequence execution. -11. Run formatting, clippy, host tests, ZK tests, and dependency checks. - -Do not leave both expanders in production. A temporary test-only reference path is acceptable during the rewrite, but the final branch should have one canonical production expander. +```bash +cargo hax -C -p jolt-program --no-default-features \; into \ + --output-dir /tmp/jolt-program-hax-lean-ergonomic \ + -i '-** +jolt_program::expand::**' \ + lean +``` + +The command emits Lean for the selected expansion namespace. The generated file +is currently about 14k lines. It no longer includes serde or ark-serialization +dependencies in the no-default configuration, and it reflects the exact +instruction-temp register bound. + +The emitted Lean does not typecheck end-to-end in a scratch Lake project. The +current blockers are in extraction packaging and prelude coverage: + +- missing or stubbed cross-crate models for `common` and `jolt-riscv`; +- missing Hax Lean models for some `Vec` operations; +- missing iterator/fold helpers such as `Iterator.position` and `fold_return`; +- Lean typeclass/universe synthesis failures around generated derive support; +- downstream unknown identifiers after earlier environment failures. + +These blockers are not production Rust requirements. The next extraction work +should improve the Lean environment or upstream Hax/Aeneas models rather than +rewriting clear Rust into extractor-specific control flow. + +## Remaining Work Outside This PR + +- Make registered inlines systematic using the inline recipe plan above. +- Decide whether very large lowerers such as division and AMO helpers should be + split further for human readability. This should be motivated by code quality, + not by extractor output size alone. +- Continue improving Hax/Aeneas support in a maintained Lean environment: + multi-crate models for `common` and `jolt-riscv`, standard-library models for + `Vec`/iterator/fold helpers, and generated derive/typeclass support. +- Add row-diff tooling around the compact parity fixture so fixture updates are + easier to review. +- Shrink the legacy bare `JoltInstructionKind` enum to final-only variants once + source-only expansion recipes no longer need source rows shaped as `JoltInstructionRow`. + +## Implementation Checklist + +- [x] RV64-only program expansion boundary. +- [x] `LookupInstructionKind` removed; `LookupInstruction` remains as the typed + lookup-backed view. +- [x] True source-vs-final typed instruction split. +- [x] `ExpansionBuilder` recipe API. +- [x] `ExpansionState` materializer with owned allocator state. +- [x] Exact virtual-register partition for reserved, instruction-temp, and + inline-temp registers. +- [x] Explicit symbolic temp lifetime validation. +- [x] Infallible pure recipe-recording methods. +- [x] Central target legality and metadata stamping. +- [x] Inline-provider validation and reset-row handling. +- [x] Serialization feature boundary. +- [x] Compact provider-free expansion parity fixture. +- [x] Informational Hax extraction pass. ## Acceptance Criteria -- [ ] `jolt-program::expand` no longer has a production `InstrAssembler<'a>` that stores a borrowed allocator. -- [ ] Expansion recipes are represented as syntactic lowering data or shallow operations; the grammar does not model instruction execution semantics. -- [ ] The spec keeps execution semantics on a separate track, starting with a hand-modeled Lean abstract machine slice and expansion-correctness theorem rather than extraction from `tracer` or MLIR-as-semantics. -- [ ] The spec names source, intermediate, and target phases with MLIR-ready legality predicates, even while Rust remains the implementation substrate. -- [ ] `NormalizedInstruction` remains the core row type, with only minimal stage/result wrappers for source, expanded, stamped, pass-through, and literal policies. -- [ ] Recipe temps have explicit lifetimes and are materialized into concrete virtual registers by a deterministic allocation/resource-materialization step. -- [ ] Recipe checks reject invalid operand shapes, unchecked literal rows, bounded-capacity overflow, and temp-lifetime mistakes where temp scopes are represented as data. -- [ ] Family lowerers are shallow and do not call `expand_instruction`. -- [ ] Recursive expansion happens in one central depth-first driver. -- [ ] Temporary-register release and inline reset are explicit expansion operations. -- [ ] Allocator state is represented by bitsets, not a heap-backed reset list. -- [ ] Per-source expansion uses bounded buffers, with explicit overflow errors. -- [ ] Synthetic sequence metadata is stamped during final row construction, not by mutating already-built rows. -- [ ] Source pass-through rows and deliberate literal rows preserve the current metadata policy exactly, with tests for the weird cases. -- [ ] Provider-free core expansion has a concrete, non-generic entry point over `NormalizedInstruction` and `ExpansionState`. -- [ ] Inline provider support is an adapter outside the provider-free core. -- [ ] Provider-free expansion preserves advice-load lowering exactly: - `AdviceLB/LH/LW/LD` emit `VirtualAdviceLoad` plus sign extension where - needed. -- [ ] `VirtualAdvice` payload assignment remains outside provider-free - expansion; `NormalizedInstruction` does not grow an advice payload field. -- [ ] Trusted/untrusted advice memory and polynomial commitments remain - preprocessing/proof responsibilities, not expansion responsibilities. -- [ ] Expansion output matches PR #1490 baseline fixtures exactly. -- [ ] Hax and Aeneas can extract metadata stamping and at least one shallow family lowerer without pulling in execution/preprocess/serialization modules. -- [ ] Dependency checks still show no `tracer` dependency from `jolt-program` or `jolt-riscv`. - -## Resolved And Sharpened Questions - -### Fixed Buffer Capacities - -Initial answer: use conservative fixed capacities for provider-free expansion: - -```rust -const MAX_FINAL_ROWS_PER_SOURCE: usize = 64; -const MAX_SHALLOW_OPS_PER_LOWERING: usize = 64; -const MAX_WORK_OPS_PER_SOURCE: usize = 128; -``` - -A throwaway measurement against the current `expand_instruction` implementation on representative provider-free rows found the largest final expansion was `SCW` at 37 rows. The next largest cases were word AMO min/max at 24 rows, `DIV`/`REM` at 24 rows, `DIVW`/`REMW` at 21 rows, word AMO arithmetic at 19 rows, and `AMOSWAPW` at 18 rows. - -These constants are intentionally not tight. They leave room for the worklist -driver to carry explicit `Release` operations and for small future edits without -immediately changing stack layout. The implementation PR should still add a -fixture test that expands every provider-free source-only kind with -representative operands and asserts: - -- final row count is below `MAX_FINAL_ROWS_PER_SOURCE`; -- maximum shallow recipe output is below `MAX_SHALLOW_OPS_PER_LOWERING`; -- maximum observed work-stack depth is below `MAX_WORK_OPS_PER_SOURCE`. - -Inline expansion should not use these provider-free constants until inline recipes are also moved into the grammar. Registered inlines are outside this phase's extraction target and can continue using a heap-backed adapter or a separately measured inline capacity. - -### Grammar Surface - -Decision: start with ordinary Rust data or shallow lowerers plus a small -`RecipeBuilder` only where it pays for itself; do not start with a proc macro. - -Plain data is the extraction-friendly source of truth, but the first -implementation should not overbuild the grammar. A builder is acceptable only -if `finish()` returns inspectable syntactic expansion data or bounded -`ExpansionOp` buffers. The builder must not be a new imperative assembler whose -methods hide arbitrary recursive emission logic. - -Start with the smallest surface that covers the ported families. `Seq`, -`Emit`, `If`, `WithTemp`/explicit lifetime markers, `Fragment`, `Release`, -`ReturnLiteral`, and `Fail` are likely useful concepts. `RegRef`, `ImmRef`, and -`Cond` should be pared down to the forms actually needed by provider-free -expansion. Do not preserve the larger `RegExpr`/`ImmExpr`/`CondExpr` sketch just -because it appears in this spec. - -A proc-macro DSL can be a later ergonomics layer, but only if it emits the same checked recipe definitions. That keeps Ari's article's lesson without inheriting the main proc-macro drawback: `syn` sees syntax, not Rust types. Type-dependent behavior should stay in the recipe data or in small typed adapters outside the extraction-critical core. - -### MLIR Boundary - -Decision: keep the immediate rewrite Rust-first, but make the Rust data model -MLIR-ready without implying an execution-semantics IR. - -The implementation PR should not add a Melior/MLIR dependency to -`jolt-program::expand`. That would make an already large rewrite harder to -review and would move the extraction experiment into compiler-infrastructure -integration before the expansion behavior is clean. The right near-term target -is a small Rust expansion IR with explicit source/intermediate/target legality -checks, syntactic rewrite recipes or shallow lowerers, explicit temp lifetimes, -resource materialization, and focused validators/checks. - -Those concepts should be named and shaped as if they may later become MLIR -dialects or ops. In particular, avoid recipe APIs that depend on Rust closures, -trait-object callbacks, hidden mutation, or call-stack effects. A future MLIR -version should be able to represent the same phases as `riscv.norm`, -`expand`, and `jolt.bytecode` dialects without changing the expansion contract -or the parity fixtures. - -For execution semantics, MLIR should be treated as a carrier for op structure, -legality interfaces, lowering passes, and documentation, not as the first proof -semantics source. The first proof semantics source should be a Lean transition -relation over an abstract machine state. If a later semantic DSL is introduced, -it should be able to emit Lean definitions, a Rust reference interpreter, and -MLIR op metadata from the same effect descriptions. - -### Execution Semantics Follow-Up - -Decision: make instruction semantics a separate follow-up spec rather than -folding it into bytecode expansion. - -The first follow-up should hand-model a provider-free slice in Lean: - -- define an abstract `MachineState` with architectural registers, memory, - program counter, trap state, and any Jolt target-state components needed by - virtual helper rows; -- define `step` for a small source/target instruction slice; -- define `exec_seq` for target bytecode rows; -- define the projection from Jolt target state back to architectural state; -- prove or test one expansion-correctness theorem of the form "executing the - expanded sequence refines executing the source row"; -- differentially test the Lean/reference semantics against `tracer` for the - same slice. - -Only after that slice should the project decide whether a semantic DSL is worth -the cost. The DSL option is attractive if it can generate Lean semantics and a -Rust reference interpreter, but it should be driven by the hand-modeled slice -rather than designed in the abstract. - -### Expansion Rank - -Decision: do not require `ExpansionRank` in Pass 1. - -The central driver should enforce a fuel or recursion-depth bound and return an -explicit error on runaway expansion. That is sufficient for the next rewrite and -is much simpler to review. - -If the recipe surface becomes declarative enough later, a dependency-graph or -rank validator can be added as a follow-up. The important Pass 1 invariant is -that recursive expansion goes through one bounded driver instead of being hidden -inside hand-written Rust call structure. - -### Inline Provider Output - -Decision for this phase: the provider-free core should reject `InstructionKind::Inline`; the tracer adapter should return finalized `NormalizedInstruction` rows, not core `ExpansionOp` values. - -That matches the current shape: `TracerInlineExpansionProvider` builds inline sequences with tracer's inline assembler and its own `VirtualRegisterAllocator`, then returns normalized rows. The `ExpansionAllocator` passed into `expand_inline` is currently unused by the tracer provider. The provider also handles inline register reset rows before returning. - -Trying to force current registered inlines into `ExpansionOp` now would broaden the PR into a second DSL migration. That should be a later spec if maintainers want inline recipes to share the same grammar. For this phase, inline support should be an adapter outside the extraction target: - -```rust -pub enum InlineExpansion { - FinalizedRows(Vec), -} -``` - -The adapter must preserve the existing `rd = x0` remapping behavior before dispatching to the provider. - -### Advice Channels - -Decision: preserve today's three advice channels but name them separately. - -Provider-free expansion owns only the `AdviceLB/LH/LW/LD` syntactic lowering to -`VirtualAdviceLoad` plus sign extension. `VirtualAdviceLoad` reads from the -runtime advice tape during execution; expansion records only the byte length. - -`VirtualAdvice` is different: it is a target row whose concrete tracer -instruction carries a trace-time `advice: u64` payload. That payload is patched -by tracer/inline/LR-SC logic today and should remain outside provider-free -expansion. Do not add an advice payload to `NormalizedInstruction` in this -rewrite. - -Trusted and untrusted advice bytes are a third channel: they are Jolt device -memory regions committed as advice polynomials by the prover. They should stay -in preprocessing/proof code and should not be folded into bytecode expansion. - -The later semantics/advice follow-up can introduce an `AdvicePlan` if needed. -For now, model advice abstractly as an oracle/tape in the execution semantics -track and keep expansion responsible only for emitting the rows that consume -advice. - -### Serialization Derives - -Decision for the rewrite PR: do not feature-gate `serde` or `ark-serialize` as part of the first compiler-native rewrite. Keep the extraction start set rooted in `expand::grammar`, `expand::core`, `expand::allocator`, `expand::buffer`, and `expand::metadata`, and retest Hax/Aeneas after the call graph no longer goes through `InstrAssembler<'a>`. - -Current `jolt-riscv` has unconditional `serde` and `ark_serialize` derives on `InstructionKind`, `NormalizedInstruction`, and `NormalizedOperands`; `jolt-program` also depends on both unconditionally. Feature-gating those derives may still be useful, but it is a workspace-facing dependency cleanup rather than the core bytecode-expansion redesign. If Hax/Aeneas still pull serialization impls after the grammar rewrite, make this the next narrow change: - -```toml -[features] -default = ["std", "serde", "ark-serialize"] -serde = ["dep:serde"] -ark-serialize = ["dep:ark-serialize"] -``` - -and gate the derives/impls in `jolt-riscv`. - -### Release Timing - -Decision: ordered `ExpansionOp::Release(register)` is expressive enough for current provider-free expansion. - -The current code has two release patterns: - -- end-of-sequence releases, such as `MULH`, `MULHSU`, loads, stores, AMOs, shifts, and division; -- mid-sequence releases, such as CSR swap temps, `ECALL` temps, `EBREAK`/`MRET` jump-discard registers, and the staged temps in `SCD`/`SCW`. - -Both are modeled by placing `Release` exactly after the last emitted row that may recursively use the temp. The central driver must process a row's full recursive expansion before moving to the following operation; with that rule, an ordered release marker preserves the current allocator reuse behavior. The parity suite should still include allocation-number assertions for `CSRRW rd == rs1`, `CSRRS rd == rs1`, `ECALL`, `SCD`, and `SCW`, because those are the cases most likely to reveal a release-order bug. - -### `csr == 0` - -Decision for this PR: reject `CSRRW`/`CSRRS` with CSR address `0` as `ExpansionError::UnsupportedCsr(0)`. - -At `a3448e6da44f`, `expand_csrrw` and `expand_csrrs` returned `NormalizedInstruction::default()` when `csr == 0`, bypassing the normal assembler finalizer at the source level. The bug hunt found that this is not a harmless no-op once expansion is owned by `jolt-program`: the default row's address is `0`, and `BytecodePCMapper` skips address-zero rows, so a decoded CSR-zero source row can fail to receive an entry-bytecode index. - -The compiler-native rewrite should therefore make this a first-class source validation rule, not a legacy literal. The recipe may express it as an explicit `If { cond: CsrEq(0), then_body: Fail(UnsupportedCsr), ... }`, or the validator may reject it before recipe interpretation. Either way, tests should assert the public error is `UnsupportedCsr(0)` and should not include CSR-zero in parity fixtures that preserve historical output. +- [x] No live RV32/SV32 tracer execution path remains. +- [x] ELF32/RV32 inputs are rejected before expansion. +- [x] Lookup-backed instruction identity uses `LookupInstruction` rather than a + separate kind enum. +- [x] Decoded-source and expanded-bytecode instruction identities are separated + at the typed row/profile boundary. +- [x] The old production `InstrAssembler<'a>` expansion path is removed from + `jolt-program::expand`. +- [x] Concrete provider-free lowerers return `ExpandedInstructionSequence` + recipes and do not accept `&mut ExpansionAllocator`. +- [x] The central driver is the only provider-free component that recursively + expands helper rows. +- [x] Symbolic temp lifetimes are represented in recipe data and materialized + centrally. +- [x] The exact eight-register instruction-temp pool is enforced before + materialization. +- [x] Provider-free expansion rejects `Inline`; registered inline support stays + behind `InlineExpansionProvider`. +- [x] `jolt-program::expand` remains independent of tracer CPU execution, + prover, transcript, PCS, and ELF parser dependencies. +- [x] Formatting, clippy in host and host+zk modes, focused expansion tests, and + `muldiv` e2e tests pass. diff --git a/specs/inline-expansion-grammar.md b/specs/inline-expansion-grammar.md new file mode 100644 index 0000000000..138e5a5df3 --- /dev/null +++ b/specs/inline-expansion-grammar.md @@ -0,0 +1,362 @@ +# Spec: Inline Expansion Grammar + +| Field | Value | +|-------------|-------| +| Author(s) | Quang Dao | +| Created | 2026-05-13 | +| Status | proposed | +| PR | follow-up to [#1522](https://github.com/a16z/jolt/pull/1522) | + +## Summary + +PR #1522 makes inline opcodes source-only and removes `Inline` from the final +Jolt instruction universe. That fixes the phase identity problem, but registered +inline expansion still uses the older tracer-oriented `InstrAssembler` API: +inline crates emit concrete `tracer::Instruction` values through an +inventory-discovered callback, and `tracer` then normalizes those instructions +back into final `JoltInstructionRow` values. This follow-up should move static +registered inline expansion onto the same explicit recipe/materializer grammar +used by `jolt-program::expand` for built-in source-only rows, while keeping +runtime advice generation in tracer-owned host code. + +The result should be one static bytecode expansion substrate: + +```text +built-in source-only expansion ─┐ + v + ExpansionOp grammar + v +registered inline expansion ────┘ + | + v + one materializer / one metadata path + | + v + JoltInstructionRow +``` + +## Intent + +### Goal + +Replace the tracer `InstrAssembler` static inline expansion path with a +`jolt-program` recipe builder that emits final/source row templates and is +materialized by the existing `ExpansionState`. + +The implementation should introduce or generalize these boundaries: + +- an inline-capable expansion builder in `jolt-program::expand` that can express + the helper operations currently emitted by `tracer::utils::inline_helpers::InstrAssembler`; +- an inline provider contract whose static expansion output is an explicit + recipe or final row sequence, not `Vec`; +- an SDK-facing `InlineOp` API that lets shipped inline crates declare static + sequences without importing tracer instruction types; +- a separate runtime advice path for inlines that need CPU/memory-dependent + witness values. + +### Invariants + +- Static inline expansion must emit the same final `JoltInstructionRow` sequence + as the current `InstrAssembler` path, modulo intentionally documented bug + fixes locked by equivalence tests. +- Registered inline source identity remains `(opcode, funct3, funct7)` plus + `InlineExtension`; there is still no final `JoltInstructionKind::Inline`. +- Profile gating remains two-stage: `SourceExtension::JoltInline` permits inline + source rows, and `InlineExtension` permits a particular registered inline + package. +- Sequence metadata remains centralized. Final rows emitted by inline expansion + must have the same `is_first_in_sequence`, `virtual_sequence_remaining`, source + address, and compressed-tail metadata as today. +- Inline virtual-register allocation must preserve the current register layout: + + ```text + vr32..vr39: reserved persistent virtual registers + vr40..vr47: per-source-instruction temporary registers + vr48.. : provider-allocated inline registers reset at inline finalization + ``` + +- Static inline expansion must not depend on `Cpu`, memory devices, advice + tapes, tracer `Cycle`, prover code, transcript code, file I/O, global mutable + state, `Arc>`, or RAII drop semantics. +- Normal expansion errors should be represented as `ExpansionError`, not panics. +- Runtime inline advice generation may remain host/tracer-owned and may depend + on `Cpu`; it must not be part of the formalization-critical static expansion + grammar. +- The implementation PR must do a full static inline expansion cutover for all + shipped inlines. Do not leave some inline packages on the old static + `InstrAssembler -> tracer::Instruction` path as a compatibility layer. + +### Non-Goals + +- This spec does not require making runtime advice generation Lean-extractable. + Advice depends on CPU and memory state and remains an execution concern. +- This spec does not require changing inline guest SDK semantics or inline + opcode assignments. +- This spec does not require changing final Jolt bytecode semantics or lookup + tables. +- This spec does not require removing tracer's execution-time virtual register + allocator if it is still needed for concrete tracing after static expansion is + cut over. +- This spec does not require replacing inventory registration in the same PR if + the registration surface can call an extraction-friendly static builder. + Inventory is a host discovery mechanism; it must not hide static expansion + semantics inside callback-only code. + +## Evaluation + +### Acceptance Criteria + +- [ ] `InlineOp::build_sequence` no longer returns `Vec`. +- [ ] Static inline expansion no longer imports or uses + `tracer::utils::inline_helpers::InstrAssembler`. +- [ ] `TracerInlineExpansionProvider` no longer converts provider output through + `try_jolt_instruction_row()` on tracer instructions. +- [ ] All shipped inline crates emit static expansion through the new + `jolt-program` inline recipe/final-row builder: + - [ ] `jolt-inlines/sha2` + - [ ] `jolt-inlines/blake2` + - [ ] `jolt-inlines/blake3` + - [ ] `jolt-inlines/keccak256` + - [ ] `jolt-inlines/bigint` + - [ ] `jolt-inlines/secp256k1` + - [ ] `jolt-inlines/grumpkin` + - [ ] `jolt-inlines/p256` +- [ ] Inline virtual-register resets are produced through + `ExpansionAllocator::take_registers_for_reset()` or an equivalent plain-owned + allocator path, not through `VirtualRegisterAllocator::get_registers_for_reset()`. +- [ ] Static inline expansion has equivalence tests against the old emitted + sequences or checked fixtures before deleting the old path. +- [ ] The old static `InstrAssembler` path is deleted or narrowed to + execution-only code that is no longer used for program bytecode expansion. +- [ ] Existing inline tests continue to pass. + +### Testing Strategy + +Run the normal checks: + +```bash +cargo fmt -q +cargo clippy --all --features host -q --all-targets -- -D warnings +cargo clippy --all --features host,zk -q --all-targets -- -D warnings +cargo nextest run -p jolt-program --cargo-quiet +cargo nextest run -p tracer --cargo-quiet --features test-utils +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk +``` + +Add focused tests: + +- one equivalence test per inline package comparing old static output to the new + recipe output before deleting the old path; +- profile tests proving disabled `InlineExtension`s still reject registered + inline rows; +- allocator tests proving inline registers are reset and ordinary instruction + temps are released; +- metadata tests proving inline sequences are stamped by the same central + metadata path as built-in expansion; +- a negative test proving provider-free expansion still rejects + `SourceInstructionKind::Inline`. + +### Performance + +Static expansion should not regress guest program construction measurably. The +new builder may be more structured than `InstrAssembler`, but it should avoid +extra recursive conversions through tracer `Instruction` values. If performance +is measured, benchmark at least one inline-heavy guest or direct expansion of +all shipped inline fixtures before and after the migration. + +## Design + +### Architecture + +Current registered inline static expansion path: + +```text +SourceInstruction::Inline + | + v +TracerInlineExpansionProvider + | + +-- inventory lookup by (opcode, funct3, funct7) + +-- profile.supports_inline(extension) + +-- build_sequence(InstrAssembler, FormatInline) + v +Vec + | + +-- try_jolt_instruction_row() + +-- JoltInstruction::try_from(row) + v +Vec + | + v +stamp_inline_sequence(...) + | + v +Vec +``` + +Target static expansion path: + +```text +SourceInstruction::Inline + | + v +InlineExpansionProvider + | + +-- registration lookup by (opcode, funct3, funct7) + +-- profile.supports_inline(extension) + +-- build_recipe(InlineExpansionBuilder, InlineOperands) + v +InlineExpansionRecipe / ExpandedInstructionSequence + | + v +ExpansionState materializer + | + +-- allocate/release explicit temps + +-- append inline-register reset rows + +-- validate final target rows + +-- stamp sequence metadata centrally + v +Vec +``` + +Runtime advice remains separate: + +```text +tracer executes inline source instruction + | + +-- registration lookup + +-- build_advice(InlineAdviceContext, operands, Cpu) + +-- trace expanded rows with advice populated +``` + +The static recipe builder should be close to the current `ExpansionBuilder` +style: + +```rust +let mut asm = InlineExpansionBuilder::new(source)?; +let a = asm.allocate_inline()?; +let b = asm.allocate_inline()?; + +asm.emit_i(JoltInstructionKind::LD, a, reg(rs1), offset); +asm.emit_r(JoltInstructionKind::XOR, b, a, reg(rs2)); +asm.emit_s(JoltInstructionKind::SD, reg(rd), b, 0); + +asm.release_inline(a)?; +asm.release_inline(b)?; +asm.finalize() +``` + +The exact API can differ, but it should preserve the important properties: +plain owned state, explicit allocation/release, `Result` errors, row-template +output, and no tracer instruction dependency. + +### Modules To Touch + +Expected core changes: + +- `crates/jolt-program/src/expand/grammar.rs` + - expose or generalize `ExpansionOp`, `RowTemplate`, `RegisterOperand`, and + `ExpansionBuilder` enough for inline recipes; + - add inline-specific helpers only where they express real inline invariants. +- `crates/jolt-program/src/expand/materialize.rs` + - materialize inline recipes with the same row validation and metadata + discipline as built-in recipes; + - avoid a second metadata implementation. +- `crates/jolt-program/src/expand/allocator.rs` + - make inline register allocation and reset reporting the single static + expansion allocator path. +- `crates/jolt-program/src/expand/mod.rs` + - revise `InlineExpansionProvider` so static provider output is not + `Vec` produced from tracer instructions; + - keep provider-free expansion rejecting inline source rows. +- `jolt-inlines/sdk/src/host.rs` + - revise `InlineOp` and registration macros; + - split static sequence construction from runtime advice construction; + - move reusable assembler helpers that are static-expansion-safe into the new + builder API or thin extension traits over it. +- `tracer/src/instruction/inline.rs` + - simplify `TracerInlineExpansionProvider`; + - keep runtime tracing/advice behavior, but stop using tracer instruction + normalization as the static expansion output path. +- `tracer/src/utils/inline_helpers.rs` + - delete, move, or narrow the old `InstrAssembler` once shipped inlines no + longer depend on it for static expansion. +- `tracer/src/utils/virtual_registers.rs` + - keep only execution-time allocator behavior that remains necessary; static + expansion should use `ExpansionAllocator`. + +Expected inline package migrations: + +- `jolt-inlines/sha2/src/sequence_builder.rs` +- `jolt-inlines/blake2/src/sequence_builder.rs` +- `jolt-inlines/blake3/src/sequence_builder.rs` +- `jolt-inlines/keccak256/src/sequence_builder.rs` +- `jolt-inlines/bigint/src/multiplication/sequence_builder.rs` +- `jolt-inlines/secp256k1/src/sequence_builder.rs` +- `jolt-inlines/grumpkin/src/sequence_builder.rs` +- `jolt-inlines/p256/src/sequence_builder.rs` + +### Expected Change Size + +Approximate size: + +```text +pilot only: 400-900 LoC changed +full cutover: 2,000-4,000 LoC changed +worst case: 5,000+ LoC if old tracer helpers are deleted completely +``` + +The implementation PR should target the full static expansion cutover. A pilot +can be an internal sequence of commits, but the PR should not merge with only +some inline packages using the new grammar and others still using the old static +tracer builder path. + +### Alternatives Considered + +1. Keep the current provider adapter. + + This preserves working behavior and keeps this PR smaller, but leaves static + inline expansion in callback-heavy tracer code and blocks a clean extraction + story. + +2. Move all inline registration and advice into `jolt-program`. + + This would make `jolt-program` depend on tracer CPU/memory/advice concerns. + Static sequence expansion belongs near program construction; advice + generation does not. + +3. Add a second inline-only materializer. + + This would duplicate sequence metadata, legality checks, and allocator + behavior. Inline expansion should share the existing `ExpansionState` path + wherever possible. + +## Documentation + +No Jolt book update is required unless inline registration is a public SDK +surface. The implementation should update: + +- `specs/source-jolt-instruction-split.md` to mark the follow-up complete; +- `specs/bytecode-expansion-crate.md` if it still describes the old + `InstrAssembler`-based registered inline bridge as the current compromise; +- inline SDK docs/comments showing how to implement an `InlineOp`. + +## Execution + +Suggested implementation order: + +1. Add the new inline recipe/builder API in `jolt-program::expand`. +2. Add a temporary test harness that compares old `InstrAssembler` output with + new recipe output for one inline package. +3. Port the smallest representative inline first, then port the remaining + shipped inline packages. +4. Split runtime advice construction away from static sequence construction. +5. Remove the static `InstrAssembler -> tracer::Instruction` provider path. +6. Run the full validation stack and inline package tests. + +## References + +- [PR #1522](https://github.com/a16z/jolt/pull/1522) +- [`specs/source-jolt-instruction-split.md`](source-jolt-instruction-split.md) +- [`specs/bytecode-expansion-crate.md`](bytecode-expansion-crate.md) diff --git a/specs/source-jolt-instruction-split.md b/specs/source-jolt-instruction-split.md new file mode 100644 index 0000000000..fb97860df1 --- /dev/null +++ b/specs/source-jolt-instruction-split.md @@ -0,0 +1,1479 @@ +# Spec: Source/Jolt Instruction Split + +| Field | Value | +|-------------|-------| +| Author(s) | Quang Dao | +| Created | 2026-05-12 | +| Status | implementation in progress | +| PR | [#1522](https://github.com/a16z/jolt/pull/1522) | + +## Summary + +PR #1518 moved bytecode expansion into `jolt-program`, but it deliberately kept +one broad row type: decoded source rows and expanded Jolt bytecode rows both +flowed through `NormalizedInstruction { instruction_kind: JoltInstructionKind }`. +That kept the code working, but the type names hid an important phase boundary. +This PR should make that boundary real: decoded program instructions are source +instructions, expansion recipes consume source instructions, and +bytecode/preprocessing/tracing/proof rows are Jolt instructions. The initial +implementation slices should replace the old row-with-kind shape with +marker-carrying source/final enums, add decoded source/final payload structs, +and cut the decode/expand boundary over so source rows flow in and final rows +flow out. The later slices remove the separate source/final kind enums, remove +source-only rows from the final enum, and require proof metadata queries to +start from a final `JoltInstruction`. + +The same cutover should clean up registered inlines. Inline opcodes are source +program opcodes identified by `(opcode, funct3, funct7)`; they are not final +Jolt bytecode rows. The inline provider contract should therefore accept source +inline rows and return final Jolt rows, without routing through the old fake +final inline identity. The final enum should not contain an `Inline` variant at +all. + +This split should also prepare Jolt for a more modular instruction world without +turning this PR into a general profile system or a single all-knowing instruction +macro. The near-term requirement is to create one universal source instruction +enum and one universal final Jolt instruction enum, with variants carrying the +existing marker structs. The zero-payload instantiation, e.g. +`SourceInstruction<()>` or `JoltInstruction<()>`, is the kind-level view; the +row-payload instantiation, e.g. `SourceInstruction` or +`JoltInstruction`, is the concrete row. There should not +be a second bare `SourceInstructionKind` / `JoltInstructionKind` enum whose +variants mirror the generic enums. Profiles should work like an MLIR conversion +target: the operation universe is stable, while each selected profile defines +which source rows are accepted and which final rows are legal after expansion. +`jolt-program` should own decode and expansion facts. `tracer` should own +execution semantics. Lookup/proving crates should own lookup-table and circuit +metadata. Source instructions must not expose lookup tables, circuit flags, or +instruction flags; those are properties of final Jolt rows after expansion. + +## Intent + +### Goal + +Introduce explicit source/final instruction row enums plus a profile legality +layer across decode, expansion, tracer conversion, bytecode preprocessing, and +inline expansion: + +- universal instruction enums: the source of truth for shipped source/final row + identities, canonical operation names, final-row compact binary tags, and + enum-to-marker-struct dispatch. The zero-payload instantiation of each enum is + the instruction kind. Source identity is the canonical namespaced string; + compact `u16` tags are reserved for final Jolt bytecode rows. +- profile legality: the source of truth for which source instructions decode + under a selected profile and which final rows are legal after expansion. +- profile-local dense indexes: generated indexes used by profile-specific + tables only, never the persistent identity of an instruction. +- crate-local decorations: the source of truth for crate-specific behavior and + metadata such as decode encodings, expansion dispatch, tracer execution, + lookup-table routing, circuit flags, and instruction flags. +- `SourceInstruction`: decoded source-program + instruction enum, with variants such as `ADD(Add)`, `ADDW(AddW)`, and + `Inline(Inline)`. `SourceInstruction<()>` is the source kind-level view. +- `SourceInstructionPayload`: decoded source row payload, including address, + operands, compression metadata, and inline dispatch metadata when applicable. + It must not contain a duplicate source kind field. +- `JoltInstruction`: final expanded + bytecode/proof/tracer instruction enum, with variants such as `Add(Add)`, + `Ld(Ld)`, and `VirtualSignExtendWord(VirtualSignExtendWord)`. + `JoltInstruction<()>` is the final kind-level view. +- `JoltInstructionPayload`: final row payload, including operands, address, + virtual-sequence metadata, and compression-tail metadata. It must not contain + a duplicate final kind field. + +The old `NormalizedInstruction` row should not remain as a compatibility shim. +If a temporary name is needed while editing, it must be removed before the PR is +ready for review. + +The concrete `SourceInstruction` and `JoltInstruction` types should remain +closed Rust enums. That is useful for serialization, match exhaustiveness, +static dispatch, and proof performance. Unlike profile-specific generated +enums, these enums should represent Jolt's shipped operation universe. Profiles +then provide explicit positive legality checks, in the same spirit as MLIR's +separation between operation definitions and conversion-target legality. +Keeping a `SourceInstructionKind` or `JoltInstructionKind` type name is +acceptable only if it is a pure alias for the zero-payload instantiation and is +the canonical API spelling for that view; it must not hide a separate enum or a +compatibility layer. + +Profile support is part of the main goal, not a stretch goal. The PR should +leave source/final row types as phase-specific universal operation enums, with +profile legality deciding what is accepted for a given compiled configuration. + +### Invariants + +- Decoding preserves all currently supported RV64 and Jolt custom source + opcodes, including registered inline dispatch metadata. +- Source and final instruction universes are explicit and separate. Adding a + source opcode should not automatically add a final Jolt bytecode row, and + adding a target-only virtual row should not automatically make it decodable + from guest program bytes. +- There is one enum representation per phase. `SourceInstruction<()>` is the + source kind view and `SourceInstruction` is the + concrete decoded source row. `JoltInstruction<()>` is the final kind view and + `JoltInstruction` is the concrete final row. A + separate bare enum with the same variants is forbidden. +- Instruction identity is not Rust declaration order. Each source/final row + identity has a canonical operation name such as `rv64.add` or + `jolt.virtual.sign_extend_word`. Compact numeric tags are a binary encoding + of final-row names for serialization, fixtures, and other persistent + cross-crate references. Profile-local dense indexes may be regenerated for + selected legality sets, but they must be derived from canonical names/tags and + tied to the selected profile/catalog fingerprint wherever a persisted artifact + later consumes those dense indexes. +- Source/final enum variants carry the marker structs directly, e.g. + `SourceInstruction::ADD(Add)` and + `JoltInstruction::VirtualSignExtendWord(VirtualSignExtendWord)`. + This makes enum dispatch delegate naturally to marker-struct impls without a + separate variant-to-struct lookup table or `kind -> struct` reconstruction. +- Crate-specific facts are not centralized in `jolt-riscv`: they are declared + by the crate that owns the behavior and are keyed by the same marker structs + or source/final row enum variants. +- The current Jolt source profile is explicit in code. The target legality set + is computed from the selected source extensions plus selected inline + extensions: it is the positive set of final rows that can be emitted by those + expansions. +- Expansion behavior is unchanged relative to `main` after PR #1518 for the + representative checked fixture corpus. The durable guard is a compact + canonical-row baseline in `jolt-eval` that hashes the serialized + `SourceInstruction` corpus and final `JoltInstruction` + expansion stream after normalizing intentional type-name changes. +- Final Jolt bytecode cannot contain source-only opcodes: + `Inline`, `ADDW`, `LW`, `SW`, AMOs, traps, CSR rows, shifts, DIV/REM, advice + source loads, and other source-only expansion inputs must be rejected before + preprocessing. +- `JoltInstruction` contains only final-row variants, so it must not contain + `Inline`, `ADDW`, `LW`, `SW`, or other source-only expansion inputs. Profile + target legality is a separate positive check over those final variants. +- Source and final row payloads carry different semantic metadata: + source payloads know decode/inline identity; final payloads know + virtual-sequence position. The enum variant, not the payload, is the + instruction identity in both phases. +- `rd = x0` rewriting remains centralized in expansion for source rows and is + not reimplemented in tracer as an independent policy. +- Registered inline expansion remains behind a provider boundary. The provider + may live in `tracer`, but its input must be a source inline row and its output + must be validated `JoltInstruction` rows. +- Concrete execution semantics remain tracer-owned. The source/final enums must + not own CPU state mutation, RAM/advice side effects, concrete cycle + construction, or inline advice generation. +- Lookup/proving metadata remains owned by the lookup/proving side of the codebase. + `JoltInstruction` may provide stable final-row identities, but it must not + encode lookup-table flags, circuit flags, instruction flags, or proof-system + routing policy. +- Source rows do not have proof metadata. `SourceInstruction`, + `SourceInstructionPayload`, and source-only concrete tracer instructions must not + implement or be accepted by flag or lookup-table APIs. Asking for + `circuit_flags`, `instruction_flags`, or `lookup_table` requires a final + `JoltInstruction` view. Some final + rows still return no lookup table, e.g. final no-op/system-like rows such as + `FENCE`, loads/stores, or host-I/O rows; that is different from source rows + having a lookup-table notion. +- Proof code that starts from concrete tracer cycles must first construct a + `JoltTraceCycle`. This adapter pairs `&Cycle` dynamic witness data with the + final `JoltInstruction` used for static proof + metadata. A plain `Cycle` remains the right source for register/RAM/advice + values and lookup operands/outputs; only `JoltTraceCycle` or final + `JoltInstruction` rows may answer lookup-table routing, circuit flags, or + instruction flags. +- Expansion definitions should stay readable for humans authoring and reviewing + instruction lowerings. The refactor may change the underlying recipe and row + types, but the call-site syntax for ordinary expansions should remain at + least as easy to parse as the current builder style. +- The resulting instruction and expansion code should remain extraction + friendly. The PR does not need to make Hax or Aeneas fully compile the + extracted output today, but it should avoid Rust patterns that are inherently + difficult to translate into proof-oriented languages. +- `jolt-program::expand` remains independent of tracer CPU state, advice tapes, + concrete tracer cycles, PCS/prover code, and ELF parsing. +- Prover/verifier behavior is unchanged. Bytecode preprocessing, PC mapping, + instruction flags, lookup-table routing, and trace witness generation must + see the same final Jolt rows as before. +- Adding a future instruction must not renumber existing serialized source or + final row identities. If a new row changes a profile-local dense index used by + preprocessing or proving tables, the corresponding profile/catalog + fingerprint must change so stale artifacts are rejected rather than silently + reused. + +### Non-Goals + +- Do not redesign lookup-table metadata or reintroduce `LookupInstructionKind`. + The typed lookup-backed view should be the final-row `JoltInstruction` enum + itself, not a second parallel instruction enum. +- Do not move lookup-table flags, circuit flags, instruction flags, or other + proving-system metadata into `jolt-riscv`. +- Do not change RISC-V or Jolt instruction semantics. +- Do not change the registered inline algorithms themselves. +- Do not require Hax/Aeneas extraction to compile in this PR. Current extraction + tools have temporary limitations, and this PR should not contort otherwise + good Rust APIs around those limitations. +- Do not introduce deprecated aliases, conversion shims, or dual public APIs. + This is a full cutover. +- Do not make `jolt-program` depend on `tracer`. +- Do not implement downstream user-authored extension profiles in this PR. The + shipped source profiles must be explicit and compile-time selected, but + third-party profile composition can remain follow-up work. + +## Evaluation + +### Acceptance Criteria + +- [x] `NormalizedInstruction` is removed or fully renamed into one of the two + phase-specific types, with no compatibility alias. +- [x] `jolt-program::image::decode_instruction` returns + `SourceInstruction` rather than a normalized final row. +- [x] `jolt-program::expand` accepts `SourceInstruction` at public boundaries + and returns typed final instructions. The current implementation spells + the final payload type as concrete `Vec`; if the final + enum is later made generic, this becomes + `Vec>`. +- [x] `SourceInstructionKind` and `JoltInstructionKind` are not separate enums. + The kind-level APIs use `SourceInstruction<()>` and `JoltInstruction<()>` + directly, or a canonical type alias to those zero-payload instantiations. +- [x] Recursive expansion internally distinguishes source helper dispatch from + direct target-row emission: helper recursion is keyed by + `SourceInstruction<()>`, while direct final emission is keyed by + `JoltInstruction<()>`. Source-only recipe builders receive source-row + context, not synthetic final Jolt payload inputs. +- [x] `JoltInstruction` no longer has `Inline` and contains only universal + shipped final-row variants. Profile-specific target legality is checked by + a positive computed target legality closure rather than by changing the + enum shape. +- [x] `SourceInstruction` contains decoded RV64 source opcodes, Jolt custom + source opcodes, and one source-only `Inline(Inline)` variant. + Individual registered inline opcodes are represented by `SourceInlineKey`, + not by one enum variant per inline package entry. +- [x] Source and final typed row universes are separate closed enums whose + variants carry marker structs, e.g. `ADD(Add)`, and are not generated + differently for each profile. +- [ ] `SourceInstructionPayload` and `JoltInstructionPayload` do not carry + duplicate kind fields. The enum variant is the instruction identity, and + the payload carries only row data. +- [x] Canonical operation names and stable `u16` Jolt tags are explicit for the + universal source/final enums supported by this catalog. Serialization does + not depend on Rust enum declaration order, generated display order, or + which profile is selected, and those identities live on + `SourceInstruction<()>` / `JoltInstruction<()>` rather than on separate + bare tag enums. +- [x] Any dense instruction indexes used by profile-specific preprocessing, + lookup, or proving tables are generated from compact tags for that + selected profile and are not used as persistent instruction identity. +- [x] Decode metadata and operand parsing are declared in `jolt-program`, keyed + by `SourceInstruction<()>` / marker structs, not duplicated as an + unrelated target-row opcode list. +- [x] Source-only expansion dispatch is declared/generated in `jolt-program`, + keyed by `SourceInstruction<()>` / marker structs, and does not live in + `jolt-riscv`. +- [x] Lookup-table routing, circuit flags, and instruction flags remain owned by + the lookup/proving crates. They are not fields in the `jolt-riscv` + row enum definitions or in a mega `jolt_instruction!` declaration. +- [x] Source rows and decoded tracer instructions cannot be used as lookup or + flag subjects. `Flags`, `InstructionLookupTable`, and Jolt-core + `InstructionLookup` impls must be available only for final Jolt row + views, with proof code converting trace cycles to + `JoltInstruction` before querying proof metadata. +- [x] Tracer still owns concrete execution semantics. No row enum macro in + `jolt-riscv` mutates CPU/RAM/advice state or constructs concrete tracer + cycles. +- [x] Broad mirrored source/target tag enums are gone. Closed typed enums + remain, with marker-struct payloads and profile legality layered on top. + Compact final tags are methods/metadata on `JoltInstruction<()>`, not a + second final-kind enum. +- [x] The profile/extension layer uses the concrete names `SourceExtension`, + `JoltTargetExtension`, `InlineExtension`, and `JoltInstructionProfile`. +- [x] The default profile corresponds to the current supported + RV64IMAC Jolt behavior, with room for shipped source presets such as + `RV64IM_JOLT`, `RV64IMAC_JOLT`, and `RV64IMAC_JOLT_ALL_INLINES`. + Future profiles can be added by selecting source/inline extensions and + recomputing positive legality sets without changing the row enum shapes. +- [x] Inline dispatch metadata is represented directly on `SourceInstruction` + as a `SourceInlineKey` payload, not packed into `NormalizedOperands::imm`. +- [x] `InlineExpansionProvider` accepts source inline data and returns final + `JoltInstruction` rows. +- [x] Concrete expansion files remain ergonomic for human authors: common + lowering code should read like a small instruction sequence, not like + serialized grammar data or generated tables. +- [x] The refactor does not introduce extraction-hostile Rust patterns in the + instruction row enums, row types, or expansion pipeline. +- [x] Tracer concrete `Instruction`/`Cycle` APIs execute final Jolt rows, while + decoded source instructions convert through the expansion path before + trace execution. +- [x] No verifier-facing crate imports tracer just to name source or final row + types. +- [x] Existing expansion fixture/hash tests still pass against `main`'s + post-#1518 behavior, and any fixture hash regeneration is backed by a + structural source-to-final stream equivalence check rather than reviewer + trust in changed serialization bytes. +- [x] `cargo tree -p jolt-program` has no `tracer` dependency. + +### Testing Strategy + +Run the usual validation stack: + +```bash +cargo fmt -q +cargo clippy --all --features host -q --all-targets -- -D warnings +cargo clippy --all --features host,zk -q --all-targets -- -D warnings +cargo nextest run -p jolt-program --cargo-quiet +cargo nextest run -p tracer --cargo-quiet --features test-utils +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host +cargo nextest run -p jolt-core muldiv --cargo-quiet --features host,zk +``` + +Add or update tests for: + +- decoded inline rows preserving `(opcode, funct3, funct7)` without `imm` + packing; +- provider-free expansion rejecting source inline rows; +- registered inline provider returning only target-legal `JoltInstruction` + rows; +- final bytecode rejecting every source-only kind at preprocessing; +- proof metadata boundary tests proving source rows and source-only tracer + instructions are not accepted by flag or lookup-table APIs, while final Jolt + rows still expose flags and optional lookup tables; +- trace/proof tests that derive circuit flags, instruction flags, and lookup + tables from final `JoltInstruction` views rather than + decoded source `Instruction` values; +- canonical-name and compact-tag tests proving existing identities do not change + when generated enum order changes or new instructions are appended; +- profile dense-index tests proving unsupported rows have no index and supported + rows map to a contiguous profile-local range; +- profile/catalog fingerprint tests proving a changed legal row set changes the + artifact identity used by preprocessing/proving; +- tracer execution of expanded rows after the source/final cutover; +- fixture parity for the existing representative source corpus; +- a `jolt-eval` invariant named `source_to_jolt_expansion_equivalence` that + compares the new source-to-final expansion stream against the pre-refactor + normalized expansion semantics modulo intentional row type and compact-tag + renames. + +### Performance + +This should be a type-boundary refactor with no intended runtime regression. +Avoid extra allocation in the hot trace path: source rows should be expanded +once into final rows, and final rows should be reused by preprocessing/tracing +where the current code already does so. If the implementation introduces a +conversion allocation around tracer execution, remove it or document why it is +outside hot loops before review. + +### Readability + +The implementation should preserve the readability win from PR #1518: changing +the symbolic expansion plumbing must not make ordinary expansion files harder +to read. It is acceptable to rename types and builder methods when that makes +the source/final boundary clearer, but adding a new opcode or auditing an +existing lowering should still look like writing a short instruction sequence. + +Prefer code shaped like this: + +```rust +let mut asm = ExpansionBuilder::new(instruction); + +asm.emit_r( + Sub, + rd(instruction)?, + rs1(instruction)?, + rs2(instruction)?, +); +asm.emit_i( + VirtualSignExtendWord, + rd(instruction)?, + rd(instruction)?, + 0, +); + +asm.finalize() +``` + +Avoid exposing representation-oriented recipe construction at ordinary call +sites unless it is genuinely clearer for that instruction: + +```rust +ExpandedInstructionSequence::new( + instruction, + [ + ExpansionOp::Emit(RowTemplate::r(...)), + ExpansionOp::Emit(RowTemplate::i(...)), + ], +) +``` + +The row-enum/profile work has the same constraint. Metadata can become more +structured, but instruction authors should not need to mentally execute macro +grammar to understand whether an opcode is source-only, target-only, +lookup-backed, side-effecting, or part of a default profile. + +### Expansion Equivalence + +The expansion fixture hash may change when `NormalizedInstruction` is replaced +by phase-specific row types, even if the emitted program is semantically +unchanged. Regenerating that fixture is therefore not itself sufficient evidence. +The durable guard should live in `jolt-eval` as +`source_to_jolt_expansion_equivalence`: a compact canonical-row baseline over +the representative source corpus and its emitted final-row expansion stream. + +The invariant should serialize enough row structure to fail on differences in +source opcode identity, final opcode identity, operands, addresses, virtual +sequence metadata, and compression-tail metadata. It does not need to keep the +entire pre-refactor byte stream alive as a compatibility layer; intentional +type-name changes, compact-tag changes, and the removal of the old +fake final inline row should be normalized into the new +canonical row representation before hashing. + +### Extraction Friendliness + +This PR should keep the code on a path toward clean extraction into theorem +prover targets such as Lean. That does not mean optimizing the Rust code around +today's Hax/Aeneas limitations, especially when those limitations are likely to +move. It does mean avoiding choices that are structurally unfriendly to +extraction. + +Prefer: + +- plain data types with explicit fields over packed, phase-dependent encodings; +- small enums and structs with local invariants over trait-object or callback + heavy control flow; +- total conversion functions that return typed errors over implicit panics; +- simple iterator/loop structure where it keeps the code just as readable; +- profile/legalization metadata that can be inspected as data, not only through macro + expansion side effects. + +Avoid introducing: + +- `dyn` dispatch or closure-heavy APIs in the core instruction/expansion path; +- hidden global state for profile/legalization decisions; +- unsafe code in row enum, decode, or expansion plumbing; +- encodings where a field has unrelated meanings depending on an instruction + phase; +- procedural macro magic that makes the generated instruction set difficult to + audit or mirror in extracted code. + +Extraction-friendliness is subordinate to correctness, performance, and idiomatic +Rust, but it should influence tie-breakers when two designs are otherwise +comparable. + +## Design + +### Architecture + +The target data flow is: + +```text +ELF / decoded word + -> SourceInstruction + -> jolt-program::expand + -> Vec + -> bytecode preprocessing / tracer execution / proof lookup metadata +``` + +`jolt-riscv` owns shared instruction row identity because it is the lowest common +crate shared by decode, expansion, tracer, bytecode preprocessing, and lookup +metadata. That ownership should stay deliberately small. It should provide: + +- the marker structs such as `Add`, `AddW`, and + `VirtualSignExtendWord`; +- the universal source/final row enums whose variants carry those marker + structs; +- stable enum variant names, canonical operation names, and compact final-row + binary tags used for serialization; +- row structs and profile-independent row-shape types shared across crates. + +It should not provide a mega declaration that also names decode opcodes, +expansion bodies, side-effect policy, tracer execution, lookup-table routing, +circuit flags, or instruction flags. Those are real facts, but they belong to +the crates that use and test them. + +The row enums should use the same pattern as the former `LookupInstruction`: +each variant wraps the associated marker struct instantiated with a row payload. +That means the enum itself owns the variant-to-struct relationship; there is no +separate mapping table or second kind enum to keep in sync. The same enum also +serves as the kind view when `T = ()`. + +```rust +pub struct SourceInstructionPayload { + pub address: usize, + pub operands: NormalizedOperands, + pub inline: Option, + pub is_compressed: bool, +} + +pub enum SourceInstruction { + NoOp(Noop), + Unimpl(Unimpl), + ADD(Add), + ADDW(AddW), + LW(Lw), + SW(Sw), + Inline(Inline), + // ...all shipped source-program rows... +} + +pub struct JoltInstructionPayload { + pub address: usize, + pub operands: NormalizedOperands, + pub virtual_sequence_remaining: Option, + pub is_first_in_sequence: bool, + pub is_compressed: bool, +} + +pub enum JoltInstruction { + Noop(Noop), + Add(Add), + Ld(Ld), + Sd(Sd), + VirtualSignExtendWord(VirtualSignExtendWord), + VirtualHostIO(VirtualHostIO), + // ...all shipped final Jolt rows... +} +``` + +```rust +// Kind-level values are just the zero-payload enum instantiations. +let source_kind: SourceInstruction<()> = SourceInstruction::ADD(Add(())); +let jolt_kind: JoltInstruction<()> = JoltInstruction::Add(Add(())); + +// Concrete rows are the same enum instantiations with row payloads. +let source_row: SourceInstruction = + SourceInstruction::ADD(Add(source_payload)); +let jolt_row: JoltInstruction = + JoltInstruction::Add(Add(jolt_payload)); +``` + +The exact payload/spec type names can change if implementation uncovers a better +name, but the ownership relationship should not: the universal enums define what +shipped row identities exist, the selected source profile defines which source +rows decode, and the target closure defines which final rows may be emitted by +this profile. Source operands may continue to use normalized register fields for +ordinary rows; inline dispatch metadata should not be stored in an immediate +field. + +### Canonical Names, Tags, And Profile Indexes + +Do not use Rust enum declaration order as instruction identity. Source and +final row enums should have canonical operation names that are explicit, +reviewed, and namespace-qualified. This follows the MLIR model: the durable +semantic identity is a dialect-like namespace plus an operation mnemonic, while +compact encodings are an implementation detail. + +Examples: + +```text +rv64.add +rv64.mul +rv64.fadd_s +jolt.virtual.sign_extend_word +jolt.field.bn254.mul +jolt.inline.sha2.compress +wasm.i32.add +``` + +RISC-V instruction encodings are useful decode metadata, not sufficient Jolt +row identities. A RISC-V instruction is usually identified by an encoding +pattern such as `(opcode, funct3, funct7)` or by compressed-instruction fields, +not by the 7-bit opcode alone. Several instructions share the same opcode, +compressed rows live in a different encoding space, and Jolt virtual/final-only +rows have no source-program RISC-V encoding at all. Therefore `jolt-program` +should own RISC-V encoding facts for decode, while `jolt-riscv` owns canonical +operation names for source/final row identity. + +For Jolt's binary artifacts, every operation supported by the shipped Jolt +catalog has a compact stable tag. The name is the reviewable semantic identity; +the tag is the compact serialization of that identity for bytecode rows, +fixtures, preprocessing keys, and proof-adjacent tables. This tag only needs to +cover operations Jolt actually supports in the current catalog, not every +operation in every possible future source ISA. A stable `u16` tag is therefore +the preferred starting point: it keeps bytecode row serialization as compact as +today while decoupling identity from Rust enum declaration order. + +Use metadata like this in the generated catalog data: + +```rust +pub trait SourceInstructionMeta { + const CANONICAL_NAME: &'static str; + const SOURCE_EXTENSION: Option; + const HAS_SIDE_EFFECTS: bool = false; +} + +pub trait JoltInstructionMeta { + const CANONICAL_NAME: &'static str; + const JOLT_TAG: JoltInstructionTag; + const TARGET_EXTENSION: Option; +} + +impl SourceInstructionMeta for Add { + const CANONICAL_NAME: &'static str = "rv64.add"; + const SOURCE_EXTENSION: Option = Some(SourceExtension::Rv64I); +} + +impl JoltInstructionMeta for Add { + const CANONICAL_NAME: &'static str = "rv64.add"; + const JOLT_TAG: JoltInstructionTag = JoltInstructionTag(0x0101); + const TARGET_EXTENSION: Option = + Some(JoltTargetExtension::IntegerCore); +} + +impl SourceInstructionMeta for AddW { + const CANONICAL_NAME: &'static str = "rv64.addw"; + const SOURCE_EXTENSION: Option = Some(SourceExtension::Rv64I); +} + +impl JoltInstructionMeta for VirtualSignExtendWord { + const CANONICAL_NAME: &'static str = "jolt.virtual.sign_extend_word"; + const JOLT_TAG: JoltInstructionTag = JoltInstructionTag(0x8001); + const TARGET_EXTENSION: Option = + Some(JoltTargetExtension::VirtualArithmetic); +} +``` + +The enum dispatchors delegate to those marker-struct implementations: + +```rust +impl SourceInstruction<()> { + pub const ALL: &'static [Self] = &[ + Self::NoOp(Noop(())), + Self::ADD(Add(())), + Self::ADDW(AddW(())), + Self::Inline(Inline(())), + // ... + ]; + + pub const fn canonical_name(&self) -> &'static str { + match self { + Self::NoOp(_) => "jolt.pseudo.noop", + Self::ADD(_) => as SourceInstructionMeta>::CANONICAL_NAME, + Self::ADDW(_) => as SourceInstructionMeta>::CANONICAL_NAME, + Self::Inline(_) => "jolt.inline.dispatch", + // ... + } + } +} + +impl JoltInstruction<()> { + pub const fn tag(&self) -> JoltInstructionTag { + match self { + Self::Noop(_) => JoltInstructionTag(0x0000), + Self::Add(_) => as JoltInstructionMeta>::JOLT_TAG, + Self::VirtualSignExtendWord(_) => + as JoltInstructionMeta>::JOLT_TAG, + // ... + } + } +} +``` + +The tag is allocated only for final rows supported by Jolt bytecode and proving +tables. Future source ISA operations may be known by canonical name without +receiving a Jolt tag until expansion can emit them as a supported final row. +Numeric tag ranges may be used as a readability aid, but they must not be the +source of architectural meaning. The robust partition is the canonical name namespace: `rv64.*`, +`jolt.virtual.*`, `jolt.field.*`, `jolt.inline.*`, `wasm.*`, and future +namespaces. If a row moves from source-only to final-only, or from an inline +helper to a first-class operation, its name and tag should change only if its +semantic identity changes. + +Profile-local dense indexes are a separate concept. They may be compact and may +change when a profile's legal final-row set changes: + +```rust +pub struct ProfileInstructionIndex(u16); + +impl Rv64imacJolt { + pub const fn jolt_dense_index( + instruction: JoltInstruction<()>, + ) -> Option { + match instruction { + JoltInstruction::Add(_) => Some(ProfileInstructionIndex(0)), + JoltInstruction::Addi(_) => Some(ProfileInstructionIndex(1)), + JoltInstruction::Mul(_) => Some(ProfileInstructionIndex(2)), + JoltInstruction::Ld(_) => Some(ProfileInstructionIndex(3)), + JoltInstruction::VirtualSignExtendWord(_) => Some(ProfileInstructionIndex(4)), + _ => None, + } + } +} +``` + +Dense indexes are appropriate for profile-specific preprocessing, lookup, or +proving tables. Concrete examples include arrays of per-profile final-row +metadata, profile-specific legality bitsets, lookup-routing tables keyed by the +selected final-row set, and any proving key/preprocessing structure that wants a +contiguous `[0, profile_instruction_count)` coordinate. Dense indexes are not +appropriate for canonical identity, serialization source of truth, fixture +identity, or cross-profile references. Follow-up work should key any persisted +artifact that relies on dense indexes by the selected profile/catalog +fingerprint so that stale tables cannot be reused after adding or removing +legal rows. + +Profiles should not change the Rust enum shape. This is intentionally closer to +MLIR than to profile-specific generated Rust APIs: operations exist in the +universe, and a conversion target/profile declares which operations are legal at +a given point in the pipeline. Selecting `RV64IM_JOLT` should therefore reject +atomic source rows during decode and exclude atomic-produced final rows from the +computed target legality set, but it should not generate a different +`SourceInstruction` type from `RV64IMAC_JOLT`. + +### Ownership Boundaries + +The universal row enums own only cross-crate identity. Crate-local decorations +own the behavior-specific facts, using the same marker structs as join keys. + +`jolt-program` owns source decoding, operand parsing, positive source/target +legality, and source-to-final expansion dispatch. Its local metadata can be a +trait, free-function table, or macro that expands to ordinary matches: + +```rust +trait DecodeSpec { + const SOURCE_EXTENSION: SourceExtension; + const FORMAT: SourceFormat; + const ENCODING: SourceEncoding; +} + +impl DecodeSpec for Add<()> { + const SOURCE_EXTENSION: SourceExtension = SourceExtension::Rv64I; + const FORMAT: SourceFormat = SourceFormat::R; + const ENCODING: SourceEncoding = SourceEncoding::R { + opcode: 0b0110011, + funct3: 0b000, + funct7: 0b0000000, + }; +} + +impl DecodeSpec for AddW<()> { + const SOURCE_EXTENSION: SourceExtension = SourceExtension::Rv64I; + const FORMAT: SourceFormat = SourceFormat::R; + const ENCODING: SourceEncoding = SourceEncoding::R { + opcode: 0b0111011, + funct3: 0b000, + funct7: 0b0000000, + }; +} +``` + +Profile checks consume kind-level enum values, not a second kind enum: + +```rust +impl JoltInstructionProfile { + pub fn supports_source(self, instruction: SourceInstruction<()>) -> bool { + instruction + .source_extension() + .is_none_or(|extension| self.source_extensions.contains(&extension)) + } + + pub fn supports_jolt(self, instruction: JoltInstruction<()>) -> bool { + instruction + .target_extension() + .is_none_or(|extension| self.supports_target_extension(extension)) + } +} +``` + +Expansion dispatch should be local to `jolt-program` as well: + +```rust +trait SourceExpansion { + fn expand_source( + &self, + allocator: &mut ExpansionAllocator, + ) -> Result>, ExpansionError>; +} + +impl SourceExpansion for AddW { + fn expand_source( + &self, + allocator: &mut ExpansionAllocator, + ) -> Result>, ExpansionError> { + expand_addw(self, allocator) + } +} +``` + +`tracer` owns concrete execution semantics: + +- CPU register and PC mutation; +- RAM and device side effects; +- advice tape interaction and inline advice generation; +- concrete `Instruction` / `Cycle` construction and execution tests; +- tracer-internal adapters from final `JoltInstruction` rows to concrete + execution instructions. + +That can be expressed with tracer-local implementations keyed by the shared +marker structs or by the final row enum. For example: + +```rust +trait Execute { + fn execute(cpu: &mut Cpu, operands: &JoltOperands) -> Cycle; +} + +impl Execute for Add<()> { + fn execute(cpu: &mut Cpu, operands: &JoltOperands) -> Cycle { + existing_add_execute(cpu, operands) + } +} +``` + +Lookup/proving code owns lookup metadata, lookup-table routing, circuit flags, +and instruction flags. Those facts may also be generated from a local macro, but +the macro should live with the lookup/proving owner, not in the universal row +enum definition: + +```rust +trait LookupMetadata { + const LOOKUP: LookupSupport; + const CIRCUIT_FLAGS: &'static [CircuitFlag]; + const INSTRUCTION_FLAGS: &'static [InstructionFlag]; +} + +impl LookupMetadata for Add<()> { + const LOOKUP: LookupSupport = LookupSupport::Instruction; + const CIRCUIT_FLAGS: &'static [CircuitFlag] = + &[CircuitFlag::AddOperands, CircuitFlag::WriteLookupOutputToRD]; + const INSTRUCTION_FLAGS: &'static [InstructionFlag] = + &[InstructionFlag::LeftOperandIsRs1Value, InstructionFlag::RightOperandIsRs2Value]; +} +``` + +That metadata is final-row metadata, not source metadata. A source row such as +`SourceInstruction::Ebreak(Ebreak)` or +`SourceInstruction::Inline(Inline)` can be decoded, +profile-checked, executed by tracer policy, or expanded, but it cannot answer +which lookup table it uses and it cannot answer Jolt R1CS flags. The question is +ill-typed until expansion produces a final row: + +```rust +let source: SourceInstruction = decode(word, profile)?; +let final_rows: Vec> = + expand_source(source, profile, provider)?; + +for row in final_rows { + let circuit_flags = row.circuit_flags(); + let instruction_flags = row.instruction_flags(); + let lookup_table = row.lookup_table(); // Option: some final rows still use no table. +} +``` + +Concretely, lookup/flag traits should be shaped so source payloads do not satisfy +their bounds: + +```rust +impl Flags for Add { /* final-row flags */ } + +impl InstructionLookupTable + for Add +{ + fn lookup_table(&self) -> Option> { + Some(LookupTableKind::RangeCheck(Default::default())) + } +} +``` + +The bound matters: `Add` is a decoded source +instruction payload and should not compile as a lookup/flag subject. +`Add` is a final proof row payload and may compile. +Source-only marker structs such as `Ebreak`, `Ecall`, `Inline`, AMOs, CSRs, and +narrow loads/stores should not get lookup/flag impls just because they share the +marker type system; they become proof metadata only after expansion emits final +Jolt rows. + +When proof code needs both dynamic cycle values and final-row metadata, it should +use a small adapter instead of repeatedly converting ad hoc: + +```rust +/// Proof-facing view of a tracer cycle whose instruction is backed by a final +/// Jolt bytecode row. +/// +/// A tracer `Cycle` still owns dynamic witness data such as register reads, RAM +/// accesses, lookup operands, and lookup outputs. Static proof metadata such as +/// circuit flags, instruction flags, and lookup-table routing must come from the +/// final `JoltInstruction` stored here. Constructing +/// this adapter is the phase-boundary check: decoded source-only instructions +/// are rejected before proving code can ask proof-metadata questions about them. +pub struct JoltTraceCycle<'a> { + cycle: &'a Cycle, + instruction: JoltInstruction, +} + +impl<'a> JoltTraceCycle<'a> { + #[inline(always)] + pub fn try_new(cycle: &'a Cycle) -> Result>; + + #[inline(always)] + pub fn cycle(&self) -> &'a Cycle; + + #[inline(always)] + pub fn instruction(&self) -> &JoltInstruction; +} +``` + +The intended usage pattern is: + +```rust +let jolt_cycle = JoltTraceCycle::try_new(cycle)?; + +// Static proof metadata comes from the final Jolt row. +let table = jolt_cycle.lookup_table(); +let circuit_flags = jolt_cycle.circuit_flags(); +let instruction_flags = jolt_cycle.instruction_flags(); + +// Dynamic witness values still come from the concrete executed cycle. +let lookup_index = jolt_cycle.to_lookup_index(); +let lookup_output = jolt_cycle.to_lookup_output(); +let ram_access = jolt_cycle.cycle().ram_access(); +``` + +`JoltTraceCycle` is an interim boundary, not the ideal permanent trace shape. +It is constructed on demand in prover/witness hot paths that still receive raw +tracer `Cycle` values. The wrapper methods and forwarding trait impls should be +`#[inline(always)]`, because the type is intended to make the phase boundary +visible at compile time without adding per-cycle abstraction overhead. + +The cleaner endpoint is a single normalization pass immediately after tracing: + +```rust +pub struct ProofTraceRow { + pub instruction: JoltInstruction, + pub registers: RegisterState, + pub ram_access: RamAccess, +} +``` + +`crates/jolt-program` already has the equivalent `TraceRow` / `OwnedTrace` +shape. A future cleanup should make `jolt-core` prover inputs consume that +normalized proof trace instead of `Arc>`. In that end state, tracer +execution still owns concrete `Cycle` construction and CPU/RAM side effects, +but proving code never sees source-only cycle variants and no longer needs to +adapt each cycle repeatedly with `JoltTraceCycle::try_new`. + +Expansion bodies are also not moved into `jolt-riscv`, because `jolt-riscv` +must remain below `jolt-program` in the dependency graph. `jolt-riscv` can expose +`SourceInstruction::ADDW(AddW)`. `jolt-program` says +whether `ADDW` is legal in the active profile, how it lowers, and can generate +the dispatcher that routes the `ADDW` variant through the `AddW` marker to the +human-written `expand_addw` body. + +### Rows And Profiles + +The current code generates both instruction-kind enums from one broad macro +list. That makes the type split mostly nominal: every source opcode also exists +as a final Jolt opcode unless a later legality check rejects it. This PR should +replace that with two universal row enums plus crate-local metadata whose +first-order facts are named deliberately and consistently. + +Use these names unless implementation uncovers a concrete conflict: + +```rust +pub enum SourceExtension { + /// RV64I instruction semantics and 64-bit base encodings. + Rv64I, + /// RV64M multiply/divide source instructions. + Rv64M, + /// RV64A atomic source instructions. + Rv64A, + /// RVC compressed encodings. These decode into ordinary source rows. + Rv64C, + /// CSR source instructions currently accepted by decode. + Zicsr, + /// Privileged source instructions currently accepted by decode, such as MRET. + RvPrivileged, + /// Jolt custom guest opcodes decoded from the custom instruction space. + JoltCustom, + /// Registered inline source opcodes keyed by opcode/funct3/funct7. + JoltInline, +} + +pub enum JoltTargetExtension { + /// Base integer arithmetic, comparisons, and immediate operations. + IntegerCore, + /// Integer multiplication operations retained as final Jolt instructions. + IntegerMultiply, + /// Branches, jumps, and other control-flow operations. + ControlFlow, + /// 64-bit load/store operations. Narrow memory operations lower into these. + LoadStore64, + /// Advice-producing and advice-consuming virtual operations. + Advice, + /// Host I/O virtual operations. + HostIO, + /// Virtual assertions used by expansion and inlines. + VirtualAssertions, + /// Virtual arithmetic helpers used by division, word ops, and carries. + VirtualArithmetic, + /// Virtual shift and rotate helpers used by source lowering. + VirtualShifts, + /// Bit-manipulation helpers used mainly by custom ops and crypto inlines. + BitManipulation, +} + +pub enum InlineExtension { + Sha2, + Keccak256, + Blake2, + Blake3, + BigInt256, + Secp256k1, + Grumpkin, + P256, +} +``` + +The durable requirement is that source decode support, target bytecode legality, +inline registration support, lookup-table support, side-effect metadata, circuit +flags, and instruction flags are separate compile-time facts with separate +owners. The PR should not add a separate `InstructionPhase` enum unless the +implementation needs it internally; phase is already determined by whether a row +identity appears in `SourceInstruction`, `JoltInstruction`, or both. +Closed universal enums are acceptable and desired, but the source of truth for +profile legality must be explicit profile metadata plus crate-local decorations, +not a monolithic enum and not `!is_source_only`. + +The current default source profile should include: + +- `SourceExtension::Rv64I`: base integer, 64-bit loads/stores, branches, jumps, + `FENCE`, `ECALL`, and `EBREAK`; +- `SourceExtension::Rv64M`: multiply/divide/remainder source opcodes, + including W-suffix forms; +- `SourceExtension::Rv64A`: LR/SC and AMO source opcodes; +- `SourceExtension::Rv64C`: compressed encodings, which uncompress into + ordinary source instructions rather than adding separate source kinds; +- `SourceExtension::Zicsr`: `CSRRW` and `CSRRS`; +- `SourceExtension::RvPrivileged`: `MRET`; +- `SourceExtension::JoltCustom`: custom decoded source rows such as + `VirtualRev8W`, `VirtualAssertEQ`, `VirtualHostIO`, `AdviceLB/LH/LW/LD`, and + `VirtualAdviceLen`; +- `SourceExtension::JoltInline`: source rows whose dispatch payload is keyed by + `(opcode, funct3, funct7)`. + +The computed target legality closure for the current default profile should +include these `JoltTargetExtension` families: + +- `JoltTargetExtension::IntegerCore`: final instructions such as `ADD`, `ADDI`, + `SUB`, `LUI`, `AUIPC`, `AND`, `ANDI`, `OR`, `ORI`, `XOR`, `XORI`, `SLT`, + `SLTI`, `SLTU`, and `SLTIU`; +- `JoltTargetExtension::IntegerMultiply`: final multiply instructions such as + `MUL` and `MULHU`; source-only multiply/divide rows lower before final + bytecode; +- `JoltTargetExtension::ControlFlow`: final branch/jump instructions such as + `BEQ`, `BNE`, `BLT`, `BGE`, `BLTU`, `BGEU`, `JAL`, `JALR`, and `FENCE`; +- `JoltTargetExtension::LoadStore64`: final `LD` and `SD` instructions; +- `JoltTargetExtension::Advice`: `VirtualAdvice`, `VirtualAdviceLen`, and + `VirtualAdviceLoad`; +- `JoltTargetExtension::HostIO`: `VirtualHostIO`; +- `JoltTargetExtension::VirtualAssertions`: `VirtualAssertEQ`, + `VirtualAssertLTE`, `VirtualAssertValidDiv0`, + `VirtualAssertValidUnsignedRemainder`, `VirtualAssertMulUNoOverflow`, + `VirtualAssertWordAlignment`, and `VirtualAssertHalfwordAlignment`; +- `JoltTargetExtension::VirtualArithmetic`: `VirtualMULI`, + `VirtualMovsign`, `VirtualPow2`, `VirtualPow2I`, `VirtualPow2W`, + `VirtualPow2IW`, `VirtualChangeDivisor`, `VirtualChangeDivisorW`, + `VirtualSignExtendWord`, and `VirtualZeroExtendWord`; +- `JoltTargetExtension::VirtualShifts`: `VirtualSRL`, `VirtualSRLI`, + `VirtualSRA`, `VirtualSRAI`, `VirtualShiftRightBitmask`, + `VirtualShiftRightBitmaskI`, `VirtualROTRI`, and `VirtualROTRIW`; +- `JoltTargetExtension::BitManipulation`: `ANDN`, `VirtualRev8W`, and the + `VirtualXORROT*` / `VirtualXORROTW*` rows used by crypto inlines. + +Source sentinel rows `NoOp` and `Unimpl` are always available and should not be +modeled as extension-gated capabilities. Final bytecode treats `NoOp` as the +only target-legal sentinel row; `Unimpl` is decode/source-side only and must be +rejected before preprocessing. + +The inline profile metadata should use the registered inline package names as +first-class entries, not treat every inline as one anonymous extension. This +does not mean `SourceInstruction` gets one variant per inline operation. +Source decoding uses one `SourceInstruction::Inline(Inline)` +row plus a `SourceInlineKey` payload; the `InlineExtension` profile gates which +registered `(opcode, funct3, funct7)` keys are accepted and which provider is +allowed to expand them. Current entries are: + +- `InlineExtension::Sha2`: `SHA256_INLINE`, `SHA256_INIT_INLINE`; +- `InlineExtension::Keccak256`: `KECCAK256_INLINE`; +- `InlineExtension::Blake2`: `BLAKE2_INLINE`; +- `InlineExtension::Blake3`: `BLAKE3_INLINE`, `BLAKE3_KEYED64_INLINE`; +- `InlineExtension::BigInt256`: `BIGINT256_MUL_INLINE`; +- `InlineExtension::Secp256k1`: the `SECP256K1_*` inline family; +- `InlineExtension::Grumpkin`: `GRUMPKIN_DIVQ_ADV`, `GRUMPKIN_DIVR_ADV`; +- `InlineExtension::P256`: the `P256_*` inline family. + +`Inline` itself is source-only and must remain illegal in finalized bytecode. +Registered inline providers may emit ordinary target rows plus virtual helper +rows, but provider output must be validated against the computed target legality +closure before preprocessing. + +The near-term profile shape is source-driven: + +```rust +pub struct JoltInstructionProfile { + pub source_extensions: &'static [SourceExtension], + pub inline_extensions: &'static [InlineExtension], +} + +pub const RV64IMAC_JOLT: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: &[ + SourceExtension::Rv64I, + SourceExtension::Rv64M, + SourceExtension::Rv64A, + SourceExtension::Rv64C, + SourceExtension::Zicsr, + SourceExtension::RvPrivileged, + SourceExtension::JoltCustom, + SourceExtension::JoltInline, + ], + inline_extensions: &[], +}; + +pub const RV64IMAC_JOLT_ALL_INLINES: JoltInstructionProfile = JoltInstructionProfile { + source_extensions: RV64IMAC_JOLT.source_extensions, + inline_extensions: &[ + InlineExtension::Sha2, + InlineExtension::Keccak256, + InlineExtension::Blake2, + InlineExtension::Blake3, + InlineExtension::BigInt256, + InlineExtension::Secp256k1, + InlineExtension::Grumpkin, + InlineExtension::P256, + ], +}; +``` + +`JoltTargetExtension` remains useful profile metadata: it groups final rows into +semantic families such as integer core, host I/O, virtual arithmetic, and crypto +helper rows. It should not be a second hand-selected profile axis in this PR, +and it should not encode lookup-table or proving-system policy. Given a selected +`JoltInstructionProfile`, `jolt-program` derives the final target legality set +from: + +- direct final rows emitted by source instructions enabled by + `source_extensions`; +- recursive helper rows reachable from those expansions; +- final rows emitted by enabled `inline_extensions`; +- the target-legal `NoOp` sentinel row. + +This makes `RV64IM_JOLT` naturally produce a smaller legal final closure than +`RV64IMAC_JOLT` when atomics/compressed-only source paths are disabled, without +requiring callers to maintain a parallel target list. The Rust enum remains the +same universal shipped final-row enum; the selected profile changes which rows +decode and which final rows pass preprocessing legality. Cross-profile proof +artifact compatibility is not a goal of this PR: circuit/preprocessing keys are +tied to the selected compile-time profile, compact tags, dense-index maps, and +legality sets. + +Reserve these shipped preset names: + +- `RV64IM_JOLT`: base RV64I+M profile without atomics or compressed encodings; +- `RV64IMAC_JOLT`: current base RV64IMAC source profile with the inline source + mechanism available; +- `RV64IMAC_JOLT_ALL_INLINES`: current workspace-wide profile with all listed + `InlineExtension` packages enabled. + +Profile selection should be explicit and tied to shipped profile constants, not +runtime plugin loading. This PR threads a `JoltInstructionProfile` value through +decode, expansion, sequence stamping, and bytecode preprocessing boundaries so +those phases do not silently read `RV64IMAC_JOLT` as hidden global policy. +Top-level callers that want the current default behavior pass `RV64IMAC_JOLT`; +callers that expand registered inline packages pass a profile whose +`inline_extensions` contain the relevant package entries, such as +`RV64IMAC_JOLT_ALL_INLINES`. The selected profile affects legality tables and +closure checks, not the Rust enum shape. + +Inline inventory registration remains link-time, but availability is +profile-checked. If a workspace links `jolt-inlines-sha2` while the selected +profile does not include `InlineExtension::Sha2`, decode/expansion must reject +the registered key before it can enter finalized bytecode. The PR does not need +to make such a crate fail to compile. + +For this PR, the implementation must include the universal source/final row +enums, crate-local metadata, and shipped current source profiles above. It does +not need arbitrary third-party profile composition, but decode legality, +final-bytecode legality, and inline availability should all flow from +profile/legalization facts and crate-owned decorations rather than from a broad +shared instruction-kind list. + +Lookup support should not introduce `LookupInstructionKind` or preserve a +parallel `LookupInstruction` enum. Keep the existing boundary in spirit: +`JoltInstruction` is the typed lookup/proof-facing view of supported final +rows, and `LookupTableKind` identifies concrete lookup tables in +`jolt-lookup-tables`. +A future `LookupTableProfile` may name the compile-time table set for a +selected proof profile, but it should be table-oriented, not another +instruction-kind enum. + +Expansion should expose: + +```rust +pub fn expand_instruction( + instruction: &SourceInstruction, + allocator: &mut ExpansionAllocator, +) -> Result>, ExpansionError>; + +pub trait InlineExpansionProvider { + fn expand_inline( + &mut self, + instruction: &SourceInstruction, + allocator: &mut ExpansionAllocator, + ) -> Result>, ExpansionError>; +} +``` + +Inside `jolt-program::expand`, the builder should keep two operations: + +- `emit_*`: append target-legal `JoltInstruction` rows directly. +- `expand_*`: recursively lower helper `SourceInstruction` rows. + +This models the current semantics more accurately than pretending every +recursive helper is pure RISC-V or every emitted row is already final. + +The source-only expansion dispatcher should be generated from `jolt-program` +metadata or from a small `jolt-program` macro keyed by the instruction marker +structs, not maintained as an unrelated final-kind match list. The +generated shape should read conceptually like: + +```rust +match instruction { + SourceInstruction::ADD(add) => emit_direct(add), + SourceInstruction::ADDW(addw) => addw.expand_source(allocator), + SourceInstruction::LW(lw) => lw.expand_source(allocator), + SourceInstruction::Inline(inline) => inline_provider.expand_inline(inline, allocator), + _ => Err(ExpansionError::UnsupportedInstruction), +} +``` + +That does not require moving `expand_addw` into `jolt-riscv`; it requires the +dispatch edge to be generated from `jolt-program`'s local `AddW` expansion +metadata instead of copied into a separate list that can drift from the source +row enum. + +Tracer should become phase-aware: + +- decode-facing APIs consume or produce `SourceInstruction`; +- expanded trace execution consumes `JoltInstruction` or concrete tracer + `Instruction` built from `JoltInstruction`; +- execution semantics remain implemented on tracer concrete instruction types, + not in `jolt-riscv` row enum macros; +- `RISCVInstruction` conversions should not require + `From + Into + From + + Into` on the same trait; +- inline source rows should never be reconstructed from a final row. + +Registered inlines can still be implemented in `tracer` for this PR, because +they need inventory registration and advice-generation hooks. The important +cleanup is the boundary: `TracerInlineExpansionProvider` should no longer parse +a fake final `Inline` row, and `jolt-inlines-sdk::InlineOp` should either return +final `JoltInstruction` rows directly or use an assembler whose output is final +`JoltInstruction` rows. If moving every inline builder off tracer's +`Instruction` enum proves too large, keep that as an internal tracer adapter +only and ensure the public provider contract is already final-row typed. + +### Alternatives Considered + +Keeping `SourceInstructionKind` and `JoltInstructionKind` as mirror bare tag +enums is the smallest change, but it keeps the reviewer concern intact: names +improve, but the compiler cannot enforce phase boundaries and enum dispatch +still needs a separate mapping back to marker structs. + +Generating profile-specific source/final enum shapes would maximize type-level +profile safety: if `RV64IM_JOLT` excludes atomics, the atomic variants could +literally be absent. That is not the preferred shape for this PR. It makes +downstream exhaustive matches profile-dependent and is less aligned with MLIR, +where operations exist in a stable universe and a conversion target decides +which operations are legal at a particular stage. Jolt should instead keep +stable universal shipped source/final enums and make profile legality explicit. + +Putting every fact into an expanded `jolt_instruction!` declaration would remove +some match duplication, but it would also make `jolt-riscv` aware of details it +should not own: decode quirks, expansion bodies, tracer state mutation, +lookup-table routing, circuit flags, and instruction flags. That is the wrong +dependency direction. This PR should instead use universal marker-carrying +source/final row enums plus crate-local decorations. + +Adding a separate `InstructionCatalogEntry` table beside the existing marker +structs and then filling it with every fact has the same problem in a different +shape. A separate table is not needed for the enum variant -> marker struct +mapping if the variants carry marker structs directly. A small macro/list may +still be used to avoid hand-writing both enums and stable serialization matches, +but it should generate `ADD(Add)`-style variants rather than a separate +bare-kind enum plus a parallel mapping table. + +Making the enums fully dynamic, MLIR-style operation IDs would maximize +extensibility but is the wrong first move for Jolt. Instruction identity feeds +serialization, bytecode preprocessing, static flag dispatch, lookup routing, +and proof circuits. Closed universal enums are a better fit: the operation +universe can be MLIR-like and stable while the compiled prover remains +specialized by explicit profile legality sets. + +Using Cargo features alone as the profile system would also be too coarse. +Features can select profiles or extension groups, but they should not be the +only place where instruction identity or legality lives. Decode membership, +target legality, lookup table routing, side effects, and lowering behavior +should be queryable through their owning crates, keyed by shared instruction +identities. + +Making only `jolt-program` phase-aware is also insufficient. Tracer currently +converts concrete `Instruction` through the same normalized row and also handles +some expansion-ish behavior during tracing. If tracer remains untyped by phase, +the next cleanup will immediately need another cross-crate churn. + +Removing tracer's concrete `Instruction` enum entirely is too broad. The enum +still owns execution behavior, cycle construction, and tests. This PR should +separate source/final row APIs while preserving tracer's concrete execution +model where it is useful. + +Moving concrete execution semantics into `jolt-riscv` would also cross the wrong +boundary. `jolt-riscv` can describe stable identity, source/final membership, +and shared row shape. It should not know how to mutate a tracer `Cpu`, update +RAM/device state, generate advice, or build concrete tracer cycles. + +Moving lookup/proving metadata into `jolt-riscv` has the same ownership smell. +`jolt-riscv` should be blissfully unaware of the proving system's lookup-table +flags, circuit flags, and instruction flags. Those facts should be declared in +the lookup/proving owner and keyed by `JoltInstruction` variants or marker +structs. + +Moving all registered inline implementations into `jolt-program` would make the +provider-free crate depend on inventory/advice/tracer concerns. That reverses +the dependency boundary from #1518. A provider contract with final-row output is +the smaller clean architecture. + +## Documentation + +Update `specs/compiler-native-bytecode-expansion.md` after implementation to +mark the deferred source/final split complete and link to this spec. No Jolt +book update is required unless public SDK APIs expose the new names directly. + +## Execution + +Current implementation status: + +- The branch already removed `NormalizedInstruction` in favor of phase-specific + source/final types. +- `SourceInstruction` and `JoltInstruction` exist and their variants + carry marker structs such as `Add(Add)`. +- Decode, expansion, bytecode preprocessing, inline expansion, and tracer + conversion are already mostly phase-aware. +- `SourceInstructionKind` and `JoltInstructionKind` are canonical aliases of + `SourceInstruction<()>` and `JoltInstruction<()>`, not separate mirror enums. +- `SourceInstructionRow` is currently the source row payload name, and + `JoltInstructionRow` is currently the final row payload name. The end-state + should make clear that those are payloads, not separate instruction + identities. Renaming them to `SourceInstructionPayload` and + `JoltInstructionPayload` is preferred if it does not create disproportionate + churn. +- `JoltInstructionRow` currently carries `instruction_kind`; in the end-state, + the `JoltInstruction` enum variant is the final identity, so the final + payload must not carry a duplicate kind field. +- Static source extension, target extension, canonical-name, side-effect, and + final-tag facts now live behind marker-struct metadata traits. `profile.rs` + consumes enum dispatchors instead of owning a second hand-written instruction + catalog. Metadata macros require explicit source-extension, target-extension, + and side-effect classifications for every marker they see; adding a new marker + without those classifications is a compile-time error rather than a silent + default. +- Source serde now uses canonical names, final serde now uses compact final + tags, and the tests cover both serde and canonical serialization round-trips. +- Source-to-tracer conversion no longer constructs a fake final + `JoltInstructionRow` for source-only instructions. It now rebuilds concrete + tracer instructions from `SourceInstructionRow`, and a boundary test checks + that a source-only row such as `ADDW` does not acquire fabricated final proof + metadata. +- Tracer conversion from final rows is generated from the final + `JoltInstruction` universe, not the broader source universe. The registered + inline assembler also builds temporary source rows before expansion, instead + of using `JoltInstructionRow` as a generic row-shaped constructor. +- `jolt-riscv` still owns `Flags` bitfield types and generated final-row + `Flags` impls. Lookup/flag queries are now final-row-only, but the long-term + ownership question remains: ideally the RV64 catalog crate should not know + Jolt R1CS circuit flags or witness-routing instruction flags. +- `jolt-core` prover internals still pass `Arc>` through many + witness-generation and sumcheck paths. `JoltTraceCycle` makes final-row proof + metadata explicit at each use site, but a cleaner follow-up is to normalize + once from tracer `Cycle` into `TraceRow` / `ProofTraceRow`. +- R1CS input materialization now constructs the next-row `JoltTraceCycle` once + for each current row and reuses it for next-PC, next-unexpanded-PC, and + next-flag queries. This avoids repeated proof-metadata adaptation in that + local path without introducing a cache or alternate trace representation. + +1. [x] Remove `NormalizedInstruction` or fully rename it into phase-specific + types, with no compatibility alias. +2. [x] Add universal `SourceInstruction` and `JoltInstruction` enums whose + variants carry marker structs, following the former `LookupInstruction` + pattern. +3. [ ] Rename or clarify the payload structs so the distinction is explicit: + `SourceInstruction` and + `JoltInstruction`. If existing names are kept, + document that they are payload structs and not instruction identities. +4. [x] Remove separate `SourceInstructionKind` and `JoltInstructionKind` enum + definitions. The source kind view is `SourceInstruction<()>`; the final kind + view is `JoltInstruction<()>`. A retained `*Kind` name must be a pure + canonical alias to the zero-payload enum instantiation, not a compatibility + shim over a second representation. +5. [ ] Remove duplicate kind fields from row payloads. The enum variant is the + instruction identity, both for source rows and final rows. +6. [x] Add marker-struct metadata traits: + `SourceInstructionMeta` for canonical source name, source extension, and + side-effect metadata; `JoltInstructionMeta` for canonical final name, stable + final `u16` tag, and target extension. +7. [x] Move the large `profile.rs` `source_extension`, `jolt_target_extension`, + `has_side_effects`, canonical-name, and final-tag matches into marker-struct + metadata impls plus enum dispatchors over `SourceInstruction<()>` and + `JoltInstruction<()>`. +8. [x] Update source serialization to use `SourceInstruction<()>::canonical_name` + and final serialization to use `JoltInstruction<()>::tag` / + `JoltInstruction::from_tag`, without relying on enum declaration order. +9. [x] Update `JoltInstructionProfile` APIs, dense-index maps, fingerprinting, + and default-profile legality tests to accept `SourceInstruction<()>` and + `JoltInstruction<()>`. +10. [x] Update `jolt-program` decode helpers to return `SourceInstruction<()>` + while parsing bits, then attach `SourceInstructionPayload` once operands, + address, inline key, and compression metadata are known. +11. [x] Update expansion templates and builders so helper recursion is keyed by + `SourceInstruction<()>` and direct final-row emission is keyed by + `JoltInstruction<()>`. +12. [x] Update `ProgramError`, `ExpansionError`, preprocessing errors, and tests + to report zero-payload instruction values rather than separate kind enums. +13. [ ] Update source-to-final direct conversion. Native source rows that do not + need lowering should convert by matching the `SourceInstruction` variant + and building the corresponding `JoltInstruction` + variant. Source-only rows return the original `SourceInstruction<()>`. +14. [ ] Cut bytecode preprocessing, `JoltProgram`, execution rows, tracer + conversions, and proof imports over to final `JoltInstruction` rows with + no duplicate final-kind field. +15. [x] Remove `Inline` from the final `JoltInstruction` universe and move + inline metadata to source-only types. +16. [x] Thread explicit `JoltInstructionProfile` values through decode, + expansion, sequence stamping, and bytecode preprocessing. +17. [x] Update `TracerInlineExpansionProvider` and `jolt-inlines-sdk` boundaries + so registered inline expansion accepts source inline rows and returns + validated final rows. +18. [ ] Move flag ownership out of `jolt-riscv` or narrow it to pure final-row + helper types owned by the proving side. If keeping bitfield definitions in a + shared crate is the smallest durable step, per-instruction metadata impls + must still be final-row-only and not callable from source rows. +19. [x] Add `JoltTraceCycle<'a>` and use it in proof code that needs both dynamic + tracer cycle values and static final-row metadata. +20. [x] Remove legacy flag and lookup-table APIs from decoded/source instruction + subjects. +21. [x] Constrain `jolt-lookup-tables::InstructionLookupTable` impls to final-row + payloads and remove lookup-table impls for source-only markers that cannot + appear in `JoltInstruction`. +22. [ ] Update Jolt-core R1CS, instruction lookup, bytecode read-RAF, and tests + to use final `JoltInstruction` rows after the + duplicate-kind removal. +23. [x] Add compile-time or API-boundary tests for the proof-metadata boundary. +24. [x] Add default-profile legality, canonical-name, compact-tag, dense-index, + and expansion-equivalence tests. +25. [ ] Replace raw `Arc>` prover inputs with a normalized proof trace + row shape equivalent to `jolt_program::execution::TraceRow`, after checking + all witness-generation paths that still need tracer-specific data. This + should subsume most `JoltTraceCycle::try_new` call sites and make + source-only cycles unrepresentable in prover internals. + +## References + +- [PR #1518](https://github.com/a16z/jolt/pull/1518) +- [MLIR Language Reference](https://mlir.llvm.org/docs/LangRef/) +- [MLIR Operation Definition Specification](https://mlir.llvm.org/docs/DefiningDialects/Operations/) +- [MLIR Bytecode Format](https://mlir.llvm.org/docs/BytecodeFormat/) +- [`specs/compiler-native-bytecode-expansion.md`](compiler-native-bytecode-expansion.md) +- [`specs/bytecode-expansion-crate.md`](bytecode-expansion-crate.md) +- [`specs/inline-expansion-grammar.md`](inline-expansion-grammar.md) +- Archived extraction audit: + `/Users/quang.dao/Documents/Notes/jolt-pr1518-extraction-audit-hax-aeneas.md` diff --git a/src/main.rs b/src/main.rs index ba7a081129..e18dcb5d98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -222,6 +222,11 @@ fn build_command(args: JoltBuildArgs) -> Result<()> { "--cfg=getrandom_backend=\"custom\"", ]; + if args.base.mode == StdMode::Std { + // Rust 1.95 gates generated target specs behind this flag; zeroos-build sets RUSTC_BOOTSTRAP. + jolt_rustflags.push("-Zunstable-options"); + } + // Also set medany for C code (cc crate reads CFLAGS_{target}). let cflags_target = match args.base.mode { StdMode::Std => "riscv64imac_zero_linux_musl", diff --git a/stack/branches.tsv b/stack/branches.tsv new file mode 100644 index 0000000000..964cb259cb --- /dev/null +++ b/stack/branches.tsv @@ -0,0 +1,18 @@ +# order branch title pathspecs +00 stack/00-stack-automation chore: add refactor audit stack automation .github/workflows/refactor-audit-stack.yml STACK.md stack +01 stack/01-foundation-helpers refactor: add modular foundation helpers crates/jolt-field crates/jolt-poly crates/jolt-riscv crates/jolt-transcript +02 stack/02-lookup-table-core-abi refactor: align lookup table ABI with core crates/jolt-lookup-tables +03 stack/03-public-io-preprocessing refactor: add public I/O preprocessing helpers common crates/jolt-program/src/preprocess +04 stack/04-commitment-opening-infra refactor: add commitment and opening infrastructure crates/jolt-crypto crates/jolt-openings crates/jolt-dory crates/jolt-hyperkzg +05 stack/05-jolt-claims-crate feat: add jolt-claims crate crates/jolt-claims +06 stack/06-jolt-r1cs-builder-lowering refactor: add R1CS builder and lowering support crates/jolt-r1cs +07 stack/07-committed-sumcheck-r1cs feat: add committed sumcheck and R1CS support crates/jolt-sumcheck +08 stack/08-jolt-blindfold-crate feat: add jolt-blindfold crate crates/jolt-blindfold +08a stack/08a-jolt-core-blindfold-hardening fix: harden jolt-core BlindFold construction jolt-core/src/subprotocols/blindfold jolt-core/src/zkvm/bytecode/read_raf_checking.rs jolt-core/src/zkvm/prover.rs jolt-core/src/zkvm/ram/read_write_checking.rs jolt-core/src/zkvm/verifier.rs +09 stack/09-jolt-verifier-crate feat: add jolt-verifier crate .config/nextest.toml .semgrep/jolt-verifier-boundaries.yml crates/jolt-verifier specs/jolt-verifier-model-crate.md examples/advice-consumer/guest +10 stack/10-jolt-prover-spec docs: add prover model crate spec specs/jolt-prover-model-crate.md +11 stack/11-extended-jolt-field-inline-wrapper-spec docs: add extended Jolt field inline wrapper spec recursion_references.md specs/extended-jolt-field-inline-wrapper.md +12 stack/12-selected-verifier-integration-spec docs: add selected verifier integration spec specs/selected-verifier-integration.md +13 stack/13-field-inline-protocol-spec docs: add field inline protocol spec specs/field-inline-protocol.md +14 stack/14-dory-assist-protocol-spec docs: add Dory assist protocol spec specs/dory-assist-protocol.md +15 stack/15-wrapper-protocol-spec docs: add wrapper protocol spec specs/wrapper-protocol.md diff --git a/stack/open-prs.sh b/stack/open-prs.sh new file mode 100755 index 0000000000..8b0ae5c7a0 --- /dev/null +++ b/stack/open-prs.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: stack/open-prs.sh [--apply] [--base BRANCH] [--source REF] [--start-at NN] + +Creates or updates draft PRs for branches in stack/branches.tsv. Default mode is +a dry run. + +Options: + --apply create/edit PRs; without this, print planned gh commands + --base NAME base branch for the first PR (default: main) + --source REF source branch named in PR bodies (default: refactor/audit-prep) + --start-at NN first stack item to create/update (default: 07, or STACK_START_AT) + -h, --help show this help +EOF +} + +apply=0 +first_base="main" +source_ref="refactor/audit-prep" +start_at="${STACK_START_AT:-07}" + +while (($#)); do + case "$1" in + --apply) + apply=1 + ;; + --base) + first_base="${2:?--base requires a branch name}" + shift + ;; + --source) + source_ref="${2:?--source requires a ref}" + shift + ;; + --start-at) + start_at="${2:?--start-at requires an order number}" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +plan_file="stack/branches.tsv" +base_branch="$first_base" +start_seen=0 + +while IFS=$'\t' read -r order branch title pathspecs; do + [[ -z "${order:-}" || "$order" == \#* ]] && continue + + if [[ -n "$start_at" && "$start_seen" -eq 0 ]]; then + if [[ "$order" != "$start_at" ]]; then + continue + fi + start_seen=1 + fi + + body_file="$(mktemp)" + cat > "$body_file" < $base_branch" + if ((apply)); then + gh pr create --draft --base "$base_branch" --head "$branch" --title "$title" --body-file "$body_file" + rm -f "$body_file" + else + echo " gh pr create --draft --base '$base_branch' --head '$branch' --title '$title' --body-file '$body_file'" + fi + fi + + base_branch="$branch" +done < "$plan_file" + +if [[ -n "$start_at" && "$start_seen" -eq 0 ]]; then + echo "no stack item with start order $start_at" >&2 + exit 1 +fi + +if ((!apply)); then + echo + echo "dry run only; pass --apply to create/update PRs" +fi diff --git a/stack/update-stack.sh b/stack/update-stack.sh new file mode 100755 index 0000000000..2d4428c07c --- /dev/null +++ b/stack/update-stack.sh @@ -0,0 +1,395 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: stack/update-stack.sh [--apply] [--rebuild] [--commit] [--push] [--cargo-metadata] [--check-coverage] [--from REF] [--base REF] [--start-at NN] [--only NN] + +Materializes the draft PR stack described by stack/branches.tsv. + +Default mode is a dry run. With --apply, the script creates or updates stack +branches by restoring branch-owned paths from the source ref. It applies +incremental root manifest changes for new workspace crates. + +Options: + --apply perform git operations; without this, print the planned actions + --rebuild reset existing stack branches to their configured base first + --commit commit restored changes using the title from stack/branches.tsv + --push push updated stack branches to origin with --force-with-lease + --cargo-metadata + run cargo metadata after manifest changes to refresh Cargo.lock + --check-coverage + fail if a source-ref diff path is not assigned to a stack slice + --from REF source ref to slice from (default: refactor/audit-prep) + --base REF base ref for the first stack branch (default: origin/main) + --start-at NN first stack item to sync (default: 07, or STACK_START_AT) + --only NN update only one stack item, e.g. 08 + -h, --help show this help + +Examples: + stack/update-stack.sh + stack/update-stack.sh --apply --only 08 + stack/update-stack.sh --apply --rebuild --commit --push --cargo-metadata --check-coverage --from origin/refactor/audit-prep --start-at 07 +EOF +} + +apply=0 +rebuild=0 +commit_changes=0 +push_changes=0 +cargo_metadata=0 +check_coverage=0 +source_ref="refactor/audit-prep" +base_ref="origin/main" +start_at="${STACK_START_AT:-07}" +only="" + +while (($#)); do + case "$1" in + --apply) + apply=1 + ;; + --rebuild) + rebuild=1 + ;; + --commit) + commit_changes=1 + ;; + --push) + push_changes=1 + ;; + --cargo-metadata) + cargo_metadata=1 + ;; + --check-final|--check-coverage) + check_coverage=1 + ;; + --from) + source_ref="${2:?--from requires a ref}" + shift + ;; + --base) + base_ref="${2:?--base requires a ref}" + shift + ;; + --start-at) + start_at="${2:?--start-at requires an order number}" + shift + ;; + --only) + only="${2:?--only requires an order number}" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +plan_file="stack/branches.tsv" +if [[ ! -f "$plan_file" ]]; then + echo "missing $plan_file" >&2 + exit 1 +fi + +git rev-parse --verify "$source_ref^{commit}" >/dev/null +git rev-parse --verify "$base_ref^{commit}" >/dev/null + +if ((apply)); then + if ! git diff --quiet || ! git diff --cached --quiet; then + echo "working tree is dirty; commit or stash before applying stack updates" >&2 + exit 1 + fi +fi + +ensure_after() { + local file="$1" + local anchor="$2" + local line="$3" + + if grep -Fxq "$line" "$file"; then + return + fi + if ! grep -Fxq "$anchor" "$file"; then + echo "anchor not found in $file: $anchor" >&2 + exit 1 + fi + + local tmp + tmp="$(mktemp)" + awk -v anchor="$anchor" -v line="$line" ' + { print } + $0 == anchor { print line } + ' "$file" > "$tmp" + mv "$tmp" "$file" +} + +pathspec_matches_ref() { + local ref="$1" + local pathspec="$2" + + [[ -n "$(git ls-tree -r --name-only "$ref" -- "$pathspec")" ]] +} + +pathspec_matches_index() { + local pathspec="$1" + + [[ -n "$(git ls-files -- "$pathspec")" ]] +} + +restore_owned_paths() { + local ref="$1" + shift + + local existing_pathspecs=() + local missing_pathspecs=() + local existing_pathspec_count=0 + local missing_pathspec_count=0 + local pathspec + + for pathspec in "$@"; do + if pathspec_matches_ref "$ref" "$pathspec" || pathspec_matches_index "$pathspec"; then + existing_pathspecs+=("$pathspec") + existing_pathspec_count=$((existing_pathspec_count + 1)) + else + missing_pathspecs+=("$pathspec") + missing_pathspec_count=$((missing_pathspec_count + 1)) + fi + done + + if ((existing_pathspec_count)); then + git restore --source "$ref" -- "${existing_pathspecs[@]}" + fi + + if ((missing_pathspec_count)); then + for pathspec in "${missing_pathspecs[@]}"; do + echo " skipped missing optional path: $pathspec" + done + fi +} + +effective_owned_paths() { + local order="$1" + local pathspecs="$2" + local output="$3" + local current_paths=() + read -r -a current_paths <<< "$pathspecs" + + if ((${#current_paths[@]})); then + git diff --name-only "$base_ref...$source_ref" -- "${current_paths[@]}" | sort -u > "$output" + else + : > "$output" + fi + + local after_current=0 + local later_order + local later_branch + local later_title + local later_pathspecs + local later_paths=() + local later_file + local remaining_file + + while IFS=$'\t' read -r later_order later_branch later_title later_pathspecs; do + [[ -z "${later_order:-}" || "$later_order" == \#* ]] && continue + + if [[ "$later_order" == "$order" ]]; then + after_current=1 + continue + fi + if ((!after_current)); then + continue + fi + + read -r -a later_paths <<< "$later_pathspecs" + if ((${#later_paths[@]} == 0)); then + continue + fi + + later_file="$(mktemp)" + remaining_file="$(mktemp)" + git diff --name-only "$base_ref...$source_ref" -- "${later_paths[@]}" | sort -u > "$later_file" + comm -23 "$output" "$later_file" > "$remaining_file" + mv "$remaining_file" "$output" + rm -f "$later_file" + done < "$plan_file" +} + +apply_manifest_rules() { + local order="$1" + + case "$order" in + 05) + ensure_after Cargo.toml 'members = [' ' "crates/jolt-claims",' + ensure_after Cargo.toml 'jolt-core = { path = "./jolt-core", default-features = false }' 'jolt-claims = { path = "./crates/jolt-claims" }' + ;; + 06) + ensure_after Cargo.toml 'jolt-poly = { path = "./crates/jolt-poly" }' 'jolt-r1cs = { path = "./crates/jolt-r1cs" }' + ;; + 08) + ensure_after Cargo.toml ' "crates/jolt-claims",' ' "crates/jolt-blindfold",' + ensure_after Cargo.toml 'jolt-core = { path = "./jolt-core", default-features = false }' 'jolt-blindfold = { path = "./crates/jolt-blindfold" }' + ;; + 09) + ensure_after Cargo.toml ' "crates/jolt-openings",' ' "crates/jolt-verifier",' + ensure_after Cargo.toml ' "examples/advice-demo/guest",' ' "examples/advice-consumer/guest",' + ensure_after Cargo.toml 'jolt-openings = { path = "./crates/jolt-openings" }' 'jolt-verifier = { path = "./crates/jolt-verifier" }' + ;; + esac +} + +previous_branch="$base_ref" +matched=0 +start_seen=0 +updated_branches=() +updated_branch_count=0 +coverage_file="" +if ((check_coverage)); then + coverage_file="$(mktemp)" + printf '%s\n' Cargo.lock Cargo.toml > "$coverage_file" +fi + +while IFS=$'\t' read -r order branch title pathspecs; do + [[ -z "${order:-}" || "$order" == \#* ]] && continue + + if [[ -n "$start_at" && "$start_seen" -eq 0 ]]; then + if [[ "$order" != "$start_at" ]]; then + if ((check_coverage)); then + skipped_paths_file="$(mktemp)" + effective_owned_paths "$order" "$pathspecs" "$skipped_paths_file" + cat "$skipped_paths_file" >> "$coverage_file" + rm -f "$skipped_paths_file" + fi + continue + fi + start_seen=1 + fi + + if [[ -n "$only" && "$order" != "$only" ]]; then + previous_branch="$branch" + continue + fi + + matched=1 + echo + echo "[$order] $branch" + echo " base: $previous_branch" + echo " title: $title" + echo " paths: $pathspecs" + + owned_paths_file="$(mktemp)" + effective_owned_paths "$order" "$pathspecs" "$owned_paths_file" + + if ((check_coverage)); then + cat "$owned_paths_file" >> "$coverage_file" + fi + + if ((!apply)); then + previous_branch="$branch" + continue + fi + + if git show-ref --verify --quiet "refs/heads/$branch"; then + if ((rebuild)); then + git switch "$branch" + git reset --hard "$previous_branch" + else + git switch "$branch" + git merge --ff-only "$previous_branch" + fi + else + git switch -c "$branch" "$previous_branch" + fi + + owned_pathspecs=() + owned_pathspec_count=0 + while IFS= read -r owned_pathspec; do + owned_pathspecs+=("$owned_pathspec") + owned_pathspec_count=$((owned_pathspec_count + 1)) + done < "$owned_paths_file" + if ((owned_pathspec_count)); then + restore_owned_paths "$source_ref" "${owned_pathspecs[@]}" + fi + apply_manifest_rules "$order" + + if ((cargo_metadata)); then + cargo metadata -q >/dev/null + fi + + echo " restored paths from $source_ref" + + if ((commit_changes)); then + git add -A + if git diff --cached --quiet; then + echo " no changes to commit" + else + git commit -m "$title" + fi + else + echo " review, then:" + echo " git add " + echo " git commit -m \"$title\"" + fi + + updated_branches+=("$branch") + updated_branch_count=$((updated_branch_count + 1)) + + previous_branch="$branch" +done < "$plan_file" + +if [[ -n "$start_at" && "$start_seen" -eq 0 ]]; then + echo "no stack item with start order $start_at" >&2 + exit 1 +fi + +if [[ -n "$only" && "$matched" -eq 0 ]]; then + echo "no stack item with order $only" >&2 + exit 1 +fi + +if ((!apply)); then + echo + echo "dry run only; pass --apply to create/update branches" + exit 0 +fi + +if ((check_coverage)) && [[ -z "$only" ]]; then + echo + echo "checking stack path coverage against $base_ref...$source_ref" + all_paths="$(mktemp)" + covered_paths="$(mktemp)" + uncovered_paths="$(mktemp)" + git diff --name-only "$base_ref...$source_ref" | sort -u > "$all_paths" + sort -u "$coverage_file" > "$covered_paths" + comm -23 "$all_paths" "$covered_paths" > "$uncovered_paths" + if [[ -s "$uncovered_paths" ]]; then + echo "source diff has paths not assigned to any stack slice:" >&2 + cat "$uncovered_paths" >&2 + exit 1 + fi +fi + +if ((push_changes)); then + echo + echo "pushing $updated_branch_count stack branches" + if ((updated_branch_count)); then + for branch in "${updated_branches[@]}"; do + remote_sha="$(git ls-remote --heads origin "$branch" | awk '{print $1}')" + if [[ -n "$remote_sha" ]]; then + git push --force-with-lease="refs/heads/$branch:$remote_sha" origin "$branch:refs/heads/$branch" + else + git push origin "$branch:refs/heads/$branch" + fi + done + fi +fi diff --git a/tracer/Cargo.toml b/tracer/Cargo.toml index 406c3700af..9c5fb85823 100644 --- a/tracer/Cargo.toml +++ b/tracer/Cargo.toml @@ -35,8 +35,8 @@ std = [ test-utils = ["dep:rand", "jolt-riscv/test-utils"] [dependencies] -jolt-program = { workspace = true, features = ["image"] } -jolt-riscv.workspace = true +jolt-program = { workspace = true, features = ["image", "serialization"] } +jolt-riscv = { workspace = true, features = ["serialization"] } jolt-platform.workspace = true fnv.workspace = true object.workspace = true diff --git a/tracer/src/emulator/cpu.rs b/tracer/src/emulator/cpu.rs index 79600106c8..06ae86201c 100644 --- a/tracer/src/emulator/cpu.rs +++ b/tracer/src/emulator/cpu.rs @@ -160,11 +160,9 @@ struct ActiveMarker { #[derive(Clone, Debug)] pub struct Cpu { clock: u64, - pub(crate) xlen: Xlen, + pub(crate) privilege_mode: PrivilegeMode, wfi: bool, - // using only lower 32bits of x, pc, and csr registers - // for 32-bit mode pub x: [i64; REGISTER_COUNT as usize], #[allow(dead_code)] f: [f64; 32], @@ -187,12 +185,6 @@ pub struct Cpu { pub advice_tape: AdviceTape, } -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Xlen { - Bit32, - Bit64, // @TODO: Support Bit128 -} - /// Width of an LR/SC reservation set. Ordered `Word < Doubleword` so /// `reservation_covers` can compare with `>=` — an 8-byte reservation set /// covers a 4-byte SC write, but not vice versa. @@ -300,11 +292,8 @@ fn _get_trap_type_name(trap_type: &TrapType) -> &'static str { } } -fn get_trap_cause(trap: &Trap, xlen: &Xlen) -> u64 { - let interrupt_bit = match xlen { - Xlen::Bit32 => 0x80000000_u64, - Xlen::Bit64 => 0x8000000000000000_u64, - }; +fn get_trap_cause(trap: &Trap) -> u64 { + let interrupt_bit = 0x8000000000000000_u64; match trap.trap_type { TrapType::InstructionAddressMisaligned => 0, TrapType::InstructionAccessFault => 1, @@ -340,14 +329,13 @@ impl Cpu { pub fn new(terminal: Box) -> Self { let mut cpu = Self { clock: 0, - xlen: Xlen::Bit64, privilege_mode: PrivilegeMode::Machine, wfi: false, x: [0; REGISTER_COUNT as usize], f: [0.0; 32], pc: 0, csr: [0; CSR_CAPACITY], - mmu: Mmu::new(Xlen::Bit64, terminal), + mmu: Mmu::new(terminal), reservation: 0, is_reservation_set: false, reservation_width: ReservationWidth::Word, @@ -380,19 +368,6 @@ impl Cpu { self.pc = value; } - /// Updates XLEN, 32-bit or 64-bit - /// - /// # Arguments - /// * `xlen` - pub fn update_xlen(&mut self, xlen: Xlen) { - self.xlen = xlen; - self.unsigned_data_mask = match xlen { - Xlen::Bit32 => 0xffffffff, - Xlen::Bit64 => 0xffffffffffffffff, - }; - self.mmu.update_xlen(xlen); - } - /// Reads integer register content /// /// # Arguments @@ -482,7 +457,7 @@ impl Cpu { } let original_word = self.fetch()?; - let instruction_address = normalize_u64(self.pc, &self.xlen); + let instruction_address = normalize_u64(self.pc); let is_compressed = (original_word & 0x3) != 0x3; let word = match is_compressed { false => { @@ -491,7 +466,7 @@ impl Cpu { } true => { self.pc = self.pc.wrapping_add(2); // 16-bit length compressed instruction - uncompress_instruction(original_word & 0xffff, self.xlen) + uncompress_instruction(original_word & 0xffff) } }; @@ -507,7 +482,7 @@ impl Cpu { self.trace_len += 1; } else { instr.trace(self, trace); - self.trace_len += instr.inline_sequence(&self.vr_allocator, self.xlen).len(); + self.trace_len += instr.inline_sequence(&self.vr_allocator).len(); } // check if current instruction is real or not for cycle profiling @@ -633,7 +608,7 @@ impl Cpu { fn handle_trap(&mut self, trap: Trap, instruction_address: u64, is_interrupt: bool) -> bool { let current_privilege_encoding = get_privilege_encoding(&self.privilege_mode) as u64; - let cause = get_trap_cause(&trap, &self.xlen); + let cause = get_trap_cause(&trap); // First, determine which privilege mode should handle the trap. // @TODO: Check if this logic is correct @@ -723,54 +698,21 @@ impl Cpu { // Interrupt can be maskable by xie csr register // where x is a new privilege mode. - match trap.trap_type { - TrapType::UserSoftwareInterrupt => { - if usie == 0 { - return false; - } - } - TrapType::SupervisorSoftwareInterrupt => { - if ssie == 0 { - return false; - } - } - TrapType::MachineSoftwareInterrupt => { - if msie == 0 { - return false; - } - } - TrapType::UserTimerInterrupt => { - if utie == 0 { - return false; - } - } - TrapType::SupervisorTimerInterrupt => { - if stie == 0 { - return false; - } - } - TrapType::MachineTimerInterrupt => { - if mtie == 0 { - return false; - } - } - TrapType::UserExternalInterrupt => { - if ueie == 0 { - return false; - } - } - TrapType::SupervisorExternalInterrupt => { - if seie == 0 { - return false; - } - } - TrapType::MachineExternalInterrupt => { - if meie == 0 { - return false; - } - } - _ => {} + let interrupt_enabled = match trap.trap_type { + TrapType::UserSoftwareInterrupt => usie != 0, + TrapType::SupervisorSoftwareInterrupt => ssie != 0, + TrapType::MachineSoftwareInterrupt => msie != 0, + TrapType::UserTimerInterrupt => utie != 0, + TrapType::SupervisorTimerInterrupt => stie != 0, + TrapType::MachineTimerInterrupt => mtie != 0, + TrapType::UserExternalInterrupt => ueie != 0, + TrapType::SupervisorExternalInterrupt => seie != 0, + TrapType::MachineExternalInterrupt => meie != 0, + _ => true, }; + if !interrupt_enabled { + return false; + } } // So, this trap should be taken @@ -892,7 +834,6 @@ impl Cpu { // SSTATUS, SIE, and SIP are subsets of MSTATUS, MIE, and MIP pub fn read_csr_raw(&self, address: u16) -> u64 { match address { - // @TODO: Mask should consider of 32-bit mode CSR_FFLAGS_ADDRESS => self.csr[CSR_FCSR_ADDRESS as usize] & 0x1f, CSR_FRM_ADDRESS => (self.csr[CSR_FCSR_ADDRESS as usize] >> 5) & 0x7, CSR_SSTATUS_ADDRESS => self.csr[CSR_MSTATUS_ADDRESS as usize] & 0x80000003000de162, @@ -967,36 +908,24 @@ impl Cpu { #[allow(dead_code)] fn update_addressing_mode(&mut self, value: u64) { - let addressing_mode = match self.xlen { - Xlen::Bit32 => match value & 0x80000000 { - 0 => AddressingMode::None, - _ => AddressingMode::SV32, - }, - Xlen::Bit64 => match value >> 60 { - 0 => AddressingMode::None, - 8 => AddressingMode::SV39, - 9 => AddressingMode::SV48, - _ => { - #[cfg(feature = "std")] - tracing::error!("Unknown addressing_mode {:x}", value >> 60); - panic!(); - } - }, - }; - let ppn = match self.xlen { - Xlen::Bit32 => value & 0x3fffff, - Xlen::Bit64 => value & 0xfffffffffff, + let addressing_mode = match value >> 60 { + 0 => AddressingMode::None, + 8 => AddressingMode::SV39, + 9 => AddressingMode::SV48, + _ => { + #[cfg(feature = "std")] + tracing::error!("Unknown addressing_mode {:x}", value >> 60); + panic!(); + } }; + let ppn = value & 0xfffffffffff; self.mmu.update_addressing_mode(addressing_mode); self.mmu.update_ppn(ppn); } // @TODO: Rename to better name? pub(crate) fn sign_extend(&self, value: i64) -> i64 { - match self.xlen { - Xlen::Bit32 => value as i32 as i64, - Xlen::Bit64 => value, - } + value } // @TODO: Rename to better name? @@ -1006,10 +935,7 @@ impl Cpu { // @TODO: Rename to better name? pub(crate) fn most_negative(&self) -> i64 { - match self.xlen { - Xlen::Bit32 => i32::MIN as i64, - Xlen::Bit64 => i64::MIN, - } + i64::MIN } /// Disassembles an instruction pointed by Program Counter. @@ -1030,7 +956,7 @@ impl Cpu { false => original_word, true => { original_word &= 0xffff; - uncompress_instruction(original_word, self.xlen) + uncompress_instruction(original_word) } }; @@ -1162,7 +1088,6 @@ impl Cpu { pub fn save_state_with_empty_memory(&self) -> Cpu { Cpu { clock: self.clock, - xlen: self.xlen, privilege_mode: self.privilege_mode, wfi: self.wfi, x: self.x, @@ -1241,11 +1166,8 @@ pub fn get_register_name(num: usize) -> &'static str { } } -fn normalize_u64(value: u64, width: &Xlen) -> u64 { - match width { - Xlen::Bit32 => value as u32 as u64, - Xlen::Bit64 => value, - } +fn normalize_u64(value: u64) -> u64 { + value } #[cfg(test)] @@ -1273,18 +1195,6 @@ mod test_cpu { assert_eq!(0xffffffffffffffff, cpu.read_pc()); } - #[test] - fn update_xlen() { - let mut cpu = create_cpu(); - assert!(matches!(cpu.xlen, Xlen::Bit64)); - cpu.update_xlen(Xlen::Bit32); - assert!(matches!(cpu.xlen, Xlen::Bit32)); - cpu.update_xlen(Xlen::Bit64); - assert!(matches!(cpu.xlen, Xlen::Bit64)); - // Note: cpu.update_xlen() updates cpu.mmu.xlen, too. - // The test for mmu.xlen should be in Mmu? - } - #[test] fn read_register() { let mut cpu = create_cpu(); diff --git a/tracer/src/emulator/mmu.rs b/tracer/src/emulator/mmu.rs index b917d55b4f..4623e04e3a 100644 --- a/tracer/src/emulator/mmu.rs +++ b/tracer/src/emulator/mmu.rs @@ -7,7 +7,7 @@ use crate::instruction::{RAMRead, RAMWrite}; use common::constants::{RAM_START_ADDRESS, STACK_CANARY_SIZE}; use common::jolt_device::JoltDevice; -use super::cpu::{get_privilege_mode, PrivilegeMode, Trap, TrapType, Xlen}; +use super::cpu::{get_privilege_mode, PrivilegeMode, Trap, TrapType}; use super::terminal::Terminal; /// Emulates Memory Management Unit. It holds the Main memory and peripheral @@ -18,7 +18,6 @@ use super::terminal::Terminal; #[derive(Clone, Debug)] pub struct Mmu { clock: u64, - xlen: Xlen, ppn: u64, addressing_mode: AddressingMode, privilege_mode: PrivilegeMode, @@ -34,7 +33,6 @@ pub struct Mmu { #[derive(Clone, Debug, Copy)] pub enum AddressingMode { None, - SV32, SV39, SV48, // @TODO: Implement } @@ -48,7 +46,6 @@ enum MemoryAccessType { fn _get_addressing_mode_name(mode: &AddressingMode) -> &'static str { match mode { AddressingMode::None => "None", - AddressingMode::SV32 => "SV32", AddressingMode::SV39 => "SV39", AddressingMode::SV48 => "SV48", } @@ -61,10 +58,9 @@ impl Mmu { /// * `xlen` /// * `terminal` /// * `tracer` - pub fn new(xlen: Xlen, _terminal: Box) -> Self { + pub fn new(_terminal: Box) -> Self { Mmu { clock: 0, - xlen, ppn: 0, addressing_mode: AddressingMode::None, privilege_mode: PrivilegeMode::Machine, @@ -74,14 +70,6 @@ impl Mmu { } } - /// Updates XLEN, 32-bit or 64-bit - /// - /// # Arguments - /// * `xlen` - pub fn update_xlen(&mut self, xlen: Xlen) { - self.xlen = xlen; - } - /// Initializes Main memory. This method is expected to be called only once. /// /// # Arguments @@ -129,10 +117,7 @@ impl Mmu { } fn get_effective_address(&self, address: u64) -> u64 { - match self.xlen { - Xlen::Bit32 => address & 0xffffffff, - Xlen::Bit64 => address, - } + address } #[inline] @@ -531,10 +516,7 @@ impl Mmu { /// state is used in Jolt to construct the witnesses in `read_write_memory.rs`. fn trace_load(&mut self, effective_address: u64) -> RAMRead { let word_address = (effective_address >> 2) << 2; - let bytes = match self.xlen { - Xlen::Bit32 => 4, - Xlen::Bit64 => 8, - }; + let bytes = 8; if word_address < DRAM_BASE { let mut value_bytes = [0u8; 8]; for i in 0..bytes { @@ -565,10 +547,7 @@ impl Mmu { /// construct the witnesses in `read_write_memory.rs`. fn trace_store_byte(&mut self, effective_address: u64, value: u64) -> RAMWrite { self.assert_effective_store_address(effective_address); - let bytes = match self.xlen { - Xlen::Bit32 => 4, - Xlen::Bit64 => 8, - }; + let bytes = 8; let word_address = (effective_address >> 2) << 2; let pre_value = if effective_address < DRAM_BASE { @@ -610,10 +589,7 @@ impl Mmu { /// construct the witnesses in `read_write_memory.rs`. fn trace_store_halfword(&mut self, effective_address: u64, value: u64) -> RAMWrite { self.assert_effective_store_address(effective_address); - let bytes = match self.xlen { - Xlen::Bit32 => 4, - Xlen::Bit64 => 8, - }; + let bytes = 8; let word_address = (effective_address >> 2) << 2; let pre_value = if effective_address < DRAM_BASE { @@ -655,10 +631,7 @@ impl Mmu { /// in `read_write_memory.rs`. fn trace_store(&mut self, effective_address: u64, value: u64) -> RAMWrite { self.assert_effective_store_address(effective_address); - let bytes = match self.xlen { - Xlen::Bit32 => 4, - Xlen::Bit64 => 8, - }; + let bytes = 8; if effective_address < DRAM_BASE { let mut pre_value_bytes = [0u8; 8]; @@ -902,37 +875,8 @@ impl Mmu { let address = self.get_effective_address(v_address); let p_address = match self.addressing_mode { AddressingMode::None => Ok(address), - AddressingMode::SV32 => match self.privilege_mode { - // @TODO: Optimize - PrivilegeMode::Machine => match access_type { - MemoryAccessType::Execute => Ok(address), - // @TODO: Remove magic number - _ => match (self.mstatus >> 17) & 1 { - 0 => Ok(address), - _ => { - let privilege_mode = get_privilege_mode((self.mstatus >> 11) & 3); - match privilege_mode { - PrivilegeMode::Machine => Ok(address), - _ => { - let current_privilege_mode = self.privilege_mode; - self.update_privilege_mode(privilege_mode); - let result = self.translate_address(v_address, access_type); - self.update_privilege_mode(current_privilege_mode); - result - } - } - } - }, - }, - PrivilegeMode::User | PrivilegeMode::Supervisor => { - let vpns = [(address >> 12) & 0x3ff, (address >> 22) & 0x3ff]; - self.traverse_page(address, 2 - 1, self.ppn, &vpns, access_type) - } - _ => Ok(address), - }, AddressingMode::SV39 => match self.privilege_mode { // @TODO: Optimize - // @TODO: Remove duplicated code with SV32 PrivilegeMode::Machine => match access_type { MemoryAccessType::Execute => Ok(address), // @TODO: Remove magic number @@ -979,21 +923,11 @@ impl Mmu { access_type: &MemoryAccessType, ) -> Result { let pagesize = 4096; - let ptesize = match self.addressing_mode { - AddressingMode::SV32 => 4, - _ => 8, - }; + let ptesize = 8; let pte_address = parent_ppn * pagesize + vpns[level as usize] * ptesize; - let pte = match self.addressing_mode { - AddressingMode::SV32 => self.load_word_raw(pte_address) as u64, - _ => self.load_doubleword_raw(pte_address), - }; - let ppn = match self.addressing_mode { - AddressingMode::SV32 => (pte >> 10) & 0x3fffff, - _ => (pte >> 10) & 0xfffffffffff, - }; + let pte = self.load_doubleword_raw(pte_address); + let ppn = (pte >> 10) & 0xfffffffffff; let ppns = match self.addressing_mode { - AddressingMode::SV32 => [(pte >> 10) & 0x3ff, (pte >> 20) & 0xfff, 0 /*dummy*/], AddressingMode::SV39 => [ (pte >> 10) & 0x1ff, (pte >> 19) & 0x1ff, @@ -1038,10 +972,7 @@ impl Mmu { MemoryAccessType::Write => 1 << 7, _ => 0, }); - match self.addressing_mode { - AddressingMode::SV32 => self.store_word_raw(pte_address, new_pte as u32), - _ => self.store_doubleword_raw(pte_address, new_pte), - }; + self.store_doubleword_raw(pte_address, new_pte); } match access_type { @@ -1064,33 +995,21 @@ impl Mmu { let offset = v_address & 0xfff; // [11:0] // @TODO: Optimize - let p_address = match self.addressing_mode { - AddressingMode::SV32 => match level { - 1 => { - if ppns[0] != 0 { - return Err(()); - } - (ppns[1] << 22) | (vpns[0] << 12) | offset - } - 0 => (ppn << 12) | offset, - _ => panic!(), // Shouldn't happen - }, - _ => match level { - 2 => { - if ppns[1] != 0 || ppns[0] != 0 { - return Err(()); - } - (ppns[2] << 30) | (vpns[1] << 21) | (vpns[0] << 12) | offset + let p_address = match level { + 2 => { + if ppns[1] != 0 || ppns[0] != 0 { + return Err(()); } - 1 => { - if ppns[0] != 0 { - return Err(()); - } - (ppns[2] << 30) | (ppns[1] << 21) | (vpns[0] << 12) | offset + (ppns[2] << 30) | (vpns[1] << 21) | (vpns[0] << 12) | offset + } + 1 => { + if ppns[0] != 0 { + return Err(()); } - 0 => (ppn << 12) | offset, - _ => panic!(), // Shouldn't happen - }, + (ppns[2] << 30) | (ppns[1] << 21) | (vpns[0] << 12) | offset + } + 0 => (ppn << 12) | offset, + _ => panic!(), // Shouldn't happen }; // println!("PA:{:X}", p_address); @@ -1102,7 +1021,6 @@ impl Mmu { pub fn save_state_with_empty_memory(&self) -> Mmu { Mmu { clock: self.clock, - xlen: self.xlen, ppn: self.ppn, addressing_mode: self.addressing_mode, privilege_mode: self.privilege_mode, @@ -1218,7 +1136,7 @@ mod test_mmu { fn setup_mmu() -> Mmu { let terminal = Box::new(DummyTerminal::default()); - let mut mmu = Mmu::new(Xlen::Bit64, terminal); + let mut mmu = Mmu::new(terminal); let memory_config = MemoryConfig { program_size: Some(1024), ..Default::default() diff --git a/tracer/src/emulator/mod.rs b/tracer/src/emulator/mod.rs index c6d900f75c..ccecd984ed 100644 --- a/tracer/src/emulator/mod.rs +++ b/tracer/src/emulator/mod.rs @@ -27,7 +27,7 @@ pub mod memory; pub mod mmu; pub mod terminal; -use self::cpu::{Cpu, Xlen}; +use self::cpu::Cpu; use self::elf_analyzer::ElfAnalyzer; use self::terminal::Terminal; @@ -265,11 +265,7 @@ impl Emulator { // Detected whether the elf file is riscv-tests. // Setting up CPU and Memory depending on it. - self.cpu.update_xlen(match header.e_width { - 32 => Xlen::Bit32, - 64 => Xlen::Bit64, - _ => panic!("No happen"), - }); + assert_eq!(header.e_width, 64, "tracer only supports RV64 ELF inputs"); if self.tohost_addr != 0 { self.is_test = true; @@ -302,14 +298,6 @@ impl Emulator { self.cpu.update_pc(header.e_entry); } - /// Updates XLEN (the width of an integer register in bits) in CPU. - /// - /// # Arguments - /// * `xlen` - pub fn update_xlen(&mut self, xlen: Xlen) { - self.cpu.update_xlen(xlen); - } - /// Returns immutable reference to `self.cpu`. pub fn get_cpu(&self) -> &Cpu { &self.cpu diff --git a/tracer/src/execution_backend.rs b/tracer/src/execution_backend.rs index 8558261540..5180937567 100644 --- a/tracer/src/execution_backend.rs +++ b/tracer/src/execution_backend.rs @@ -5,7 +5,7 @@ use jolt_program::execution::{ RamRead as ProgramRamRead, RamWrite as ProgramRamWrite, RegisterRead, RegisterState, RegisterWrite, TraceError, TraceInputs, TraceOutput, TraceRow, }; -use jolt_riscv::NormalizedInstruction; +use jolt_riscv::JoltInstructionRow; use crate::instruction::{Cycle, RAMAccess}; @@ -48,7 +48,10 @@ impl ExecutionBackend for TracerBackend { None, ); - let rows = cycles.into_iter().map(trace_row_from_cycle).collect(); + let rows = cycles + .into_iter() + .map(trace_row_from_cycle) + .collect::, _>>()?; Ok(TraceOutput::new( OwnedTrace::new(rows), device, @@ -59,17 +62,19 @@ impl ExecutionBackend for TracerBackend { } } -fn trace_row_from_cycle(cycle: Cycle) -> TraceRow { - TraceRow { - instruction: normalized_instruction(&cycle), +fn trace_row_from_cycle(cycle: Cycle) -> Result { + Ok(TraceRow { + instruction: jolt_instruction_row(&cycle)?, registers: register_state(&cycle), ram_access: cycle.ram_access().into(), - } + }) } -fn normalized_instruction(cycle: &Cycle) -> NormalizedInstruction { +fn jolt_instruction_row(cycle: &Cycle) -> Result { let instruction = cycle.instruction(); - (&instruction).into() + instruction + .try_jolt_instruction_row() + .map_err(|_| TraceError::Backend("execution trace contained a source-only instruction")) } fn register_state(cycle: &Cycle) -> RegisterState { diff --git a/tracer/src/instruction/addi.rs b/tracer/src/instruction/addi.rs index 40a6699efa..2cd5b74301 100644 --- a/tracer/src/instruction/addi.rs +++ b/tracer/src/instruction/addi.rs @@ -20,8 +20,7 @@ impl ADDI { cpu.write_register( self.operands.rd as usize, cpu.sign_extend( - cpu.x[self.operands.rs1 as usize] - .wrapping_add(normalize_imm(self.operands.imm, &cpu.xlen)), + cpu.x[self.operands.rs1 as usize].wrapping_add(normalize_imm(self.operands.imm)), ), ); } diff --git a/tracer/src/instruction/addiw.rs b/tracer/src/instruction/addiw.rs index d5fe5c4566..450149bad0 100644 --- a/tracer/src/instruction/addiw.rs +++ b/tracer/src/instruction/addiw.rs @@ -21,8 +21,7 @@ impl ADDIW { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - cpu.x[self.operands.rs1 as usize] - .wrapping_add(normalize_imm(self.operands.imm, &cpu.xlen)) as i32 + cpu.x[self.operands.rs1 as usize].wrapping_add(normalize_imm(self.operands.imm)) as i32 as i64, ); } @@ -30,7 +29,7 @@ impl ADDIW { impl RISCVTrace for ADDIW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/addw.rs b/tracer/src/instruction/addw.rs index ce7e2e0c81..d32b2fed47 100644 --- a/tracer/src/instruction/addw.rs +++ b/tracer/src/instruction/addw.rs @@ -26,7 +26,7 @@ impl ADDW { impl RISCVTrace for ADDW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/advice_lb.rs b/tracer/src/instruction/advice_lb.rs index 458b8d2500..794f18945e 100644 --- a/tracer/src/instruction/advice_lb.rs +++ b/tracer/src/instruction/advice_lb.rs @@ -27,7 +27,7 @@ impl AdviceLB { impl RISCVTrace for AdviceLB { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/advice_ld.rs b/tracer/src/instruction/advice_ld.rs index 782e2d7248..d8022bf86d 100644 --- a/tracer/src/instruction/advice_ld.rs +++ b/tracer/src/instruction/advice_ld.rs @@ -27,7 +27,7 @@ impl AdviceLD { impl RISCVTrace for AdviceLD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/advice_lh.rs b/tracer/src/instruction/advice_lh.rs index d509d3b0e2..3e86988736 100644 --- a/tracer/src/instruction/advice_lh.rs +++ b/tracer/src/instruction/advice_lh.rs @@ -27,7 +27,7 @@ impl AdviceLH { impl RISCVTrace for AdviceLH { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/advice_lw.rs b/tracer/src/instruction/advice_lw.rs index fd003c2c30..018ba8e692 100644 --- a/tracer/src/instruction/advice_lw.rs +++ b/tracer/src/instruction/advice_lw.rs @@ -27,7 +27,7 @@ impl AdviceLW { impl RISCVTrace for AdviceLW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoaddd.rs b/tracer/src/instruction/amoaddd.rs index 09f2c3aa03..bd2d8adbe4 100644 --- a/tracer/src/instruction/amoaddd.rs +++ b/tracer/src/instruction/amoaddd.rs @@ -39,7 +39,7 @@ impl AMOADDD { impl RISCVTrace for AMOADDD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoaddw.rs b/tracer/src/instruction/amoaddw.rs index 15ba7b8be5..fc17d7582f 100644 --- a/tracer/src/instruction/amoaddw.rs +++ b/tracer/src/instruction/amoaddw.rs @@ -38,7 +38,7 @@ impl AMOADDW { impl RISCVTrace for AMOADDW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoandd.rs b/tracer/src/instruction/amoandd.rs index acc93d237a..46d1edeb18 100644 --- a/tracer/src/instruction/amoandd.rs +++ b/tracer/src/instruction/amoandd.rs @@ -39,7 +39,7 @@ impl AMOANDD { impl RISCVTrace for AMOANDD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoandw.rs b/tracer/src/instruction/amoandw.rs index 5237cde096..093d728ab7 100644 --- a/tracer/src/instruction/amoandw.rs +++ b/tracer/src/instruction/amoandw.rs @@ -38,7 +38,7 @@ impl AMOANDW { impl RISCVTrace for AMOANDW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amomaxd.rs b/tracer/src/instruction/amomaxd.rs index 1edbccbd18..c5e089ad85 100644 --- a/tracer/src/instruction/amomaxd.rs +++ b/tracer/src/instruction/amomaxd.rs @@ -42,7 +42,7 @@ impl AMOMAXD { impl RISCVTrace for AMOMAXD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amomaxud.rs b/tracer/src/instruction/amomaxud.rs index 6b682a4496..f8442d9183 100644 --- a/tracer/src/instruction/amomaxud.rs +++ b/tracer/src/instruction/amomaxud.rs @@ -42,7 +42,7 @@ impl AMOMAXUD { impl RISCVTrace for AMOMAXUD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amomaxuw.rs b/tracer/src/instruction/amomaxuw.rs index fdfc27371c..4ecbaa9673 100644 --- a/tracer/src/instruction/amomaxuw.rs +++ b/tracer/src/instruction/amomaxuw.rs @@ -42,7 +42,7 @@ impl AMOMAXUW { impl RISCVTrace for AMOMAXUW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amomaxw.rs b/tracer/src/instruction/amomaxw.rs index 859f9c97a4..c81c130d35 100644 --- a/tracer/src/instruction/amomaxw.rs +++ b/tracer/src/instruction/amomaxw.rs @@ -42,7 +42,7 @@ impl AMOMAXW { impl RISCVTrace for AMOMAXW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amomind.rs b/tracer/src/instruction/amomind.rs index fcad0cefee..ee60c4b761 100644 --- a/tracer/src/instruction/amomind.rs +++ b/tracer/src/instruction/amomind.rs @@ -42,7 +42,7 @@ impl AMOMIND { impl RISCVTrace for AMOMIND { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amominud.rs b/tracer/src/instruction/amominud.rs index 247cb41a33..7e86a859ea 100644 --- a/tracer/src/instruction/amominud.rs +++ b/tracer/src/instruction/amominud.rs @@ -42,7 +42,7 @@ impl AMOMINUD { impl RISCVTrace for AMOMINUD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amominuw.rs b/tracer/src/instruction/amominuw.rs index 136fea2e6a..ed9446cdcb 100644 --- a/tracer/src/instruction/amominuw.rs +++ b/tracer/src/instruction/amominuw.rs @@ -42,7 +42,7 @@ impl AMOMINUW { impl RISCVTrace for AMOMINUW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amominw.rs b/tracer/src/instruction/amominw.rs index 8b7ebc5a02..d778b996a2 100644 --- a/tracer/src/instruction/amominw.rs +++ b/tracer/src/instruction/amominw.rs @@ -42,7 +42,7 @@ impl AMOMINW { impl RISCVTrace for AMOMINW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoord.rs b/tracer/src/instruction/amoord.rs index d811578c61..61f7590b24 100644 --- a/tracer/src/instruction/amoord.rs +++ b/tracer/src/instruction/amoord.rs @@ -39,7 +39,7 @@ impl AMOORD { impl RISCVTrace for AMOORD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoorw.rs b/tracer/src/instruction/amoorw.rs index c21353cf88..0096559729 100644 --- a/tracer/src/instruction/amoorw.rs +++ b/tracer/src/instruction/amoorw.rs @@ -38,7 +38,7 @@ impl AMOORW { impl RISCVTrace for AMOORW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoswapd.rs b/tracer/src/instruction/amoswapd.rs index 5d5223f92a..b33feccb9b 100644 --- a/tracer/src/instruction/amoswapd.rs +++ b/tracer/src/instruction/amoswapd.rs @@ -37,7 +37,7 @@ impl AMOSWAPD { impl RISCVTrace for AMOSWAPD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoswapw.rs b/tracer/src/instruction/amoswapw.rs index bf2ad59e85..a27a96f1f7 100644 --- a/tracer/src/instruction/amoswapw.rs +++ b/tracer/src/instruction/amoswapw.rs @@ -38,7 +38,7 @@ impl AMOSWAPW { impl RISCVTrace for AMOSWAPW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoxord.rs b/tracer/src/instruction/amoxord.rs index 410628f855..0e64381fc1 100644 --- a/tracer/src/instruction/amoxord.rs +++ b/tracer/src/instruction/amoxord.rs @@ -38,7 +38,7 @@ impl AMOXORD { impl RISCVTrace for AMOXORD { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/amoxorw.rs b/tracer/src/instruction/amoxorw.rs index e0e71d31c4..25bfab673c 100644 --- a/tracer/src/instruction/amoxorw.rs +++ b/tracer/src/instruction/amoxorw.rs @@ -38,7 +38,7 @@ impl AMOXORW { impl RISCVTrace for AMOXORW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/andi.rs b/tracer/src/instruction/andi.rs index cb87005df3..4011a7d3c0 100644 --- a/tracer/src/instruction/andi.rs +++ b/tracer/src/instruction/andi.rs @@ -19,9 +19,7 @@ impl ANDI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - cpu.sign_extend( - cpu.x[self.operands.rs1 as usize] & normalize_imm(self.operands.imm, &cpu.xlen), - ), + cpu.sign_extend(cpu.x[self.operands.rs1 as usize] & normalize_imm(self.operands.imm)), ); } } diff --git a/tracer/src/instruction/auipc.rs b/tracer/src/instruction/auipc.rs index d8e9847caf..0120ec2687 100644 --- a/tracer/src/instruction/auipc.rs +++ b/tracer/src/instruction/auipc.rs @@ -18,7 +18,7 @@ declare_riscv_instr!( impl AUIPC { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { let pc = self.address as i64; - let imm = normalize_imm(self.operands.imm, &cpu.xlen); + let imm = normalize_imm(self.operands.imm); cpu.write_register( self.operands.rd as usize, cpu.sign_extend(pc.wrapping_add(imm)), diff --git a/tracer/src/instruction/csrrs.rs b/tracer/src/instruction/csrrs.rs index 05a4f23a90..a3180091d8 100644 --- a/tracer/src/instruction/csrrs.rs +++ b/tracer/src/instruction/csrrs.rs @@ -47,7 +47,7 @@ impl CSRRS { impl RISCVTrace for CSRRS { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { // Don't call self.execute() - the inline sequence handles all register writes. - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { diff --git a/tracer/src/instruction/csrrw.rs b/tracer/src/instruction/csrrw.rs index 0402abbc91..d2c90c2534 100644 --- a/tracer/src/instruction/csrrw.rs +++ b/tracer/src/instruction/csrrw.rs @@ -59,7 +59,7 @@ impl RISCVTrace for CSRRW { // Generate and execute inline sequence // The inline sequence reads from virtual register and writes to rd, // then writes rs1 to virtual register. - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { diff --git a/tracer/src/instruction/div.rs b/tracer/src/instruction/div.rs index 0fc499813c..4146b93c32 100644 --- a/tracer/src/instruction/div.rs +++ b/tracer/src/instruction/div.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -42,33 +39,17 @@ impl RISCVTrace for DIV { let x = cpu.x[self.operands.rs1 as usize]; let y = cpu.x[self.operands.rs2 as usize]; - let (quotient, remainder) = match cpu.xlen { - Xlen::Bit32 => { - if y == 0 { - (u32::MAX as u64, (x as i32).unsigned_abs() as u64) - } else if x == cpu.most_negative() && y == -1 { - (x as u32 as u64, 0) - } else { - let quotient = x as i32 / y as i32; - let remainder = (x as i32 % y as i32).unsigned_abs(); - (quotient as u32 as u64, remainder as u64) - } - } - Xlen::Bit64 => { - if y == 0 { - (u64::MAX, x.unsigned_abs()) - } else if x == cpu.most_negative() && y == -1 { - (x as u64, 0) - } else { - let quotient = x / y; - let remainder = (x % y).unsigned_abs(); - (quotient as u64, remainder) - } - } + let (quotient, remainder) = if y == 0 { + (u64::MAX, x.unsigned_abs()) + } else if x == cpu.most_negative() && y == -1 { + (x as u64, 0) + } else { + let quotient = x / y; + let remainder = (x % y).unsigned_abs(); + (quotient as u64, remainder) }; - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient, remainder]); let mut trace = trace; diff --git a/tracer/src/instruction/divu.rs b/tracer/src/instruction/divu.rs index 6f6b362c59..f497ae0b9e 100644 --- a/tracer/src/instruction/divu.rs +++ b/tracer/src/instruction/divu.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -39,20 +36,9 @@ impl RISCVTrace for DIVU { let x = cpu.x[self.operands.rs1 as usize] as u64; let y = cpu.x[self.operands.rs2 as usize] as u64; - let quotient = if y == 0 { - match cpu.xlen { - Xlen::Bit32 => u32::MAX as u64, - Xlen::Bit64 => u64::MAX, - } - } else { - match cpu.xlen { - Xlen::Bit32 => ((x as u32) / (y as u32)) as u64, - Xlen::Bit64 => x / y, - } - }; - - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let quotient = x.checked_div(y).unwrap_or(u64::MAX); + + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient]); let mut trace = trace; diff --git a/tracer/src/instruction/divuw.rs b/tracer/src/instruction/divuw.rs index 6956400e0f..17ab4f5add 100644 --- a/tracer/src/instruction/divuw.rs +++ b/tracer/src/instruction/divuw.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -42,21 +39,9 @@ impl RISCVTrace for DIVUW { let x = cpu.x[self.operands.rs1 as usize] as u32; let y = cpu.x[self.operands.rs2 as usize] as u32; - let quotient = match cpu.xlen { - Xlen::Bit32 => { - panic!("DIVUW is invalid in 32b mode"); - } - Xlen::Bit64 => { - if y == 0 { - u32::MAX as u64 // 32-bit operation: quotient is u32::MAX - } else { - (x / y) as u64 - } - } - }; + let quotient = x.checked_div(y).map_or(u32::MAX as u64, u64::from); - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient]); let mut trace = trace; diff --git a/tracer/src/instruction/divw.rs b/tracer/src/instruction/divw.rs index d196dcbfe8..ebed8825cd 100644 --- a/tracer/src/instruction/divw.rs +++ b/tracer/src/instruction/divw.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -44,25 +41,17 @@ impl RISCVTrace for DIVW { let x = cpu.x[self.operands.rs1 as usize] as i32; let y = cpu.x[self.operands.rs2 as usize] as i32; - let (quotient, remainder) = match cpu.xlen { - Xlen::Bit32 => { - panic!("DIVW is invalid in 32b mode"); - } - Xlen::Bit64 => { - if y == 0 { - (-1i32, x.unsigned_abs()) - } else if y == -1 && x == i32::MIN { - (i32::MIN, 0) //overflow - } else { - let quotient = x / y; - let remainder = x % y; - (quotient, remainder.unsigned_abs()) - } - } + let (quotient, remainder) = if y == 0 { + (-1i32, x.unsigned_abs()) + } else if y == -1 && x == i32::MIN { + (i32::MIN, 0) //overflow + } else { + let quotient = x / y; + let remainder = x % y; + (quotient, remainder.unsigned_abs()) }; - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient as u64, remainder as u64]); let mut trace = trace; diff --git a/tracer/src/instruction/ebreak.rs b/tracer/src/instruction/ebreak.rs index d96a902487..0f233cb0b1 100644 --- a/tracer/src/instruction/ebreak.rs +++ b/tracer/src/instruction/ebreak.rs @@ -28,7 +28,7 @@ impl EBREAK { impl RISCVTrace for EBREAK { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/ecall.rs b/tracer/src/instruction/ecall.rs index 83bea52943..5501d296f2 100644 --- a/tracer/src/instruction/ecall.rs +++ b/tracer/src/instruction/ecall.rs @@ -52,7 +52,7 @@ impl ECALL { impl RISCVTrace for ECALL { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/format/mod.rs b/tracer/src/instruction/format/mod.rs index 5953c4ee93..536e62c61c 100644 --- a/tracer/src/instruction/format/mod.rs +++ b/tracer/src/instruction/format/mod.rs @@ -1,4 +1,4 @@ -use crate::emulator::cpu::{Cpu, Xlen}; +use crate::emulator::cpu::Cpu; pub use jolt_riscv::NormalizedOperands; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; @@ -58,16 +58,9 @@ pub fn normalize_register_value(cpu: &Cpu, reg: usize) -> u64 { } _ => cpu.x[reg], }; - let xlen = cpu.xlen; - match xlen { - Xlen::Bit32 => value as u32 as u64, - Xlen::Bit64 => value as u64, - } + value as u64 } -pub fn normalize_imm(imm: u64, xlen: &Xlen) -> i64 { - match xlen { - Xlen::Bit32 => imm as i32 as i64, - Xlen::Bit64 => imm as i64, - } +pub fn normalize_imm(imm: u64) -> i64 { + imm as i64 } diff --git a/tracer/src/instruction/inline.rs b/tracer/src/instruction/inline.rs index d7c019f1e9..017fac062f 100644 --- a/tracer/src/instruction/inline.rs +++ b/tracer/src/instruction/inline.rs @@ -8,15 +8,16 @@ //! The INLINE instruction iterates these registrations to find the matching builder. use super::{ - format::{format_inline::FormatInline, InstructionFormat, NormalizedOperands}, + format::{format_inline::FormatInline, InstructionFormat}, Cycle, Instruction, RISCVInstruction, RISCVTrace, }; use crate::{ - emulator::cpu::{Cpu, Xlen}, - instruction::NormalizedInstruction, + emulator::cpu::Cpu, + instruction::SourceInstruction, utils::{inline_helpers::InstrAssembler, virtual_registers::VirtualRegisterAllocator}, }; use jolt_program::expand::{ExpansionAllocator, ExpansionError, InlineExpansionProvider}; +use jolt_riscv::{InlineExtension, JoltInstruction, JoltInstructionProfile}; use serde::{Deserialize, Serialize}; const FIRST_INSTRUCTION_TEMP_REGISTER: u8 = common::constants::RISCV_REGISTER_COUNT + 8; @@ -32,6 +33,7 @@ pub struct InlineRegistration { pub opcode: u32, pub funct3: u32, pub funct7: u32, + pub extension: InlineExtension, pub name: &'static str, pub build_sequence: InlineSequenceFn, pub build_advice: AdviceFn, @@ -77,10 +79,11 @@ impl TracerInlineExpansionProvider { impl InlineExpansionProvider for TracerInlineExpansionProvider { fn expand_inline( &mut self, - instruction: &NormalizedInstruction, + instruction: &SourceInstruction, _allocator: &mut ExpansionAllocator, - ) -> Result, ExpansionError> { - let Instruction::INLINE(inline) = Instruction::try_from_normalized(*instruction) + profile: JoltInstructionProfile, + ) -> Result, ExpansionError> { + let Instruction::INLINE(inline) = Instruction::try_from_source_instruction(*instruction) .map_err(|_| ExpansionError::MalformedInstruction("malformed inline instruction"))? else { return Err(ExpansionError::MalformedInstruction( @@ -88,21 +91,32 @@ impl InlineExpansionProvider for TracerInlineExpansionProvider { )); }; - if !is_inline_registered(inline.opcode, inline.funct3, inline.funct7) { + let registration = inventory::iter:: + .into_iter() + .find(|r| { + r.opcode == inline.opcode && r.funct3 == inline.funct3 && r.funct7 == inline.funct7 + }) + .ok_or(ExpansionError::UnsupportedInstruction)?; + if !profile.supports_inline(registration.extension) { return Err(ExpansionError::UnsupportedInstruction); } - let _remapped_rd_guard = instruction.operands.rd.and_then(|rd| { + let _remapped_rd_guard = instruction.row().operands.rd.and_then(|rd| { (FIRST_INSTRUCTION_TEMP_REGISTER..LAST_INSTRUCTION_TEMP_REGISTER) .contains(&rd) .then(|| self.allocator.allocate()) }); - Ok(inline - .inline_sequence(&self.allocator, Xlen::Bit64) + let asm = InstrAssembler::new_inline(inline.address, inline.is_compressed, &self.allocator); + (registration.build_sequence)(asm, inline.operands) .into_iter() - .map(|instruction| instruction.normalize()) - .collect()) + .map(|instruction| { + let row = instruction + .try_jolt_instruction_row() + .map_err(ExpansionError::IllegalSourceInstruction)?; + JoltInstruction::try_from(row).map_err(ExpansionError::IllegalTargetInstruction) + }) + .collect::, _>>() } } @@ -140,6 +154,10 @@ impl RISCVInstruction for INLINE { &self.operands } + fn source_kind(&self) -> jolt_riscv::SourceInstructionKind { + jolt_riscv::SourceInstructionKind::Inline + } + fn new(word: u32, address: u64, _validate: bool, is_compressed: bool) -> Self { Self { opcode: word & 0x7f, @@ -179,13 +197,9 @@ impl INLINE { panic!("Inline instructions must use trace(), not exec()"); } - pub fn inline_sequence( - &self, - allocator: &VirtualRegisterAllocator, - xlen: Xlen, - ) -> Vec { + pub fn inline_sequence(&self, allocator: &VirtualRegisterAllocator) -> Vec { let reg = find_inline(self.opcode, self.funct3, self.funct7); - let asm = InstrAssembler::new_inline(self.address, self.is_compressed, xlen, allocator); + let asm = InstrAssembler::new_inline(self.address, self.is_compressed, allocator); (reg.build_sequence)(asm, self.operands) } } @@ -204,14 +218,9 @@ impl RISCVTrace for INLINE { let reg = find_inline(self.opcode, self.funct3, self.funct7); let advice_allocator = VirtualRegisterAllocator::new(); - let asm = InstrAssembler::new_inline( - self.address, - self.is_compressed, - cpu.xlen, - &advice_allocator, - ); + let asm = InstrAssembler::new_inline(self.address, self.is_compressed, &advice_allocator); if let Some(mut advice) = (reg.build_advice)(asm, self.operands, cpu) { - let mut inline_sequence = self.inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = self.inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence.iter_mut() { if let Instruction::VirtualAdvice(va) = instr { @@ -235,7 +244,7 @@ impl RISCVTrace for INLINE { self.funct7 ); } else { - let inline_sequence = self.inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = self.inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace_raw(cpu, trace.as_deref_mut()); @@ -244,32 +253,37 @@ impl RISCVTrace for INLINE { } } -impl From for INLINE { - fn from(_: NormalizedInstruction) -> Self { - unimplemented!("Inline::from(NormalizedInstruction) should not be called"); +#[cfg(test)] +mod tests { + use super::*; + use crate::instruction::addi::ADDI; + + const TEST_INLINE_WORD: u32 = 0xfc00_602b; + + fn test_sequence(mut asm: InstrAssembler, _operands: FormatInline) -> Vec { + asm.emit_i::(40, 0, 0); + asm.finalize_inline() } -} -impl jolt_riscv::JoltInstruction for INLINE {} - -impl From for NormalizedInstruction { - fn from(instr: INLINE) -> Self { - let mut operands: NormalizedOperands = instr.operands.into(); - operands.imm = (instr.opcode | (instr.funct3 << 7) | (instr.funct7 << 10)) as i128; - NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Inline, - address: instr.address as usize, - operands, - virtual_sequence_remaining: instr.virtual_sequence_remaining, - is_first_in_sequence: instr.is_first_in_sequence, - is_compressed: instr.is_compressed, - } + fn test_advice( + _asm: InstrAssembler, + _operands: FormatInline, + _cpu: &mut Cpu, + ) -> Option> { + None } -} -#[cfg(test)] -mod tests { - use super::*; + inventory::submit! { + InlineRegistration { + opcode: 0x2b, + funct3: 0x6, + funct7: 0x7e, + extension: InlineExtension::Sha2, + name: "TEST_INLINE_PROFILE", + build_sequence: test_sequence, + build_advice: test_advice, + } + } #[test] fn test_inline_parsing() { @@ -305,12 +319,47 @@ mod tests { fn provider_rejects_unregistered_inline() { let mut provider = TracerInlineExpansionProvider::new(); let mut allocator = ExpansionAllocator::new(); - let instruction: NormalizedInstruction = - INLINE::new(0xfe00_7fab, 0x8000_0000, false, false).into(); + let instruction = Instruction::from(INLINE::new(0xfe00_7fab, 0x8000_0000, false, false)) + .source_instruction(); assert!(matches!( - provider.expand_inline(&instruction, &mut allocator), + provider.expand_inline( + &instruction, + &mut allocator, + jolt_riscv::RV64IMAC_JOLT_ALL_INLINES, + ), Err(ExpansionError::UnsupportedInstruction) )); } + + #[test] + fn provider_rejects_registered_inline_disabled_by_profile() { + let mut provider = TracerInlineExpansionProvider::new(); + let mut allocator = ExpansionAllocator::new(); + let instruction = + Instruction::from(INLINE::new(TEST_INLINE_WORD, 0x8000_0000, false, false)) + .source_instruction(); + + assert!(matches!( + provider.expand_inline(&instruction, &mut allocator, jolt_riscv::RV64IMAC_JOLT,), + Err(ExpansionError::UnsupportedInstruction) + )); + } + + #[test] + fn provider_accepts_registered_inline_enabled_by_profile() { + let mut provider = TracerInlineExpansionProvider::new(); + let mut allocator = ExpansionAllocator::new(); + let instruction = + Instruction::from(INLINE::new(TEST_INLINE_WORD, 0x8000_0000, false, false)) + .source_instruction(); + + assert!(provider + .expand_inline( + &instruction, + &mut allocator, + jolt_riscv::RV64IMAC_JOLT_ALL_INLINES, + ) + .is_ok()); + } } diff --git a/tracer/src/instruction/jal.rs b/tracer/src/instruction/jal.rs index c676125c6a..dd7ee7da62 100644 --- a/tracer/src/instruction/jal.rs +++ b/tracer/src/instruction/jal.rs @@ -26,8 +26,7 @@ impl JAL { } cpu.write_register(self.operands.rd as usize, cpu.sign_extend(cpu.pc as i64)); } - cpu.pc = ((self.address as i64).wrapping_add(normalize_imm(self.operands.imm, &cpu.xlen))) - as u64; + cpu.pc = ((self.address as i64).wrapping_add(normalize_imm(self.operands.imm))) as u64; } } diff --git a/tracer/src/instruction/jalr.rs b/tracer/src/instruction/jalr.rs index f71132c413..1aaae02e05 100644 --- a/tracer/src/instruction/jalr.rs +++ b/tracer/src/instruction/jalr.rs @@ -17,8 +17,8 @@ impl JALR { // self.address is the instruction address. fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { let tmp = cpu.sign_extend(cpu.pc as i64); - cpu.pc = (cpu.x[self.operands.rs1 as usize] - .wrapping_add(normalize_imm(self.operands.imm, &cpu.xlen)) as u64) + cpu.pc = (cpu.x[self.operands.rs1 as usize].wrapping_add(normalize_imm(self.operands.imm)) + as u64) & !1; if self.operands.rd != 0 { // Skip returns (rd=0) and non-standard link registers diff --git a/tracer/src/instruction/lb.rs b/tracer/src/instruction/lb.rs index 594e6211a4..a48bcdb712 100644 --- a/tracer/src/instruction/lb.rs +++ b/tracer/src/instruction/lb.rs @@ -32,7 +32,7 @@ impl LB { impl RISCVTrace for LB { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lbu.rs b/tracer/src/instruction/lbu.rs index 124015d737..bdad85b453 100644 --- a/tracer/src/instruction/lbu.rs +++ b/tracer/src/instruction/lbu.rs @@ -33,7 +33,7 @@ impl LBU { impl RISCVTrace for LBU { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lh.rs b/tracer/src/instruction/lh.rs index dcbcb3775d..428081a4fb 100644 --- a/tracer/src/instruction/lh.rs +++ b/tracer/src/instruction/lh.rs @@ -34,7 +34,7 @@ impl LH { impl RISCVTrace for LH { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lhu.rs b/tracer/src/instruction/lhu.rs index 9b7e0b18bf..27d208f978 100644 --- a/tracer/src/instruction/lhu.rs +++ b/tracer/src/instruction/lhu.rs @@ -35,7 +35,7 @@ impl LHU { impl RISCVTrace for LHU { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lrd.rs b/tracer/src/instruction/lrd.rs index 6b398890a5..9da0cf7fb0 100644 --- a/tracer/src/instruction/lrd.rs +++ b/tracer/src/instruction/lrd.rs @@ -44,7 +44,7 @@ impl RISCVTrace for LRD { let address = cpu.x[self.operands.rs1 as usize] as u64; cpu.set_reservation(address, ReservationWidth::Doubleword); - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lrw.rs b/tracer/src/instruction/lrw.rs index 71b620ca3d..9fb2d62625 100644 --- a/tracer/src/instruction/lrw.rs +++ b/tracer/src/instruction/lrw.rs @@ -44,7 +44,7 @@ impl RISCVTrace for LRW { let address = cpu.x[self.operands.rs1 as usize] as u64; cpu.set_reservation(address, ReservationWidth::Word); - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { diff --git a/tracer/src/instruction/lui.rs b/tracer/src/instruction/lui.rs index 40d88e59c2..6dd9d45a88 100644 --- a/tracer/src/instruction/lui.rs +++ b/tracer/src/instruction/lui.rs @@ -17,10 +17,7 @@ declare_riscv_instr!( impl LUI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - cpu.write_register( - self.operands.rd as usize, - normalize_imm(self.operands.imm, &cpu.xlen), - ); + cpu.write_register(self.operands.rd as usize, normalize_imm(self.operands.imm)); } } diff --git a/tracer/src/instruction/lw.rs b/tracer/src/instruction/lw.rs index c20ad25f22..cb1b0dd46c 100644 --- a/tracer/src/instruction/lw.rs +++ b/tracer/src/instruction/lw.rs @@ -34,7 +34,7 @@ impl LW { impl RISCVTrace for LW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/lwu.rs b/tracer/src/instruction/lwu.rs index bebbbef3f4..3c022004b2 100644 --- a/tracer/src/instruction/lwu.rs +++ b/tracer/src/instruction/lwu.rs @@ -35,7 +35,7 @@ impl LWU { impl RISCVTrace for LWU { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/mod.rs b/tracer/src/instruction/mod.rs index d9e48ce98d..08537a23b5 100644 --- a/tracer/src/instruction/mod.rs +++ b/tracer/src/instruction/mod.rs @@ -152,12 +152,13 @@ use virtual_zero_extend_word::VirtualZeroExtendWord; use self::inline::INLINE; -use crate::emulator::cpu::{Cpu, Xlen}; +use crate::emulator::cpu::Cpu; use crate::utils::virtual_registers::{is_supported_csr, VirtualRegisterAllocator}; use derive_more::From; use format::{InstructionFormat, InstructionRegisterState, NormalizedOperands}; -use jolt_riscv::InstructionKind; -pub use jolt_riscv::NormalizedInstruction; +pub use jolt_riscv::JoltInstructionRow; +use jolt_riscv::{JoltInstructionKind, SourceInlineKey, SourceInstructionKind, RV64IMAC_JOLT}; +pub use jolt_riscv::{SourceInstruction, SourceInstructionRow}; pub mod format; @@ -360,14 +361,7 @@ impl From<()> for RAMAccess { } } -pub trait RISCVInstruction: - std::fmt::Debug - + Sized - + Copy - + Into - + From - + Into -{ +pub trait RISCVInstruction: std::fmt::Debug + Sized + Copy + Into { const MASK: u32; const MATCH: u32; @@ -375,6 +369,7 @@ pub trait RISCVInstruction: type RAMAccess: Default + Into + Copy + std::fmt::Debug; fn operands(&self) -> &Self::Format; + fn source_kind(&self) -> SourceInstructionKind; fn new(word: u32, address: u64, validate: bool, compressed: bool) -> Self; #[cfg(any(feature = "test-utils", test))] fn random(rng: &mut rand::rngs::StdRng) -> Self { @@ -385,8 +380,7 @@ pub trait RISCVInstruction: fn execute(&self, cpu: &mut Cpu, ram_access: &mut Self::RAMAccess); fn has_side_effects(&self) -> bool { - let instruction: NormalizedInstruction = (*self).into(); - instruction.instruction_kind.has_side_effects() + self.source_kind().has_side_effects() } } @@ -411,9 +405,9 @@ where } } -macro_rules! define_rv32im_enums { +macro_rules! define_rv64imac_enums { ( - instructions: [$($instr:ident),* $(,)?] + instructions: [$($instr:ident => $marker:ident => $canonical_name:expr),* $(,)?] ) => { #[derive(Debug, IntoStaticStr, From, Clone, Serialize, Deserialize, EnumIter)] pub enum Instruction { @@ -563,10 +557,10 @@ macro_rules! define_rv32im_enums { impl Instruction { pub fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let normalized = self.normalize(); + let source = self.source_instruction(); // Rewrite instructions with rd=x0 via inline_sequence so the // constraint system never sees rd=x0. - if normalized.operands.rd == Some(0) + if source.row().operands.rd == Some(0) && !matches!( self, Instruction::SCW(_) @@ -576,7 +570,7 @@ macro_rules! define_rv32im_enums { | Instruction::INLINE(_) ) { - let inline_sequence = self.inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = self.inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace_raw(cpu, trace.as_deref_mut()); @@ -629,8 +623,73 @@ macro_rules! define_rv32im_enums { } } - pub fn normalize(&self) -> NormalizedInstruction { - self.into() + pub fn try_jolt_instruction_row(&self) -> Result { + match self { + Instruction::NoOp => Ok(Default::default()), + Instruction::UNIMPL => Err(SourceInstructionKind::Unimpl), + $( + Instruction::$instr(instr) => { + let source_kind = + jolt_riscv::SourceInstruction::$marker( + jolt_riscv::instructions::$marker(()) + ); + let Some(instruction_kind) = source_kind.jolt_kind() else { + return Err(source_kind); + }; + Ok(JoltInstructionRow { + instruction_kind, + address: instr.address as usize, + operands: instr.operands.into(), + virtual_sequence_remaining: instr.virtual_sequence_remaining, + is_first_in_sequence: instr.is_first_in_sequence, + is_compressed: instr.is_compressed, + }) + }, + )* + Instruction::INLINE(_) => Err(SourceInstructionKind::Inline), + } + } + + pub fn source_instruction(&self) -> SourceInstruction { + if let Instruction::INLINE(inline) = self { + return SourceInstruction::new( + SourceInstructionKind::Inline, + SourceInstructionRow { + address: inline.address as usize, + operands: inline.operands.into(), + inline: Some(SourceInlineKey { + opcode: inline.opcode as u8, + funct3: inline.funct3 as u8, + funct7: inline.funct7 as u8, + }), + is_compressed: inline.is_compressed, + }, + ); + } + match self { + Instruction::NoOp => SourceInstruction::new( + SourceInstructionKind::NoOp, + SourceInstructionRow::default(), + ), + Instruction::UNIMPL => SourceInstruction::new( + SourceInstructionKind::Unimpl, + SourceInstructionRow::default(), + ), + $( + Instruction::$instr(instr) => SourceInstruction::new( + jolt_riscv::SourceInstruction::$marker( + jolt_riscv::instructions::$marker(()) + ), + SourceInstructionRow { + address: instr.address as usize, + operands: instr.operands.into(), + inline: None, + is_compressed: instr.is_compressed, + }, + ), + )* + Instruction::INLINE(_) => unreachable!("inline source returned above"), + } } pub fn has_side_effects(&self) -> bool { @@ -644,45 +703,60 @@ macro_rules! define_rv32im_enums { } } - pub fn inline_sequence(&self, allocator: &VirtualRegisterAllocator, xlen: Xlen) -> Vec { + pub fn inline_sequence(&self, allocator: &VirtualRegisterAllocator) -> Vec { if let Instruction::INLINE(inline) = self { - return inline.inline_sequence(allocator, xlen); + return inline.inline_sequence(allocator); } let mut expansion_allocator = jolt_program::expand::ExpansionAllocator::new(); jolt_program::expand::expand_instruction( - &self.normalize(), + &self.source_instruction(), &mut expansion_allocator, + RV64IMAC_JOLT, ) .expect("jolt-program bytecode expansion failed") .into_iter() + .map(JoltInstructionRow::from) .map(|instruction| { - Instruction::try_from_normalized(instruction) + Instruction::try_from_jolt_instruction_row(instruction) .expect("jolt-program expansion produced an instruction unknown to tracer") }) .collect() } - pub fn try_from_normalized(instruction: NormalizedInstruction) -> Result { - match instruction.instruction_kind { - InstructionKind::NoOp => Ok(Instruction::NoOp), - InstructionKind::Unimpl => Ok(Instruction::UNIMPL), + pub fn try_from_jolt_instruction_row(instruction: JoltInstructionRow) -> Result { + instruction_from_final_jolt_row(instruction) + } + + pub fn try_from_source_instruction(instruction: SourceInstruction) -> Result { + let kind = instruction.kind(); + let row = instruction.into_row(); + if kind == SourceInstructionKind::Inline { + let inline = row + .inline + .ok_or("missing inline source metadata")?; + return Ok(INLINE { + opcode: inline.opcode as u32, + funct3: inline.funct3 as u32, + funct7: inline.funct7 as u32, + address: row.address as u64, + operands: row.operands.into(), + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: row.is_compressed, + } + .into()); + } + match kind { + jolt_riscv::SourceInstruction::Noop(_) => Ok(Instruction::NoOp), + jolt_riscv::SourceInstruction::Unimplemented(_) => Ok(Instruction::UNIMPL), $( - InstructionKind::$instr => Ok(<$instr as From>::from(instruction).into()), + jolt_riscv::SourceInstruction::$marker(_) => Ok( + <$instr as From>::from(row).into() + ), )* - InstructionKind::Inline => { - let metadata = instruction.operands.imm as u32; - let inline = INLINE { - opcode: metadata & 0x7f, - funct3: (metadata >> 7) & 0x7, - funct7: (metadata >> 10) & 0x7f, - address: instruction.address as u64, - operands: instruction.operands.into(), - virtual_sequence_remaining: instruction.virtual_sequence_remaining, - is_first_in_sequence: instruction.is_first_in_sequence, - is_compressed: instruction.is_compressed, - }; - Ok(inline.into()) - } + jolt_riscv::SourceInstruction::InlineDispatch(_) => { + unreachable!("inline source returned above") + }, } } @@ -718,46 +792,88 @@ macro_rules! define_rv32im_enums { Instruction::INLINE(instr) => {instr.is_compressed = is_compressed;} } } - } - impl From<&Instruction> for NormalizedInstruction { - fn from(instr: &Instruction) -> Self { - match instr { - Instruction::NoOp => Default::default(), - Instruction::UNIMPL => Default::default(), + pub fn virtual_sequence_remaining(&self) -> Option { + match self { + Instruction::NoOp | Instruction::UNIMPL => None, $( - Instruction::$instr(instr) => NormalizedInstruction { - instruction_kind: InstructionKind::$instr, - address: instr.address as usize, - operands: instr.operands.into(), - virtual_sequence_remaining: instr.virtual_sequence_remaining, - is_first_in_sequence: instr.is_first_in_sequence, - is_compressed: instr.is_compressed, - }, + Instruction::$instr(instr) => instr.virtual_sequence_remaining, )* - Instruction::INLINE(instr) => NormalizedInstruction { - instruction_kind: InstructionKind::Inline, + Instruction::INLINE(instr) => instr.virtual_sequence_remaining, + } + } + } + + }; +} + +jolt_riscv::for_each_instruction_kind!(define_rv64imac_enums); + +macro_rules! define_final_jolt_row_conversion { + ( + instructions: [$($instr:ident => $marker:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + fn instruction_from_final_jolt_row( + instruction: JoltInstructionRow, + ) -> Result { + match instruction.instruction_kind { + JoltInstructionKind::NoOp => Ok(Instruction::NoOp), + $( + jolt_riscv::JoltInstruction::$marker(_) => { + Ok(<$instr as From>::from(instruction).into()) + } + )* + } + } + }; +} + +jolt_riscv::for_each_jolt_instruction_kind!(define_final_jolt_row_conversion); + +macro_rules! impl_final_jolt_row_data { + ( + instructions: [$($instr:ident => $marker:ident => ($tag:expr, $canonical_name:expr)),* $(,)?] + ) => { + $( + impl jolt_riscv::JoltInstructionRowData for $instr {} + + impl_final_jolt_row_data!(@from_row $instr); + + impl From<$instr> for JoltInstructionRow { + fn from(instr: $instr) -> JoltInstructionRow { + JoltInstructionRow { + instruction_kind: jolt_riscv::JoltInstruction::$marker( + jolt_riscv::instructions::$marker(()) + ), address: instr.address as usize, - operands: { - let mut operands: NormalizedOperands = instr.operands.into(); - operands.imm = inline_metadata(instr.opcode, instr.funct3, instr.funct7); - operands - }, + operands: instr.operands.into(), + is_compressed: instr.is_compressed, virtual_sequence_remaining: instr.virtual_sequence_remaining, is_first_in_sequence: instr.is_first_in_sequence, - is_compressed: instr.is_compressed, - }, + } + } + } + )* + }; + + (@from_row VirtualAdvice) => {}; + + (@from_row $instr:ident) => { + impl From for $instr { + fn from(row: JoltInstructionRow) -> Self { + Self { + address: row.address as u64, + operands: row.operands.into(), + virtual_sequence_remaining: row.virtual_sequence_remaining, + is_first_in_sequence: row.is_first_in_sequence, + is_compressed: row.is_compressed, } } } }; } -jolt_riscv::for_each_instruction_kind!(define_rv32im_enums); - -fn inline_metadata(opcode: u32, funct3: u32, funct7: u32) -> i128 { - (opcode | (funct3 << 7) | (funct7 << 10)) as i128 -} +jolt_riscv::for_each_jolt_instruction_kind!(impl_final_jolt_row_data); impl CanonicalSerialize for Instruction { fn serialize_with_mode( @@ -811,7 +927,7 @@ impl Instruction { return false; } - match self.normalize().virtual_sequence_remaining { + match self.virtual_sequence_remaining() { None => true, // ordinary instruction Some(0) => true, // "anchor" of a inline sequence Some(_) => false, // helper within the sequence @@ -936,7 +1052,7 @@ impl Instruction { (0b110, 0b0000000) => Ok(OR::new(instr, address, true, compressed).into()), (0b111, 0b0000000) => Ok(AND::new(instr, address, true, compressed).into()), - // RV32M extension + // M extension (0b000, 0b0000001) => Ok(MUL::new(instr, address, true, compressed).into()), (0b001, 0b0000001) => Ok(MULH::new(instr, address, true, compressed).into()), (0b010, 0b0000001) => Ok(MULHSU::new(instr, address, true, compressed).into()), @@ -1107,7 +1223,7 @@ impl Instruction { } // @TODO: Optimize -pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { +pub fn uncompress_instruction(halfword: u32) -> u32 { let op = halfword & 0x3; // [1:0] let funct3 = (halfword >> 13) & 0x7; // [15:13] @@ -1147,8 +1263,7 @@ pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { return (offset << 20) | ((rs1 + 8) << 15) | (2 << 12) | ((rd + 8) << 7) | 0x3; } 3 => { - // @TODO: Support C.FLW in 32-bit mode - // C.LD in 64-bit mode + // C.LD // ld rd+8, offset(rs1+8) let rs1 = (halfword >> 7) & 0x7; // [9:7] let rd = (halfword >> 2) & 0x7; // [4:2] @@ -1193,7 +1308,6 @@ pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { | 0x23; } 7 => { - // @TODO: Support C.FSW in 32-bit mode // C.SD // sd rs2+8, offset(rs1+8) let rs1 = (halfword >> 7) & 0x7; // [9:7] @@ -1242,47 +1356,22 @@ pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { } } 1 => { - match xlen { - Xlen::Bit32 => { - // C.JAL (RV32C only) - // jal x1, offset - let offset = match halfword & 0x1000 { - 0x1000 => 0xfffff000, - _ => 0 - } | // offset[31:12] <= [12] - ((halfword >> 1) & 0x800) | // offset[11] <= [12] - ((halfword >> 7) & 0x10) | // offset[4] <= [11] - ((halfword >> 1) & 0x300) | // offset[9:8] <= [10:9] - ((halfword << 2) & 0x400) | // offset[10] <= [8] - ((halfword >> 1) & 0x40) | // offset[6] <= [7] - ((halfword << 1) & 0x80) | // offset[7] <= [6] - ((halfword >> 2) & 0xe) | // offset[3:1] <= [5:3] - ((halfword << 3) & 0x20); // offset[5] <= [2] - let imm = ((offset >> 1) & 0x80000) | // imm[19] <= offset[20] - ((offset << 8) & 0x7fe00) | // imm[18:9] <= offset[10:1] - ((offset >> 3) & 0x100) | // imm[8] <= offset[11] - ((offset >> 12) & 0xff); // imm[7:0] <= offset[19:12] - return (imm << 12) | (1 << 7) | 0x6f; - } - Xlen::Bit64 => { - // C.ADDIW (RV64C only) - let r = (halfword >> 7) & 0x1f; - let imm = match halfword & 0x1000 { + // C.ADDIW + let r = (halfword >> 7) & 0x1f; + let imm = match halfword & 0x1000 { 0x1000 => 0xffffffc0, _ => 0 } | // imm[31:6] <= [12] ((halfword >> 7) & 0x20) | // imm[5] <= [12] ((halfword >> 2) & 0x1f); // imm[4:0] <= [6:2] - if r == 0 { - // Reserved - } else if imm == 0 { - // sext.w rd - return (r << 15) | (r << 7) | 0x1b; - } else { - // addiw r, r, imm - return (imm << 20) | (r << 15) | (r << 7) | 0x1b; - } - } + if r == 0 { + // Reserved + } else if imm == 0 { + // sext.w rd + return (r << 15) | (r << 7) | 0x1b; + } else { + // addiw r, r, imm + return (imm << 20) | (r << 15) | (r << 7) | 0x1b; } } 2 => { @@ -1558,7 +1647,6 @@ pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { // r == 0 is reserved instruction } 3 => { - // @TODO: Support C.FLWSP in 32-bit mode // C.LDSP // ld rd, offset(x2) let rd = (halfword >> 7) & 0x1f; @@ -1654,7 +1742,6 @@ pub fn uncompress_instruction(halfword: u32, xlen: Xlen) -> u32 { | 0x23; } 7 => { - // @TODO: Support C.FSWSP in 32-bit mode // C.SDSP // sd rs, offset(x2) let rs2 = (halfword >> 2) & 0x1f; // [6:2] @@ -1688,10 +1775,12 @@ impl RISCVCycle { #[cfg(any(feature = "test-utils", test))] pub fn random(&self, rng: &mut rand::rngs::StdRng) -> Self { let instruction = T::random(rng); + let concrete: Instruction = instruction.into(); + let source_instruction = concrete.source_instruction(); let register_state = <::RegisterState as InstructionRegisterState>::random( rng, - &Into::::into(instruction).operands, + &source_instruction.row().operands, ); Self { instruction, @@ -1705,9 +1794,37 @@ impl RISCVCycle { mod tests { use super::*; + #[test] + fn source_only_tracer_conversion_does_not_fabricate_final_kind() { + let source = SourceInstruction::new( + SourceInstructionKind::ADDW, + SourceInstructionRow { + address: 0x1234, + operands: NormalizedOperands { + rd: Some(7), + rs1: Some(5), + rs2: Some(6), + imm: 0, + }, + inline: None, + is_compressed: true, + }, + ); + + let instruction = Instruction::try_from_source_instruction(source).unwrap(); + assert!(instruction.try_jolt_instruction_row().is_err()); + let Instruction::ADDW(addw) = instruction else { + panic!("expected ADDW tracer instruction"); + }; + assert_eq!(addw.address, 0x1234); + assert_eq!(addw.virtual_sequence_remaining, None); + assert!(!addw.is_first_in_sequence); + assert!(addw.is_compressed); + } + #[test] // Check that the size of Cycle is as expected. - fn rv32im_cycle_size() { + fn rv64imac_cycle_size() { let size = size_of::(); let expected = 96; assert_eq!( diff --git a/tracer/src/instruction/mret.rs b/tracer/src/instruction/mret.rs index 3b6f67d85a..ac8cee7f17 100644 --- a/tracer/src/instruction/mret.rs +++ b/tracer/src/instruction/mret.rs @@ -52,7 +52,7 @@ impl RISCVTrace for MRET { // Generate and execute inline sequence // The inline sequence reads mepc from virtual register (source of truth for proofs) - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { diff --git a/tracer/src/instruction/mulh.rs b/tracer/src/instruction/mulh.rs index 6a8f0c65d8..56b340f321 100644 --- a/tracer/src/instruction/mulh.rs +++ b/tracer/src/instruction/mulh.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -19,23 +16,16 @@ impl MULH { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - match cpu.xlen { - Xlen::Bit32 => cpu.sign_extend( - (cpu.x[self.operands.rs1 as usize] * cpu.x[self.operands.rs2 as usize]) >> 32, - ), - Xlen::Bit64 => { - (((cpu.x[self.operands.rs1 as usize] as i128) - * (cpu.x[self.operands.rs2 as usize] as i128)) - >> 64) as i64 - } - }, + (((cpu.x[self.operands.rs1 as usize] as i128) + * (cpu.x[self.operands.rs2 as usize] as i128)) + >> 64) as i64, ); } } impl RISCVTrace for MULH { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/mulhsu.rs b/tracer/src/instruction/mulhsu.rs index ddbed49fff..74a76b6172 100644 --- a/tracer/src/instruction/mulhsu.rs +++ b/tracer/src/instruction/mulhsu.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -19,18 +16,9 @@ impl MULHSU { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - match cpu.xlen { - Xlen::Bit32 => cpu.sign_extend( - cpu.x[self.operands.rs1 as usize] - .wrapping_mul(cpu.x[self.operands.rs2 as usize] as u32 as i64) - >> 32, - ), - Xlen::Bit64 => { - ((cpu.x[self.operands.rs1 as usize] as i128 as u128) - .wrapping_mul(cpu.x[self.operands.rs2 as usize] as u64 as u128) - >> 64) as i64 - } - }, + ((cpu.x[self.operands.rs1 as usize] as i128 as u128) + .wrapping_mul(cpu.x[self.operands.rs2 as usize] as u64 as u128) + >> 64) as i64, ); } } @@ -47,7 +35,7 @@ impl MULHSU { impl RISCVTrace for MULHSU { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); @@ -69,7 +57,6 @@ mod tests { #[test] fn test_mulhsu_negative_rs1() { let mut cpu = Cpu::new(Box::new(DefaultTerminal::default())); - cpu.update_xlen(Xlen::Bit64); // MULHSU rd=x1, rs1=x2, rs2=x3 let instr = MULHSU::with_regs(1, 2, 3); diff --git a/tracer/src/instruction/mulhu.rs b/tracer/src/instruction/mulhu.rs index 24f83bb115..835b7eae1f 100644 --- a/tracer/src/instruction/mulhu.rs +++ b/tracer/src/instruction/mulhu.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, RISCVInstruction, RISCVTrace}; @@ -19,18 +16,9 @@ impl MULHU { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - match cpu.xlen { - Xlen::Bit32 => cpu.sign_extend( - (((cpu.x[self.operands.rs1 as usize] as u32 as u64) - * (cpu.x[self.operands.rs2 as usize] as u32 as u64)) - >> 32) as i64, - ), - Xlen::Bit64 => { - ((cpu.x[self.operands.rs1 as usize] as u64 as u128) - .wrapping_mul(cpu.x[self.operands.rs2 as usize] as u64 as u128) - >> 64) as i64 - } - }, + ((cpu.x[self.operands.rs1 as usize] as u64 as u128) + .wrapping_mul(cpu.x[self.operands.rs2 as usize] as u64 as u128) + >> 64) as i64, ); } } diff --git a/tracer/src/instruction/mulw.rs b/tracer/src/instruction/mulw.rs index 5923809aff..bdc170b7b1 100644 --- a/tracer/src/instruction/mulw.rs +++ b/tracer/src/instruction/mulw.rs @@ -25,7 +25,7 @@ impl MULW { impl RISCVTrace for MULW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { diff --git a/tracer/src/instruction/ori.rs b/tracer/src/instruction/ori.rs index 990636be61..b5d41f6c0c 100644 --- a/tracer/src/instruction/ori.rs +++ b/tracer/src/instruction/ori.rs @@ -19,9 +19,7 @@ impl ORI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - cpu.sign_extend( - cpu.x[self.operands.rs1 as usize] | normalize_imm(self.operands.imm, &cpu.xlen), - ), + cpu.sign_extend(cpu.x[self.operands.rs1 as usize] | normalize_imm(self.operands.imm)), ); } } diff --git a/tracer/src/instruction/rem.rs b/tracer/src/instruction/rem.rs index eb495d0de0..61af03acb7 100644 --- a/tracer/src/instruction/rem.rs +++ b/tracer/src/instruction/rem.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -45,33 +42,17 @@ impl RISCVTrace for REM { let x = cpu.x[self.operands.rs1 as usize]; let y = cpu.x[self.operands.rs2 as usize]; - let (quotient, remainder) = match cpu.xlen { - Xlen::Bit32 => { - if y == 0 { - (u32::MAX as u64, (x as i32).unsigned_abs() as u64) - } else if x == cpu.most_negative() && y == -1 { - (x as u32 as u64, 0) - } else { - let quotient = x as i32 / y as i32; - let remainder = (x as i32 % y as i32).unsigned_abs(); - (quotient as u32 as u64, remainder as u64) - } - } - Xlen::Bit64 => { - if y == 0 { - (u64::MAX, x.unsigned_abs()) - } else if x == cpu.most_negative() && y == -1 { - (x as u64, 0) - } else { - let quotient = x / y; - let remainder = x % y; - (quotient as u64, remainder.unsigned_abs()) - } - } + let (quotient, remainder) = if y == 0 { + (u64::MAX, x.unsigned_abs()) + } else if x == cpu.most_negative() && y == -1 { + (x as u64, 0) + } else { + let quotient = x / y; + let remainder = x % y; + (quotient as u64, remainder.unsigned_abs()) }; - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient, remainder]); let mut trace = trace; diff --git a/tracer/src/instruction/remu.rs b/tracer/src/instruction/remu.rs index 47fd8553de..03440d5247 100644 --- a/tracer/src/instruction/remu.rs +++ b/tracer/src/instruction/remu.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -34,13 +31,9 @@ impl REMU { impl RISCVTrace for REMU { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let quotient = if cpu.unsigned_data(cpu.x[self.operands.rs2 as usize]) == 0 { - match cpu.xlen { - Xlen::Bit32 => u32::MAX as u64, - Xlen::Bit64 => u64::MAX, - } + u64::MAX } else { cpu.unsigned_data(cpu.x[self.operands.rs1 as usize]) / cpu.unsigned_data(cpu.x[self.operands.rs2 as usize]) diff --git a/tracer/src/instruction/remuw.rs b/tracer/src/instruction/remuw.rs index de009778c9..2aed760201 100644 --- a/tracer/src/instruction/remuw.rs +++ b/tracer/src/instruction/remuw.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -42,21 +39,9 @@ impl RISCVTrace for REMUW { let x = cpu.x[self.operands.rs1 as usize] as u32; let y = cpu.x[self.operands.rs2 as usize] as u32; - let quotient = match cpu.xlen { - Xlen::Bit32 => { - panic!("REMUW is invalid in 32b mode"); - } - Xlen::Bit64 => { - if y == 0 { - u32::MAX as u64 // 32-bit operation: quotient is u32::MAX - } else { - (x / y) as u64 - } - } - }; + let quotient = x.checked_div(y).map_or(u32::MAX as u64, u64::from); - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient]); let mut trace = trace; diff --git a/tracer/src/instruction/remw.rs b/tracer/src/instruction/remw.rs index 9a58a64742..067c805e28 100644 --- a/tracer/src/instruction/remw.rs +++ b/tracer/src/instruction/remw.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{ fill_virtual_advice, format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, @@ -44,25 +41,17 @@ impl RISCVTrace for REMW { let x = cpu.x[self.operands.rs1 as usize] as i32; let y = cpu.x[self.operands.rs2 as usize] as i32; - let (quotient, remainder) = match cpu.xlen { - Xlen::Bit32 => { - panic!("REMW is invalid in 32b mode"); - } - Xlen::Bit64 => { - if y == 0 { - (-1i32, x.unsigned_abs()) - } else if y == -1 && x == i32::MIN { - (i32::MIN, 0) //overflow - } else { - let quotient = x / y; - let remainder = x % y; - (quotient, remainder.unsigned_abs()) - } - } + let (quotient, remainder) = if y == 0 { + (-1i32, x.unsigned_abs()) + } else if y == -1 && x == i32::MIN { + (i32::MIN, 0) //overflow + } else { + let quotient = x / y; + let remainder = x % y; + (quotient, remainder.unsigned_abs()) }; - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); fill_virtual_advice(&mut inline_sequence, &[quotient as u64, remainder as u64]); let mut trace = trace; diff --git a/tracer/src/instruction/sb.rs b/tracer/src/instruction/sb.rs index bd9076aa49..85b79a6be3 100644 --- a/tracer/src/instruction/sb.rs +++ b/tracer/src/instruction/sb.rs @@ -29,7 +29,7 @@ impl SB { impl RISCVTrace for SB { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/scd.rs b/tracer/src/instruction/scd.rs index e1fd4c130e..d10bbe47fa 100644 --- a/tracer/src/instruction/scd.rs +++ b/tracer/src/instruction/scd.rs @@ -47,8 +47,7 @@ impl RISCVTrace for SCD { // See SCD::exec — SC.D needs an 8-byte reservation set. let success = cpu.reservation_covers(address, ReservationWidth::Doubleword); - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); // Patch v_success (1=success, 0=failure) into the first VirtualAdvice // in the sequence. Locating it by type avoids fragility against diff --git a/tracer/src/instruction/scw.rs b/tracer/src/instruction/scw.rs index 22c195250d..cbdecd24be 100644 --- a/tracer/src/instruction/scw.rs +++ b/tracer/src/instruction/scw.rs @@ -49,8 +49,7 @@ impl RISCVTrace for SCW { // doubleword) whose set covers the 4 bytes being written. let success = cpu.reservation_covers(address, ReservationWidth::Word); - let mut inline_sequence = - Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let mut inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); // Patch v_success (1=success, 0=failure) into the first VirtualAdvice // in the sequence. Locating it by type avoids fragility against diff --git a/tracer/src/instruction/sh.rs b/tracer/src/instruction/sh.rs index 1d54744fe1..4a2ccefb2e 100644 --- a/tracer/src/instruction/sh.rs +++ b/tracer/src/instruction/sh.rs @@ -30,7 +30,7 @@ impl SH { impl RISCVTrace for SH { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/sll.rs b/tracer/src/instruction/sll.rs index c29cdec643..cde44c9a95 100644 --- a/tracer/src/instruction/sll.rs +++ b/tracer/src/instruction/sll.rs @@ -1,7 +1,4 @@ -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use serde::{Deserialize, Serialize}; use super::{format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -16,10 +13,7 @@ declare_riscv_instr!( impl SLL { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -32,7 +26,7 @@ impl SLL { impl RISCVTrace for SLL { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/slli.rs b/tracer/src/instruction/slli.rs index eda57618cb..92698da36a 100644 --- a/tracer/src/instruction/slli.rs +++ b/tracer/src/instruction/slli.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -17,10 +14,7 @@ declare_riscv_instr!( impl SLLI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -32,7 +26,7 @@ impl SLLI { impl RISCVTrace for SLLI { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); @@ -43,14 +37,12 @@ impl RISCVTrace for SLLI { #[cfg(test)] #[expect(clippy::unwrap_used)] mod tests { - use crate::emulator::cpu::{Cpu, Xlen}; + use crate::emulator::cpu::Cpu; use crate::emulator::default_terminal::DefaultTerminal; use crate::instruction::{uncompress_instruction, Instruction}; fn setup_rv64_cpu() -> Cpu { - let mut cpu = Cpu::new(Box::new(DefaultTerminal::default())); - cpu.update_xlen(Xlen::Bit64); - cpu + Cpu::new(Box::new(DefaultTerminal::default())) } /// c.slli RV64 encoding roundtrip: assembles the compressed halfword, runs it @@ -65,7 +57,7 @@ mod tests { // bits [11:7] = rd, bits [6:2] = imm[4:0], op = 0b10 at bits [1:0]. let halfword: u32 = ((shamt_hi as u32) << 12) | ((rd as u32) << 7) | ((shamt5 as u32) << 2) | 0b10; - let word = uncompress_instruction(halfword, Xlen::Bit64); + let word = uncompress_instruction(halfword); let decoded = Instruction::decode(word, 0x80000000, true).unwrap(); let Instruction::SLLI(ref slli) = decoded else { panic!("expected SLLI after uncompress; got {decoded:?}"); @@ -117,7 +109,7 @@ mod tests { // c.slli x0, 52: funct3=000 (implicit 0), bit12=shamt[5]=1, // rd=x0 (bits[11:7]=0), shamt[4:0]=20 at bits[6:2], op=10. let halfword: u32 = (1u32 << 12) | ((52u32 & 0x1f) << 2) | 0b10; - let word = uncompress_instruction(halfword, Xlen::Bit64); + let word = uncompress_instruction(halfword); assert_ne!( word, 0xffffffff, "c.slli x0, 52 should decode (HINT), not return the sentinel" diff --git a/tracer/src/instruction/slliw.rs b/tracer/src/instruction/slliw.rs index 8952959c65..0f301dc1c0 100644 --- a/tracer/src/instruction/slliw.rs +++ b/tracer/src/instruction/slliw.rs @@ -26,7 +26,7 @@ impl SLLIW { impl RISCVTrace for SLLIW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/sllw.rs b/tracer/src/instruction/sllw.rs index ee0aa13ed9..416ddcadea 100644 --- a/tracer/src/instruction/sllw.rs +++ b/tracer/src/instruction/sllw.rs @@ -27,7 +27,7 @@ impl SLLW { impl RISCVTrace for SLLW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/slti.rs b/tracer/src/instruction/slti.rs index 2156e86c87..53678a5f85 100644 --- a/tracer/src/instruction/slti.rs +++ b/tracer/src/instruction/slti.rs @@ -19,7 +19,7 @@ impl SLTI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - match cpu.x[self.operands.rs1 as usize] < normalize_imm(self.operands.imm, &cpu.xlen) { + match cpu.x[self.operands.rs1 as usize] < normalize_imm(self.operands.imm) { true => 1, false => 0, }, diff --git a/tracer/src/instruction/sltiu.rs b/tracer/src/instruction/sltiu.rs index 98ce43d44d..a5c843f5dc 100644 --- a/tracer/src/instruction/sltiu.rs +++ b/tracer/src/instruction/sltiu.rs @@ -20,7 +20,7 @@ impl SLTIU { cpu.write_register( self.operands.rd as usize, match cpu.unsigned_data(cpu.x[self.operands.rs1 as usize]) - < cpu.unsigned_data(normalize_imm(self.operands.imm, &cpu.xlen)) + < cpu.unsigned_data(normalize_imm(self.operands.imm)) { true => 1, false => 0, diff --git a/tracer/src/instruction/sra.rs b/tracer/src/instruction/sra.rs index 1bde25b9f9..04ac069d70 100644 --- a/tracer/src/instruction/sra.rs +++ b/tracer/src/instruction/sra.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -17,10 +14,7 @@ declare_riscv_instr!( impl SRA { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -33,7 +27,7 @@ impl SRA { impl RISCVTrace for SRA { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/srai.rs b/tracer/src/instruction/srai.rs index 80f99cb009..24e41bfd2e 100644 --- a/tracer/src/instruction/srai.rs +++ b/tracer/src/instruction/srai.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -17,10 +14,7 @@ declare_riscv_instr!( impl SRAI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -32,7 +26,7 @@ impl SRAI { impl RISCVTrace for SRAI { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/sraiw.rs b/tracer/src/instruction/sraiw.rs index 53c35b0c33..223bf3b5cb 100644 --- a/tracer/src/instruction/sraiw.rs +++ b/tracer/src/instruction/sraiw.rs @@ -27,7 +27,7 @@ impl SRAIW { impl RISCVTrace for SRAIW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/sraw.rs b/tracer/src/instruction/sraw.rs index fcac5988a8..bc26cb2ba8 100644 --- a/tracer/src/instruction/sraw.rs +++ b/tracer/src/instruction/sraw.rs @@ -27,7 +27,7 @@ impl SRAW { impl RISCVTrace for SRAW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/srl.rs b/tracer/src/instruction/srl.rs index fb9f0b6ca3..b2985c2bd4 100644 --- a/tracer/src/instruction/srl.rs +++ b/tracer/src/instruction/srl.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -17,10 +14,7 @@ declare_riscv_instr!( impl SRL { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -34,7 +28,7 @@ impl SRL { impl RISCVTrace for SRL { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/srli.rs b/tracer/src/instruction/srli.rs index 6d03c9be58..1b3fc65360 100644 --- a/tracer/src/instruction/srli.rs +++ b/tracer/src/instruction/srli.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, Cycle, Instruction, RISCVInstruction, RISCVTrace}; @@ -17,10 +14,7 @@ declare_riscv_instr!( impl SRLI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - let mask = match cpu.xlen { - Xlen::Bit32 => 0x1f, - Xlen::Bit64 => 0x3f, - }; + let mask = 0x3f; cpu.write_register( self.operands.rd as usize, cpu.sign_extend( @@ -33,7 +27,7 @@ impl SRLI { impl RISCVTrace for SRLI { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/srliw.rs b/tracer/src/instruction/srliw.rs index 7b6e5b0bd1..39d0502e69 100644 --- a/tracer/src/instruction/srliw.rs +++ b/tracer/src/instruction/srliw.rs @@ -27,7 +27,7 @@ impl SRLIW { impl RISCVTrace for SRLIW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/srlw.rs b/tracer/src/instruction/srlw.rs index 7cad378b4e..b3d0b75cd2 100644 --- a/tracer/src/instruction/srlw.rs +++ b/tracer/src/instruction/srlw.rs @@ -27,7 +27,7 @@ impl SRLW { impl RISCVTrace for SRLW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/subw.rs b/tracer/src/instruction/subw.rs index afa05f20ed..d28dbd6484 100644 --- a/tracer/src/instruction/subw.rs +++ b/tracer/src/instruction/subw.rs @@ -29,7 +29,7 @@ impl SUBW { impl RISCVTrace for SUBW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/sw.rs b/tracer/src/instruction/sw.rs index 85b781f7c7..34b2d610a5 100644 --- a/tracer/src/instruction/sw.rs +++ b/tracer/src/instruction/sw.rs @@ -30,7 +30,7 @@ impl SW { impl RISCVTrace for SW { fn trace(&self, cpu: &mut Cpu, trace: Option<&mut Vec>) { - let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator, cpu.xlen); + let inline_sequence = Instruction::from(*self).inline_sequence(&cpu.vr_allocator); let mut trace = trace; for instr in inline_sequence { instr.trace(cpu, trace.as_deref_mut()); diff --git a/tracer/src/instruction/test.rs b/tracer/src/instruction/test.rs index 6a5d895117..d35f25796d 100644 --- a/tracer/src/instruction/test.rs +++ b/tracer/src/instruction/test.rs @@ -3,7 +3,8 @@ use std::panic; use crate::emulator::cpu::Cpu; use crate::instruction::format::{InstructionFormat, InstructionRegisterState}; -use crate::instruction::NormalizedInstruction; +#[cfg(test)] +use jolt_riscv::RV64IMAC_JOLT; #[cfg(test)] use super::{ @@ -18,7 +19,7 @@ use super::{ subw::SUBW, sw::SW, }; -use super::{RISCVInstruction, RISCVTrace}; +use super::{Instruction, RISCVInstruction, RISCVTrace}; use crate::emulator::terminal::DummyTerminal; @@ -58,10 +59,7 @@ fn test_rng() -> StdRng { #[test] fn jolt_program_rv64_decode_matches_tracer_normalization() { - use crate::{ - emulator::cpu::Xlen, - instruction::{uncompress_instruction, Instruction}, - }; + use crate::instruction::{uncompress_instruction, Instruction}; let address = DRAM_BASE; let cases = [ @@ -84,15 +82,20 @@ fn jolt_program_rv64_decode_matches_tracer_normalization() { (0x3001_10f3, false), (0x0000_50db, false), (0x0020_802b, false), - (uncompress_instruction(0x107a, Xlen::Bit64), true), + (uncompress_instruction(0x107a), true), ]; for (word, compressed) in cases { let expected = Instruction::decode(word, address, compressed) .unwrap() - .normalize(); - let actual = - jolt_program::image::decode::decode_instruction(word, address, compressed).unwrap(); + .source_instruction(); + let actual = jolt_program::image::decode::decode_instruction( + word, + address, + compressed, + RV64IMAC_JOLT, + ) + .unwrap(); assert_eq!(actual, expected, "word={word:08x} compressed={compressed}"); } } @@ -106,11 +109,12 @@ where for _ in 0..1000 { let instruction = I::random(&mut rng); - let instr: NormalizedInstruction = instruction.into(); + let concrete: Instruction = instruction.into(); + let source = concrete.source_instruction(); let register_state = <::RegisterState as InstructionRegisterState>::random( &mut rng, - &instr.operands, + &source.row().operands, ); let mut original_cpu = Cpu::new(Box::new(DummyTerminal::default())); @@ -147,12 +151,12 @@ where } } - let rs1 = instr.operands.rs1.unwrap_or(0) as usize; + let rs1 = source.row().operands.rs1.unwrap_or(0) as usize; if let Some(rs1_val) = register_state.rs1_value() { original_cpu.write_register(rs1, rs1_val as i64); virtual_cpu.write_register(rs1, rs1_val as i64); } - let rs2 = instr.operands.rs2.unwrap_or(0) as usize; + let rs2 = source.row().operands.rs2.unwrap_or(0) as usize; if let Some(rs2_val) = register_state.rs2_value() { original_cpu.write_register(rs2, rs2_val as i64); virtual_cpu.write_register(rs2, rs2_val as i64); diff --git a/tracer/src/instruction/virtual_advice.rs b/tracer/src/instruction/virtual_advice.rs index 60f690aa82..f5a1dcf409 100644 --- a/tracer/src/instruction/virtual_advice.rs +++ b/tracer/src/instruction/virtual_advice.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::{emulator::cpu::Cpu, instruction::NormalizedInstruction}; +use crate::{ + emulator::cpu::Cpu, + instruction::{JoltInstructionRow, SourceInstructionRow}, +}; use super::{format::format_j::FormatJ, RISCVInstruction, RISCVTrace}; @@ -31,6 +34,12 @@ impl RISCVInstruction for VirtualAdvice { &self.operands } + fn source_kind(&self) -> jolt_riscv::SourceInstructionKind { + jolt_riscv::SourceInstructionKind::VirtualAdvice( + jolt_riscv::instructions::VirtualAdvice(()), + ) + } + fn new(_: u32, _: u64, _: bool, _: bool) -> Self { panic!("virtual instruction `VirtualAdvice` cannot be built from a machine word"); } @@ -54,8 +63,8 @@ impl RISCVInstruction for VirtualAdvice { } } -impl From for VirtualAdvice { - fn from(ni: NormalizedInstruction) -> Self { +impl From for VirtualAdvice { + fn from(ni: JoltInstructionRow) -> Self { Self { address: ni.address as u64, operands: ni.operands.into(), @@ -67,17 +76,15 @@ impl From for VirtualAdvice { } } -impl jolt_riscv::JoltInstruction for VirtualAdvice {} - -impl From for NormalizedInstruction { - fn from(val: VirtualAdvice) -> Self { - NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::VirtualAdvice, - address: val.address as usize, - operands: val.operands.into(), - is_compressed: val.is_compressed, - is_first_in_sequence: val.is_first_in_sequence, - virtual_sequence_remaining: val.virtual_sequence_remaining, +impl From for VirtualAdvice { + fn from(row: SourceInstructionRow) -> Self { + Self { + address: row.address as u64, + operands: row.operands.into(), + advice: 0, + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: row.is_compressed, } } } diff --git a/tracer/src/instruction/virtual_assert_valid_div0.rs b/tracer/src/instruction/virtual_assert_valid_div0.rs index 4efe494fa4..9f45a5db8a 100644 --- a/tracer/src/instruction/virtual_assert_valid_div0.rs +++ b/tracer/src/instruction/virtual_assert_valid_div0.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_b::FormatB, RISCVInstruction, RISCVTrace}; @@ -19,17 +16,8 @@ impl VirtualAssertValidDiv0 { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { let divisor = cpu.x[self.operands.rs1 as usize]; let quotient = cpu.x[self.operands.rs2 as usize]; - match cpu.xlen { - Xlen::Bit32 => { - if divisor == 0 { - assert!(quotient as u64 as u32 == u32::MAX); - } - } - Xlen::Bit64 => { - if divisor == 0 { - assert!(quotient as u64 == u64::MAX); - } - } + if divisor == 0 { + assert!(quotient as u64 == u64::MAX); } } } diff --git a/tracer/src/instruction/virtual_assert_valid_unsigned_remainder.rs b/tracer/src/instruction/virtual_assert_valid_unsigned_remainder.rs index ce0e6b03a1..3cbb7c9605 100644 --- a/tracer/src/instruction/virtual_assert_valid_unsigned_remainder.rs +++ b/tracer/src/instruction/virtual_assert_valid_unsigned_remainder.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_b::FormatB, RISCVInstruction, RISCVTrace}; @@ -21,18 +18,9 @@ impl VirtualAssertValidUnsignedRemainder { cpu: &mut Cpu, _: &mut ::RAMAccess, ) { - match cpu.xlen { - Xlen::Bit32 => { - let remainder = cpu.x[self.operands.rs1 as usize] as i32 as u32; - let divisor = cpu.x[self.operands.rs2 as usize] as i32 as u32; - assert!(divisor == 0 || remainder < divisor); - } - Xlen::Bit64 => { - let remainder = cpu.x[self.operands.rs1 as usize] as u64; - let divisor = cpu.x[self.operands.rs2 as usize] as u64; - assert!(divisor == 0 || remainder < divisor); - } - } + let remainder = cpu.x[self.operands.rs1 as usize] as u64; + let divisor = cpu.x[self.operands.rs2 as usize] as u64; + assert!(divisor == 0 || remainder < divisor); } } diff --git a/tracer/src/instruction/virtual_change_divisor.rs b/tracer/src/instruction/virtual_change_divisor.rs index 7cea56a496..d679d7ec99 100644 --- a/tracer/src/instruction/virtual_change_divisor.rs +++ b/tracer/src/instruction/virtual_change_divisor.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, RISCVInstruction, RISCVTrace}; @@ -17,25 +14,12 @@ declare_riscv_instr!( impl VirtualChangeDivisor { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => { - let dividend = cpu.x[self.operands.rs1 as usize] as i32; - let divisor = cpu.x[self.operands.rs2 as usize] as i32; - if dividend == i32::MIN && divisor == -1 { - cpu.write_register(self.operands.rd as usize, 1); - } else { - cpu.write_register(self.operands.rd as usize, divisor as i64); - } - } - Xlen::Bit64 => { - let dividend = cpu.x[self.operands.rs1 as usize]; - let divisor = cpu.x[self.operands.rs2 as usize]; - if dividend == i64::MIN && divisor == -1 { - cpu.write_register(self.operands.rd as usize, 1); - } else { - cpu.write_register(self.operands.rd as usize, divisor); - } - } + let dividend = cpu.x[self.operands.rs1 as usize]; + let divisor = cpu.x[self.operands.rs2 as usize]; + if dividend == i64::MIN && divisor == -1 { + cpu.write_register(self.operands.rd as usize, 1); + } else { + cpu.write_register(self.operands.rd as usize, divisor); } } } diff --git a/tracer/src/instruction/virtual_change_divisor_w.rs b/tracer/src/instruction/virtual_change_divisor_w.rs index e4761bc88c..43ca381abf 100644 --- a/tracer/src/instruction/virtual_change_divisor_w.rs +++ b/tracer/src/instruction/virtual_change_divisor_w.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_r::FormatR, RISCVInstruction, RISCVTrace}; @@ -17,19 +14,12 @@ declare_riscv_instr!( impl VirtualChangeDivisorW { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => { - panic!("VirtualChangeDivisorW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = cpu.x[self.operands.rs1 as usize] as i32; - let divisor = cpu.x[self.operands.rs2 as usize] as i32; - if dividend == i32::MIN && divisor == -1 { - cpu.write_register(self.operands.rd as usize, 1); - } else { - cpu.write_register(self.operands.rd as usize, divisor as i64); - } - } + let dividend = cpu.x[self.operands.rs1 as usize] as i32; + let divisor = cpu.x[self.operands.rs2 as usize] as i32; + if dividend == i32::MIN && divisor == -1 { + cpu.write_register(self.operands.rd as usize, 1); + } else { + cpu.write_register(self.operands.rd as usize, divisor as i64); } } } diff --git a/tracer/src/instruction/virtual_lw.rs b/tracer/src/instruction/virtual_lw.rs index a152b8da1f..3a785826e4 100644 --- a/tracer/src/instruction/virtual_lw.rs +++ b/tracer/src/instruction/virtual_lw.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::emulator::cpu::Xlen; use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -15,8 +14,6 @@ declare_riscv_instr!( impl VirtualLW { fn exec(&self, cpu: &mut Cpu, ram_access: &mut ::RAMAccess) { - // virtual lw is only supported on bit32. On bit64 LW doesn't use this instruction - assert_eq!(cpu.xlen, Xlen::Bit32); let address = (cpu.x[self.operands.rs1 as usize] as u64) .wrapping_add(self.operands.imm as i32 as u64); let value = cpu.get_mut_mmu().load_word(address); diff --git a/tracer/src/instruction/virtual_movsign.rs b/tracer/src/instruction/virtual_movsign.rs index a1d7efe61f..c724b9cc16 100644 --- a/tracer/src/instruction/virtual_movsign.rs +++ b/tracer/src/instruction/virtual_movsign.rs @@ -1,16 +1,10 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; -// Constants for 32-bit and 64-bit word sizes -const ALL_ONES_32: u64 = 0xFFFF_FFFF; const ALL_ONES_64: u64 = 0xFFFF_FFFF_FFFF_FFFF; -const SIGN_BIT_32: u64 = 0x8000_0000; const SIGN_BIT_64: u64 = 0x8000_0000_0000_0000; declare_riscv_instr!( @@ -26,22 +20,10 @@ impl VirtualMovsign { let val = cpu.x[self.operands.rs1 as usize] as u64; cpu.write_register( self.operands.rd as usize, - match cpu.xlen { - Xlen::Bit32 => { - if val & SIGN_BIT_32 != 0 { - // Should this be ALL_ONES_64? - ALL_ONES_32 as i64 - } else { - 0 - } - } - Xlen::Bit64 => { - if val & SIGN_BIT_64 != 0 { - ALL_ONES_64 as i64 - } else { - 0 - } - } + if val & SIGN_BIT_64 != 0 { + ALL_ONES_64 as i64 + } else { + 0 }, ); } diff --git a/tracer/src/instruction/virtual_pow2.rs b/tracer/src/instruction/virtual_pow2.rs index 0b3be31b50..b3f4021cfe 100644 --- a/tracer/src/instruction/virtual_pow2.rs +++ b/tracer/src/instruction/virtual_pow2.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -17,20 +14,10 @@ declare_riscv_instr!( impl VirtualPow2 { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => { - cpu.write_register( - self.operands.rd as usize, - 1 << (cpu.x[self.operands.rs1 as usize] as u64 % 32), - ); - } - Xlen::Bit64 => { - cpu.write_register( - self.operands.rd as usize, - 1 << (cpu.x[self.operands.rs1 as usize] as u64 % 64), - ); - } - } + cpu.write_register( + self.operands.rd as usize, + 1 << (cpu.x[self.operands.rs1 as usize] as u64 % 64), + ); } } diff --git a/tracer/src/instruction/virtual_pow2_w.rs b/tracer/src/instruction/virtual_pow2_w.rs index d1ada83a26..0d5258dcc4 100644 --- a/tracer/src/instruction/virtual_pow2_w.rs +++ b/tracer/src/instruction/virtual_pow2_w.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -17,15 +14,10 @@ declare_riscv_instr!( impl VirtualPow2W { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => panic!("VirtualPow2W is invalid in 32b mode"), - Xlen::Bit64 => { - cpu.write_register( - self.operands.rd as usize, - 1 << (cpu.x[self.operands.rs1 as usize] as u64 % 32), - ); - } - } + cpu.write_register( + self.operands.rd as usize, + 1 << (cpu.x[self.operands.rs1 as usize] as u64 % 32), + ); } } diff --git a/tracer/src/instruction/virtual_pow2i.rs b/tracer/src/instruction/virtual_pow2i.rs index 4a3400e5c7..39a90dd601 100644 --- a/tracer/src/instruction/virtual_pow2i.rs +++ b/tracer/src/instruction/virtual_pow2i.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_j::FormatJ, RISCVInstruction, RISCVTrace}; @@ -17,14 +14,7 @@ declare_riscv_instr!( impl VirtualPow2I { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => { - cpu.write_register(self.operands.rd as usize, 1 << (self.operands.imm % 32)) - } - Xlen::Bit64 => { - cpu.write_register(self.operands.rd as usize, 1 << (self.operands.imm % 64)) - } - } + cpu.write_register(self.operands.rd as usize, 1 << (self.operands.imm % 64)) } } diff --git a/tracer/src/instruction/virtual_pow2i_w.rs b/tracer/src/instruction/virtual_pow2i_w.rs index 08a6105a46..891c940b91 100644 --- a/tracer/src/instruction/virtual_pow2i_w.rs +++ b/tracer/src/instruction/virtual_pow2i_w.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_j::FormatJ, RISCVInstruction, RISCVTrace}; @@ -17,12 +14,7 @@ declare_riscv_instr!( impl VirtualPow2IW { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => panic!("VirtualPow2IW is invalid in 32b mode"), - Xlen::Bit64 => { - cpu.write_register(self.operands.rd as usize, 1 << (self.operands.imm % 32)) - } - } + cpu.write_register(self.operands.rd as usize, 1 << (self.operands.imm % 32)) } } diff --git a/tracer/src/instruction/virtual_rev8w.rs b/tracer/src/instruction/virtual_rev8w.rs index 407f17acd8..06b0ce2ffb 100644 --- a/tracer/src/instruction/virtual_rev8w.rs +++ b/tracer/src/instruction/virtual_rev8w.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -17,13 +14,8 @@ declare_riscv_instr!( impl VirtualRev8W { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit64 => { - let v = cpu.x[self.operands.rs1 as usize] as u64; - cpu.write_register(self.operands.rd as usize, rev8w(v) as i64); - } - Xlen::Bit32 => unimplemented!(), - } + let v = cpu.x[self.operands.rs1 as usize] as u64; + cpu.write_register(self.operands.rd as usize, rev8w(v) as i64); } } diff --git a/tracer/src/instruction/virtual_rotri.rs b/tracer/src/instruction/virtual_rotri.rs index d845e04af9..379015a7b6 100644 --- a/tracer/src/instruction/virtual_rotri.rs +++ b/tracer/src/instruction/virtual_rotri.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::instruction::format::format_virtual_right_shift_i::FormatVirtualRightShiftI; -use crate::{declare_riscv_instr, emulator::cpu::Cpu, emulator::cpu::Xlen}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{RISCVInstruction, RISCVTrace}; @@ -18,17 +18,7 @@ impl VirtualROTRI { // Extract rotation amount from bitmask: trailing zeros = rotation amount let shift = self.operands.imm.trailing_zeros(); - // Rotate right by `shift` respecting current XLEN width (matches ROTRI semantics) - let rotated = match cpu.xlen { - Xlen::Bit32 => { - let val_32 = cpu.x[self.operands.rs1 as usize] as u32; - val_32.rotate_right(shift) as i64 - } - Xlen::Bit64 => { - let val = cpu.x[self.operands.rs1 as usize]; - val.rotate_right(shift) - } - }; + let rotated = cpu.x[self.operands.rs1 as usize].rotate_right(shift); cpu.write_register(self.operands.rd as usize, cpu.sign_extend(rotated)); } diff --git a/tracer/src/instruction/virtual_rotriw.rs b/tracer/src/instruction/virtual_rotriw.rs index 4d0a183a37..a350b87e41 100644 --- a/tracer/src/instruction/virtual_rotriw.rs +++ b/tracer/src/instruction/virtual_rotriw.rs @@ -2,7 +2,7 @@ use common::constants::XLEN; use serde::{Deserialize, Serialize}; use crate::instruction::format::format_virtual_right_shift_i::FormatVirtualRightShiftI; -use crate::{declare_riscv_instr, emulator::cpu::Cpu, emulator::cpu::Xlen}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{RISCVInstruction, RISCVTrace}; @@ -21,15 +21,8 @@ impl VirtualROTRIW { let shift = self.operands.imm.trailing_zeros().min(XLEN as u32 / 2); // Rotate right by `shift` in lower 32bits width (matches ROTRI semantics) - let rotated = match cpu.xlen { - Xlen::Bit32 => { - panic!("ROTRIW is not supported in 32-bit mode"); - } - Xlen::Bit64 => { - let val = cpu.x[self.operands.rs1 as usize] as u64 as u32; - val.rotate_right(shift) - } - }; + let val = cpu.x[self.operands.rs1 as usize] as u64 as u32; + let rotated = val.rotate_right(shift); cpu.write_register(self.operands.rd as usize, rotated as i64); } diff --git a/tracer/src/instruction/virtual_shift_right_bitmask.rs b/tracer/src/instruction/virtual_shift_right_bitmask.rs index 5c4b56d4c3..048db72246 100644 --- a/tracer/src/instruction/virtual_shift_right_bitmask.rs +++ b/tracer/src/instruction/virtual_shift_right_bitmask.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -21,18 +18,9 @@ impl VirtualShiftRightBitmask { cpu: &mut Cpu, _: &mut ::RAMAccess, ) { - match cpu.xlen { - Xlen::Bit32 => { - let shift = cpu.x[self.operands.rs1 as usize] as u64 & 0x1F; - let ones = (1u64 << (32 - shift)) - 1; - cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); - } - Xlen::Bit64 => { - let shift = cpu.x[self.operands.rs1 as usize] as u64 & 0x3F; - let ones = (1u128 << (64 - shift)) - 1; - cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); - } - } + let shift = cpu.x[self.operands.rs1 as usize] as u64 & 0x3F; + let ones = (1u128 << (64 - shift)) - 1; + cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); } } diff --git a/tracer/src/instruction/virtual_shift_right_bitmaski.rs b/tracer/src/instruction/virtual_shift_right_bitmaski.rs index 7379fe1130..a3dd0c4e3b 100644 --- a/tracer/src/instruction/virtual_shift_right_bitmaski.rs +++ b/tracer/src/instruction/virtual_shift_right_bitmaski.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_j::FormatJ, RISCVInstruction, RISCVTrace}; @@ -21,18 +18,9 @@ impl VirtualShiftRightBitmaskI { cpu: &mut Cpu, _: &mut ::RAMAccess, ) { - match cpu.xlen { - Xlen::Bit32 => { - let shift = self.operands.imm % 32; - let ones = (1u64 << (32 - shift)) - 1; - cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); - } - Xlen::Bit64 => { - let shift = self.operands.imm % 64; - let ones = (1u128 << (64 - shift)) - 1; - cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); - } - } + let shift = self.operands.imm % 64; + let ones = (1u128 << (64 - shift)) - 1; + cpu.write_register(self.operands.rd as usize, (ones << shift) as i64); } } diff --git a/tracer/src/instruction/virtual_sign_extend_word.rs b/tracer/src/instruction/virtual_sign_extend_word.rs index c6348bface..0d2b4c2ffd 100644 --- a/tracer/src/instruction/virtual_sign_extend_word.rs +++ b/tracer/src/instruction/virtual_sign_extend_word.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -17,15 +14,10 @@ declare_riscv_instr!( impl VirtualSignExtendWord { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => panic!("VirtualSignExtend is not supported for 32-bit mode"), - Xlen::Bit64 => { - cpu.write_register( - self.operands.rd as usize, - (cpu.x[self.operands.rs1 as usize] << 32) >> 32, - ); - } - } + cpu.write_register( + self.operands.rd as usize, + (cpu.x[self.operands.rs1 as usize] << 32) >> 32, + ); } } diff --git a/tracer/src/instruction/virtual_sw.rs b/tracer/src/instruction/virtual_sw.rs index 39e928d26d..5c1fcfe58a 100644 --- a/tracer/src/instruction/virtual_sw.rs +++ b/tracer/src/instruction/virtual_sw.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use super::{format::format_s::FormatS, RISCVInstruction, RISCVTrace}; -use crate::emulator::cpu::Xlen; use crate::{declare_riscv_instr, emulator::cpu::Cpu}; declare_riscv_instr!( @@ -14,8 +13,6 @@ declare_riscv_instr!( impl VirtualSW { fn exec(&self, cpu: &mut Cpu, ram_access: &mut ::RAMAccess) { - // virtual lw is only supported on bit32. On bit64 LW doesn't use this instruction - assert_eq!(cpu.xlen, Xlen::Bit32); *ram_access = cpu .mmu .store_word( diff --git a/tracer/src/instruction/virtual_xor_rotw.rs b/tracer/src/instruction/virtual_xor_rotw.rs index 6e541546ce..e47fe26cb0 100644 --- a/tracer/src/instruction/virtual_xor_rotw.rs +++ b/tracer/src/instruction/virtual_xor_rotw.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use super::{RISCVInstruction, RISCVTrace}; use crate::instruction::format::format_r::FormatR; -use crate::{declare_riscv_instr, emulator::cpu::Cpu, emulator::cpu::Xlen}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; macro_rules! declare_xorrotw { ($name:ident, $rotation:expr) => { @@ -16,18 +16,11 @@ macro_rules! declare_xorrotw { impl $name { fn exec(&self, cpu: &mut Cpu, _: &mut <$name as RISCVInstruction>::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => { - panic!("XORROTW instructions are not supported in 32-bit mode"); - } - Xlen::Bit64 => { - let rs1_val = cpu.x[self.operands.rs1 as usize] as u32; - let rs2_val = cpu.x[self.operands.rs2 as usize] as u32; - let xor_result = rs1_val ^ rs2_val; - let rotated = xor_result.rotate_right($rotation); - cpu.write_register(self.operands.rd as usize, rotated as i64); - } - } + let rs1_val = cpu.x[self.operands.rs1 as usize] as u32; + let rs2_val = cpu.x[self.operands.rs2 as usize] as u32; + let xor_result = rs1_val ^ rs2_val; + let rotated = xor_result.rotate_right($rotation); + cpu.write_register(self.operands.rd as usize, rotated as i64); } } diff --git a/tracer/src/instruction/virtual_zero_extend_word.rs b/tracer/src/instruction/virtual_zero_extend_word.rs index 76962cbce5..4ac02fe0eb 100644 --- a/tracer/src/instruction/virtual_zero_extend_word.rs +++ b/tracer/src/instruction/virtual_zero_extend_word.rs @@ -1,9 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ - declare_riscv_instr, - emulator::cpu::{Cpu, Xlen}, -}; +use crate::{declare_riscv_instr, emulator::cpu::Cpu}; use super::{format::format_i::FormatI, RISCVInstruction, RISCVTrace}; @@ -17,15 +14,10 @@ declare_riscv_instr!( impl VirtualZeroExtendWord { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { - match cpu.xlen { - Xlen::Bit32 => panic!("VirtualExtend is not supported for 32-bit mode"), - Xlen::Bit64 => { - cpu.write_register( - self.operands.rd as usize, - cpu.x[self.operands.rs1 as usize] & 0xFFFFFFFF, - ); - } - } + cpu.write_register( + self.operands.rd as usize, + cpu.x[self.operands.rs1 as usize] & 0xFFFFFFFF, + ); } } diff --git a/tracer/src/instruction/xori.rs b/tracer/src/instruction/xori.rs index 25eb1c3a1e..bede72f107 100644 --- a/tracer/src/instruction/xori.rs +++ b/tracer/src/instruction/xori.rs @@ -19,9 +19,7 @@ impl XORI { fn exec(&self, cpu: &mut Cpu, _: &mut ::RAMAccess) { cpu.write_register( self.operands.rd as usize, - cpu.sign_extend( - cpu.x[self.operands.rs1 as usize] ^ normalize_imm(self.operands.imm, &cpu.xlen), - ), + cpu.sign_extend(cpu.x[self.operands.rs1 as usize] ^ normalize_imm(self.operands.imm)), ); } } diff --git a/tracer/src/jolt_cycle_adapter.rs b/tracer/src/jolt_cycle_adapter.rs index 56a33a343f..1e3daba13b 100644 --- a/tracer/src/jolt_cycle_adapter.rs +++ b/tracer/src/jolt_cycle_adapter.rs @@ -1,16 +1,16 @@ -use jolt_riscv::{JoltCycle, JoltInstruction}; +use jolt_riscv::{JoltCycle, JoltInstructionRowData}; #[cfg(feature = "test-utils")] use crate::instruction::format::InstructionFormat; #[cfg(feature = "test-utils")] -use crate::instruction::NormalizedInstruction; +use crate::instruction::JoltInstructionRow; use crate::instruction::{ format::InstructionRegisterState, RAMAccess, RISCVCycle, RISCVInstruction, }; -impl JoltCycle for RISCVCycle { +impl JoltCycle for RISCVCycle { type Instruction = T; fn instruction(&self) -> T { @@ -59,7 +59,7 @@ impl JoltCycle for RISCVCycle { #[cfg(feature = "test-utils")] fn random(rng: &mut rand::rngs::StdRng) -> Self { let instruction = T::random(rng); - let normalized: NormalizedInstruction = instruction.into(); + let normalized: JoltInstructionRow = instruction.into(); let register_state = <::RegisterState as InstructionRegisterState>::random( rng, diff --git a/tracer/src/lib.rs b/tracer/src/lib.rs index 98421cf00a..f325b5b1e1 100644 --- a/tracer/src/lib.rs +++ b/tracer/src/lib.rs @@ -11,13 +11,10 @@ use tracing::{error, info}; #[cfg(not(feature = "std"))] use alloc::{boxed::Box, vec::Vec}; -use common::{self, constants::RAM_START_ADDRESS, jolt_device::MemoryConfig}; -use emulator::{ - cpu::{self, Xlen}, - default_terminal::DefaultTerminal, -}; +use common::{self, jolt_device::MemoryConfig}; +use emulator::{cpu, default_terminal::DefaultTerminal}; use instruction::{Cycle, Instruction}; -use object::{Object, ObjectSection, SectionKind}; +use jolt_riscv::RV64IMAC_JOLT; pub mod emulator; pub mod execution_backend; @@ -31,13 +28,11 @@ pub use execution_backend::TracerBackend; pub use instruction::inline::{ list_registered_inlines, InlineRegistration, TracerInlineExpansionProvider, }; +pub use jolt_riscv::InlineExtension; -use crate::{ - emulator::{ - memory::{Memory, MemoryData}, - Emulator, - }, - instruction::uncompress_instruction, +use crate::emulator::{ + memory::{Memory, MemoryData}, + Emulator, }; /// Executes a RISC-V program and generates its execution trace along with emulator state checkpoints. @@ -260,8 +255,6 @@ fn setup_emulator_with_backtraces( ) -> Emulator { let term = DefaultTerminal::default(); let mut emulator = Emulator::new(Box::new(term)); - emulator.update_xlen(get_xlen()); - // Set the advice tape if provided if let Some(tape) = advice_tape { emulator.set_advice_tape(tape); @@ -656,128 +649,28 @@ impl LazyTracer for CheckpointingTracer { } #[tracing::instrument(skip_all)] -pub fn decode(elf: &[u8]) -> (Vec, Vec<(u64, u8)>, u64, u64, Xlen) { +pub fn decode(elf: &[u8]) -> (Vec, Vec<(u64, u8)>, u64, u64) { let obj = object::File::parse(elf).unwrap(); - if !matches!(&obj, object::File::Elf32(_)) { - let image = - jolt_program::image::decode_elf(elf).expect("jolt-program ELF64 decoding failed"); - let instructions = image - .instructions - .into_iter() - .map(|instruction| { - Instruction::try_from_normalized(instruction) - .expect("jolt-program image decoder produced an unknown tracer row") - }) - .collect(); - return ( - instructions, - image.memory_init, - image.program_end, - image.entry_address, - Xlen::Bit64, - ); - } - - decode_legacy(elf) -} - -fn decode_legacy(elf: &[u8]) -> (Vec, Vec<(u64, u8)>, u64, u64, Xlen) { - let obj = object::File::parse(elf).unwrap(); - let e_entry = obj.entry(); - let mut xlen = Xlen::Bit64; - if let object::File::Elf32(_) = &obj { - xlen = Xlen::Bit32; - } - - let sections = obj - .sections() - .filter(|s| s.address() >= RAM_START_ADDRESS) - .collect::>(); - - let mut instructions = Vec::new(); - let mut data = Vec::new(); - - // keeps track of the highest address used in the program as the end address - let mut program_end = RAM_START_ADDRESS; - for section in sections { - let start = section.address(); - let length = section.size(); - let end = start + length; - program_end = program_end.max(end); - - let raw_data = section.data().unwrap(); - - if let SectionKind::Text = section.kind() { - let mut offset = 0; - while offset < raw_data.len() { - let address = section.address() + offset as u64; - - // Check if we have at least 2 bytes - if offset + 1 >= raw_data.len() { - break; - } - - // Read first 2 bytes to determine instruction length - let first_halfword = u16::from_le_bytes([raw_data[offset], raw_data[offset + 1]]); - - // Check if it's a compressed instruction (lowest 2 bits != 11) - if (first_halfword & 0b11) != 0b11 { - // Compressed 16-bit instruction - let compressed_inst = first_halfword; - if compressed_inst == 0x0000 { - offset += 2; - continue; - } - - if let Ok(inst) = Instruction::decode( - uncompress_instruction(compressed_inst as u32, xlen), - address, - true, - ) { - instructions.push(inst); - } else { - eprintln!("Warning: compressed instruction {compressed_inst:04X} at address: {address:08X} failed to decode."); - instructions.push(Instruction::UNIMPL); - } - offset += 2; - } else { - // Standard 32-bit instruction - if offset + 3 >= raw_data.len() { - eprintln!("Warning: incomplete instruction at address: {address:08X}"); - break; - } - - let word = u32::from_le_bytes([ - raw_data[offset], - raw_data[offset + 1], - raw_data[offset + 2], - raw_data[offset + 3], - ]); - - if let Ok(inst) = Instruction::decode(word, address, false) { - instructions.push(inst); - } else { - eprintln!("Warning: word: {word:08X} at address: {address:08X} is not recognized as a valid instruction."); - instructions.push(Instruction::UNIMPL); - } - offset += 4; - } - } - } - let address = section.address(); - for (offset, byte) in raw_data.iter().enumerate() { - data.push((address + offset as u64, *byte)); - } - } - (instructions, data, program_end, e_entry, xlen) -} - -fn get_xlen() -> Xlen { - match common::constants::XLEN { - 32 => cpu::Xlen::Bit32, - 64 => cpu::Xlen::Bit64, - _ => panic!("Emulator only supports 32 / 64 bit registers."), - } + if matches!(&obj, object::File::Elf32(_)) { + panic!("tracer only supports RV64 ELF inputs"); + } + + let image = jolt_program::image::decode_elf(elf, RV64IMAC_JOLT) + .expect("jolt-program ELF64 decoding failed"); + let instructions = image + .instructions + .into_iter() + .map(|instruction| { + Instruction::try_from_source_instruction(instruction) + .expect("jolt-program image decoder produced an unknown tracer row") + }) + .collect(); + ( + instructions, + image.memory_init, + image.program_end, + image.entry_address, + ) } pub struct IterChunks { @@ -825,6 +718,27 @@ mod tests { ] } + fn minimal_elf32() -> Vec { + let mut elf = vec![0; 52]; + elf[0..4].copy_from_slice(b"\x7fELF"); + elf[4] = 1; // ELFCLASS32 + elf[5] = 1; // little endian + elf[6] = 1; // current ELF version + elf[16..18].copy_from_slice(&2u16.to_le_bytes()); // executable + elf[18..20].copy_from_slice(&243u16.to_le_bytes()); // RISC-V + elf[20..24].copy_from_slice(&1u32.to_le_bytes()); + elf[40..42].copy_from_slice(&52u16.to_le_bytes()); + elf[42..44].copy_from_slice(&32u16.to_le_bytes()); + elf[46..48].copy_from_slice(&40u16.to_le_bytes()); + elf + } + + #[test] + #[should_panic(expected = "tracer only supports RV64 ELF inputs")] + fn decode_rejects_elf32() { + decode(&minimal_elf32()); + } + #[test] #[should_panic(expected = "Trusted advice too long")] fn panics_when_trusted_advice_exceeds_max() { diff --git a/tracer/src/utils/inline_helpers.rs b/tracer/src/utils/inline_helpers.rs index 902bdb5faa..b0b6d8bbc0 100644 --- a/tracer/src/utils/inline_helpers.rs +++ b/tracer/src/utils/inline_helpers.rs @@ -29,7 +29,6 @@ use crate::instruction::add::ADD; use crate::instruction::addi::ADDI; use crate::instruction::and::AND; use crate::instruction::andi::ANDI; -use crate::instruction::srli::SRLI; use crate::instruction::srliw::SRLIW; use crate::instruction::format::format_assert_align::FormatAssert; @@ -44,17 +43,16 @@ use crate::instruction::format::format_virtual_right_shift_i::FormatVirtualRight use crate::instruction::format::format_virtual_right_shift_r::FormatVirtualRightShiftR; use crate::instruction::format::NormalizedOperands; -use crate::emulator::cpu::Xlen; use crate::instruction::virtual_rotri::VirtualROTRI; use crate::instruction::virtual_rotriw::VirtualROTRIW; use crate::instruction::xor::XOR; use crate::instruction::xori::XORI; use crate::instruction::Cycle; use crate::instruction::Instruction; -use crate::instruction::NormalizedInstruction; use crate::instruction::RISCVCycle; use crate::instruction::RISCVInstruction; use crate::instruction::RISCVTrace; +use crate::instruction::SourceInstructionRow; use crate::utils::virtual_registers::VirtualRegisterAllocator; use common::constants::RISCV_REGISTER_COUNT; @@ -79,8 +77,6 @@ pub struct InstrAssembler { pub address: u64, /// Whether to use RVC encodings. pub is_compressed: bool, - /// Xlen of the CPU. - pub xlen: Xlen, /// Accumulated instruction buffer. sequence: Vec, /// Whether the instruction uses the the FormatInline instruction format. @@ -94,13 +90,11 @@ impl InstrAssembler { pub fn new_inline( address: u64, is_compressed: bool, - xlen: Xlen, allocator: &VirtualRegisterAllocator, ) -> Self { Self { address, is_compressed, - xlen, sequence: Vec::new(), has_inline_instr_format: true, allocator: allocator.clone(), @@ -142,13 +136,24 @@ impl InstrAssembler { } } + #[inline] + fn source_row(&self, operands: NormalizedOperands) -> SourceInstructionRow { + SourceInstructionRow { + address: self.address as usize, + operands, + inline: None, + is_compressed: false, + } + } + #[inline] fn add_to_sequence(&mut self, inst: I) where RISCVCycle: Into, { + let instruction: Instruction = inst.into(); if self.has_inline_instr_format { - let normalized: NormalizedInstruction = inst.into(); + let normalized = instruction.source_instruction().into_row(); if !Self::is_valid_virtual_rd(normalized.operands.rd) { const MIN_INLINE_REG: u8 = RISCV_REGISTER_COUNT + VIRTUAL_INSTRUCTION_RESERVED_REGISTER_COUNT; @@ -158,15 +163,16 @@ impl InstrAssembler { ); } } - let instruction: Instruction = inst.into(); self.sequence - .extend(instruction.inline_sequence(&self.allocator, self.xlen)); + .extend(instruction.inline_sequence(&self.allocator)); } /// Emit any R-type instruction (rd, rs1, rs2). #[track_caller] #[inline] - pub fn emit_r + RISCVTrace>( + pub fn emit_r< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rd: u8, rs1: u8, @@ -174,25 +180,20 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: Some(rs2), - imm: 0, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: Some(rs2), + imm: 0, + }))); } /// Emit any I-type instruction (rd, rs1, imm). #[track_caller] #[inline] - pub fn emit_i + RISCVTrace>( + pub fn emit_i< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rd: u8, rs1: u8, @@ -200,25 +201,20 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: None, + imm: imm as i128, + }))); } /// Emit any S-type instruction (rs1, rs2, imm). #[track_caller] #[inline] - pub fn emit_s + RISCVTrace>( + pub fn emit_s< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rs1: u8, rs2: u8, @@ -226,25 +222,20 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: Some(rs2), - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: None, + rs1: Some(rs1), + rs2: Some(rs2), + imm: imm as i128, + }))); } /// Emit any Load-type instruction (rd, rs1, imm) - like FormatI but with signed imm. #[track_caller] #[inline] - pub fn emit_ld + RISCVTrace>( + pub fn emit_ld< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rd: u8, rs1: u8, @@ -252,25 +243,20 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: None, + imm: imm as i128, + }))); } /// Emit any B-type instruction (rs1, rs2, imm) - branch instructions. #[track_caller] #[inline] - pub fn emit_b + RISCVTrace>( + pub fn emit_b< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rs1: u8, rs2: u8, @@ -278,69 +264,62 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: Some(rs2), - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: None, + rs1: Some(rs1), + rs2: Some(rs2), + imm: imm as i128, + }))); } /// Emit any J-type instruction (rd, imm) - jump instructions. #[track_caller] #[inline] - pub fn emit_j + RISCVTrace>(&mut self, rd: u8, imm: u64) - where + pub fn emit_j< + Op: RISCVInstruction + RISCVTrace + From, + >( + &mut self, + rd: u8, + imm: u64, + ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: None, - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: None, + rs2: None, + imm: imm as i128, + }))); } /// Emit any U-type instruction (rd, imm) - upper immediate instructions. #[track_caller] #[inline] - pub fn emit_u + RISCVTrace>(&mut self, rd: u8, imm: u64) - where + pub fn emit_u< + Op: RISCVInstruction + RISCVTrace + From, + >( + &mut self, + rd: u8, + imm: u64, + ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: None, - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: None, + rs2: None, + imm: imm as i128, + }))); } /// Emit any virtual right shift I-type instruction (rd, rs1, imm). #[track_caller] #[inline] - pub fn emit_vshift_i + RISCVTrace>( + pub fn emit_vshift_i< + Op: RISCVInstruction + + RISCVTrace + + From, + >( &mut self, rd: u8, rs1: u8, @@ -348,25 +327,22 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: None, + imm: imm as i128, + }))); } /// Emit any virtual right shift R-type instruction (rd, rs1, rs2). #[track_caller] #[inline] - pub fn emit_vshift_r + RISCVTrace>( + pub fn emit_vshift_r< + Op: RISCVInstruction + + RISCVTrace + + From, + >( &mut self, rd: u8, rs1: u8, @@ -374,51 +350,39 @@ impl InstrAssembler { ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: Some(rd), - rs1: Some(rs1), - rs2: Some(rs2), - imm: 0, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: Some(rd), + rs1: Some(rs1), + rs2: Some(rs2), + imm: 0, + }))); } /// Emit an alignment assertion instruction (rs1, imm). #[track_caller] #[inline] - pub fn emit_align + RISCVTrace>( + pub fn emit_align< + Op: RISCVInstruction + RISCVTrace + From, + >( &mut self, rs1: u8, imm: i64, ) where RISCVCycle: Into, { - self.add_to_sequence(Op::from(NormalizedInstruction { - instruction_kind: jolt_riscv::InstructionKind::Unimpl, - address: self.address as usize, - operands: NormalizedOperands { - rd: None, - rs1: Some(rs1), - rs2: None, - imm: imm as i128, - }, - is_compressed: false, - is_first_in_sequence: false, - virtual_sequence_remaining: Some(0), - })); + self.add_to_sequence(Op::from(self.source_row(NormalizedOperands { + rd: None, + rs1: Some(rs1), + rs2: None, + imm: imm as i128, + }))); } /// Generic binary operation with constant folding. /// Automatically selects R-type vs I-type encoding based on operand types. pub fn bin< - OR: RISCVInstruction + RISCVTrace, - OI: RISCVInstruction + RISCVTrace, + OR: RISCVInstruction + RISCVTrace + From, + OI: RISCVInstruction + RISCVTrace + From, >( &mut self, rs1: Value, @@ -455,10 +419,7 @@ impl InstrAssembler { } match rs1 { Reg(rs1) => { - match self.xlen { - Xlen::Bit32 => self.emit_i::(rd, rs1, shamt as u64), - Xlen::Bit64 => self.emit_i::(rd, rs1, (shamt & 0x1f) as u64), - } + self.emit_i::(rd, rs1, (shamt & 0x1f) as u64); Reg(rd) } Imm(val) => Imm(((val as u32) >> shamt) as u64), @@ -475,10 +436,7 @@ impl InstrAssembler { let mask = ones << shamt; match rs1 { Reg(rs1_reg) => { - match self.xlen { - Xlen::Bit32 => self.emit_vshift_i::(rd, rs1_reg, mask), - Xlen::Bit64 => self.emit_vshift_i::(rd, rs1_reg, mask), - } + self.emit_vshift_i::(rd, rs1_reg, mask); Reg(rd) } Imm(val) => Imm(((val as u32).rotate_right(shamt)) as u64), @@ -560,7 +518,7 @@ mod tests { #[test] fn test_rotl64_immediate_paths() { let allocator = VirtualRegisterAllocator::new(); - let mut asm = InstrAssembler::new_inline(0, false, Xlen::Bit64, &allocator); + let mut asm = InstrAssembler::new_inline(0, false, &allocator); let dest = 0; let vectors: &[(u64, u32, u64)] = &[ (0x0000000000000001, 1, 0x0000000000000002), diff --git a/tracer/src/utils/inline_sequence_writer.rs b/tracer/src/utils/inline_sequence_writer.rs index 7c3ce7fcfc..b2ba3945e9 100644 --- a/tracer/src/utils/inline_sequence_writer.rs +++ b/tracer/src/utils/inline_sequence_writer.rs @@ -5,7 +5,6 @@ //! During actual runtime execution, these values will vary according to the specific bytecode being executed //! and should be replaced with actual runtime values. -use crate::emulator::cpu::Xlen; use crate::instruction::format::format_inline::FormatInline; use crate::instruction::Instruction; use crate::utils::inline_helpers::InstrAssembler; @@ -15,7 +14,6 @@ use std::io::{self, Write}; use std::path::Path; pub const DEFAULT_RAM_START_ADDRESS: u64 = 0x80000000; -pub const DEFAULT_XLEN: Xlen = Xlen::Bit64; pub const DEFAULT_RS1: u8 = 10; pub const DEFAULT_RS2: u8 = 11; pub const DEFAULT_RS3: u8 = 12; @@ -49,8 +47,6 @@ pub struct SequenceInputs { pub address: u64, /// Whether the instruction is compressed pub is_compressed: bool, - /// CPU architecture width (32 or 64) - pub xlen: Xlen, pub rs1: u8, pub rs2: u8, pub rs3: u8, @@ -71,18 +67,16 @@ impl From<&SequenceInputs> for InstrAssembler { InstrAssembler::new_inline( input.address, input.is_compressed, - input.xlen, &VirtualRegisterAllocator::default(), ) } } impl SequenceInputs { - pub fn new(address: u64, is_compressed: bool, xlen: Xlen, rs1: u8, rs2: u8, rs3: u8) -> Self { + pub fn new(address: u64, is_compressed: bool, rs1: u8, rs2: u8, rs3: u8) -> Self { Self { address, is_compressed, - xlen, rs1, rs2, rs3, @@ -95,7 +89,6 @@ impl Default for SequenceInputs { Self { address: DEFAULT_RAM_START_ADDRESS, is_compressed: DEFAULT_IS_COMPRESSED, - xlen: DEFAULT_XLEN, rs1: DEFAULT_RS1, rs2: DEFAULT_RS2, rs3: DEFAULT_RS3, @@ -150,10 +143,7 @@ pub fn write_inline_trace( inline_info.name, inline_info.opcode, inline_info.funct3, inline_info.funct7 )?; - let xlen = match sequence_inputs.xlen { - Xlen::Bit32 => "32", - Xlen::Bit64 => "64", - }; + let xlen = "64"; writeln!( file, @@ -180,7 +170,9 @@ fn format_instruction_with_placeholders( sequence_inputs: &SequenceInputs, ) -> String { let mut formatted = format!("{instruction:?}"); - let normalized_instr = instruction.normalize(); + let normalized_instr = instruction + .try_jolt_instruction_row() + .expect("inline sequence writer only formats final Jolt instructions"); // Replace address with placeholder (address is always in hex format) let address_pattern = format!("address: {:#x}", normalized_instr.address); diff --git a/tracer/src/utils/inline_test_harness.rs b/tracer/src/utils/inline_test_harness.rs index 39c55a6175..2746640a4f 100644 --- a/tracer/src/utils/inline_test_harness.rs +++ b/tracer/src/utils/inline_test_harness.rs @@ -3,7 +3,7 @@ //! Provides a unified testing framework for all inline instructions, //! eliminating the need for inline-specific test harnesses. -use crate::emulator::cpu::{Cpu, Xlen}; +use crate::emulator::cpu::Cpu; use crate::emulator::default_terminal::DefaultTerminal; use crate::emulator::mmu::DRAM_BASE; use crate::instruction::format::format_inline::FormatInline; @@ -72,17 +72,13 @@ pub const INLINE_RS3: u8 = 12; pub struct InlineTestHarness { pub cpu: Cpu, layout: InlineMemoryLayout, - xlen: Xlen, } impl InlineTestHarness { - pub fn new(layout: InlineMemoryLayout, xlen: Xlen) -> Self { + pub fn new(layout: InlineMemoryLayout) -> Self { let mut cpu = Cpu::new(Box::new(DefaultTerminal::default())); cpu.get_mut_mmu().init_memory(TEST_MEMORY_CAPACITY); - if xlen == Xlen::Bit32 { - cpu.update_xlen(Xlen::Bit32); - } - Self { cpu, layout, xlen } + Self { cpu, layout } } pub fn load_input32(&mut self, data: &[u32]) { @@ -273,8 +269,4 @@ impl InlineTestHarness { pub fn create_default_instruction(opcode: u32, funct3: u32, funct7: u32) -> INLINE { Self::create_instruction(opcode, funct3, funct7, INLINE_RS1, INLINE_RS2, INLINE_RS3) } - - pub fn xlen(&self) -> Xlen { - self.xlen - } } diff --git a/tracer/src/utils/instruction_macros.rs b/tracer/src/utils/instruction_macros.rs index 4c01c8a21d..6c477123df 100644 --- a/tracer/src/utils/instruction_macros.rs +++ b/tracer/src/utils/instruction_macros.rs @@ -31,6 +31,13 @@ macro_rules! declare_riscv_instr { &self.operands } + fn source_kind(&self) -> ::jolt_riscv::SourceInstructionKind { + match ::jolt_riscv::SourceInstructionKind::from_name(stringify!($name)) { + Some(kind) => kind, + None => unreachable!("unknown tracer instruction source kind"), + } + } + fn new(word: u32, address: u64, validate: bool, compressed: bool) -> Self { if validate { debug_assert_eq!( @@ -72,31 +79,17 @@ macro_rules! declare_riscv_instr { } } - impl From<$crate::instruction::NormalizedInstruction> for $name { - fn from(ni: $crate::instruction::NormalizedInstruction) -> Self { + impl From<$crate::instruction::SourceInstructionRow> for $name { + fn from(row: $crate::instruction::SourceInstructionRow) -> Self { Self { - address: ni.address as u64, - operands: ni.operands.into(), - virtual_sequence_remaining: ni.virtual_sequence_remaining, - is_first_in_sequence: ni.is_first_in_sequence, - is_compressed: ni.is_compressed, + address: row.address as u64, + operands: row.operands.into(), + virtual_sequence_remaining: None, + is_first_in_sequence: false, + is_compressed: row.is_compressed, } } } - impl ::jolt_riscv::JoltInstruction for $name {} - - impl From<$name> for $crate::instruction::NormalizedInstruction { - fn from(instr: $name) -> $crate::instruction::NormalizedInstruction { - $crate::instruction::NormalizedInstruction { - instruction_kind: ::jolt_riscv::InstructionKind::$name, - address: instr.address as usize, - operands: instr.operands.into(), - is_compressed: instr.is_compressed, - virtual_sequence_remaining: instr.virtual_sequence_remaining, - is_first_in_sequence: instr.is_first_in_sequence, - } - } - } }; } diff --git a/transpiler/src/symbolic_proof.rs b/transpiler/src/symbolic_proof.rs index 75311d49a3..54798fd796 100644 --- a/transpiler/src/symbolic_proof.rs +++ b/transpiler/src/symbolic_proof.rs @@ -355,11 +355,18 @@ pub fn symbolize_proof( "stage5_sumcheck", ); - // === Symbolize stage 6 sumcheck proof === - let stage6_sumcheck = symbolize_sumcheck_variant::( - &real_proof.stage6_sumcheck_proof, + // === Symbolize stage 6a sumcheck proof === + let stage6a_sumcheck = symbolize_sumcheck_variant::( + &real_proof.stage6a_sumcheck_proof, &mut alloc, - "stage6_sumcheck", + "stage6a_sumcheck", + ); + + // === Symbolize stage 6b sumcheck proof === + let stage6b_sumcheck = symbolize_sumcheck_variant::( + &real_proof.stage6b_sumcheck_proof, + &mut alloc, + "stage6b_sumcheck", ); // === Symbolize stage 7 sumcheck proof === @@ -390,7 +397,8 @@ pub fn symbolize_proof( stage3_sumcheck_proof: stage3_sumcheck, stage4_sumcheck_proof: stage4_sumcheck, stage5_sumcheck_proof: stage5_sumcheck, - stage6_sumcheck_proof: stage6_sumcheck, + stage6a_sumcheck_proof: stage6a_sumcheck, + stage6b_sumcheck_proof: stage6b_sumcheck, stage7_sumcheck_proof: stage7_sumcheck, joint_opening_proof: AstProof::default(), untrusted_advice_commitment, diff --git a/z3-verifier/src/cpu_constraints.rs b/z3-verifier/src/cpu_constraints.rs index def62c5339..5817153da6 100644 --- a/z3-verifier/src/cpu_constraints.rs +++ b/z3-verifier/src/cpu_constraints.rs @@ -26,7 +26,6 @@ use tracer::instruction::{ blt::BLT, bltu::BLTU, bne::BNE, - ecall::ECALL, fence::FENCE, format::{ format_assert_align::FormatAssert, format_b::FormatB, format_fence::FormatFence, @@ -275,8 +274,11 @@ impl JoltState { } fn assert_input_matches(&self, solver: &mut Solver, instr: &Instruction, other: &Self) { - let flags = instr.circuit_flags(); - let instruction_flags = instr.instruction_flags(); + let row = instr + .try_jolt_instruction_row() + .expect("Z3 instruction constraints require a final Jolt row"); + let flags = row.circuit_flags(); + let instruction_flags = row.instruction_flags(); self.flags .iter() @@ -480,7 +482,6 @@ test_instruction_constraints!(BGEU, FormatB); test_instruction_constraints!(BLT, FormatB); test_instruction_constraints!(BLTU, FormatB); test_instruction_constraints!(BNE, FormatB); -test_instruction_constraints!(ECALL, FormatI); test_instruction_constraints!(FENCE, FormatFence); test_instruction_constraints!(JAL, FormatJ); test_instruction_constraints!(JALR, FormatI); diff --git a/z3-verifier/src/virtual_sequences.rs b/z3-verifier/src/virtual_sequences.rs index 4ec71925ca..8f866822f8 100644 --- a/z3-verifier/src/virtual_sequences.rs +++ b/z3-verifier/src/virtual_sequences.rs @@ -4,7 +4,6 @@ use common::constants::{REGISTER_COUNT, RISCV_REGISTER_COUNT}; use std::env; use std::fmt::Write; use tracer::{ - emulator::cpu::Xlen, instruction::{ add::ADD, addi::ADDI, @@ -127,13 +126,12 @@ struct SymbolicCpu { x: [BV; REGISTER_COUNT as usize], advice_vars: Vec, asserts: Vec, - xlen: Xlen, bv_bits: u32, word_bits: u32, } impl SymbolicCpu { - fn new(var_prefix: &str, xlen: Xlen, bv_bits: u32) -> Self { + fn new(var_prefix: &str, bv_bits: u32) -> Self { assert!(bv_bits.is_power_of_two() && (8..=64).contains(&bv_bits)); assert!(bv_bits.is_multiple_of(2)); let word_bits = bv_bits / 2; @@ -148,7 +146,6 @@ impl SymbolicCpu { x: regs, advice_vars: Vec::new(), asserts, // x0 is always 0 - xlen, bv_bits, word_bits, } @@ -188,21 +185,11 @@ impl SymbolicCpu { } fn sign_extend(&self, bv: &BV) -> BV { - match self.xlen { - Xlen::Bit32 => bv - .extract(self.word_bits - 1, 0) - .sign_ext(self.bv_bits - self.word_bits), - Xlen::Bit64 => bv.clone(), - } + bv.clone() } fn unsigned_data(&self, bv: &BV) -> BV { - match self.xlen { - Xlen::Bit32 => bv - .extract(self.word_bits - 1, 0) - .zero_ext(self.bv_bits - self.word_bits), - Xlen::Bit64 => bv.clone(), - } + bv.clone() } } @@ -233,7 +220,7 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { } Instruction::ADDI(ADDI { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); - let imm = normalize_imm(operands.imm, &cpu.xlen); + let imm = normalize_imm(operands.imm); cpu.x[operands.rd as usize] = cpu.sign_extend(&(rs1 + imm)); } Instruction::AND(AND { operands, .. }) => { @@ -243,7 +230,7 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { } Instruction::ANDI(ANDI { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); - let imm = normalize_imm(scale_imm_u64(operands.imm, cpu), &cpu.xlen); + let imm = normalize_imm(scale_imm_u64(operands.imm, cpu)); cpu.x[operands.rd as usize] = cpu.sign_extend(&(rs1 & imm)); } Instruction::ANDN(ANDN { operands, .. }) => { @@ -252,7 +239,7 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { cpu.x[operands.rd as usize] = cpu.sign_extend(&(rs1 & rs2.bvnot())); } Instruction::LUI(LUI { operands, .. }) => { - let imm = normalize_imm(operands.imm, &cpu.xlen); + let imm = normalize_imm(operands.imm); cpu.x[operands.rd as usize] = BV::from_i64(imm, cpu.bv_bits); } Instruction::MUL(MUL { operands, .. }) => { @@ -263,22 +250,12 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { Instruction::MULHU(MULHU { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); let rs2 = cpu.x[operands.rs2 as usize].clone(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let lhs = rs1.extract(cpu.word_bits - 1, 0); - let rhs = rs2.extract(cpu.word_bits - 1, 0); - let product = lhs.zero_ext(cpu.word_bits) * rhs.zero_ext(cpu.word_bits); - cpu.sign_extend(&product.extract(cpu.word_bits * 2 - 1, cpu.word_bits)) - } - Xlen::Bit64 => { - let product = rs1.zero_ext(cpu.bv_bits) * rs2.zero_ext(cpu.bv_bits); - product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits) - } - } + let product = rs1.zero_ext(cpu.bv_bits) * rs2.zero_ext(cpu.bv_bits); + cpu.x[operands.rd as usize] = product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits) } Instruction::ORI(ORI { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); - let imm = normalize_imm(scale_imm_u64(operands.imm, cpu), &cpu.xlen); + let imm = normalize_imm(scale_imm_u64(operands.imm, cpu)); cpu.x[operands.rd as usize] = cpu.sign_extend(&(rs1 | imm)); } Instruction::SUB(SUB { operands, .. }) => { @@ -314,11 +291,7 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { let divisor = cpu.x[operands.rs1 as usize].clone(); let quotient = cpu.x[operands.rs2 as usize].clone(); let ones = cpu.bv_ones(); - let word_ones = BV::from_u64(u64::MAX, cpu.word_bits); - cpu.asserts.push(divisor.eq(0).implies(match cpu.xlen { - Xlen::Bit32 => quotient.extract(cpu.word_bits - 1, 0).eq(&word_ones), - Xlen::Bit64 => quotient.eq(&ones), - })); + cpu.asserts.push(divisor.eq(0).implies(quotient.eq(&ones))); } Instruction::VirtualAssertValidUnsignedRemainder(VirtualAssertValidUnsignedRemainder { operands, @@ -326,18 +299,9 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); let rs2 = cpu.x[operands.rs2 as usize].clone(); - cpu.asserts.push(match cpu.xlen { - Xlen::Bit32 => { - let remainder = rs1.extract(cpu.word_bits - 1, 0); - let divisor = rs2.extract(cpu.word_bits - 1, 0); - divisor.eq(0) | remainder.bvult(&divisor) - } - Xlen::Bit64 => { - let remainder = rs1; - let divisor = rs2; - divisor.eq(0) | remainder.bvult(&divisor) - } - }); + let remainder = rs1; + let divisor = rs2; + cpu.asserts.push(divisor.eq(0) | remainder.bvult(&divisor)); } Instruction::VirtualAssertWordAlignment(VirtualAssertWordAlignment { @@ -349,60 +313,31 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { Instruction::VirtualChangeDivisor(VirtualChangeDivisor { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); let rs2 = cpu.x[operands.rs2 as usize].clone(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let dividend = rs1.extract(cpu.word_bits - 1, 0); - let divisor = rs2.extract(cpu.word_bits - 1, 0); - let word_min = SymbolicCpu::signed_min(cpu.word_bits); - let word_ones = BV::from_u64(u64::MAX, cpu.word_bits); - (dividend.eq(&word_min) & divisor.eq(&word_ones)).ite( - &BV::from_u64(1, cpu.bv_bits), - &divisor.sign_ext(cpu.bv_bits - cpu.word_bits), - ) - } - Xlen::Bit64 => { - let dividend = rs1; - let divisor = rs2; - let min = SymbolicCpu::signed_min(cpu.bv_bits); - let ones = cpu.bv_ones(); - (dividend.eq(&min) & divisor.eq(&ones)) - .ite(&BV::from_u64(1, cpu.bv_bits), &divisor) - } - } + let dividend = rs1; + let divisor = rs2; + let min = SymbolicCpu::signed_min(cpu.bv_bits); + let ones = cpu.bv_ones(); + cpu.x[operands.rd as usize] = + (dividend.eq(&min) & divisor.eq(&ones)).ite(&BV::from_u64(1, cpu.bv_bits), &divisor) } Instruction::VirtualChangeDivisorW(VirtualChangeDivisorW { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); let rs2 = cpu.x[operands.rs2 as usize].clone(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - panic!("VirtualChangeDivisorW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = rs1.extract(cpu.word_bits - 1, 0); - let divisor = rs2.extract(cpu.word_bits - 1, 0); - let word_min = SymbolicCpu::signed_min(cpu.word_bits); - let word_ones = BV::from_u64(u64::MAX, cpu.word_bits); - (dividend.eq(&word_min) & divisor.eq(&word_ones)).ite( - &BV::from_u64(1, cpu.bv_bits), - &divisor.sign_ext(cpu.bv_bits - cpu.word_bits), - ) - } - } + let dividend = rs1.extract(cpu.word_bits - 1, 0); + let divisor = rs2.extract(cpu.word_bits - 1, 0); + let word_min = SymbolicCpu::signed_min(cpu.word_bits); + let word_ones = BV::from_u64(u64::MAX, cpu.word_bits); + cpu.x[operands.rd as usize] = (dividend.eq(&word_min) & divisor.eq(&word_ones)).ite( + &BV::from_u64(1, cpu.bv_bits), + &divisor.sign_ext(cpu.bv_bits - cpu.word_bits), + ) } Instruction::VirtualMovsign(VirtualMovsign { operands, .. }) => { let val = cpu.x[operands.rs1 as usize].clone(); let ones = cpu.bv_ones(); let zero = cpu.bv_zero(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let sign_bit = val.extract(cpu.word_bits - 1, cpu.word_bits - 1); - sign_bit.eq(1).ite(&ones, &zero) - } - Xlen::Bit64 => { - let sign_bit = val.extract(cpu.bv_bits - 1, cpu.bv_bits - 1); - sign_bit.eq(1).ite(&ones, &zero) - } - }; + let sign_bit = val.extract(cpu.bv_bits - 1, cpu.bv_bits - 1); + cpu.x[operands.rd as usize] = sign_bit.eq(1).ite(&ones, &zero); } Instruction::VirtualAdvice(VirtualAdvice { operands, .. }) => { let advice_var = BV::new_const( @@ -414,23 +349,15 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { } Instruction::VirtualPow2(VirtualPow2 { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => cpu - .bv_u64(1) - .bvshl(rs1 & cpu.bv_u64((cpu.word_bits - 1) as u64)), - Xlen::Bit64 => cpu - .bv_u64(1) - .bvshl(rs1 & cpu.bv_u64((cpu.bv_bits - 1) as u64)), - }; + cpu.x[operands.rd as usize] = cpu + .bv_u64(1) + .bvshl(rs1 & cpu.bv_u64((cpu.bv_bits - 1) as u64)); } Instruction::VirtualPow2W(VirtualPow2W { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => panic!("VirtualPow2W is invalid in 32b mode"), - Xlen::Bit64 => cpu - .bv_u64(1) - .bvshl(rs1 & cpu.bv_u64((cpu.word_bits - 1) as u64)), - }; + cpu.x[operands.rd as usize] = cpu + .bv_u64(1) + .bvshl(rs1 & cpu.bv_u64((cpu.word_bits - 1) as u64)); } Instruction::VirtualSRA(VirtualSRA { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); @@ -445,45 +372,25 @@ fn symbolic_exec(instr: &Instruction, cpu: &mut SymbolicCpu) { cpu.x[operands.rd as usize] = rs1.bvlshr(&shift); } Instruction::VirtualShiftRightBitmask(VirtualShiftRightBitmask { operands, .. }) => { - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let shift = cpu.x[operands.rs1 as usize].clone() - & cpu.bv_u64((cpu.word_bits - 1) as u64); - let inv_shift: BV = cpu.bv_u64(cpu.word_bits as u64) - &shift; - let ones = (cpu.bv_u64(1).bvshl(&inv_shift)) - 1; - ones.bvshl(&shift) - } - Xlen::Bit64 => { - let shift = - cpu.x[operands.rs1 as usize].clone() & cpu.bv_u64((cpu.bv_bits - 1) as u64); - let inv_shift: BV = cpu.bv_u64(cpu.bv_bits as u64) - &shift; - let ones = (BV::from_u64(1, cpu.bv_bits * 2) - .bvshl(inv_shift.zero_ext(cpu.bv_bits))) - - 1; - ones.bvshl(shift.zero_ext(cpu.bv_bits)) - .extract(cpu.bv_bits - 1, 0) - } - } + let shift = cpu.x[operands.rs1 as usize].clone() & cpu.bv_u64((cpu.bv_bits - 1) as u64); + let inv_shift: BV = cpu.bv_u64(cpu.bv_bits as u64) - &shift; + let ones = + (BV::from_u64(1, cpu.bv_bits * 2).bvshl(inv_shift.zero_ext(cpu.bv_bits))) - 1; + cpu.x[operands.rd as usize] = ones + .bvshl(shift.zero_ext(cpu.bv_bits)) + .extract(cpu.bv_bits - 1, 0) } Instruction::VirtualSignExtendWord(VirtualSignExtendWord { operands, .. }) => { - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => panic!("VirtualSignExtendWord is not supported for 32-bit mode"), - Xlen::Bit64 => { - let val = cpu.x[operands.rs1 as usize].clone(); - val.extract(cpu.word_bits - 1, 0) - .sign_ext(cpu.bv_bits - cpu.word_bits) - } - } + let val = cpu.x[operands.rs1 as usize].clone(); + cpu.x[operands.rd as usize] = val + .extract(cpu.word_bits - 1, 0) + .sign_ext(cpu.bv_bits - cpu.word_bits) } Instruction::VirtualZeroExtendWord(VirtualZeroExtendWord { operands, .. }) => { - cpu.x[operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => panic!("VirtualExtend is not supported for 32-bit mode"), - Xlen::Bit64 => { - let val = cpu.x[operands.rs1 as usize].clone(); - val.extract(cpu.word_bits - 1, 0) - .zero_ext(cpu.bv_bits - cpu.word_bits) - } - } + let val = cpu.x[operands.rs1 as usize].clone(); + cpu.x[operands.rd as usize] = val + .extract(cpu.word_bits - 1, 0) + .zero_ext(cpu.bv_bits - cpu.word_bits) } Instruction::VirtualMULI(VirtualMULI { operands, .. }) => { let rs1 = cpu.x[operands.rs1 as usize].clone(); @@ -553,16 +460,15 @@ fn test_correctness( let mut solver = Solver::new(); solver.set_params(&solver_params); let allocator = VirtualRegisterAllocator::default(); - let xlen = Xlen::Bit64; let bv_bits = verifier_bv_bits(); - let mut cpu = SymbolicCpu::new("cpu1", xlen, bv_bits); + let mut cpu = SymbolicCpu::new("cpu1", bv_bits); let cpu_initial = cpu.clone(); let mut cpu_expected = cpu.clone(); expected(instr, &mut cpu_expected); let instruction: Instruction = (*instr).into(); - let seq = instruction.inline_sequence(&allocator, xlen); + let seq = instruction.inline_sequence(&allocator); for instr in seq { symbolic_exec(&instr, &mut cpu); } @@ -619,11 +525,10 @@ fn test_consistency(instr: &Instruction) { let mut solver = Solver::new(); solver.set_params(&solver_params); let allocator = VirtualRegisterAllocator::default(); - let xlen = Xlen::Bit64; let bv_bits = verifier_bv_bits(); let (mut cpu1, mut cpu2) = ( - SymbolicCpu::new("cpu1", xlen, bv_bits), - SymbolicCpu::new("cpu2", xlen, bv_bits), + SymbolicCpu::new("cpu1", bv_bits), + SymbolicCpu::new("cpu2", bv_bits), ); let cpu1_initial = cpu1.clone(); @@ -631,7 +536,7 @@ fn test_consistency(instr: &Instruction) { solver += &x1.eq(x2); } - let seq = instr.inline_sequence(&allocator, xlen); + let seq = instr.inline_sequence(&allocator); for instr in &seq { symbolic_exec(instr, &mut cpu1); symbolic_exec(instr, &mut cpu2); @@ -653,7 +558,10 @@ fn test_consistency(instr: &Instruction) { SatResult::Unsat => {} SatResult::Sat => { let mut msg = "Found differing outputs:\n".to_string(); - let operands = instr.normalize().operands; + let operands = instr + .try_jolt_instruction_row() + .expect("virtual sequence verifier only formats final Jolt instructions") + .operands; let model = solver.get_model().unwrap(); let eval = |bv: &BV| model.eval(bv, true).unwrap().as_u64().unwrap(); for i in 0..RISCV_REGISTER_COUNT as usize { @@ -745,7 +653,7 @@ macro_rules! test_sequence { test_sequence!(ADDIW, FormatI, |instr: &ADDIW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; - let imm = normalize_imm(instr.operands.imm, &cpu.xlen); + let imm = normalize_imm(instr.operands.imm); cpu.x[instr.operands.rd as usize] = cpu.sign_ext_word(&(rs1 + imm).extract(cpu.word_bits - 1, 0)); }); @@ -758,74 +666,46 @@ test_sequence!(ADDW, FormatR, |instr: &ADDW, cpu| { test_sequence!(DIV, FormatR, |instr: &DIV, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - todo!() - } - Xlen::Bit64 => { - let dividend = rs1; - let divisor = rs2; - let ones = cpu.bv_ones(); - let min = SymbolicCpu::signed_min(cpu.bv_bits); - divisor.eq(0).ite( - &ones, - &(dividend.eq(&min) & divisor.eq(&ones)).ite(dividend, &(dividend.bvsdiv(divisor))), - ) - } - }; + let dividend = rs1; + let divisor = rs2; + let ones = cpu.bv_ones(); + let min = SymbolicCpu::signed_min(cpu.bv_bits); + cpu.x[instr.operands.rd as usize] = divisor.eq(0).ite( + &ones, + &(dividend.eq(&min) & divisor.eq(&ones)).ite(dividend, &(dividend.bvsdiv(divisor))), + ); }); test_sequence!(DIVU, FormatR, |instr: &DIVU, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - todo!() - } - Xlen::Bit64 => { - let dividend = rs1; - let divisor = rs2; - let ones = cpu.bv_ones(); - divisor.eq(0).ite(&ones, &(dividend.bvudiv(divisor))) - } - }; + let dividend = rs1; + let divisor = rs2; + let ones = cpu.bv_ones(); + cpu.x[instr.operands.rd as usize] = divisor.eq(0).ite(&ones, &(dividend.bvudiv(divisor))); }); test_sequence!(DIVUW, FormatR, |instr: &DIVUW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - panic!("DIVUW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = cpu.word_extract(rs1); - let divisor = cpu.word_extract(rs2); - let q = divisor - .eq(0) - .ite(&cpu.word_ones(), &(dividend.bvudiv(&divisor))); - cpu.sign_ext_word(&q) - } - }; + let dividend = cpu.word_extract(rs1); + let divisor = cpu.word_extract(rs2); + let q = divisor + .eq(0) + .ite(&cpu.word_ones(), &(dividend.bvudiv(&divisor))); + cpu.x[instr.operands.rd as usize] = cpu.sign_ext_word(&q); }); test_sequence!(DIVW, FormatR, |instr: &DIVW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - panic!("DIVW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = cpu.word_extract(rs1); - let divisor = cpu.word_extract(rs2); - let word_min = SymbolicCpu::signed_min(cpu.word_bits); - let word_ones = cpu.word_ones(); - let q = divisor.eq(0).ite( - &word_ones, - &(dividend.eq(&word_min) & divisor.eq(&word_ones)) - .ite(÷nd, &(dividend.bvsdiv(&divisor))), - ); - cpu.sign_ext_word(&q) - } - }; + let dividend = cpu.word_extract(rs1); + let divisor = cpu.word_extract(rs2); + let word_min = SymbolicCpu::signed_min(cpu.word_bits); + let word_ones = cpu.word_ones(); + let q = divisor.eq(0).ite( + &word_ones, + &(dividend.eq(&word_min) & divisor.eq(&word_ones)) + .ite(÷nd, &(dividend.bvsdiv(&divisor))), + ); + cpu.x[instr.operands.rd as usize] = cpu.sign_ext_word(&q); }); // Memory operations are not tested at the moment // test_sequence!(LB, FormatLoad); @@ -837,38 +717,18 @@ test_sequence!(DIVW, FormatR, |instr: &DIVW, cpu| { test_sequence!(MULH, FormatR, |instr: &MULH, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let lhs = cpu.word_extract(rs1); - let rhs = cpu.word_extract(rs2); - let product = lhs.sign_ext(cpu.word_bits) * rhs.sign_ext(cpu.word_bits); - cpu.sign_extend(&product.extract(cpu.word_bits * 2 - 1, cpu.word_bits)) - } - Xlen::Bit64 => { - let lhs = rs1; - let rhs = rs2; - let product = lhs.sign_ext(cpu.bv_bits) * rhs.sign_ext(cpu.bv_bits); - product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits) - } - }; + let lhs = rs1; + let rhs = rs2; + let product = lhs.sign_ext(cpu.bv_bits) * rhs.sign_ext(cpu.bv_bits); + cpu.x[instr.operands.rd as usize] = product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits); }); test_sequence!(MULHSU, FormatR, |instr: &MULHSU, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - let lhs = cpu.word_extract(rs1); - let rhs = cpu.word_extract(rs2); - let product = lhs.sign_ext(cpu.word_bits) * rhs.zero_ext(cpu.word_bits); - cpu.sign_extend(&product.extract(cpu.word_bits * 2 - 1, cpu.word_bits)) - } - Xlen::Bit64 => { - let lhs = rs1; - let rhs = rs2; - let product = lhs.sign_ext(cpu.bv_bits) * rhs.zero_ext(cpu.bv_bits); - product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits) - } - }; + let lhs = rs1; + let rhs = rs2; + let product = lhs.sign_ext(cpu.bv_bits) * rhs.zero_ext(cpu.bv_bits); + cpu.x[instr.operands.rd as usize] = product.extract(cpu.bv_bits * 2 - 1, cpu.bv_bits); }); test_sequence!(MULW, FormatR, |instr: &MULW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; @@ -879,92 +739,57 @@ test_sequence!(MULW, FormatR, |instr: &MULW, cpu| { test_sequence!(REM, FormatR, |instr: &REM, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - todo!() - } - Xlen::Bit64 => { - let dividend = rs1; - let divisor = rs2; - let min = SymbolicCpu::signed_min(cpu.bv_bits); - let ones = cpu.bv_ones(); - let zero = cpu.bv_zero(); - divisor.eq(0).ite( - dividend, - &(dividend.eq(&min) & divisor.eq(&ones)).ite(&zero, &(dividend.bvsrem(divisor))), - ) - } - }; + let dividend = rs1; + let divisor = rs2; + let min = SymbolicCpu::signed_min(cpu.bv_bits); + let ones = cpu.bv_ones(); + let zero = cpu.bv_zero(); + cpu.x[instr.operands.rd as usize] = divisor.eq(0).ite( + dividend, + &(dividend.eq(&min) & divisor.eq(&ones)).ite(&zero, &(dividend.bvsrem(divisor))), + ); }); test_sequence!(REMU, FormatR, |instr: &REMU, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - todo!() - } - Xlen::Bit64 => { - let dividend = rs1; - let divisor = rs2; - divisor.eq(0).ite(dividend, &(dividend.bvurem(divisor))) - } - }; + let dividend = rs1; + let divisor = rs2; + cpu.x[instr.operands.rd as usize] = divisor.eq(0).ite(dividend, &(dividend.bvurem(divisor))); }); test_sequence!(REMUW, FormatR, |instr: &REMUW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - panic!("REMUW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = cpu.word_extract(rs1); - let divisor = cpu.word_extract(rs2); - let r = divisor.eq(0).ite(÷nd, &(dividend.bvurem(&divisor))); - cpu.sign_ext_word(&r) - } - }; + let dividend = cpu.word_extract(rs1); + let divisor = cpu.word_extract(rs2); + let r = divisor.eq(0).ite(÷nd, &(dividend.bvurem(&divisor))); + cpu.x[instr.operands.rd as usize] = cpu.sign_ext_word(&r); }); test_sequence!(REMW, FormatR, |instr: &REMW, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - cpu.x[instr.operands.rd as usize] = match cpu.xlen { - Xlen::Bit32 => { - panic!("REMW is invalid in 32b mode"); - } - Xlen::Bit64 => { - let dividend = cpu.word_extract(rs1); - let divisor = cpu.word_extract(rs2); - let word_min = SymbolicCpu::signed_min(cpu.word_bits); - let word_ones = cpu.word_ones(); - let word_zero = cpu.word_u64(0); - let r = divisor.eq(0).ite( - ÷nd, - &(dividend.eq(&word_min) & divisor.eq(&word_ones)) - .ite(&word_zero, &(dividend.bvsrem(&divisor))), - ); - cpu.sign_ext_word(&r) - } - }; + let dividend = cpu.word_extract(rs1); + let divisor = cpu.word_extract(rs2); + let word_min = SymbolicCpu::signed_min(cpu.word_bits); + let word_ones = cpu.word_ones(); + let word_zero = cpu.word_u64(0); + let r = divisor.eq(0).ite( + ÷nd, + &(dividend.eq(&word_min) & divisor.eq(&word_ones)) + .ite(&word_zero, &(dividend.bvsrem(&divisor))), + ); + cpu.x[instr.operands.rd as usize] = cpu.sign_ext_word(&r); }); // test_sequence!(SB, FormatS); // test_sequence!(SH, FormatS); test_sequence!(SLL, FormatR, |instr: &SLL, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - let shift = rs2 - & match cpu.xlen { - Xlen::Bit32 => cpu.bv_u64((cpu.word_bits - 1) as u64), - Xlen::Bit64 => cpu.bv_u64((cpu.bv_bits - 1) as u64), - }; + let shift = rs2 & cpu.bv_u64((cpu.bv_bits - 1) as u64); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&rs1.bvshl(&shift)); }); test_sequence!(SLLI, FormatI, |instr: &SLLI, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; - let mask = match cpu.xlen { - Xlen::Bit32 => cpu.word_bits - 1, - Xlen::Bit64 => cpu.bv_bits - 1, - }; + let mask = cpu.bv_bits - 1; let shift = BV::from_u64(instr.operands.imm & mask as u64, cpu.bv_bits); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&rs1.bvshl(&shift)); }); @@ -985,19 +810,12 @@ test_sequence!(SLLW, FormatR, |instr: &SLLW, cpu| { test_sequence!(SRA, FormatR, |instr: &SRA, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - let shift = rs2 - & match cpu.xlen { - Xlen::Bit32 => cpu.bv_u64((cpu.word_bits - 1) as u64), - Xlen::Bit64 => cpu.bv_u64((cpu.bv_bits - 1) as u64), - }; + let shift = rs2 & cpu.bv_u64((cpu.bv_bits - 1) as u64); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&rs1.bvashr(&shift)); }); test_sequence!(SRAI, FormatI, |instr: &SRAI, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; - let mask = match cpu.xlen { - Xlen::Bit32 => cpu.word_bits - 1, - Xlen::Bit64 => cpu.bv_bits - 1, - }; + let mask = cpu.bv_bits - 1; let shift = BV::from_u64(instr.operands.imm & mask as u64, cpu.bv_bits); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&rs1.bvashr(&shift)); }); @@ -1018,19 +836,12 @@ test_sequence!(SRAW, FormatR, |instr: &SRAW, cpu| { test_sequence!(SRL, FormatR, |instr: &SRL, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; let rs2 = &cpu.x[instr.operands.rs2 as usize]; - let shift = rs2 - & match cpu.xlen { - Xlen::Bit32 => cpu.bv_u64((cpu.word_bits - 1) as u64), - Xlen::Bit64 => cpu.bv_u64((cpu.bv_bits - 1) as u64), - }; + let shift = rs2 & cpu.bv_u64((cpu.bv_bits - 1) as u64); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&cpu.unsigned_data(rs1).bvlshr(&shift)); }); test_sequence!(SRLI, FormatI, |instr: &SRLI, cpu| { let rs1 = &cpu.x[instr.operands.rs1 as usize]; - let mask = match cpu.xlen { - Xlen::Bit32 => cpu.word_bits - 1, - Xlen::Bit64 => cpu.bv_bits - 1, - }; + let mask = cpu.bv_bits - 1; let shift = BV::from_u64(instr.operands.imm & mask as u64, cpu.bv_bits); cpu.x[instr.operands.rd as usize] = cpu.sign_extend(&cpu.unsigned_data(rs1).bvlshr(&shift)); }); diff --git a/zklean-extractor/src/instruction.rs b/zklean-extractor/src/instruction.rs index 53a30e87a7..1c66fc4e28 100644 --- a/zklean-extractor/src/instruction.rs +++ b/zklean-extractor/src/instruction.rs @@ -1,12 +1,11 @@ use jolt_core::zkvm::{ instruction::{ - CircuitFlags, Flags as _, InstructionLookup as _, InterleavedBitsMarker as _, - SupportedInstruction as _, + CircuitFlags, Flags as _, InterleavedBitsMarker as _, SupportedInstruction as _, }, r1cs::inputs::JoltR1CSInputs, }; use strum::IntoEnumIterator as _; -use tracer::instruction::Instruction; +use tracer::instruction::{Instruction, JoltInstructionRow}; use crate::{ constants::JoltParameterSet, @@ -30,8 +29,8 @@ pub enum OperandInterleaving { impl OperandInterleaving { /// Extract the operand interleaving for an instruction - fn instruction_interleaving(instr: &Instruction) -> Self { - if instr.circuit_flags().is_interleaved_operands() { + fn instruction_interleaving(instruction: &JoltInstructionRow) -> Self { + if instruction.circuit_flags().is_interleaved_operands() { Self::Interleaved } else { Self::Concatenated @@ -48,21 +47,26 @@ impl std::fmt::Display for OperandInterleaving { } } -/// Wrapper around a JoltInstruction +/// Wrapper around a JoltInstructionRowData // TODO: Make this generic over the instruction set #[derive(Debug, Clone)] pub struct ZkLeanInstruction { instruction: tracer::instruction::Instruction, + row: JoltInstructionRow, interleaving: OperandInterleaving, phantom: std::marker::PhantomData, } impl From for ZkLeanInstruction { fn from(value: Instruction) -> Self { - let interleaving = OperandInterleaving::instruction_interleaving(&value); + let row = value + .try_jolt_instruction_row() + .expect("ZkLean extraction instruction must be a final Jolt row"); + let interleaving = OperandInterleaving::instruction_interleaving(&row); Self { instruction: value, + row, interleaving, phantom: std::marker::PhantomData, } @@ -80,7 +84,9 @@ impl ZkLeanInstruction { pub fn iter() -> impl Iterator { Instruction::iter().filter_map(|instr| match instr { Instruction::NoOp | Instruction::UNIMPL | Instruction::INLINE(_) => None, - _ if instr.is_supported_instruction() => Some(Self::from(instr)), + _ if instr.is_supported_instruction() && instr.try_jolt_instruction_row().is_ok() => { + Some(Self::from(instr)) + } _ => None, }) } @@ -94,17 +100,17 @@ impl ZkLeanInstruction { let name = self.name(); let num_variables = 2 * J::XLEN; let interleaving = self.interleaving; - let lookup_table = match self - .instruction - .lookup_table() - .map(|t| ZkLeanLookupTable::from(t).name()) + let lookup_table = match jolt_core::zkvm::instruction::InstructionLookup::< + { common::constants::XLEN }, + >::lookup_table(&self.row) + .map(|t| ZkLeanLookupTable::from(t).name()) { None => String::from("sorry /-No lookup table for this instruction-/"), Some(t) => format!("{t} : Vector f {num_variables} -> f"), }; let circuit_flags = CircuitFlags::iter() .filter_map(|f| { - if self.instruction.circuit_flags()[f] { + if self.row.circuit_flags()[f] { Some(input_to_field_name(&JoltR1CSInputs::OpFlags(f))) } else { None diff --git a/zklean-extractor/src/lookups.rs b/zklean-extractor/src/lookups.rs index a95f80c76c..d72a5b7f3b 100644 --- a/zklean-extractor/src/lookups.rs +++ b/zklean-extractor/src/lookups.rs @@ -7,7 +7,7 @@ use crate::{ DefaultMleAst, }; -/// Wrapper around a JoltInstruction +/// Wrapper around a JoltInstructionRowData // TODO: Can we tie the XLEN to the JoltParameterSet somehow? Seem hard w/o const generic // exprs... #[derive(Debug, Clone)]